Début d'implémentation de la résolution des duplicats

This commit is contained in:
Jean-Marie Favreau 2024-01-07 19:41:14 +01:00
parent f3d4f1fef2
commit 9f40d480ab
12 changed files with 263 additions and 25 deletions

View File

@ -107,6 +107,9 @@ class DuplicatedEvents(models.Model):
def nb_duplicated(self): def nb_duplicated(self):
return Event.objects.filter(possibly_duplicated=self).count() return Event.objects.filter(possibly_duplicated=self).count()
def get_duplicated(self):
return Event.objects.filter(possibly_duplicated=self)
def merge_into(self, other): def merge_into(self, other):
# for all objects associated to this group # for all objects associated to this group
for e in Event.objects.filter(possibly_duplicated=self): for e in Event.objects.filter(possibly_duplicated=self):
@ -129,6 +132,19 @@ class DuplicatedEvents(models.Model):
return result return result
def get_item_comparion(self, attr):
values = [getattr(e, attr) for e in self.get_duplicated()]
if isinstance(values[0], list):
hashable_values = "; ".join([str(v) for v in values])
else:
hashable_values = values
if len(set(hashable_values)) == 1:
return { "similar": True, "key": attr, "values": values[0] }
else:
return { "similar": False, "key": attr, "values": values }
def get_items_comparison(self):
return [self.get_item_comparion(e) for e in Event.data_fields() + ["local_image"]]
class Event(models.Model): class Event(models.Model):
@ -489,7 +505,7 @@ class Event(models.Model):
def data_fields(): def data_fields():
return ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image", "image_alt", "image_alt", "reference_urls", "recurrences"] return ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image", "image_alt", "reference_urls", "recurrences"]
def same_event_by_data(self, other): def same_event_by_data(self, other):
for attr in Event.data_fields(): for attr in Event.data_fields():

View File

