Amélioration de la gestion des dupliqués

- beaucoup de bugs corrigés
- stabilisation du fonctionnement général
- amélioration des solutions de correction manuelles
This commit is contained in:
Jean-Marie Favreau 2024-11-10 20:20:24 +01:00
parent 0a66a858c5
commit 18ca7200a0
8 changed files with 448 additions and 267 deletions

View File

@ -25,6 +25,9 @@ from .models import (
Place,
Category,
)
from django.conf import settings
from django.core.files import File
from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
@ -74,6 +77,8 @@ class CategorisationRuleImportForm(ModelForm):
class EventForm(ModelForm):
old_local_image = CharField(widget=HiddenInput(), required=False)
class Meta:
model = Event
exclude = [
@ -81,7 +86,7 @@ class EventForm(ModelForm):
"modified_date",
"moderated_date",
"import_sources",
"uuids"
"image"
]
widgets = {
"start_day": TextInput(
@ -101,6 +106,7 @@ class EventForm(ModelForm):
"end_day": TextInput(attrs={"type": "date"}),
"end_time": TextInput(attrs={"type": "time"}),
"other_versions": HiddenInput(),
"uuids": MultipleHiddenInput(),
"reference_urls": DynamicArrayWidgetURLs(),
"tags": DynamicArrayWidgetTags(),
}
@ -144,6 +150,16 @@ class EventForm(ModelForm):
return end_time
def clean(self):
super().clean()
# when cloning an existing event, we need to copy the local image
if self.cleaned_data['local_image'] is None and not self.cleaned_data['old_local_image'] is None:
basename = self.cleaned_data['old_local_image']
old = settings.MEDIA_ROOT + "/" + basename
self.cleaned_data['local_image'] = File(name=basename, file=open(old, "rb"))
class BatchImportationForm(Form):
json = CharField(
@ -224,13 +240,14 @@ class FixDuplicates(Form):
return self.cleaned_data["action"].startswith("Remove")
def get_selected_event_code(self):
if self.is_action_select() or self.is_action_remove():
if self.is_action_select() or self.is_action_remove() or self.is_action_update():
return int(self.cleaned_data["action"].split("-")[-1])
else:
return None
def get_selected_event(self, edup):
selected = self.get_selected_event_code()
logger.warning("selected " + str(selected))
for e in edup.get_duplicated():
if e.pk == selected:
return e
@ -254,12 +271,21 @@ class MergeDuplicates(Form):
def __init__(self, *args, **kwargs):
self.duplicates = kwargs.pop("duplicates", None)
nb_events = self.duplicates.nb_duplicated()
self.event = kwargs.pop("event", None)
self.events = list(self.duplicates.get_duplicated())
nb_events = len(self.events)
super().__init__(*args, **kwargs)
choices = [
("event" + i, _("Value of event {}").format(i)) for i in auc[0:nb_events]
]
if self.event:
choices = [("event_" + str(self.event.pk), _("Value of the selected version"))] + \
[
("event_" + str(e.pk), _("Value of version {}").format(e.pk)) for e in self.events if e != self.event
]
else:
choices = [
("event_" + str(e.pk), _("Value of version {}").format(e.pk)) for e in self.events
]
for f in self.duplicates.get_items_comparison():
if not f["similar"]:
@ -274,7 +300,7 @@ class MergeDuplicates(Form):
def as_grid(self):
result = '<div class="grid">'
for i, e in enumerate(self.duplicates.get_duplicated()):
for i, e in enumerate(self.events):
result += '<div class="grid entete-badge">'
result += '<div class="badge-large">' + int_to_abc(i) + "</div>"
result += "<ul>"
@ -310,50 +336,82 @@ class MergeDuplicates(Form):
)
else:
result += "<fieldset>"
if key in self.errors:
result += '<div class="message error"><ul>'
for err in self.errors[key]:
result += "<li>" + err + "</li>"
result += "</ul></div>"
result += '<div class="grid comparison-item">'
if hasattr(self, "cleaned_data"):
checked = self.cleaned_data.get(key)
else:
checked = self.fields[key].initial
for i, (v, radio) in enumerate(
zip(e["values"], self.fields[e["key"]].choices)
):
result += '<div class="duplicated">'
id = "id_" + key + "_" + str(i)
value = "event" + auc[i]
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if value in checked:
result += " checked"
else:
result += ' type="radio"'
if checked == value:
result += " checked"
result += ' value="' + value + '"'
result += ">"
result += (
'<div class="badge-small">'
+ int_to_abc(i)
+ "</div>"
+ str(field_to_html(v, e["key"]))
+ "</div>"
)
i = 0
if self.event:
idx = self.events.index(self.event)
result += self.comparison_item(key, i, e["values"][idx], self.fields[e["key"]].choices[idx], self.event, checked)
i += 1
for (v, radio, ev) in zip(e["values"], self.fields[e["key"]].choices, self.events):
if self.event is None or ev != self.event:
result += self.comparison_item(key, i, v, radio, ev, checked)
i += 1
result += "</div></fieldset>"
return mark_safe(result)
def get_selected_events_id(self, key):
def comparison_item(self, key, i, v, radio, ev, checked):
result = '<div class="duplicated">'
id = "id_" + key + "_" + str(ev.pk)
value = "event_" + str(ev.pk)
result += '<input id="' + id + '" name="' + key + '"'
if key in MergeDuplicates.checkboxes_fields:
result += ' type="checkbox"'
if value in checked:
result += " checked"
else:
result += ' type="radio"'
if checked == value:
result += " checked"
result += ' value="' + value + '"'
result += ">"
result += (
'<div class="badge-small">'
+ int_to_abc(i)
+ "</div>")
result += "<div>"
if key == "image":
result += str(field_to_html(ev.local_image, "local_image")) + "</div>"
result += "<div>Lien d'import&nbsp;: "
result += (str(field_to_html(v, key)) + "</div>")
result += "</div>"
return result
def get_selected_events(self, key):
value = self.cleaned_data.get(key)
if key not in self.fields:
return None
else:
if isinstance(value, list):
return [auc.rfind(v[-1]) for v in value]
selected = [int(v.split("_")[-1]) for v in value]
result = []
for s in selected:
for e in self.duplicates.get_duplicated():
if e.pk == selected:
result.append(e)
break
return result
else:
return auc.rfind(value[-1])
selected = int(value.split("_")[-1])
for e in self.duplicates.get_duplicated():
if e.pk == selected:
return e
return None
class ModerationQuestionForm(ModelForm):

File diff suppressed because it is too large Load Diff

View File

@ -280,8 +280,6 @@ class DuplicatedEvents(models.Model):
for e in events:
if event is None:
event = e
if e != event and e.same_uuid(event):
e.status = Event.STATUS.TRASH
if not event is None:
event.status = Event.STATUS.PUBLISHED
self.representative = event
@ -1202,6 +1200,8 @@ class Event(models.Model):
max_date = None
uuids = set()
logger.warning("===================== 000")
# for each event, check if it's a new one, or a one to be updated
for event in events:
sdate = date.fromisoformat(event.start_day)
@ -1225,7 +1225,9 @@ class Event(models.Model):
# check if the event has already be imported (using uuid)
same_events = event.find_same_events_by_uuid()
logger.warning("===================== A")
if len(same_events) != 0:
logger.warning("===================== B")
# check if one event has been imported and not modified in this list
same_imported = Event.find_last_pure_import(same_events)
@ -1233,15 +1235,18 @@ class Event(models.Model):
if not same_imported:
for e in same_events:
if event.similar(e):
logger.warning("===================== C")
same_imported = e
break
if same_imported:
logger.warning("===================== D")
# reopen DuplicatedEvents if required
if not event.similar(same_imported) and same_imported.other_versions:
if same_imported.status != Event.STATUS.TRASH:
if same_imported.other_versions.is_published():
if same_imported.other_versions.representative != same_imported:
logger.warning("===================== E")
same_imported.other_versions.representative = None
same_imported.other_versions.save()
@ -1250,6 +1255,7 @@ class Event(models.Model):
same_imported.prepare_save()
to_update.append(same_imported)
else:
logger.warning("===================== F")
# otherwise, the new event possibly a duplication of the remaining others.
# check if it should be published
@ -1260,6 +1266,7 @@ class Event(models.Model):
# it will be imported
to_import.append(event)
else:
logger.warning("===================== G")
# if uuid is unique (or not available), check for similar events
similar_events = event.find_similar_events()

View File

@ -3,7 +3,7 @@
{% load utils_extra %}
{% load event_extra %}
{% block title %}{% block og_title %}Fusionner les événements dupliqués{% endblock %}{% endblock %}
{% block title %}{% block og_title %}Création d'une nouvelle version par fusion{% endblock %}{% endblock %}
{% load cat_extra %}
{% block entete_header %}
@ -13,9 +13,9 @@
{% block content %}
<article>
<header>
<h1>Fusionner les événements dupliqués</h1>
<h1>Création d'une nouvelle version par fusion</h1>
<p>Pour chacun des champs non identiques, choisissez la version qui vous convient pour créer un événement
résultat de la fusion. Les événements source seront masqués.</p>
résultat de la fusion. La version ainsi créée deviendra la version représentative.</p>
</header>
<form method="post">

View File

@ -0,0 +1,32 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load event_extra %}
{% block title %}{% block og_title %}Mise à jour par sélection de champs{% endblock %}{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<article>
<header>
<h1>Mise à jour par sélection de champs</h1>
<p>Vous allez mettre à jour la version <span class="badge-small">A</span> en sélectionnant
pour chaque champ les valeurs des autres versions.</p>
<p>La version ainsi créée mise à jour deviendra la version représentative.</p>
</header>
<form method="post">
{% csrf_token %}
{{ form.as_grid }}
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'fix_duplicate' object.pk %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Appliquer">
</div>
</form>
</article>
{% endblock %}

View File

@ -120,7 +120,7 @@ def field_to_html(field, key):
return mark_safe('<a href="' + field + '">' + field + "</a>")
elif key == "local_image":
if field:
return mark_safe('<img src="' + field.url + '" />')
return mark_safe('<img class="preview" src="' + field.url + '" />')
else:
return "-"
else:

View File

@ -120,6 +120,7 @@ urlpatterns = [
),
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),
path("duplicates/<int:pk>/merge", merge_duplicate, name="merge_duplicate"),
path("duplicates/<int:pk>/update/<int:epk>", update_duplicate_event, name="update_event"),
path("mquestions/", ModerationQuestionListView.as_view(), name="view_mquestions"),
path(
"mquestions/add", ModerationQuestionCreateView.as_view(), name="add_mquestion"

View File

@ -579,9 +579,22 @@ class EventUpdateView(
obj.set_no_modification_date_changed()
obj.save()
result["other_versions"] = obj.other_versions
if obj.local_image:
result["old_local_image"] = obj.local_image.name
return result
def form_valid(self, form):
original = self.get_object()
# if an image is uploaded, it removes the url stored
if form.cleaned_data['local_image'] != original.local_image:
form.instance.image = None
form.instance.import_sources = None
return super().form_valid(form)
class EventDeleteView(
SuccessMessageMixin, PermissionRequiredMixin, LoginRequiredMixin, DeleteView
):
@ -685,7 +698,6 @@ class EventCreateView(SuccessMessageMixin, CreateView):
return _("The event has been submitted and will be published as soon as it has been validated by the moderation team.")
def import_from_details(request):
form = EventForm(request.POST, is_authenticated=request.user.is_authenticated)
if form.is_valid():
@ -780,7 +792,7 @@ def import_from_urls(request):
# for each not new, add a message
for uc in ucat:
if uc.exists() and not uc.is_new():
if uc.is_event_visible(): # TODO
if uc.is_event_visible():
messages.info(
request,
mark_safe(_('{} has not been submitted since it''s already known: {}.').format(uc.url, uc.get_link()))
@ -1429,6 +1441,54 @@ class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView):
template_name = "agenda_culturel/duplicate.html"
@login_required(login_url="/accounts/login/")
@permission_required(
["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"]
)
def update_duplicate_event(request, pk, epk):
edup = get_object_or_404(DuplicatedEvents, pk=pk)
event = get_object_or_404(Event, pk=epk)
form = MergeDuplicates(duplicates=edup, event=event)
if request.method == "POST":
form = MergeDuplicates(request.POST, duplicates=edup)
if form.is_valid():
events = edup.get_duplicated()
for f in edup.get_items_comparison():
if not f["similar"]:
selected = form.get_selected_events(f["key"])
if not selected is None:
if isinstance(selected, list):
values = [
x
for x in [getattr(s, f["key"]) for s in selected]
if x is not None
]
if len(values) != 0:
if isinstance(values[0], str):
setattr(event, f["key"], "\n".join(values))
else:
setattr(event, f["key"], sum(values, []))
else:
setattr(event, f["key"], getattr(selected, f["key"]))
if f["key"] == "image":
setattr(event, "local_image", getattr(selected, "local_image"))
event.other_versions.fix(event)
event.save()
messages.info(request, _("Update successfully completed."))
return HttpResponseRedirect(event.get_absolute_url())
return render(
request,
"agenda_culturel/update_duplicate.html",
context={"form": form, "object": edup, "event": event},
)
@login_required(login_url="/accounts/login/")
@permission_required(
["agenda_culturel.change_event", "agenda_culturel.change_duplicatedevents"]
@ -1448,13 +1508,13 @@ def merge_duplicate(request, pk):
if f["similar"]:
new_event_data[f["key"]] = getattr(events[0], f["key"])
else:
selected = form.get_selected_events_id(f["key"])
selected = form.get_selected_events(f["key"])
if selected is None:
new_event_data[f["key"]] = None
elif isinstance(selected, list):
values = [
x
for x in [getattr(events[s], f["key"]) for s in selected]
for x in [getattr(s, f["key"]) for s in selected]
if x is not None
]
if len(values) == 0:
@ -1465,16 +1525,16 @@ def merge_duplicate(request, pk):
else:
new_event_data[f["key"]] = sum(values, [])
else:
new_event_data[f["key"]] = getattr(events[selected], f["key"])
# local_image field follows image field
new_event_data[f["key"]] = getattr(selected, f["key"])
if f["key"] == "image" and "local_image" not in new_event_data:
new_event_data["local_image"] = getattr(events[selected], "local_image")
new_event_data["local_image"] = getattr(selected, "local_image")
# create a new event that merge the selected events
new_event = Event(**new_event_data)
new_event.status = Event.STATUS.PUBLISHED
new_event.other_versions = edup
new_event.save()
edup.fix(new_event)
messages.info(request, _("Creation of a merged event has been successfully completed."))
@ -1535,7 +1595,7 @@ def fix_duplicate(request, pk):
elif form.is_action_remove():
# one element is removed from the set
event = form.get_selected_event(edup)
if selected is None:
if event is None:
messages.error(request, _("The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime."))
return HttpResponseRedirect(edup.get_absolute_url())
else:
@ -1553,12 +1613,12 @@ def fix_duplicate(request, pk):
elif form.is_action_update():
# otherwise, a new event will be created using a merging process
event = form.get_selected_event(edup)
if selected is None:
if event is None:
messages.error(request, _("The selected item is no longer included in the list of duplicates. Someone else has probably modified the list in the meantime."))
return HttpResponseRedirect(edup.get_absolute_url())
else:
return HttpResponseRedirect(
reverse_lazy("update_event", args=[edup.ok, event.pk])
reverse_lazy("update_event", args=[edup.pk, event.pk])
)
else:
# otherwise, a new event will be created using a merging process