From 89af4f42c50ad6a195f3e94c96fa326ce6f48c58 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Mon, 27 Sep 2021 17:26:40 +0200
Subject: [PATCH 3/3] api: add post method on agenda endpoint (#57103)

---
 chrono/api/serializers.py |  45 +++++++++++++++-
 chrono/api/views.py       |  18 ++++++-
 tests/api/test_all.py     | 110 ++++++++++++++++++++++++++++++++++++++
 3 files changed, 171 insertions(+), 2 deletions(-)

diff --git a/chrono/api/serializers.py b/chrono/api/serializers.py
index 41c9f79..85de836 100644
--- a/chrono/api/serializers.py
+++ b/chrono/api/serializers.py
@@ -1,13 +1,14 @@
+from django.contrib.auth.models import Group
 from django.utils.translation import ugettext_lazy as _
 from rest_framework import serializers
 from rest_framework.exceptions import ValidationError
 
-from chrono.agendas.models import AbsenceReason, Booking, Event
+from chrono.agendas.models import AbsenceReason, Agenda, Booking, Event
 
 
 class StringOrListField(serializers.ListField):
     def to_internal_value(self, data):
         if isinstance(data, str):
             data = [s.strip() for s in data.split(',') if s.strip()]
         return super().to_internal_value(data)
 
@@ -152,8 +153,50 @@ class EventSerializer(serializers.ModelSerializer):
             'publication_date',
             'places',
             'waiting_list_places',
             'label',
             'description',
             'pricing',
             'url',
         ]
+
+
+class AgendaSerializer(serializers.ModelSerializer):
+    edit_role = serializers.CharField(required=False)
+    view_role = serializers.CharField(required=False)
+
+    class Meta:
+        model = Agenda
+        fields = [
+            'slug',
+            'label',
+            'kind',
+            'minimal_booking_delay',
+            'minimal_booking_delay_in_working_days',
+            'maximal_booking_delay',
+            'anonymize_delay',
+            'edit_role',
+            'view_role',
+        ]
+
+    def get_role(self, value):
+        try:
+            return Group.objects.get(name=value)
+        except Group.DoesNotExist:
+            raise serializers.ValidationError(_('unknown role: %s' % value))
+
+    def validate_edit_role(self, value):
+        return self.get_role(value)
+
+    def validate_view_role(self, value):
+        return self.get_role(value)
+
+    def validate(self, attrs):
+        super().validate(attrs)
+        if attrs['minimal_booking_delay_in_working_days'] and attrs.get('kind', 'events') != 'events':
+            raise ValidationError(
+                {
+                    'minimal_booking_delay_in_working_days': _('Option not available on %s agenda')
+                    % attrs['kind']
+                }
+            )
+        return attrs
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 3936f5c..ec0f14b 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -685,17 +685,21 @@ def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_li
         cancel_callback_url=translate_to_publik_url(payload.get('cancel_callback_url', '')),
         user_display_label=payload.get('user_display_label', ''),
         extra_data=extra_data,
         color=color,
     )
 
 
 class Agendas(APIView):
-    permission_classes = ()
+    serializer_class = serializers.AgendaSerializer
+
+    def get_permissions(self):
+        permission_classes = [] if self.request.method == 'GET' else [permissions.IsAuthenticated]
+        return [permission() for permission in permission_classes]
 
     def get(self, request, format=None):
         agendas_queryset = (
             Agenda.objects.all()
             .select_related('absence_reasons_group')
             .prefetch_related('resources', 'absence_reasons_group__absence_reasons')
             .order_by('label')
         )
@@ -723,16 +727,28 @@ class Agendas(APIView):
                 not e.full for e in agenda.get_open_events(prefetched_queryset=True)
             ):
                 # exclude agendas without open events
                 continue
             agendas.append(get_agenda_detail(request, agenda))
 
         return Response({'err': 0, 'data': agendas})
 
+    def post(self, request, format=None):
+        serializer = self.serializer_class(data=request.data)
+        if not serializer.is_valid():
+            raise APIError(
+                _('invalid payload'),
+                err_class='invalid payload',
+                errors=serializer.errors,
+                http_status=status.HTTP_400_BAD_REQUEST,
+            )
+        agenda = serializer.save()
+        return Response({'err': 0, 'data': [get_agenda_detail(request, agenda)]})
+
 
 agendas = Agendas.as_view()
 
 
 class AgendaDetail(APIView):
     """
     Retrieve an agenda instance.
     """
