diff --git a/src/agenda_culturel/admin.py b/src/agenda_culturel/admin.py
index 9b14d33..9aa7044 100644
--- a/src/agenda_culturel/admin.py
+++ b/src/agenda_culturel/admin.py
@@ -16,6 +16,7 @@ from .models import (
ReferenceLocation,
StaticContent,
Tag,
+ UserProfile,
)
admin.site.register(Category)
@@ -28,6 +29,7 @@ admin.site.register(Place)
admin.site.register(Message)
admin.site.register(ReferenceLocation)
admin.site.register(Organisation)
+admin.site.register(UserProfile)
class URLWidget(DynamicArrayWidget):
diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py
index 9cea6e9..19e7915 100644
--- a/src/agenda_culturel/forms.py
+++ b/src/agenda_culturel/forms.py
@@ -261,9 +261,24 @@ class CategorisationRuleImportForm(ModelForm):
fields = "__all__"
+class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
+ def validate(self, value):
+ pass
+
+
class EventForm(GroupFormMixin, ModelForm):
required_css_class = "required"
+ new_tags = MultipleChoiceFieldAcceptAll(
+ label=_("New tags"),
+ help_text=_(
+ "Create new labels (sparingly). Note: by starting your tag with the characters “TW:”, you"
+ "ll create a “trigger warning” tag, and the associated events will be announced as such."
+ ),
+ widget=DynamicArrayWidget(),
+ required=False,
+ )
+
old_local_image = CharField(widget=HiddenInput(), required=False)
simple_cloning = CharField(widget=HiddenInput(), required=False)
cloning = CharField(widget=HiddenInput(), required=False)
@@ -311,10 +326,13 @@ 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_moderation_expert = kwargs.pop("is_moderation_expert", False)
super().__init__(*args, **kwargs)
if not is_authenticated:
del self.fields["status"]
del self.fields["organisers"]
+ if not self.is_moderation_expert:
+ del self.fields["new_tags"]
self.fields["category"].queryset = self.fields["category"].queryset.order_by(
"name"
)
@@ -370,10 +388,14 @@ class EventForm(GroupFormMixin, ModelForm):
self.fields["category"].group_id = "meta-admin"
self.fields["tags"].group_id = "meta-admin"
self.fields["status"].group_id = "meta-admin"
+ if self.is_moderation_expert:
+ self.fields["new_tags"].group_id = "meta-admin"
else:
self.add_group("meta", _("Meta information"))
self.fields["category"].group_id = "meta"
self.fields["tags"].group_id = "meta"
+ if self.is_moderation_expert:
+ self.fields["new_tags"].group_id = "meta"
def is_clone_from_url(self):
return self.cloning
@@ -407,9 +429,15 @@ class EventForm(GroupFormMixin, ModelForm):
return end_time
+ def clean_new_tags(self):
+ return list(set(self.cleaned_data.get("new_tags")))
+
def clean(self):
super().clean()
+ if self.is_moderation_expert and self.cleaned_data.get("new_tags") is not None:
+ self.cleaned_data["tags"] += self.cleaned_data.get("new_tags")
+
# when cloning an existing event, we need to copy the local image
if (
(
@@ -431,11 +459,6 @@ class EventFormWithContact(SimpleContactForm, EventForm):
pass
-class MultipleChoiceFieldAcceptAll(MultipleChoiceField):
- def validate(self, value):
- pass
-
-
class EventModerateForm(ModelForm):
required_css_class = "required"
@@ -468,10 +491,13 @@ class EventModerateForm(ModelForm):
widgets = {"status": RadioSelect}
def __init__(self, *args, **kwargs):
+ self.is_moderation_expert = kwargs.pop("is_moderation_expert", False)
super().__init__(*args, **kwargs)
self.fields["category"].queryset = self.fields["category"].queryset.order_by(
"name"
)
+ if not self.is_moderation_expert:
+ del self.fields["new_tags"]
self.fields["category"].empty_label = None
self.fields["category"].initial = Category.get_default_category()
self.fields["tags"].choices = Tag.get_tag_groups(all=True)
@@ -485,7 +511,7 @@ class EventModerateForm(ModelForm):
if self.cleaned_data["tags"] is None:
self.cleaned_data["tags"] = []
- if self.cleaned_data.get("new_tags") is not None:
+ if self.is_moderation_expert and self.cleaned_data.get("new_tags") is not None:
self.cleaned_data["tags"] += self.cleaned_data.get("new_tags")
self.cleaned_data["tags"] = list(set(self.cleaned_data["tags"]))
diff --git a/src/agenda_culturel/migrations/0155_userprofile.py b/src/agenda_culturel/migrations/0155_userprofile.py
new file mode 100644
index 0000000..24e53c1
--- /dev/null
+++ b/src/agenda_culturel/migrations/0155_userprofile.py
@@ -0,0 +1,38 @@
+# Generated by Django 4.2.19 on 2025-03-14 16:35
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("auth", "0012_alter_user_first_name_max_length"),
+ ("agenda_culturel", "0154_tag_message"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="UserProfile",
+ fields=[
+ (
+ "user",
+ models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE,
+ primary_key=True,
+ serialize=False,
+ to=settings.AUTH_USER_MODEL,
+ ),
+ ),
+ (
+ "is_moderation_expert",
+ models.BooleanField(
+ default=False,
+ help_text="This user is an expert in moderation, and the interface features additional functionalities.",
+ verbose_name="Expert moderation user",
+ ),
+ ),
+ ],
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0156_alter_userprofile_options.py b/src/agenda_culturel/migrations/0156_alter_userprofile_options.py
new file mode 100644
index 0000000..7dd3387
--- /dev/null
+++ b/src/agenda_culturel/migrations/0156_alter_userprofile_options.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.19 on 2025-03-14 16:42
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("agenda_culturel", "0155_userprofile"),
+ ]
+
+ operations = [
+ migrations.AlterModelOptions(
+ name="userprofile",
+ options={
+ "verbose_name": "User profile",
+ "verbose_name_plural": "User profiles",
+ },
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0157_auto_20250314_1645.py b/src/agenda_culturel/migrations/0157_auto_20250314_1645.py
new file mode 100644
index 0000000..aa0c631
--- /dev/null
+++ b/src/agenda_culturel/migrations/0157_auto_20250314_1645.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.19 on 2025-03-14 16:45
+
+from django.db import migrations
+
+
+def create_profiles(apps, schema_editor):
+ User = apps.get_model("auth", "User")
+ UserProfile = apps.get_model("agenda_culturel", "UserProfile")
+
+ for instance in User.objects.all():
+ if not hasattr(instance, "userprofile"):
+ UserProfile.objects.create(user=instance)
+ instance.userprofile.is_moderation_expert = True
+
+ instance.userprofile.save()
+
+
+def create_profiles_backward(apps, schema_editor):
+ pass
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("agenda_culturel", "0156_alter_userprofile_options"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ code=create_profiles,
+ reverse_code=create_profiles_backward,
+ ),
+ ]
diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py
index ca6f6f0..bef644e 100644
--- a/src/agenda_culturel/models.py
+++ b/src/agenda_culturel/models.py
@@ -40,6 +40,8 @@ from django_resized import ResizedImageField
from icalendar import Calendar as icalCal
from icalendar import Event as icalEvent
from location_field.models.spatial import LocationField
+from django.dispatch import receiver
+from django.db.models.signals import post_save
from .calendar import CalendarDay
from .import_tasks.extractor import Extractor
@@ -50,6 +52,37 @@ from .import_tasks.generic_extractors.fbevent import (
logger = logging.getLogger(__name__)
+class UserProfile(models.Model):
+ user = models.OneToOneField(
+ User,
+ on_delete=models.CASCADE,
+ primary_key=True,
+ )
+
+ is_moderation_expert = models.BooleanField(
+ verbose_name=_("Expert moderation user"),
+ help_text=_(
+ "This user is an expert in moderation, and the interface features additional functionalities."
+ ),
+ default=False,
+ )
+
+ class Meta:
+ verbose_name = _("User profile")
+ verbose_name_plural = _("User profiles")
+
+ def __str__(self):
+ return _("User profile") + " (" + self.user.username + ")"
+
+
+@receiver(post_save, sender=User)
+def update_profile_signal(sender, instance, created, **kwargs):
+ if not hasattr(instance, "userprofile"):
+ UserProfile.objects.create(user=instance)
+
+ instance.userprofile.save()
+
+
def remove_accents(input_str):
if input_str is None:
return None
diff --git a/src/agenda_culturel/templates/agenda_culturel/edit-buttons-inc.html b/src/agenda_culturel/templates/agenda_culturel/edit-buttons-inc.html
index 353f3a4..564100e 100644
--- a/src/agenda_culturel/templates/agenda_culturel/edit-buttons-inc.html
+++ b/src/agenda_culturel/templates/agenda_culturel/edit-buttons-inc.html
@@ -9,7 +9,9 @@
{% if local %}
Voir la version locale {% picto_from_name "eye" %}
{% else %}
- modifier & modérer {% picto_from_name "edit-3" %}
+ {% if user.userprofile.is_moderation_expert %}
+ modifier & modérer {% picto_from_name "edit-3" %}
+ {% endif %}
{% endif %}
{% endwith %}
{% else %}
diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py
index 2da7fca..55c1ee3 100644
--- a/src/agenda_culturel/views.py
+++ b/src/agenda_culturel/views.py
@@ -472,6 +472,9 @@ class EventUpdateView(
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["is_authenticated"] = self.request.user.is_authenticated
+ kwargs["is_moderation_expert"] = (
+ self.request.user.userprofile.is_moderation_expert
+ )
kwargs["is_cloning"] = self.is_cloning
kwargs["is_simple_cloning"] = self.is_simple_cloning
return kwargs
@@ -548,6 +551,13 @@ class EventModerateView(
template_name = "agenda_culturel/event_form_moderate.html"
form_class = EventModerateForm
+ def get_form_kwargs(self):
+ kwargs = super().get_form_kwargs()
+ kwargs["is_moderation_expert"] = (
+ self.request.user.userprofile.is_moderation_expert
+ )
+ return kwargs
+
def get_success_message(self, cleaned_data):
txt = (
_(" A message has been sent to the person who proposed the event.")