@ -627,9 +627,9 @@ aside nav a.selected {
background: var(--primary-focus); background: var(--primary-focus);
} }
aside nav a.badge { aside nav a.badge {
float: right;
z-index: 1; z-index: 1;
margin-top: -2em; margin-top: -2em;
float: right;
} }
.body-fixed { .body-fixed {
@ -720,7 +720,50 @@ aside nav a.badge {
} }
article article { article article {
margin-top: 3em; margin: 3em 0.5em;
margin-bottom: 3em;
padding: 0.6em 0.2em; padding: 0.6em 0.2em;
}
.badge-circle {
display: block;
float: left;
width: 2.5em;
height: 2.5em;
line-height: 2.5em;
text-align: center;
border-radius: 50%;
color: var(--secondary-inverse);
background: $primary-500;
}
.entete-badge {
grid-template-columns: 2.5em repeat(auto-fit, minmax(0%, 1fr));
.badge-large {
@extend .badge-circle;
font-size: 140%;
}
li {
list-style-type: none;
}
}
.comparison-item {
margin: 0.4em;
.duplicated {
padding: 0.5em;
background: rgba(255, 0, 76, 0.1);
border-radius: 0.5em;
}
.badge-small {
@extend .badge-circle;
font-size: 70%;
width: 2em;
height: 2em;
line-height: 2em;
margin-right: 1em;
}
} }

View File

@ -0,0 +1,62 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% block title %}Événements possiblement dupliqués{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<div class="grid two-columns">
<article>
<header>
<h1>Événements possiblement dupliqués</h1>
</header>
<div>
{% for obj in paginator_filter %}
{% with obj.get_duplicated as events %}
<article>
<header><a href="">Possible duplication&nbsp;:</a> {{ events|length }} événements le {{ events.0.start_day }}
</header>
<ul>
{% for e in events %}
<li>{{ e.start_day }}{% if e.start_time %} à {{ e.start_time }}{% endif %}&nbsp;: <a href="{{ e.get_absolute_url }}">{{ e.title }}</a> créé le {{ e.created_date }}</li>
{% endfor %}
</ul>
<footer class="infos-and-buttons">
<div class="infos"></div>
<div class="buttons">
<a role="button" href="{% url 'fix_duplicate' obj.pk %}">Corriger {% picto_from_name "tool" %}</a>
</div>
</footer>
</article>
{% endwith %}
{% endfor %}
</div>
<footer>
<span>
{% if paginator_filter.has_previous %}
<a href="?page=1" role="button">&laquo; premier</a>
<a href="?page={{ paginator_filter.previous_page_number }}" role="button">précédent</a>
{% endif %}
<span>
Page {{ paginator_filter.number }} sur {{ paginator_filter.paginator.num_pages }}
</span>
{% if paginator_filter.has_next %}
<a href="?page={{ paginator_filter.next_page_number }}" role="button">suivant</a>
<a href="?page={{ paginator_filter.paginator.num_pages }}" role="button">dernier &raquo;</a>
{% endif %}
</span>
</footer>
</article>
{% include "agenda_culturel/side-nav.html" with current="duplicates" %}
</div>
{% endblock %}

View File

@ -0,0 +1,48 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load event_extra %}
{% block title %}Événements possiblement dupliqués{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<article>
<header>
<h1>Corriger des événements possiblement dupliqués</h1>
<p>Les événements ci-dessous ont été détectés ou signalés comme possiblement dupliqué.
Les éléments qui diffèrent ont été dupliqués et mis en évidence. </p>
</header>
<div class="grid">
{% for e in object.get_duplicated %}
<div class="grid entete-badge">
<div class="badge-large">
{{ forloop.counter }}
</div>
<ul>
<li>Création&nbsp;: {{ e.created_date }}</li>
<li>Dernière modification&nbsp;: {{ e.modified_date }}</li>
{% if e.imported_date %}<li>Dernière importation&nbsp;: {{ e.imported_date }}</li>{% endif %}
</ul>
</div>
{% endfor %}
</div>
{% for e in object.get_items_comparison %}
<h3>{% event_field_verbose_name e.key %}</h3>
{% if e.similar %}
<div class="comparison-item">Identique&nbsp;: {% field_to_html e.values e.key %}</div>
{% else %}
<div class="grid comparison-item">
{% for i in e.values %}
<div class="duplicated"><div class="badge-small">{{ forloop.counter }}</div> {% field_to_html i e.key %}</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
</article>
{% endblock %}

View File

@ -70,6 +70,11 @@
{% endfor %} {% endfor %}
</ul> </ul>
</nav> </nav>
{% if user.is_authenticated %}
<footer>
<a role="button" href="{% url 'fix_duplicate' event.possibly_duplicated.pk %}">Corriger {% picto_from_name "tool" %}</a>
</footer>
{% endif %}
</article> </article>
{% endif %} {% endif %}

View File

@ -25,6 +25,7 @@
{% load event_extra %} {% load event_extra %}
{% load contactmessages_extra %} {% load contactmessages_extra %}
{% load utils_extra %} {% load utils_extra %}
{% load duplicated_extra %}
<body> <body>
<div id="main-nav"> <div id="main-nav">
<div class="header"> <div class="header">
@ -46,8 +47,9 @@
</li> </li>
<li> <li>
{% if user.is_authenticated %} {% if user.is_authenticated %}
{% show_badges_events %} {% show_badges_events "bottom" %}
{% show_badge_contactmessages %} {% show_badge_duplicated "bottom" %}
{% show_badge_contactmessages "bottom" %}
{{ user.username }} @ {{ user.username }} @
{% endif %} {% endif %}
<a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a> <a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a>

View File

@ -1,5 +1,6 @@
{% load event_extra %} {% load event_extra %}
{% load contactmessages_extra %} {% load contactmessages_extra %}
{% load duplicated_extra %}
<aside> <aside>
<article> <article>
<head> <head>
@ -17,18 +18,10 @@
</head> </head>
<nav> <nav>
<ul> <ul>
<li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events %}</li> <li><a {% if current == "moderation" %}class="selected" {% endif %}href="{% url 'moderation' %}">Derniers événements soumis</a>{% show_badges_events "left" %}</li>
<li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages %}</li> <li><a {% if current == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages "left" %}</li>
</ul> <li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
</nav> <li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Importations par lot</a></li>
</article>
<article>
<head>
<h2>Importations</h2>
</head>
<nav>
<ul>
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Importations par lot</a>{% show_badge_contactmessages %}</li>
</ul> </ul>
</nav> </nav>
</article> </article>

View File

@ -12,9 +12,9 @@ register = template.Library()
@register.simple_tag @register.simple_tag
def show_badge_contactmessages(): def show_badge_contactmessages(placement="top"):
nb_open = ContactMessage.nb_open_contactmessages() nb_open = ContactMessage.nb_open_contactmessages()
if nb_open != 0: if nb_open != 0:
return mark_safe('<a href="' + reverse_lazy("contactmessages") + '?closed=false" class="badge" data-tooltip="' + str(nb_open) + ' message' + pluralize(nb_open) + ' à traiter">' + picto_from_name("mail") + " " + str(nb_open) + '</a>') return mark_safe('<a href="' + reverse_lazy("contactmessages") + '?closed=false" class="badge" data-placement="' + placement + '"data-tooltip="' + str(nb_open) + ' message' + pluralize(nb_open) + ' à traiter">' + picto_from_name("mail") + " " + str(nb_open) + '</a>')
else: else:
return "" return ""