diff --git a/tests/api/test_all.py b/tests/api/test_all.py
index be6601d..dd24e7f 100644
--- a/tests/api/test_all.py
+++ b/tests/api/test_all.py
@@ -587,8 +587,118 @@ def test_virtual_agendas_meetingtypes_api(app):
                 'id': 'meeting1',
                 'duration': 30,
                 'api': {
                     'datetimes_url': 'http://testserver/api/agenda/virtual-agenda/meetings/meeting1/datetimes/',
                 },
             },
         ]
     }
+
+
+@pytest.mark.freeze_time('2021-07-09')
+def test_add_agenda(app, user, settings):
+    api_url = '/api/agenda/'
+
+    # no authentication
+    resp = app.post(api_url, status=401)
+    assert resp.json['detail'] == 'Authentication credentials were not provided.'
+
+    # wrong password
+    app.authorization = ('Basic', ('john.doe', 'wrong'))
+    resp = app.post(api_url, status=401)
+    assert resp.json['detail'] == 'Invalid username/password.'
+
+    app.authorization = ('Basic', ('john.doe', 'password'))
+
+    # missing fields
+    resp = app.post(api_url, status=400)
+    assert resp.json['err']
+    assert resp.json['errors'] == {'label': ['This field is required.'], 'slug': ['This field is required.']}
+
+    # wrong contents
+    params = {
+        'label': 'foo',
+        'slug': 'foo',
+        'kind': 'oups',
+        'minimal_booking_delay': 'oups',
+        'minimal_booking_delay_in_working_days': 'oups',
+        'anonymize_delay': 'oups',
+        'edit_role': 'oups',
+        'view_role': 'plop',
+    }
+    resp = app.post(api_url, params=params, status=400)
+    assert resp.json['err']
+    assert resp.json['errors'] == {
+        'kind': ['"oups" is not a valid choice.'],
+        'minimal_booking_delay': ['A valid integer is required.'],
+        'minimal_booking_delay_in_working_days': ['Must be a valid boolean.'],
+        'anonymize_delay': ['A valid integer is required.'],
+        'edit_role': ['unknown role: oups'],
+        'view_role': ['unknown role: plop'],
+    }
+
+    # slug already used
+    meeting_agenda = Agenda(label='Foo bar Meeting', kind='meetings')
+    meeting_agenda.save()
+    params = {
+        'label': 'foo',
+        'slug': meeting_agenda.slug,
+    }
+    resp = app.post(api_url, params=params, status=400)
+    assert resp.json['err']
+    assert resp.json['errors'] == {'slug': ['agenda with this Identifier already exists.']}
+
+    # option only available on events agenda
+    params = {
+        'label': 'foo',
+        'slug': 'foo',
+        'kind': 'meetings',
+        'minimal_booking_delay_in_working_days': True,
+    }
+    resp = app.post(api_url, params=params, status=400)
+    assert resp.json['err']
+    assert resp.json['errors'] == {
+        'minimal_booking_delay_in_working_days': ['Option not available on meetings agenda']
+    }
+
+    settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
+    edit_group = Group.objects.create(name='Edit')
+    view_group = Group.objects.create(name='View')
+
+    # add a meetings agenda
+    params = {
+        'label': 'foo Meetings',
+        'slug': 'foo-meetings',
+        'kind': 'meetings',
+        'minimal_booking_delay': 1,
+        'maximal_booking_delay': 3,
+        'anonymize_delay': 30,
+        'edit_role': 'Edit',
+        'view_role': 'View',
+    }
+    resp = app.post(api_url, params=params)
+    assert not resp.json['err']
+    assert len(resp.json['data']) == 1
+    agenda = Agenda.objects.get(slug='foo-meetings')
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
+    assert agenda.edit_role == edit_group
+    assert agenda.view_role == view_group
+
+    # add an events agenda
+    params = {
+        'label': 'foo Events',
+        'slug': 'foo-events',
+        'kind': 'events',
+        'minimal_booking_delay': 1,
+        'minimal_booking_delay_in_working_days': True,
+        'maximal_booking_delay': 3,
+        'anonymize_delay': 30,
+        'edit_role': 'Edit',
+        'view_role': 'View',
+    }
+    resp = app.post(api_url, params=params)
+    assert not resp.json['err']
+    assert len(resp.json['data']) == 1
+    agenda = Agenda.objects.get(slug='foo-events')
+    assert agenda.edit_role == edit_group
+    assert agenda.view_role == view_group
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12)
-- 
2.30.2

