diff --git a/src/agenda_culturel/forms.py b/src/agenda_culturel/forms.py
index 59d87c2..93bed5c 100644
--- a/src/agenda_culturel/forms.py
+++ b/src/agenda_culturel/forms.py
@@ -2,7 +2,7 @@ from django.forms import ModelForm, ValidationError, TextInput, Form, URLField,
from datetime import date
from django_better_admin_arrayfield.forms.widgets import DynamicArrayWidget
-from .models import Event, BatchImportation, RecurrentImport, CategorisationRule
+from .models import Event, BatchImportation, RecurrentImport, CategorisationRule, ModerationAnswer, ModerationQuestion
from django.utils.translation import gettext_lazy as _
from string import ascii_uppercase as auc
from .templatetags.utils_extra import int_to_abc
@@ -235,3 +235,18 @@ class MergeDuplicates(Form):
return [auc.rfind(v[-1]) for v in value]
else:
return auc.rfind(value[-1])
+
+
+class ModerationQuestionForm(ModelForm):
+ class Meta:
+ model = ModerationQuestion
+ fields = '__all__'
+
+class ModerationAnswerForm(ModelForm):
+ class Meta:
+ model = ModerationAnswer
+ exclude = ['question']
+ widgets = {
+ 'adds_tags': DynamicArrayWidgetTags(),
+ 'removes_tags': DynamicArrayWidgetTags()
+ }
diff --git a/src/agenda_culturel/migrations/0039_moderationquestion_moderationanswer.py b/src/agenda_culturel/migrations/0039_moderationquestion_moderationanswer.py
new file mode 100644
index 0000000..d9ecdd2
--- /dev/null
+++ b/src/agenda_culturel/migrations/0039_moderationquestion_moderationanswer.py
@@ -0,0 +1,34 @@
+# Generated by Django 4.2.7 on 2024-04-01 14:28
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django_better_admin_arrayfield.models.fields
+
+
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0038_auto_20240331_1815'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ModerationQuestion',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('question', models.CharField(help_text='Text that will be shown to moderators', max_length=512, unique=True, verbose_name='Question')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='ModerationAnswer',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('answer', models.CharField(help_text='Text that will be shown to moderators', max_length=512, unique=True, verbose_name='Answer')),
+ ('adds_tags', django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.CharField(max_length=64), blank=True, help_text='A list of tags that will be added if you choose this answer.', null=True, size=None, verbose_name='Adds tags')),
+ ('removes_tags', django_better_admin_arrayfield.models.fields.ArrayField(base_field=models.CharField(max_length=64), blank=True, help_text='A list of tags that will be removed if you choose this answer.', null=True, size=None, verbose_name='Removes tags')),
+ ('description', models.ForeignKey(help_text='Associated question from moderation', on_delete=django.db.models.deletion.CASCADE, to='agenda_culturel.moderationquestion', verbose_name='Question')),
+ ],
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0040_auto_20240403_1924.py b/src/agenda_culturel/migrations/0040_auto_20240403_1924.py
new file mode 100644
index 0000000..18aaeb4
--- /dev/null
+++ b/src/agenda_culturel/migrations/0040_auto_20240403_1924.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.7 on 2024-04-03 17:24
+
+from django.db import migrations
+from django.contrib.auth.management import create_permissions
+from django.contrib.auth.models import Group, Permission
+
+
+def update_groups_permissions(apps, schema_editor):
+
+ all_perms = Permission.objects.all()
+ moderator_perms = [i for i in all_perms if i.content_type.app_label == 'agenda_culturel' and i.content_type.model in ['event', 'duplicatedevents']]
+ read_mod_perms = [i for i in moderator_perms if i.codename.startswith('view_')]
+
+ # set permissions for receptionists
+ qanda_perms = [i for i in all_perms if i.content_type.app_label == 'agenda_culturel' and i.content_type.model in ['moderationquestion', 'moderationanswer']]
+ Group.objects.get(name="Q&A Manager").permissions.add(*qanda_perms)
+ Group.objects.get(name="Q&A Manager").permissions.add(*read_mod_perms)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0039_moderationquestion_moderationanswer'),
+ ]
+
+ operations = [
+ migrations.RunPython(update_groups_permissions),
+ ]
diff --git a/src/agenda_culturel/migrations/0041_rename_description_moderationanswer_question.py b/src/agenda_culturel/migrations/0041_rename_description_moderationanswer_question.py
new file mode 100644
index 0000000..9e1e384
--- /dev/null
+++ b/src/agenda_culturel/migrations/0041_rename_description_moderationanswer_question.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2024-04-03 20:01
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0040_auto_20240403_1924'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='moderationanswer',
+ old_name='description',
+ new_name='question',
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0042_alter_moderationanswer_answer.py b/src/agenda_culturel/migrations/0042_alter_moderationanswer_answer.py
new file mode 100644
index 0000000..e0aee61
--- /dev/null
+++ b/src/agenda_culturel/migrations/0042_alter_moderationanswer_answer.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.7 on 2024-04-03 21:13
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0041_rename_description_moderationanswer_question'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='moderationanswer',
+ name='answer',
+ field=models.CharField(help_text='Text that will be shown to moderators', max_length=512, verbose_name='Answer'),
+ ),
+ ]
diff --git a/src/agenda_culturel/migrations/0043_alter_moderationanswer_question.py b/src/agenda_culturel/migrations/0043_alter_moderationanswer_question.py
new file mode 100644
index 0000000..7cfb659
--- /dev/null
+++ b/src/agenda_culturel/migrations/0043_alter_moderationanswer_question.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.7 on 2024-04-03 21:14
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('agenda_culturel', '0042_alter_moderationanswer_answer'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='moderationanswer',
+ name='question',
+ field=models.ForeignKey(help_text='Associated question from moderation', on_delete=django.db.models.deletion.CASCADE, related_name='answers', to='agenda_culturel.moderationquestion', verbose_name='Question'),
+ ),
+ ]
diff --git a/src/agenda_culturel/models.py b/src/agenda_culturel/models.py
index 33632ed..ef0b5f5 100644
--- a/src/agenda_culturel/models.py
+++ b/src/agenda_culturel/models.py
@@ -851,3 +851,27 @@ class CategorisationRule(models.Model):
return True
return False
+
+
+class ModerationQuestion(models.Model):
+
+ question = models.CharField(verbose_name=_('Question'), help_text=_('Text that will be shown to moderators'), max_length=512, unique=True)
+
+
+ def __str__(self):
+ char_limit = 30
+ return (self.question[:char_limit] + "...") if char_limit < len(self.question) else self.question
+
+ def get_absolute_url(self):
+ return reverse("view_mquestion", kwargs={"pk": self.pk})
+
+
+class ModerationAnswer(models.Model):
+
+ question = models.ForeignKey(ModerationQuestion, related_name="answers", verbose_name=_('Question'), help_text=_('Associated question from moderation'), on_delete=models.CASCADE)
+
+ answer = models.CharField(verbose_name=_('Answer'), help_text=_('Text that will be shown to moderators'), max_length=512)
+
+ adds_tags = ArrayField(models.CharField(max_length=64), verbose_name=_('Adds tags'), help_text=_("A list of tags that will be added if you choose this answer."), blank=True, null=True)
+ removes_tags = ArrayField(models.CharField(max_length=64), verbose_name=_('Removes tags'), help_text=_("A list of tags that will be removed if you choose this answer."), blank=True, null=True)
+
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationanswer_confirm_delete.html b/src/agenda_culturel/templates/agenda_culturel/moderationanswer_confirm_delete.html
new file mode 100644
index 0000000..09c4e9e
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationanswer_confirm_delete.html
@@ -0,0 +1,19 @@
+{% extends "agenda_culturel/page.html" %}
+
+{% block title %}Supprimer la réponse #{{ object.pk }}{% endblock %}
+
+
+{% block content %}
+
+
Suppression de la réponse de modération {{ object.pk }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationanswer_form.html b/src/agenda_culturel/templates/agenda_culturel/moderationanswer_form.html
new file mode 100644
index 0000000..516fa24
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationanswer_form.html
@@ -0,0 +1,31 @@
+{% extends "agenda_culturel/page.html" %}
+{% load static %}
+
+{% block title %}{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une réponse de modération{% endblock %}
+
+{% block entete_header %}
+
+
+
+
+
+
+{% endblock %}
+
+
+{% block content %}
+
+{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une réponse de modération
+{% if form.instance.pk %}Modifier{% else %}Ajouter{% endif %} une réponse à la question « {{ question }} »
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationquestion_confirm_delete.html b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_confirm_delete.html
new file mode 100644
index 0000000..907b9be
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_confirm_delete.html
@@ -0,0 +1,19 @@
+{% extends "agenda_culturel/page.html" %}
+
+{% block title %}Supprimer la question #{{ object.pk }}{% endblock %}
+
+
+{% block content %}
+
+Suppression de la question de modération {{ object.pk }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationquestion_detail.html b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_detail.html
new file mode 100644
index 0000000..cf8eee5
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_detail.html
@@ -0,0 +1,67 @@
+{% extends "agenda_culturel/page.html" %}
+
+{% block title %}Question de modération #{{ object.pk }}{% endblock %}
+
+{% load tag_extra %}
+{% load utils_extra %}
+{% load cat_extra %}
+
+{% block entete_header %}
+ {% css_categories %}
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ < Retour
+
+ Question de modération #{{ object.pk }}
+ {{ object.question }}
+
+
+ {% if object.answers %}
+ {% for answer in object.answers.all %}
+
+
+
+ Réponse #{{ answer.pk }} : « {{ answer.answer }} »
+ {% if answer.adds_tags %}
+ Cette réponse ajoute les étiquettes suivantes à l'événement :
+ {% for tag in answer.adds_tags %}
+ {{ tag | tag_button }}
+ {% endfor %}
+
+ {% else %}
+ Cette réponse n'ajoute pas d'étiquette à l'événement.
+ {% endif %}
+ {% if answer.removes_tags %}
+ Cette réponse supprimer les étiquettes suivantes à l'événement :
+ {% for tag in answer.removes_tags %}
+ {{ tag | tag_button }}
+ {% endfor %}
+
+ {% else %}
+ Cette réponse ne supprime pas d'étiquette à l'événement.
+ {% endif %}
+
+
+ {% endfor %}
+ {% else %}
+ Il n'y a pas encore de réponse associée à cette question.
+ {% endif %}
+
+
+{% include "agenda_culturel/side-nav.html" with current="moderationquestions" %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationquestion_form.html b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_form.html
new file mode 100644
index 0000000..b10a0a9
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_form.html
@@ -0,0 +1,22 @@
+{% extends "agenda_culturel/page.html" %}
+{% load static %}
+
+{% block title %}
+{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une question de modération
+{% endblock %}
+
+
+{% block content %}
+
+{% if form.instance.pk %}Modification{% else %}Création{% endif %} d'une question de modération
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/moderationquestion_list.html b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_list.html
new file mode 100644
index 0000000..608a9a5
--- /dev/null
+++ b/src/agenda_culturel/templates/agenda_culturel/moderationquestion_list.html
@@ -0,0 +1,65 @@
+{% extends "agenda_culturel/page.html" %}
+
+{% block title %}Questions de modération{% endblock %}
+
+{% load utils_extra %}
+{% load cat_extra %}
+{% block entete_header %}
+ {% css_categories %}
+{% endblock %}
+
+{% block content %}
+
+
+
+ {% if object_list %}
+ {% for question in object_list %}
+
+
+
+
+ {% endfor %}
+ {% else %}
+ Il n'y a aucune question définie.
+ {% endif %}
+
+
+ {% if page_obj.has_previous %}
+ « premier
+ précédent
+ {% endif %}
+
+
+ Page {{ page_obj.number }} sur {{ page_obj.paginator.num_pages }}
+
+
+ {% if page_obj.has_next %}
+ suivant
+ dernier »
+ {% endif %}
+
+
+
+
+
+{% include "agenda_culturel/side-nav.html" with current="moderationquestions" %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/agenda_culturel/templates/agenda_culturel/side-nav.html b/src/agenda_culturel/templates/agenda_culturel/side-nav.html
index 741b53c..ea93345 100644
--- a/src/agenda_culturel/templates/agenda_culturel/side-nav.html
+++ b/src/agenda_culturel/templates/agenda_culturel/side-nav.html
@@ -43,6 +43,14 @@
{% endif %}
+ {% if perms.agenda_culturel.view_moderationquestion %}
+ Paramétrage de la modération
+
+
+
+ {% endif %}
{% if user.is_staff %}
Configuration interne
diff --git a/src/agenda_culturel/urls.py b/src/agenda_culturel/urls.py
index 9c6a239..785a86e 100644
--- a/src/agenda_culturel/urls.py
+++ b/src/agenda_culturel/urls.py
@@ -55,6 +55,14 @@ urlpatterns = [
path("duplicates/", DuplicatedEventsDetailView.as_view(), name="view_duplicate"),
path("duplicates//fix", fix_duplicate, name="fix_duplicate"),
path("duplicates//merge", merge_duplicate, name="merge_duplicate"),
+ path("mquestions/", ModerationQuestionListView.as_view(), name="view_mquestions"),
+ path("mquestions/add", ModerationQuestionCreateView.as_view(), name="add_mquestion"),
+ path("mquestions//", ModerationQuestionDetailView.as_view(), name="view_mquestion"),
+ path("mquestions//edit", ModerationQuestionUpdateView.as_view(), name="edit_mquestion"),
+ path("mquestions//delete", ModerationQuestionDeleteView.as_view(), name="delete_mquestion"),
+ path("mquestions//answers/add", ModerationAnswerCreateView.as_view(), name="add_manswer"),
+ path("mquestions//answers//edit", ModerationAnswerUpdateView.as_view(), name="edit_manswer"),
+ path("mquestions//answers//delete", ModerationAnswerDeleteView.as_view(), name="delete_manswer"),
]
if settings.DEBUG:
diff --git a/src/agenda_culturel/views.py b/src/agenda_culturel/views.py
index ad62aae..6d312a3 100644
--- a/src/agenda_culturel/views.py
+++ b/src/agenda_culturel/views.py
@@ -11,9 +11,9 @@ from django.http import HttpResponseRedirect
from django.urls import reverse
import urllib
-from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm
+from .forms import EventSubmissionForm, EventForm, BatchImportationForm, FixDuplicates, SelectEventInList, MergeDuplicates, RecurrentImportForm, CategorisationRuleImportForm, ModerationQuestionForm, ModerationAnswerForm
-from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport, CategorisationRule, remove_accents
+from .models import Event, Category, StaticContent, ContactMessage, BatchImportation, DuplicatedEvents, RecurrentImport, CategorisationRule, remove_accents, ModerationQuestion, ModerationAnswer
from django.utils import timezone
from enum import StrEnum
from datetime import date, timedelta
@@ -880,4 +880,75 @@ def apply_categorisation_rules(request):
return HttpResponseRedirect(reverse_lazy("categorisation_rules"))
-
\ No newline at end of file
+
+
+#########################
+## Moderation Q&A
+#########################
+
+
+class ModerationQuestionListView(PermissionRequiredMixin, ListView):
+ model = ModerationQuestion
+ paginate_by = 10
+ permission_required = ("agenda_culturel.view_moderationquestion")
+
+class ModerationQuestionCreateView(SuccessMessageMixin, PermissionRequiredMixin, CreateView):
+ model = ModerationQuestion
+ permission_required = ("agenda_culturel.add_moderationquestion")
+
+ def get_success_url(self):
+ return reverse_lazy('view_mquestion', kwargs={'pk': self.object.pk})
+ form_class = ModerationQuestionForm
+ success_message = _('The moderation question has been created with success.')
+
+
+class ModerationQuestionDetailView(PermissionRequiredMixin, DetailView):
+ model = ModerationQuestion
+ permission_required = ("agenda_culturel.view_moderationquestion", "agenda_culturel.view_moderationanswer")
+
+
+class ModerationQuestionUpdateView(PermissionRequiredMixin, UpdateView):
+ model = ModerationQuestion
+ fields = ['question']
+ permission_required = ("agenda_culturel.change_moderationquestion")
+
+class ModerationQuestionDeleteView(PermissionRequiredMixin, DeleteView):
+ model = ModerationQuestion
+ permission_required = ("agenda_culturel.delete_moderationquestion")
+ success_url = reverse_lazy('view_mquestions')
+
+
+class ModerationAnswerCreateView(PermissionRequiredMixin, SuccessMessageMixin, CreateView):
+ model = ModerationAnswer
+ permission_required = ("agenda_culturel.add_answerquestion")
+ form_class = ModerationAnswerForm
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['question'] = get_object_or_404(ModerationQuestion, pk=self.kwargs['qpk'])
+ return context
+
+ def form_valid(self, form):
+ form.instance.question = ModerationQuestion.objects.get(pk=self.kwargs['qpk'])
+ return super().form_valid(form)
+
+ def get_success_url(self):
+ return reverse_lazy('view_mquestion', kwargs={'pk': self.kwargs['qpk']})
+
+
+class ModerationAnswerUpdateView(PermissionRequiredMixin, UpdateView):
+ model = ModerationAnswer
+ fields = ['answer', 'adds_tags', 'removes_tags']
+ permission_required = ("agenda_culturel.change_answerquestion")
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context['question'] = get_object_or_404(ModerationQuestion, pk=self.kwargs['qpk'])
+ return context
+
+ def get_success_url(self):
+ return reverse_lazy('view_mquestion', kwargs={'pk': self.kwargs['qpk']})
+
+class ModerationAnswerDeleteView(PermissionRequiredMixin, DeleteView):
+ model = ModerationAnswer
+ permission_required = ("agenda_culturel.delete_answerquestion")