Ajout de fonctions pour la gestion des éléments dupliqués

This commit is contained in:
Jean-Marie Favreau 2024-01-12 13:33:32 +01:00
parent 9f40d480ab
commit cf2b9611da
11 changed files with 441 additions and 28 deletions

View File

@ -132,7 +132,7 @@ class CalendarList:
self.events = qs.filter(
(Q(recurrence_dtend__isnull=True) & Q(recurrence_dtstart__lte=lastdatetime)) |
(Q(recurrence_dtend__isnull=False) & ~(Q(recurrence_dtstart__gt=lastdatetime) | Q(recurrence_dtend__lt=startdatetime)))
).order_by("start_day", "start_time")
).order_by("start_time")
firstdate = datetime.fromordinal(self.c_firstdate.toordinal())
if firstdate.tzinfo is None or firstdate.tzinfo.utcoffset(firstdate) is None:
@ -211,5 +211,8 @@ class CalendarWeek(CalendarList):
class CalendarDay(CalendarList):
def __init__(self, date, filter):
def __init__(self, date, filter=None):
super().__init__(date, date, filter, exact=True)
def get_events(self):
return self.calendar_days_list()[0].events

View File

@ -1,9 +1,19 @@
from django.forms import ModelForm, ValidationError, TextInput, Form, URLField, MultipleHiddenInput, Textarea, CharField
from django.forms import ModelForm, ValidationError, TextInput, Form, URLField, MultipleHiddenInput, Textarea, CharField, ChoiceField, RadioSelect, MultipleChoiceField
from datetime import date
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
from .models import Event, BatchImportation
from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
from django.utils.safestring import mark_safe
from django.utils.timezone import localtime
from django.utils.formats import localize
from .templatetags.event_extra import event_field_verbose_name, field_to_html
import logging
logger = logging.getLogger(__name__)
class EventSubmissionForm(Form):
url = URLField(max_length=512)
@ -87,3 +97,146 @@ class BatchImportationForm(ModelForm):
return cleaned_data
class FixDuplicates(Form):
action = ChoiceField()
def __init__(self, *args, **kwargs):
nb_events = kwargs.pop('nb_events', None)
super().__init__(*args, **kwargs)
if nb_events == 2:
choices = [("NotDuplicates", "Ces événements sont différents")]
choices += [("SelectA", "Ces événements sont identiques, on garde A et on met B à la corbeile")]
choices += [("SelectB", "Ces événements sont identiques, on garde B et on met A à la corbeille")]
choices += [("Merge", "Ces événements sont identiques, on fusionne à la main")]
else:
choices = [("NotDuplicates", "Ces événements sont tous différents")]
for i in auc[0:nb_events]:
choices += [("Remove" + i, "L'événement " + i + " n'est pas identique aux autres, on le rend indépendant")]
for i in auc[0:nb_events]:
choices += [("Select" + i, "Ces événements sont identiques, on garde " + i + " et on met les autres à la corbeille")]
choices += [("Merge", "Ces événements sont identiques, on fusionne à la main")]
self.fields['action'].choices = choices
def is_action_no_duplicates(self):
return self.cleaned_data["action"] == "NotDuplicates"
def is_action_select(self):
return self.cleaned_data["action"].startswith("Select")
def is_action_remove(self):
return self.cleaned_data["action"].startswith("Remove")
def get_selected_event_code(self):
if self.is_action_select() or self.is_action_remove():
return self.cleaned_data["action"][-1]
else:
return None
def get_selected_event_id(self):
selected = self.get_selected_event_code()
if selected is None:
return None
else:
return auc.rfind(selected)
def get_selected_event(self, edup):
selected = self.get_selected_event_id()
return edup.get_duplicated()[selected]
class SelectEventInList(Form):
event = ChoiceField()
def __init__(self, *args, **kwargs):
events = kwargs.pop('events', None)
super().__init__(*args, **kwargs)
self.fields['event'].choices = [(e.pk, str(e.start_day) + " " + e.title + ", " + e.location) for e in events]
class MergeDuplicates(Form):
checkboxes_fields = ["reference_urls", "description"]
def __init__(self, *args, **kwargs):
self.duplicates = kwargs.pop('duplicates', None)
nb_events = self.duplicates.nb_duplicated()
super().__init__(*args, **kwargs)
choices = [("event" + i, "Valeur de l'évenement " + i) for i in auc[0:nb_events]]
for f in self.duplicates.get_items_comparison():
if not f["similar"]:
if f["key"] in MergeDuplicates.checkboxes_fields:
self.fields[f["key"]] = MultipleChoiceField(choices=choices)
self.fields[f["key"]].initial = choices[0][0]
else:
self.fields[f["key"]] = ChoiceField(widget=RadioSelect, choices=choices)
self.fields[f["key"]].initial = choices[0][0]
def as_grid(self):
result = '<div class="grid">'
for i, e in enumerate(self.duplicates.get_duplicated()):
result += '<div class="grid entete-badge">'
result += '<div class="badge-large">' + int_to_abc(i) + '</div>'
result += '<ul>'
result += '<li><a href="' + e.get_absolute_url() + '">' + e.title + '</a></li>'
result += '<li>Création&nbsp;: ' + localize(localtime(e.created_date)) + '</li>'
result += '<li>Dernière modification&nbsp;: ' + localize(localtime(e.modified_date)) + '</li>'
if e.imported_date:
result += '<li>Dernière importation&nbsp;: ' + localize(localtime(e.imported_date)) + '</li>'
result += '</ul>'
result += '</div>'
result += '</div>'
for e in self.duplicates.get_items_comparison():
key = e["key"]
result += "<h3>" + event_field_verbose_name(e["key"]) + "</h3>"
if e["similar"]:
result += '<div class="comparison-item">Identique&nbsp;:' + str(field_to_html(e["values"], e["key"])) + '</div>'
else:
result += '<fieldset>'
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>'
result += "</div></fieldset>"
return mark_safe(result)
def get_selected_events_id(self, key):
value = self.cleaned_data.get(key)
if not key in self.fields:
return None
else:
if isinstance(value, list):
return [auc.rfind(v[-1]) for v in value]
else:
return auc.rfind(value[-1])

