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:
parent
0a66a858c5
commit
18ca7200a0
@ -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 : "
|
||||
|
||||
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
@ -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()
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user