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.")