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):
|
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():
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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 %}
|
{% 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 %}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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 ""
|
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 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
|
@ -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:
|
||||||
|
@ -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} )
|
||||||
|
Loading…
Reference in New Issue
Block a user