parent
3287085b70
commit
5dffc1f0b2
@ -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:
|
||||
|
18
src/agenda_culturel/migrations/0158_event_editing_start.py
Normal file
18
src/agenda_culturel/migrations/0158_event_editing_start.py
Normal 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),
|
||||
),
|
||||
]
|
29
src/agenda_culturel/migrations/0159_event_editing_user.py
Normal file
29
src/agenda_culturel/migrations/0159_event_editing_user.py
Normal 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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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()
|
||||
|
@ -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 !
|
||||
</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 >"
|
||||
|
@ -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 !
|
||||
</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">< 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"
|
||||
|
@ -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" %}
|
||||
|
@ -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 %}"
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user