View File

@ -134,7 +134,8 @@ class DuplicatedEvents(models.Model):
def get_item_comparion(self, attr):
values = [getattr(e, attr) for e in self.get_duplicated()]
if isinstance(values[0], list):
if len([x for x in [isinstance(i, list) for i in values] if x is True]) > 0:
hashable_values = "; ".join([str(v) for v in values])
else:
hashable_values = values
@ -144,7 +145,7 @@ class DuplicatedEvents(models.Model):
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"]]
return [self.get_item_comparion(e) for e in Event.data_fields(all=True)]
class Event(models.Model):
@ -274,6 +275,11 @@ class Event(models.Model):
# if the download is ok, then create the corresponding file object
self.local_image = File(name=basename, file=open(tmpfile, "rb"))
def set_skip_duplicate_check(self):
self.skip_duplicate_check = True
def is_skip_duplicate_check(self):
return hasattr(self, "skip_duplicate_check")
def is_in_importation_process(self):
return hasattr(self, "in_importation_process")
@ -302,7 +308,7 @@ class Event(models.Model):
# return a copy of the current object for each recurrence between first an last date (included)
def get_recurrences_between(self, firstdate, lastdate):
if self.recurrences is None:
if not self.has_recurrences():
return [self]
else:
result = []
@ -330,8 +336,7 @@ class Event(models.Model):
etime = time.fromisoformat(self.end_time) if isinstance(self.end_time, str) else time() if self.end_time is None else self.end_time
self.recurrence_dtstart = datetime.combine(sday, stime)
# TODO: see https://forge.chapril.org/jmtrivial/agenda_culturel/issues/65
if self.recurrences is None or len(self.recurrences.rrules) == 0:
if not self.has_recurrences():
if self.end_day is None:
self.dtend = None
else:
@ -353,8 +358,6 @@ class Event(models.Model):
def prepare_save(self):
self.update_modification_dates()
# TODO: update recurrences.dtstart et recurrences.dtend
self.update_recurrence_dtstartend()
# if the image is defined but not locally downloaded
@ -366,8 +369,8 @@ class Event(models.Model):
self.prepare_save()
# check for similar events if no duplicated is known
if self.possibly_duplicated is None:
# check for similar events if no duplicated is known only if the event is created
if self.pk is None and self.possibly_duplicated is None and not self.is_skip_duplicate_check():
# and if this is not an importation process
if not self.is_in_importation_process():
similar_events = self.find_similar_events()
@ -376,8 +379,9 @@ class Event(models.Model):
if len(similar_events) != 0:
self.set_possibly_duplicated(similar_events)
elif self.possibly_duplicated is not None and self.possibly_duplicated.nb_duplicated() == 1:
# delete duplicated group if it's only with one element
if self.possibly_duplicated is not None and self.possibly_duplicated.nb_duplicated() == 1:
self.possibly_duplicated.delete()
self.possibly_duplicated = None
@ -386,8 +390,6 @@ class Event(models.Model):
def from_structure(event_structure, import_source = None):
if event_structure["title"].endswith("ole"):
logger.warning("on choope {}".format(event_structure))
if "category" in event_structure and event_structure["category"] is not None:
event_structure["category"] = Category.objects.get(name=event_structure["category"])
@ -480,32 +482,45 @@ class Event(models.Model):
def set_possibly_duplicated(self, events):
# get existing groups
groups = list(set([e.possibly_duplicated for e in events] + [self.possibly_duplicated]))
groups = [g for g in groups if g is not None]
# do we have to create a new group?
if len(groups) == 0:
group = DuplicatedEvents.objects.create()
logger.warning("set possibly duplicated 0 {}".format(group))
else:
# otherwise merge existing groups
group = DuplicatedEvents.merge_groups(groups)
logger.warning("set possibly duplicated not 0 {}".format(group))
group.save()
# set the possibly duplicated group for the current object
self.possibly_duplicated = group
# and for the other events
for e in events:
e.possibly_duplicated = group
# finally save the other events
Event.objects.bulk_update(events, fields=["possibly_duplicated"])
# finally update all events (including current)
Event.objects.bulk_update(events + [self], fields=["possibly_duplicated"])
def data_fields():
return ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image", "image_alt", "reference_urls", "recurrences"]
def data_fields(all=False):
if all:
result = ["category"]
else:
result = []
result += ["title", "location", "start_day", "start_time", "end_day", "end_time", "description", "image"]
if all:
result += ["local_image"]
result += ["image_alt", "reference_urls", "recurrences"]
if all:
result += ["tags"]
return result
def same_event_by_data(self, other):
for attr in Event.data_fields():

