From 5dffc1f0b2856ca5aa3526a736efd0b63c7ef036 Mon Sep 17 00:00:00 2001
From: Jean-Marie Favreau
Date: Fri, 14 Mar 2025 22:53:59 +0100
Subject: [PATCH] =?UTF-8?q?Gestion=20des=20=C3=A9ditions=20concurrentes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Fix #329
---
src/agenda_culturel/forms.py | 1 +
.../migrations/0158_event_editing_start.py | 18 ++++++++
.../migrations/0159_event_editing_user.py | 29 +++++++++++++
src/agenda_culturel/models.py | 43 +++++++++++++++++++
.../templates/agenda_culturel/event_form.html | 28 ++++++++++--
.../agenda_culturel/event_form_moderate.html | 24 ++++++++---
.../templates/agenda_culturel/page-event.html | 11 +++++
.../single-event/event-single-inc.html | 4 +-
.../templatetags/event_extra.py | 5 +++
src/agenda_culturel/urls.py | 13 ++++++
src/agenda_culturel/views.py | 19 ++++++++
11 files changed, 185 insertions(+), 10 deletions(-)
create mode 100644 src/agenda_culturel/migrations/0158_event_editing_start.py
create mode 100644 src/agenda_culturel/migrations/0159_event_editing_user.py
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 %}