Début d'implémentation de la résolution des duplicats
This commit is contained in:
parent
f3d4f1fef2
commit
9f40d480ab
@ -107,6 +107,9 @@ class DuplicatedEvents(models.Model):
|
||||
def nb_duplicated(self):
|
||||
return Event.objects.filter(possibly_duplicated=self).count()
|
||||
|
||||
def get_duplicated(self):
|
||||
return Event.objects.filter(possibly_duplicated=self)
|
||||
|
||||
def merge_into(self, other):
|
||||
# for all objects associated to this group
|
||||
for e in Event.objects.filter(possibly_duplicated=self):
|
||||
@ -129,6 +132,19 @@ class DuplicatedEvents(models.Model):
|
||||
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):
|
||||
|
||||
@ -489,7 +505,7 @@ class Event(models.Model):
|
||||
|
||||
|
||||
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):
|
||||
for attr in Event.data_fields():
|
||||
|
@ -627,9 +627,9 @@ aside nav a.selected {
|
||||
background: var(--primary-focus);
|
||||
}
|
||||
aside nav a.badge {
|
||||
float: right;
|
||||
z-index: 1;
|
||||
margin-top: -2em;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.body-fixed {
|
||||
@ -720,7 +720,50 @@ aside nav a.badge {
|
||||
}
|
||||
|
||||
article article {
|
||||
margin-top: 3em;
|
||||
margin-bottom: 3em;
|
||||
margin: 3em 0.5em;
|
||||
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;
|
||||
}
|
||||
}
|
@ -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 :</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 %} : <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">« 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 »</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
{% include "agenda_culturel/side-nav.html" with current="duplicates" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -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 : {{ e.created_date }}</li>
|
||||
<li>Dernière modification : {{ e.modified_date }}</li>
|
||||
{% if e.imported_date %}<li>Dernière importation : {{ 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 : {% 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 %}
|
||||
|
@ -70,6 +70,11 @@
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</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>
|
||||
{% endif %}
|
||||
|
||||
|
@ -25,6 +25,7 @@
|
||||
{% load event_extra %}
|
||||
{% load contactmessages_extra %}
|
||||
{% load utils_extra %}
|
||||
{% load duplicated_extra %}
|
||||
<body>
|
||||
<div id="main-nav">
|
||||
<div class="header">
|
||||
@ -46,8 +47,9 @@
|
||||
</li>
|
||||
<li>
|
||||
{% if user.is_authenticated %}
|
||||
{% show_badges_events %}
|
||||
{% show_badge_contactmessages %}
|
||||
{% show_badges_events "bottom" %}
|
||||
{% show_badge_duplicated "bottom" %}
|
||||
{% show_badge_contactmessages "bottom" %}
|
||||
{{ user.username }} @
|
||||
{% endif %}
|
||||
<a href="{% url 'home' %}" aria-label="Retour accueil">Pommes de lune</a>
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% load event_extra %}
|
||||
{% load contactmessages_extra %}
|
||||
{% load duplicated_extra %}
|
||||
<aside>
|
||||
<article>
|
||||
<head>
|
||||
@ -17,18 +18,10 @@
|
||||
</head>
|
||||
<nav>
|
||||
<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 == "contactmessages" %}class="selected" {% endif %}href="{% url 'contactmessages' %}">Messages de contact</a>{% show_badge_contactmessages %}</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</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>
|
||||
<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 "left" %}</li>
|
||||
<li><a {% if current == "duplicates" %}class="selected" {% endif %}href="{% url 'duplicates' %}">Gestion des doublons</a>{% show_badge_duplicated "left" %}</li>
|
||||
<li><a {% if current == "imports" %}class="selected" {% endif %}href="{% url 'imports' %}">Importations par lot</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</article>
|
||||
|
@ -12,9 +12,9 @@ register = template.Library()
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def show_badge_contactmessages():
|
||||
def show_badge_contactmessages(placement="top"):
|
||||
nb_open = ContactMessage.nb_open_contactmessages()
|
||||
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:
|
||||
return ""
|
27
src/agenda_culturel/templatetags/duplicated_extra.py
Normal file
27
src/agenda_culturel/templatetags/duplicated_extra.py
Normal 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 ""
|
@ -1,7 +1,7 @@
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
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
|
||||
@ -44,10 +44,31 @@ def picto_status(event):
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def show_badges_events():
|
||||
def show_badges_events(placement="top"):
|
||||
nb_drafts = Event.nb_draft_events()
|
||||
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:
|
||||
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
|
@ -39,6 +39,8 @@ urlpatterns = [
|
||||
path("imports/", imports, name="imports"),
|
||||
path("imports/add", BatchImportationCreateView.as_view(), name="add_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:
|
||||
|
@ -13,7 +13,7 @@ import urllib
|
||||
|
||||
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 enum import StrEnum
|
||||
from datetime import date, timedelta
|
||||
@ -532,3 +532,22 @@ def cancel_import(request, pk):
|
||||
cancel_url = reverse_lazy("imports")
|
||||
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} )
|
||||
|
Loading…
Reference in New Issue
Block a user