View File

@ -20,7 +20,7 @@
{% 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><a href="{% url 'view_duplicate' obj.pk %}">Possible duplication&nbsp;:</a> {{ events|length }} événements le {{ events.0.start_day }}
</header>
<ul>
{% for e in events %}

View File

@ -16,14 +16,36 @@
<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>
{% if form %}
<p>Choisissez dans la liste ci-dessous l'action que vous voulez réaliser. À noter que
s'il y a plus de deux événements, toutes les possibilités ne sont pas disponibles, et
il vous faudra peut-être réaliser certaines opérations à la main.</p>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div class="grid">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{% url 'duplicates' %}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Appliquer">
</div>
</form>
{% else %}
<div class="infos-and-buttons">
<div class="infos"></div>
<div class="buttons">
<a role="button" href="{% url 'fix_duplicate' object.pk %}">Corriger {% picto_from_name "tool" %}</a>
</div>
</div>
{% endif %}
</header>
<div class="grid">
{% for e in object.get_duplicated %}
<div class="grid entete-badge">
<div class="badge-large">
{{ forloop.counter }}
{{ forloop.counter0|int_to_abc }}
</div>
<ul>
<li><a href="{{ e.get_absolute_url }}">{{ e.title }}</a></li>
<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 %}
@ -38,7 +60,7 @@
{% 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>
<div class="duplicated"><div class="badge-small">{{ forloop.counter0|int_to_abc }} </div> {% field_to_html i e.key %}</div>
{% endfor %}
</div>
{% endif %}

View File

@ -0,0 +1,31 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load event_extra %}
{% block title %}Fusionner les événements dupliqués{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<article>
<header>
<h1>Fusionner les événements dupliqués</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 déplacés dans la corbeille.</p>
</header>
<form method="post">
{% csrf_token %}
{{ form.as_grid }}
<div class="grid">
<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

@ -77,6 +77,13 @@
{% endif %}
</article>
{% endif %}
<article>
<a role="button" href="{% url 'set_duplicate' event.start_day.year event.start_day.month event.start_day.day event.pk %}">{% if user.is_authenticated %}
Marquer comme doublon
{% else %}
Signaler comme doublon
{% endif %}</a>
</article>
</aside>

View File

@ -0,0 +1,30 @@
{% extends "agenda_culturel/page.html" %}
{% load utils_extra %}
{% load event_extra %}
{% block title %}{% if user.is_authenticated %}Marquer comme doublon{% else %}Signaler comme doublon{% endif %}{% endblock %}
{% load cat_extra %}
{% block entete_header %}
{% css_categories %}
{% endblock %}
{% block content %}
<article>
<header>
<h1>{% if user.is_authenticated %}Marquer comme doublon{% else %}Signaler comme doublon{% endif %}</h1>
<p>De quel événement listé ci-dessous l'événement sélectionné est-il un doublon&nbsp;?</p>
<form method="post">{% csrf_token %}
{{ form.as_p }}
<div class="grid">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ event.get_absolute_url }}{% endif %}" role="button" class="secondary">Annuler</a>
<input type="submit" value="Marquer comme doublon">
</div>
</form>
</header>
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event %}
</article>
{% endblock %}

