Gestion des éditions concurrentes

Fix #329
This commit is contained in:
Jean-Marie Favreau 2025-03-14 22:53:59 +01:00
parent 3287085b70
commit 5dffc1f0b2
11 changed files with 185 additions and 10 deletions

View File

@ -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:

View File

@ -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),
),
]

View File

@ -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",
),
),
]

View File

@ -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()

View File

@ -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 @@
</p>
{% endif %}
</header>
{% 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 %}
<div class="message info">
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.
</div>
{% else %}
<div class="message warning">
Attention, <em>{{ object.editing_user }}</em> 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&nbsp;!
</div>
{% endif %}
{% endif %}
<div class="grid moderate-preview">
<div id="event-preview">
{% include "agenda_culturel/single-event/event-single-inc.html" with event=event nobuttons=1 permalink=1 h=2 %}
@ -155,9 +171,15 @@
{% if object %}
</div>
<div class="grid buttons">
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}"
role="button"
class="secondary">Annuler</a>
{% if form.is_edit_from_moderation %}
<a href="{% url 'moderate_event_force' event.pk %}"
class="secondary"
role="button">Annuler</a>
{% else %}
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}"
role="button"
class="secondary">Annuler</a>
{% endif %}
<input type="submit" value="Enregistrer">
<input type="submit"
value="Enregistrer et passer au suivant &gt;"

View File

@ -2,6 +2,7 @@
{% load static %}
{% load utils_extra %}
{% load cat_extra %}
{% load event_extra %}
{% block title %}
{% block og_title %}Modération de l'événement {{ object.title }}{% endblock %}
{% endblock %}
@ -41,11 +42,26 @@
.
</p>
</header>
{% if not event|get_modification_lock:user %}
{% if event.editing_user == user %}
<div class="message info">
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.
</div>
{% else %}
<div class="message warning">
Attention, <em>{{ event.editing_user }}</em> 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&nbsp;!
</div>
{% endif %}
{% endif %}
<form method="post" enctype="multipart/form-data" id="moderate-form">
{% csrf_token %}
<div class="grid moderate-preview">
<div id="event-preview">
{% 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 %}
<article>
@ -95,13 +111,11 @@
</div>
<div class="grid buttons">
{% if pred %}
<a href="{% url 'moderate_event' pred %}"
<a href="{% url 'moderate_event_backstep' pred event.pk %}"
class="secondary"
role="button">&lt; Revenir au précédent</a>
{% else %}
<a href="{% if request.META.HTTP_REFERER %}{{ request.META.HTTP_REFERER }}{% else %}{{ object.get_absolute_url }}{% endif %}"
role="button"
class="secondary">Annuler</a>
<a href="{% url 'administration' %}" role="button" class="secondary">Annuler</a>
{% endif %}
<input type="submit" value="Enregistrer" name="save">
<input type="submit"

View File

@ -26,6 +26,17 @@
{% css_categories %}
{% endblock %}
{% block content %}
{% if event.is_modification_locked %}
{% if event.editing_user == user %}
<div class="message info">
Vous avez ouvert cet événement en édition ou en modération dans un autre onglet (le {{ event.editing_start }}).
</div>
{% else %}
<div class="message info">
<em>{{ event.editing_user }}</em> est en train de modifier ou de modérer cet événement (le {{ event.editing_start }}).
</div>
{% endif %}
{% endif %}
<div class="grid two-columns">
<div>
{% with cache_timeout=user.is_authenticated|yesno:"30,600" %}

View File

@ -205,7 +205,7 @@
</div>
{% if not nobuttons %}
<div class="buttons">
{% if onlyedit %}
{% if editforce %}
{% if event.pure_import %}
{% with event.get_local_version as local %}
{% if local %}
@ -215,7 +215,7 @@
{% endif %}
{% endwith %}
{% else %}
<a href="{% url 'edit_event' event.id %}" role="button">modifier {% picto_from_name "edit-3" %}</a>
<a href="{% url 'edit_event_force' event.id %}" role="button">modifier {% picto_from_name "edit-3" %}</a>
{% endif %}
{% else %}
<a href="{% url 'export_event_ical' event.start_day.year event.start_day.month event.start_day.day event.id %}"

View File

@ -233,3 +233,8 @@ def tw_badge(event):
@register.filter
def get_image_uri(event, request):
return event.get_image_url(request)
@register.filter
def get_modification_lock(event, user):
return event.get_modification_lock(user)

View File

@ -201,16 +201,29 @@ urlpatterns = [
),
path("event/<int:pk>/", EventDetailView.as_view(), name="edit_event_pk"),
path("event/<int:pk>/edit", EventUpdateView.as_view(), name="edit_event"),
path(
"event/<int:pk>/edit-force", EventUpdateView.as_view(), name="edit_event_force"
),
path(
"event/<int:pk>/moderate",
EventModerateView.as_view(),
name="moderate_event",
),
path(
"event/<int:pk>/moderate-force",
EventModerateView.as_view(),
name="moderate_event_force",
),
path(
"event/<int:pk>/moderate/after/<int:pred>",
EventModerateView.as_view(),
name="moderate_event_step",
),
path(
"event/<int:pk>/moderate/back/<int:pred>",
EventModerateView.as_view(),
name="moderate_event_backstep",
),
path(
"event/<int:pk>/moderate-next",
moderate_event_next,

View File

@ -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