From 1ee53fa3bedab1e50e0b248b3f24eb8b7b3567d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 20 May 2022 09:59:06 +0200
Subject: [PATCH 3/3] pricing: simplify mixins and permission checks (#65442)

---
 lingo/agendas/models.py                       |   6 -
 lingo/agendas/views.py                        |  20 +---
 .../manager_agenda_pricing_detail.html        |   4 -
 lingo/pricing/urls.py                         |  63 +++++-----
 lingo/pricing/views.py                        | 108 +-----------------
 5 files changed, 41 insertions(+), 160 deletions(-)

diff --git a/lingo/agendas/models.py b/lingo/agendas/models.py
index e4c5153..6819c7d 100644
--- a/lingo/agendas/models.py
+++ b/lingo/agendas/models.py
@@ -67,12 +67,6 @@ class Agenda(models.Model):
 
         return False, agenda
 
-    def can_be_managed(self, user):
-        return True
-
-    def can_be_viewed(self, user):
-        return True
-
 
 class CheckTypeGroup(models.Model):
     slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
diff --git a/lingo/agendas/views.py b/lingo/agendas/views.py
index a74fd16..9fad020 100644
--- a/lingo/agendas/views.py
+++ b/lingo/agendas/views.py
@@ -14,13 +14,12 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-from django.core.exceptions import PermissionDenied
 from django.shortcuts import get_object_or_404
 
 from .models import Agenda
 
 
-class ViewableAgendaMixin:
+class AgendaMixin:
     agenda = None
 
     def set_agenda(self, **kwargs):
@@ -28,30 +27,13 @@ class ViewableAgendaMixin:
 
     def dispatch(self, request, *args, **kwargs):
         self.set_agenda(**kwargs)
-        if not self.check_permissions(request.user):
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
-    def check_permissions(self, user):
-        return True  # return self.agenda.can_be_viewed(user)
-
     def get_context_data(self, **kwargs):
         context = super().get_context_data(**kwargs)
         context['agenda'] = self.agenda
         return context
 
-
-class ManagedAgendaMixin(ViewableAgendaMixin):
-    def check_permissions(self, user):
-        return True  # XXX: return self.agenda.can_be_managed(user)
-
-    def get_form_kwargs(self):
-        kwargs = super().get_form_kwargs()
-        if not kwargs.get('instance'):
-            kwargs['instance'] = self.model()
-        kwargs['instance'].agenda = self.agenda
-        return kwargs
-
     def get_success_url(self):
         # XXX:  return reverse('lingo-manager-agenda-settings', kwargs={'pk': self.agenda.id})
         return '/'
diff --git a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
index 6443f6a..6fab390 100644
--- a/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
+++ b/lingo/pricing/templates/lingo/pricing/manager_agenda_pricing_detail.html
@@ -10,7 +10,6 @@
 <h2>
     {{ object.pricing }} ({{ object.date_start|date:'d/m/Y' }} - {{ object.date_end|date:'d/m/Y' }})
 </h2>
-{% if user_can_manage %}
 <span class="actions">
   <a class="extra-actions-menu-opener"></a>
   <ul class="extra-actions-menu">
@@ -18,7 +17,6 @@
     <li><a rel="popup" href="{% url 'lingo-manager-agenda-pricing-delete' agenda.pk object.pk %}">{% trans 'Delete' %}</a></li>
   </ul>
 </span>
-{% endif %}
 {% endblock %}
 
 {% block content %}
@@ -44,11 +42,9 @@
       {% endfor %}
     </tbody>
   </table>
-  {% if user_can_manage %}
   <p>
   <a class="pk-button" href="{% if matrix.criteria %}{% url 'lingo-manager-agenda-pricing-matrix-slug-edit' agenda.pk object.pk matrix.criteria.slug %}{% else %}{% url 'lingo-manager-agenda-pricing-matrix-edit' agenda.pk object.pk %}{% endif %}">{% trans "Edit pricing" %}</a>
   </p>
-  {% endif %}
 </div>
 </div>
 {% empty %}
diff --git a/lingo/pricing/urls.py b/lingo/pricing/urls.py
index da6e985..4080f77 100644
--- a/lingo/pricing/urls.py
+++ b/lingo/pricing/urls.py
@@ -15,137 +15,142 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 from django.conf.urls import url
+from django.contrib.admin.views.decorators import staff_member_required
 
 from . import views
 
 urlpatterns = [
-    url(r'^$', views.pricing_list, name='lingo-manager-pricing-list'),
-    url(r'^import/$', views.config_import, name='lingo-manager-pricing-config-import'),
-    url(r'^export/$', views.config_export, name='lingo-manager-pricing-config-export'),
+    url(r'^$', staff_member_required(views.pricing_list), name='lingo-manager-pricing-list'),
+    url(r'^import/$', staff_member_required(views.config_import), name='lingo-manager-pricing-config-import'),
+    url(r'^export/$', staff_member_required(views.config_export), name='lingo-manager-pricing-config-export'),
     url(
         r'^add/$',
-        views.pricing_add,
+        staff_member_required(views.pricing_add),
         name='lingo-manager-pricing-add',
     ),
     url(
         r'^(?P<pk>\d+)/$',
-        views.pricing_detail,
+        staff_member_required(views.pricing_detail),
         name='lingo-manager-pricing-detail',
     ),
     url(
         r'^(?P<pk>\d+)/edit/$',
-        views.pricing_edit,
+        staff_member_required(views.pricing_edit),
         name='lingo-manager-pricing-edit',
     ),
     url(
         r'^(?P<pk>\d+)/delete/$',
-        views.pricing_delete,
+        staff_member_required(views.pricing_delete),
         name='lingo-manager-pricing-delete',
     ),
     url(
         r'^(?P<pk>\d+)/duplicate/$',
-        views.pricing_duplicate,
+        staff_member_required(views.pricing_duplicate),
         name='lingo-manager-pricing-duplicate',
     ),
     url(
         r'^(?P<pk>\d+)/export/$',
-        views.pricing_export,
+        staff_member_required(views.pricing_export),
         name='lingo-manager-pricing-export',
     ),
     url(
         r'^(?P<pk>\d+)/variable/$',
-        views.pricing_variable_edit,
+        staff_member_required(views.pricing_variable_edit),
         name='lingo-manager-pricing-variable-edit',
     ),
     url(
         r'^(?P<pk>\d+)/category/add/$',
-        views.pricing_criteria_category_add,
+        staff_member_required(views.pricing_criteria_category_add),
         name='lingo-manager-pricing-criteria-category-add',
     ),
     url(
         r'^(?P<pk>\d+)/category/(?P<category_pk>\d+)/edit/$',
-        views.pricing_criteria_category_edit,
+        staff_member_required(views.pricing_criteria_category_edit),
         name='lingo-manager-pricing-criteria-category-edit',
     ),
     url(
         r'^(?P<pk>\d+)/category/(?P<category_pk>\d+)/delete/$',
-        views.pricing_criteria_category_delete,
+        staff_member_required(views.pricing_criteria_category_delete),
         name='lingo-manager-pricing-criteria-category-delete',
     ),
     url(
         r'^(?P<pk>\d+)/order/$',
-        views.pricing_criteria_category_order,
+        staff_member_required(views.pricing_criteria_category_order),
         name='lingo-manager-pricing-criteria-category-order',
     ),
-    url(r'^criterias/$', views.criteria_list, name='lingo-manager-pricing-criteria-list'),
+    url(
+        r'^criterias/$',
+        staff_member_required(views.criteria_list),
+        name='lingo-manager-pricing-criteria-list',
+    ),
     url(
         r'^criteria/category/add/$',
-        views.criteria_category_add,
+        staff_member_required(views.criteria_category_add),
         name='lingo-manager-pricing-criteria-category-add',
     ),
     url(
         r'^criteria/category/(?P<pk>\d+)/edit/$',
-        views.criteria_category_edit,
+        staff_member_required(views.criteria_category_edit),
         name='lingo-manager-pricing-criteria-category-edit',
     ),
     url(
         r'^criteria/category/(?P<pk>\d+)/delete/$',
-        views.criteria_category_delete,
+        staff_member_required(views.criteria_category_delete),
         name='lingo-manager-pricing-criteria-category-delete',
     ),
     url(
         r'^criteria/category/(?P<pk>\d+)/export/$',
-        views.criteria_category_export,
+        staff_member_required(views.criteria_category_export),
         name='lingo-manager-pricing-criteria-category-export',
     ),
     url(
         r'^criteria/category/(?P<pk>\d+)/order/$',
-        views.criteria_order,
+        staff_member_required(views.criteria_order),
         name='lingo-manager-pricing-criteria-order',
     ),
     url(
         r'^criteria/category/(?P<category_pk>\d+)/add/$',
-        views.criteria_add,
+        staff_member_required(views.criteria_add),
         name='lingo-manager-pricing-criteria-add',
     ),
     url(
         r'^criteria/category/(?P<category_pk>\d+)/(?P<pk>\d+)/edit/$',
-        views.criteria_edit,
+        staff_member_required(views.criteria_edit),
         name='lingo-manager-pricing-criteria-edit',
     ),
     url(
         r'^criteria/category/(?P<category_pk>\d+)/(?P<pk>\d+)/delete/$',
-        views.criteria_delete,
+        staff_member_required(views.criteria_delete),
         name='lingo-manager-pricing-criteria-delete',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/add/$',
-        views.agenda_pricing_add,
+        staff_member_required(views.agenda_pricing_add),
         name='lingo-manager-agenda-pricing-add',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/$',
-        views.agenda_pricing_detail,
+        staff_member_required(views.agenda_pricing_detail),
         name='lingo-manager-agenda-pricing-detail',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/edit/$',
-        views.agenda_pricing_edit,
+        staff_member_required(views.agenda_pricing_edit),
         name='lingo-manager-agenda-pricing-edit',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/delete/$',
-        views.agenda_pricing_delete,
+        staff_member_required(views.agenda_pricing_delete),
         name='lingo-manager-agenda-pricing-delete',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/matrix/edit/$',
-        views.agenda_pricing_matrix_edit,
+        staff_member_required(views.agenda_pricing_matrix_edit),
         name='lingo-manager-agenda-pricing-matrix-edit',
     ),
     url(
         r'^agenda/(?P<pk>\d+)/pricing/(?P<pricing_pk>\d+)/matrix/(?P<slug>[-_a-zA-Z0-9]+)/edit/$',
-        views.agenda_pricing_matrix_edit,
+        staff_member_required(views.agenda_pricing_matrix_edit),
         name='lingo-manager-agenda-pricing-matrix-slug-edit',
     ),
 ]
diff --git a/lingo/pricing/views.py b/lingo/pricing/views.py
index 40c39d6..0a57c16 100644
--- a/lingo/pricing/views.py
+++ b/lingo/pricing/views.py
@@ -21,7 +21,6 @@ from operator import itemgetter
 
 from django import forms
 from django.contrib import messages
-from django.core.exceptions import PermissionDenied
 from django.db.models import Prefetch
 from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect
 from django.shortcuts import get_object_or_404
@@ -33,7 +32,7 @@ from django.views.generic import CreateView, DeleteView, DetailView, FormView, L
 from django.views.generic.detail import SingleObjectMixin
 
 from lingo.agendas.models import Agenda
-from lingo.agendas.views import ManagedAgendaMixin, ViewableAgendaMixin
+from lingo.agendas.views import AgendaMixin
 from lingo.pricing.forms import (
     AgendaPricingForm,
     CriteriaForm,
@@ -55,11 +54,6 @@ class ConfigExportView(FormView):
     form_class = ExportForm
     template_name = 'lingo/pricing/export.html'
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def form_valid(self, form):
         response = HttpResponse(content_type='application/json')
         today = datetime.date.today()
@@ -78,11 +72,6 @@ class ConfigImportView(FormView):
     template_name = 'lingo/pricing/import.html'
     success_url = reverse_lazy('lingo-manager-pricing-list')
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def form_valid(self, form):
         try:
             config_json = json.loads(force_text(self.request.FILES['config_json'].read()))
@@ -189,11 +178,6 @@ class PricingListView(ListView):
     template_name = 'lingo/pricing/manager_pricing_list.html'
     model = Pricing
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
 
 pricing_list = PricingListView.as_view()
 
@@ -202,11 +186,6 @@ class CriteriaListView(ListView):
     template_name = 'lingo/pricing/manager_criteria_list.html'
     model = CriteriaCategory
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_queryset(self):
         return CriteriaCategory.objects.prefetch_related('criterias')
 
@@ -219,11 +198,6 @@ class PricingAddView(CreateView):
     model = Pricing
     fields = ['label']
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-detail', args=[self.object.pk])
 
@@ -235,11 +209,6 @@ class PricingDetailView(DetailView):
     template_name = 'lingo/pricing/manager_pricing_detail.html'
     model = Pricing
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_queryset(self):
         return (
             super()
@@ -260,11 +229,6 @@ class PricingEditView(UpdateView):
     model = Pricing
     fields = ['label', 'slug']
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-detail', args=[self.object.pk])
 
@@ -276,11 +240,6 @@ class PricingDeleteView(DeleteView):
     template_name = 'lingo/manager_confirm_delete.html'
     model = Pricing
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-list')
 
@@ -295,8 +254,6 @@ class PricingDuplicate(SingleObjectMixin, FormView):
 
     def dispatch(self, request, *args, **kwargs):
         self.object = self.get_object()
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_success_url(self):
@@ -313,11 +270,6 @@ pricing_duplicate = PricingDuplicate.as_view()
 class PricingExport(DetailView):
     model = Pricing
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get(self, request, *args, **kwargs):
         response = HttpResponse(content_type='application/json')
         today = datetime.date.today()
@@ -339,8 +291,6 @@ class PricingVariableEdit(FormView):
 
     def dispatch(self, request, *args, **kwargs):
         self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_context_data(self, **kwargs):
@@ -378,8 +328,6 @@ class PricingCriteriaCategoryAddView(FormView):
         self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
         if self.object.categories.count() >= 3:
             raise Http404
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_form_kwargs(self):
@@ -410,8 +358,6 @@ class PricingCriteriaCategoryEditView(FormView):
     def dispatch(self, request, *args, **kwargs):
         self.object = get_object_or_404(Pricing, pk=kwargs['pk'])
         self.category = get_object_or_404(self.object.categories, pk=kwargs['category_pk'])
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_form_kwargs(self):
@@ -447,8 +393,6 @@ class PricingCriteriaCategoryDeleteView(DeleteView):
 
     def dispatch(self, request, *args, **kwargs):
         self.pricing = get_object_or_404(Pricing, pk=kwargs['pk'])
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_queryset(self):
@@ -470,11 +414,6 @@ pricing_criteria_category_delete = PricingCriteriaCategoryDeleteView.as_view()
 class PricingCriteriaCategoryOrder(DetailView):
     model = Pricing
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get(self, request, *args, **kwargs):
         if 'new-order' not in request.GET:
             return HttpResponseBadRequest('missing new-order parameter')
@@ -499,11 +438,6 @@ class CriteriaCategoryAddView(CreateView):
     model = CriteriaCategory
     fields = ['label']
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-criteria-list')
 
@@ -516,11 +450,6 @@ class CriteriaCategoryEditView(UpdateView):
     model = CriteriaCategory
     fields = ['label', 'slug']
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-criteria-list')
 
@@ -532,11 +461,6 @@ class CriteriaCategoryDeleteView(DeleteView):
     template_name = 'lingo/manager_confirm_delete.html'
     model = CriteriaCategory
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get_success_url(self):
         return reverse('lingo-manager-pricing-criteria-list')
 
@@ -547,11 +471,6 @@ criteria_category_delete = CriteriaCategoryDeleteView.as_view()
 class CriteriaCategoryExport(DetailView):
     model = CriteriaCategory
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get(self, request, *args, **kwargs):
         response = HttpResponse(content_type='application/json')
         today = datetime.date.today()
@@ -569,11 +488,6 @@ criteria_category_export = CriteriaCategoryExport.as_view()
 class CriteriaOrder(DetailView):
     model = CriteriaCategory
 
-    def dispatch(self, request, *args, **kwargs):
-        if not request.user.is_staff:
-            raise PermissionDenied()
-        return super().dispatch(request, *args, **kwargs)
-
     def get(self, request, *args, **kwargs):
         if 'new-order' not in request.GET:
             return HttpResponseBadRequest('missing new-order parameter')
@@ -602,8 +516,6 @@ class CriteriaAddView(CreateView):
 
     def dispatch(self, request, *args, **kwargs):
         self.category_pk = kwargs.pop('category_pk')
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_form_kwargs(self):
@@ -627,8 +539,6 @@ class CriteriaEditView(UpdateView):
 
     def dispatch(self, request, *args, **kwargs):
         self.category_pk = kwargs.pop('category_pk')
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_queryset(self):
@@ -647,8 +557,6 @@ class CriteriaDeleteView(DeleteView):
 
     def dispatch(self, request, *args, **kwargs):
         self.category_pk = kwargs.pop('category_pk')
-        if not request.user.is_staff:
-            raise PermissionDenied()
         return super().dispatch(request, *args, **kwargs)
 
     def get_queryset(self):
@@ -661,7 +569,7 @@ class CriteriaDeleteView(DeleteView):
 criteria_delete = CriteriaDeleteView.as_view()
 
 
-class AgendaPricingAddView(ManagedAgendaMixin, CreateView):
+class AgendaPricingAddView(AgendaMixin, CreateView):
     template_name = 'lingo/pricing/manager_agenda_pricing_form.html'
     model = AgendaPricing
     form_class = AgendaPricingForm
@@ -673,7 +581,7 @@ class AgendaPricingAddView(ManagedAgendaMixin, CreateView):
 agenda_pricing_add = AgendaPricingAddView.as_view()
 
 
-class AgendaPricingDetailView(ViewableAgendaMixin, DetailView):
+class AgendaPricingDetailView(AgendaMixin, DetailView):
     model = AgendaPricing
     pk_url_kwarg = 'pricing_pk'
     template_name = 'lingo/pricing/manager_agenda_pricing_detail.html'
@@ -686,15 +594,11 @@ class AgendaPricingDetailView(ViewableAgendaMixin, DetailView):
             'pricing__criterias__category'
         )
 
-    def get_context_data(self, **kwargs):
-        kwargs['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
-        return super().get_context_data(**kwargs)
-
 
 agenda_pricing_detail = AgendaPricingDetailView.as_view()
 
 
-class AgendaPricingEditView(ManagedAgendaMixin, UpdateView):
+class AgendaPricingEditView(AgendaMixin, UpdateView):
     template_name = 'lingo/pricing/manager_agenda_pricing_form.html'
     model = AgendaPricing
     pk_url_kwarg = 'pricing_pk'
@@ -713,7 +617,7 @@ class AgendaPricingEditView(ManagedAgendaMixin, UpdateView):
 agenda_pricing_edit = AgendaPricingEditView.as_view()
 
 
-class AgendaPricingDeleteView(ManagedAgendaMixin, DeleteView):
+class AgendaPricingDeleteView(AgendaMixin, DeleteView):
     template_name = 'lingo/manager_confirm_delete.html'
     model = AgendaPricing
     pk_url_kwarg = 'pricing_pk'
@@ -728,7 +632,7 @@ class AgendaPricingDeleteView(ManagedAgendaMixin, DeleteView):
 agenda_pricing_delete = AgendaPricingDeleteView.as_view()
 
 
-class AgendaPricingMatrixEdit(ManagedAgendaMixin, FormView):
+class AgendaPricingMatrixEdit(AgendaMixin, FormView):
     template_name = 'lingo/pricing/manager_agenda_pricing_matrix_form.html'
 
     def set_agenda(self, **kwargs):
-- 
2.35.1

