diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py index 93d2607..92c4abb 100644 --- a/src/agenda_culturel/forms.py +++ b/src/agenda_culturel/forms.py @@ -327,6 +327,7 @@ class EventForm(GroupFormMixin, ModelForm): is_authenticated = kwargs.pop("is_authenticated", False) self.cloning = kwargs.pop("is_cloning", False) self.simple_cloning = kwargs.pop("is_simple_cloning", False) + self.is_edit_from_moderation = kwargs.pop("is_edit_from_moderation", False) self.is_moderation_expert = kwargs.pop("is_moderation_expert", False) super().__init__(*args, **kwargs) if not is_authenticated: diff --git a/src/agenda_culturel/migrations/0158_event_editing_start.py b/src/agenda_culturel/migrations/0158_event_editing_start.py new file mode 100644 index 0000000..5b492b8 --- /dev/null +++ b/src/agenda_culturel/migrations/0158_event_editing_start.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.19 on 2025-03-14 20:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("agenda_culturel", "0157_auto_20250314_1645"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="editing_start", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/src/agenda_culturel/migrations/0159_event_editing_user.py b/src/agenda_culturel/migrations/0159_event_editing_user.py new file mode 100644 index 0000000..7b426d7 --- /dev/null +++ b/src/agenda_culturel/migrations/0159_event_editing_user.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.19 on 2025-03-14 21:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("agenda_culturel", "0158_event_editing_start"), + ] + + operations = [ + migrations.AddField( + model_name="event", + name="editing_user", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_DEFAULT, + related_name="in_edition_events", + to=settings.AUTH_USER_MODEL, + verbose_name="Author currently editing/moderating the event", + ), + ), + ] diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py index 9520dee..64bbb86 100644 --- a/src/agenda_culturel/models.py +++ b/src/agenda_culturel/models.py @@ -714,6 +714,17 @@ class Event(models.Model): modified_date = models.DateTimeField(blank=True, null=True) moderated_date = models.DateTimeField(blank=True, null=True) + editing_start = models.DateTimeField(blank=True, null=True) + editing_user = models.ForeignKey( + User, + verbose_name=_("Author currently editing/moderating the event"), + null=True, + blank=True, + default=None, + on_delete=models.SET_DEFAULT, + related_name="in_edition_events", + ) + created_by_user = models.ForeignKey( User, verbose_name=_("Author of the event creation"), @@ -920,6 +931,34 @@ class Event(models.Model): else: return self.end_day if self.end_day else self.start_day + def is_modification_locked(self, now=None): + if now is None: + now = timezone.now() + limit = timezone.now() + timedelta(minutes=-10) + + return self.editing_start is not None and self.editing_start > limit + + def get_modification_lock(self, user): + now = timezone.now() + + if not self.is_modification_locked(now): + self.editing_start = now + self.editing_user = user + self.save(update_fields=["editing_start", "editing_user"]) + return True + else: + return False + + def free_modification_lock(self, user, save=True): + if user != self.editing_user: + return False + else: + self.editing_start = None + self.editing_user = None + if save: + self.save(update_fields=["editing_start", "editing_user"]) + return True + def get_dates(self): first = self.start_day last = self.get_consolidated_end_day() @@ -1351,6 +1390,10 @@ class Event(models.Model): self.moderated_date = now self.moderated_by_user = self.processing_user + # release editing lock + if self.processing_user is not None: + self.free_modification_lock(self.processing_user, False) + def get_recurrence_at_date(self, year, month, day): dtstart = timezone.make_aware( datetime(year, month, day, 0, 0), timezone.get_default_timezone() diff --git a/src/agenda_culturel/templates/agenda_culturel/event_form.html b/src/agenda_culturel/templates/agenda_culturel/event_form.html index 8a5024f..6a575cd 100644 --- a/src/agenda_culturel/templates/agenda_culturel/event_form.html +++ b/src/agenda_culturel/templates/agenda_culturel/event_form.html @@ -2,6 +2,7 @@ {% load static %} {% load cat_extra %} {% load utils_extra %} +{% load event_extra %} {% block title %} {% block og_title %} {% if object %} @@ -86,6 +87,21 @@

{% endif %} + {% if not form.is_clone_from_url and not form.is_simple_clone_from_url and not object|get_modification_lock:user %} + {% if object.editing_user == user %} +
+ Attention, vous avez ouvert en édition ou modération cet + événement dans un autre onglet (le {{ object.editing_start }}), et toute modification intermédiaire sera effacée par la dernière. + Ce message peut aussi apparaître si vous avez utilisé la navigation arrière. +
+ {% else %} +
+ Attention, {{ object.editing_user }} est en train de modifier ou de modérer cet + événement (depuis {{ object.editing_start }}), et toute modification de votre part pourrait être perdue ou écraser les modifications de l'autre personne. + Revenez plus tard ! +
+ {% endif %} + {% endif %}
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event nobuttons=1 permalink=1 h=2 %} @@ -155,9 +171,15 @@ {% if object %}
- Annuler + {% if form.is_edit_from_moderation %} + Annuler + {% else %} + Annuler + {% endif %} + {% if not event|get_modification_lock:user %} + {% if event.editing_user == user %} +
+ Attention, vous avez ouvert en édition ou modération cet + événement dans un autre onglet (le {{ event.editing_start }}), et toute modification intermédiaire sera effacée par la dernière. + Ce message peut aussi apparaître si vous avez utilisé la navigation arrière. +
+ {% else %} +
+ Attention, {{ event.editing_user }} est en train de modifier ou de modérer cet + événement (le {{ event.editing_start }}), et toute modification de votre part pourrait être perdue ou écraser les modifications de l'autre personne. + Revenez plus tard ! +
+ {% endif %} + {% endif %}
{% csrf_token %}
- {% include "agenda_culturel/single-event/event-single-inc.html" with event=event onlyedit=1 permalink=1 h=2 %} + {% include "agenda_culturel/single-event/event-single-inc.html" with event=event editforce=1 permalink=1 h=2 %} {% with event.get_concurrent_events as concurrent_events %} {% if concurrent_events %}
@@ -95,13 +111,11 @@
{% if pred %} - < Revenir au précédent {% else %} - Annuler + Annuler {% endif %} + Vous avez ouvert cet événement en édition ou en modération dans un autre onglet (le {{ event.editing_start }}). +
+ {% else %} +
+ {{ event.editing_user }} est en train de modifier ou de modérer cet événement (le {{ event.editing_start }}). +
+ {% endif %} + {% endif %}
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %} diff --git a/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html b/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html index 7f58ae1..9441b61 100644 --- a/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html +++ b/src/agenda_culturel/templates/agenda_culturel/single-event/event-single-inc.html @@ -205,7 +205,7 @@
{% if not nobuttons %}
- {% if onlyedit %} + {% if editforce %} {% if event.pure_import %} {% with event.get_local_version as local %} {% if local %} @@ -215,7 +215,7 @@ {% endif %} {% endwith %} {% else %} - modifier {% picto_from_name "edit-3" %} + modifier {% picto_from_name "edit-3" %} {% endif %} {% else %} /", EventDetailView.as_view(), name="edit_event_pk"), path("event//edit", EventUpdateView.as_view(), name="edit_event"), + path( + "event//edit-force", EventUpdateView.as_view(), name="edit_event_force" + ), path( "event//moderate", EventModerateView.as_view(), name="moderate_event", ), + path( + "event//moderate-force", + EventModerateView.as_view(), + name="moderate_event_force", + ), path( "event//moderate/after/", EventModerateView.as_view(), name="moderate_event_step", ), + path( + "event//moderate/back/", + EventModerateView.as_view(), + name="moderate_event_backstep", + ), path( "event//moderate-next", moderate_event_next, diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py index 5e9da59..ceed713 100644 --- a/src/agenda_culturel/views.py +++ b/src/agenda_culturel/views.py @@ -478,6 +478,7 @@ class EventUpdateView( self.request.user.userprofile.is_moderation_expert ) kwargs["is_cloning"] = self.is_cloning + kwargs["is_edit_from_moderation"] = self.is_edit_force() kwargs["is_simple_cloning"] = self.is_simple_cloning return kwargs @@ -489,10 +490,15 @@ class EventUpdateView( ) return mark_safe(_("The event has been successfully modified.") + txt) + def is_edit_force(self): + return "edit-force" in self.request.path.split("/") + def get_object(self, queryset=None): event = super().get_object(queryset) if event.status == Event.STATUS.DRAFT: event.status = Event.STATUS.PUBLISHED + if self.is_edit_force(): + event.free_modification_lock(self.request.user) return event def form_valid(self, form): @@ -576,6 +582,12 @@ class EventModerateView( def is_moderate_next(self): return "after" in self.request.path.split("/") + def is_moderate_back(self): + return "back" in self.request.path.split("/") + + def is_moderate_force(self): + return "moderate-force" in self.request.path.split("/") + def is_starting_moderation(self): return "pk" not in self.kwargs @@ -627,6 +639,11 @@ class EventModerateView( event = super().get_object(queryset) if event.status == Event.STATUS.DRAFT: event.status = Event.STATUS.PUBLISHED + if self.is_moderate_back(): + pred = Event.objects.filter(pk=self.kwargs["pred"]).first() + pred.free_modification_lock(self.request.user) + if self.is_moderate_force(): + event.free_modification_lock(self.request.user, False) return event def post(self, request, *args, **kwargs): @@ -670,6 +687,8 @@ def error_next_event(request, pk): def moderate_event_next(request, pk): # current event obj = Event.objects.filter(pk=pk).first() + # free lock + obj.free_modification_lock(request.user) start_day = obj.start_day start_time = obj.start_time