View File

@ -5,6 +5,7 @@ from urllib.parse import urlparse
from datetime import timedelta, date
from django.urls import reverse_lazy
from django.templatetags.static import static
from string import ascii_uppercase as auc
register = template.Library()
@ -66,3 +67,7 @@ def picto_from_name(name, datatooltip=""):
result = '<span data-tooltip="' + datatooltip + '">' + result + '</span>'
return mark_safe(result)
@register.filter
def int_to_abc(d):
return auc[int(d)]

View File

@ -24,6 +24,7 @@ urlpatterns = [
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path("event/<int:pk>/change-status/<status>", change_status_event, name="change_status_event"),
path("event/<int:pk>/delete", EventDeleteView.as_view(), name="delete_event"),
path("event/<int:year>/<int:month>/<int:day>/<int:pk>/set_duplicate", set_duplicate, name="set_duplicate"),
path("ajouter", import_from_url, name="add_event"),
path("admin/", admin.site.urls),
path('accounts/', include('django.contrib.auth.urls')),
@ -40,7 +41,9 @@ urlpatterns = [
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"),
path("duplicates/<int:pk>", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
path("duplicates/<int:pk>/fix", fix_duplicate, name="fix_duplicate"),
path("duplicates/<int:pk>/merge", merge_duplicate, name="merge_duplicate"),
]
if settings.DEBUG:

View File

@ -11,7 +11,7 @@ from django.http import HttpResponseRedirect
from django.urls import reverse
import urllib
from .forms import EventSubmissionForm, EventForm, BatchImportationForm
from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates
from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents
from django.utils import timezone
@ -35,6 +35,9 @@ from .extractors import ExtractorAllURLs
from .celery import app as celery_app, import_events_from_json, import_events_from_url
import unicodedata
import logging
logger = logging.getLogger(__name__)
def get_event_qs(request):
@ -154,7 +157,7 @@ def day_view(request, year = None, month = None, day = None):
filter = EventFilter(request.GET, get_event_qs(request), request=request)
cday = CalendarDay(day, filter)
context = {"day": day, "events": cday.calendar_days_list()[0].events, "filter": filter}
context = {"day": day, "events": cday.get_events(), "filter": filter}
return render(request, 'agenda_culturel/page-day.html', context)
@ -538,6 +541,122 @@ class DuplicatedEventsDetailView(LoginRequiredMixin, DetailView):
template_name = "agenda_culturel/fix_duplicate.html"
@login_required(login_url="/accounts/login/")
def merge_duplicate(request, pk):
edup = get_object_or_404(DuplicatedEvents, pk=pk)
form = MergeDuplicates(duplicates=edup)
if request.method == 'POST':
form = MergeDuplicates(request.POST, duplicates=edup)
if form.is_valid():
events = edup.get_duplicated()
# build fields for the new event
new_event_data = {}
for f in edup.get_items_comparison():
if f["similar"]:
new_event_data[f["key"]] = getattr(events[0], f["key"])
else:
selected = form.get_selected_events_id(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] if x is not None]
if len(values) == 0:
new_event_data[f["key"]] = None
else:
if isinstance(values[0], str):
new_event_data[f["key"]] = "\n".join(values)
else:
new_event_data[f["key"]] = sum(values, [])
else:
new_event_data[f["key"]] = getattr(events[selected], f["key"])
for specific_tag in ["uuids", "import_sources"]:
new_event_data[specific_tag] = sum([x for x in [getattr(e, specific_tag) for e in events] if x is not None], [])
# create a new event that merge the selected events
new_event = Event(**new_event_data)
new_event.set_skip_duplicate_check()
new_event.save()
# move the old ones in trash
for e in events:
e.status = Event.STATUS.TRASH
Event.objects.bulk_update(events, fields=["status"])
messages.info(request, _("La fusion a été réalisée avec succès."))
return HttpResponseRedirect(new_event.get_absolute_url())
return render(request, 'agenda_culturel/merge_duplicate.html', context={'form': form, 'object': edup})
@login_required(login_url="/accounts/login/")
def fix_duplicate(request, pk):
edup = get_object_or_404(DuplicatedEvents, pk=pk)
form = FixDuplicates(nb_events=edup.nb_duplicated())
if request.method == 'POST':
form = FixDuplicates(request.POST, nb_events=edup.nb_duplicated())
if form.is_valid():
if form.is_action_no_duplicates():
events = edup.get_duplicated()
if len(events) == 0:
date = None
else:
s_events = [e for e in events if not e.has_recurrences()]
if len(s_events) != 0:
s_event = s_events[0]
else:
s_event = events[0]
date = s_event.start_day
messages.success(request, _("Les événements ont été marqués comme non dupliqués."))
edup.delete()
if date is None:
return HttpResponseRedirect(reverse_lazy("home"))
else:
return HttpResponseRedirect(reverse_lazy("day_view", args=[date.year, date.month, date.day]))
elif form.is_action_select():
selected = form.get_selected_event(edup)
not_selected = [e for e in edup.get_duplicated() if e != selected]
nb = len(not_selected)
for e in not_selected:
e.status = Event.STATUS.TRASH
Event.objects.bulk_update(not_selected, fields=["status"])
url = selected.get_absolute_url()
edup.delete()
if nb == 1:
messages.success(request, _("L'événement sélectionné a été conservé, l'autre a été déplacé dans la corbeille."))
else:
messages.success(request, _("L'événement sélectionné a été conservé, les autres ont été déplacés dans la corbeille."))
return HttpResponseRedirect(url)
elif form.is_action_remove():
event = form.get_selected_event(edup)
event.possibly_duplicated = None
event.save()
messages.success(request, _("L'événement a été retiré du groupe et rendu indépendant."))
if edup.nb_duplicated() == 1:
return HttpResponseRedirect(event.get_absolute_url())
else:
form = FixDuplicates(nb_events=edup.nb_duplicated())
else:
return HttpResponseRedirect(reverse_lazy("merge_duplicate", args=[edup.pk]))
return render(request, 'agenda_culturel/fix_duplicate.html', context={'form': form, 'object': edup})
class DuplicatedEventsUpdateView(LoginRequiredMixin, UpdateView):
model = DuplicatedEvents
fields = ()
template_name = "agenda_culturel/fix_duplicate.html"
@login_required(login_url="/accounts/login/")
def duplicates(request):
paginator = Paginator(DuplicatedEvents.objects.all(), 10)
@ -551,3 +670,28 @@ def duplicates(request):
response = paginator.page(paginator.num_pages)
return render(request, 'agenda_culturel/duplicates.html', {'filter': filter, 'paginator_filter': response} )
def set_duplicate(request, year, month, day, pk):
event = get_object_or_404(Event, pk=pk)
cday = CalendarDay(date(year, month, day))
others = [e for e in cday.get_events() if e != event and (event.possibly_duplicated is None or event.possibly_duplicated != e.possibly_duplicated)]
form = SelectEventInList(events=others)
if request.method == 'POST':
form = SelectEventInList(request.POST, events=others)
if form.is_valid():
selected = [o for o in others if o.pk == int(form.cleaned_data["event"])]
event.set_possibly_duplicated(selected)
event.save()
if request.user.is_authenticated:
messages.success(request, _("L'événement a été marqué dupliqué avec succès."))
return HttpResponseRedirect(reverse_lazy("view_duplicate", args=[event.possibly_duplicated.pk]))
else:
messages.info(request, _("L'événement a été signalé comme dupliqué avec succès. Votre suggestion sera prochainement prise en charge par l'équipe de modération."))
return HttpResponseRedirect(event.get_absolute_url())
return render(request, 'agenda_culturel/set_duplicate.html', context={'form': form, 'event': event})