View File

@ -0,0 +1,27 @@
from django import template
from django.utils.safestring import mark_safe
from django.urls import reverse_lazy
from django.template.defaultfilters import pluralize
from agenda_culturel.models import DuplicatedEvents
from .utils_extra import picto_from_name
register = template.Library()
@register.simple_tag
def show_badge_duplicated(placement="top"):
duplicated = DuplicatedEvents.objects.all()
nb_duplicated = 0
for d in duplicated:
if d.nb_duplicated() >= 1:
nb_duplicated += 1
else:
d.delete()
if nb_duplicated != 0:
return mark_safe('<a href="' + reverse_lazy("duplicates") + '" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_duplicated) + ' dupliqué' + pluralize(nb_duplicated) + ' à valider">' + picto_from_name("copy") + " " + str(nb_duplicated) + '</a>')
else:
return ""

View File

@ -1,7 +1,7 @@
from django import template from django import template
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize, linebreaks, urlize
from agenda_culturel.models import Event from agenda_culturel.models import Event
@ -44,10 +44,31 @@ def picto_status(event):
@register.simple_tag @register.simple_tag
def show_badges_events(): def show_badges_events(placement="top"):
nb_drafts = Event.nb_draft_events() nb_drafts = Event.nb_draft_events()
if nb_drafts != 0: if nb_drafts != 0:
return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>') return mark_safe('<a href="' + reverse_lazy("moderation") + '?status=draft" class="badge" data-placement="' + placement + '" data-tooltip="' + str(nb_drafts) + ' brouillon' + pluralize(nb_drafts) + ' à valider">' + picto_from_name("calendar") + " " + str(nb_drafts) + '</a>')
else: else:
return "" return ""
@register.simple_tag
def event_field_verbose_name(the_field):
return Event._meta.get_field(the_field).verbose_name
@register.simple_tag
def field_to_html(field, key):
if field is None:
return "-"
elif key == "description":
return urlize(mark_safe(linebreaks(field)))
elif key == "reference_urls":
return mark_safe("<ul>" + "".join(['<li><a href="' + u + '">' + u + '</a></li>' for u in field]) + "</ul>")
elif key == "image":
return mark_safe('<a href="' + field + '">' + field + '</a>')
elif key == "local_image":
if field:
return mark_safe('<img src="' + field.url + '" />')
else:
return "-"
else:
return field

View File

@ -39,6 +39,8 @@ urlpatterns = [
path("imports/", imports, name="imports"), path("imports/", imports, name="imports"),
path("imports/add", BatchImportationCreateView.as_view(), name="add_import"), path("imports/add", BatchImportationCreateView.as_view(), name="add_import"),
path("imports/<int:pk>/cancel", cancel_import, name="cancel_import"), path("imports/<int:pk>/cancel", cancel_import, name="cancel_import"),
path("duplicates/", duplicates, name="duplicates"),
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="fix_duplicate"),
] ]
if settings.DEBUG: if settings.DEBUG:

View File

@ -13,7 +13,7 @@ import urllib
from .forms import EventSubmissionForm, EventForm, BatchImportationForm from .forms import EventSubmissionForm, EventForm, BatchImportationForm
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents
from django.utils import timezone from django.utils import timezone
from enum import StrEnum from enum import StrEnum
from datetime import date, timedelta from datetime import date, timedelta
@ -532,3 +532,22 @@ def cancel_import(request, pk):
cancel_url = reverse_lazy("imports") cancel_url = reverse_lazy("imports")
return render(request, 'agenda_culturel/cancel_import_confirm.html', {"object": import_process, "cancel_url": cancel_url}) return render(request, 'agenda_culturel/cancel_import_confirm.html', {"object": import_process, "cancel_url": cancel_url})
class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView):
model = DuplicatedEvents
template_name = "agenda_culturel/fix_duplicate.html"
@login_required(login_url="/accounts/login/")
def duplicates(request):
paginator = Paginator(DuplicatedEvents.objects.all(), 10)
page = request.GET.get('page')
try:
response = paginator.page(page)
except PageNotAnInteger:
response = paginator.page(1)
except EmptyPage:
response = paginator.page(paginator.num_pages)
return render(request, 'agenda_culturel/duplicates.html', {'filter': filter, 'paginator_filter': response} )