From f4320f218c33933ea06af742d7b4fe05a2f4d6a4 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Tue, 8 Dec 2020 16:27:57 +0100
Subject: [PATCH] api: add a Publik api to add events (#47337)

---
 chrono/api/urls.py  |   5 ++
 chrono/api/views.py |  79 ++++++++++++++++++++++++++++++
 tests/test_api.py   | 114 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 198 insertions(+)

diff --git a/chrono/api/urls.py b/chrono/api/urls.py
index e2f2df6..b995031 100644
--- a/chrono/api/urls.py
+++ b/chrono/api/urls.py
@@ -33,16 +33,21 @@ urlpatterns = [
         views.slot_status,
         name='api-event-status',
     ),
     url(
         r'^agenda/(?P<agenda_identifier>[\w-]+)/bookings/(?P<event_identifier>[\w-]+)/$',
         views.slot_bookings,
         name='api-event-bookings',
     ),
+    url(
+        r'^agenda/(?P<agenda_identifier>[\w-]+)/add-event/$',
+        views.agenda_add_event,
+        name='api-agenda-add-event',
+    ),
     url(
         r'^agenda/meetings/(?P<meeting_identifier>[\w-]+)/datetimes/$',
         views.meeting_datetimes,
         name='api-agenda-meeting-datetimes-legacy',
     ),
     url(r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/$', views.meeting_list, name='api-agenda-meetings'),
     url(
         r'^agenda/(?P<agenda_identifier>[\w-]+)/meetings/(?P<meeting_identifier>[\w-]+)/$',
diff --git a/chrono/api/views.py b/chrono/api/views.py
index d134495..c7058f2 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1483,8 +1483,87 @@ class BookingICS(APIView):
 
     def get(self, request, booking_pk=None, format=None):
         booking = get_object_or_404(Booking, id=booking_pk)
         response = HttpResponse(booking.get_ics(request), content_type='text/calendar')
         return response
 
 
 booking_ics = BookingICS.as_view()
+
+
+class ExceptionHandlerMixin(object):
+    def handle_exception(self, exc):
+        if not isinstance(exc, APIError):
+            exc = APIError(str(exc), err_class=str(type(exc)))
+        return exc.to_response()
+
+
+class ViewableAgendaMixin(ExceptionHandlerMixin):
+    agenda = None
+
+    def set_agenda(self, **kwargs):
+        agenda_identifier = kwargs.get('agenda_identifier')
+        try:
+            self.agenda = Agenda.objects.get(slug=agenda_identifier)
+        except Agenda.DoesNotExist:
+            raise APIError(
+                _('The agenda "%s" does not exist.') % agenda_identifier,
+                err_class='invalid agenda'
+            )
+
+    def initial(self, request, *args, **kwargs):
+        super().initial(request, *args, **kwargs)
+        self.set_agenda(**kwargs)
+        if not self.check_agenda_permissions(request.user):
+            raise APIError(
+                _('User "%s" cannot access agenda "%s".') % (
+                    str(request.user), self.agenda.slug),
+                err_class='invalid agenda'
+            )
+
+    def check_agenda_permissions(self, user):
+        return self.agenda.can_be_viewed(user)
+
+    def success(self, event):
+        response = {'err': 0, 'event_id': event.pk}
+        return Response(response)
+
+
+class ManagedAgendaMixin(ViewableAgendaMixin):
+    def check_agenda_permissions(self, user):
+        return self.agenda.can_be_managed(user)
+
+
+class EventSerializer(serializers.ModelSerializer):
+    class Meta:
+        model = Event
+        fields = [
+            'start_datetime',
+            'duration',
+            'publication_date',
+            'places',
+            'waiting_list_places',
+            'label',
+            'description',
+            'pricing',
+            'url',
+        ]
+
+
+class AgendaAddEventView(ManagedAgendaMixin, APIView):
+    permission_classes = (permissions.IsAuthenticated, )
+    serializer_class = EventSerializer
+
+    def post(self, request, *args, **kwargs):
+        serializer = self.serializer_class(data=request.data)
+        if not serializer.is_valid():
+            raise APIError(
+                _('invalid payload'),
+                err_class='invalid payload',
+                errors=serializer.errors
+            )
+        payload = serializer.validated_data
+        event = Event.objects.create(agenda=self.agenda, **payload)
+        return self.success(event)
+
+
+agenda_add_event = AgendaAddEventView.as_view()
diff --git a/tests/test_api.py b/tests/test_api.py
index b486cae..0905fb6 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -2,16 +2,17 @@
 
 import datetime
 import mock
 import urllib.parse as urlparse
 import pytest
 import sys
 
 from django.contrib.auth import get_user_model
+from django.contrib.auth.models import Group
 from django.db import connection
 from django.test import override_settings
 from django.test.utils import CaptureQueriesContext
 from django.utils.timezone import now, make_aware, localtime
 
 from chrono.agendas.models import (
     Agenda,
     Booking,
@@ -40,16 +41,30 @@ def user():
     user = User.objects.create(
         username='john.doe', first_name=u'John', last_name=u'Doe', email='john.doe@example.net'
     )
     user.set_password('password')
     user.save()
     return user
 
 
+@pytest.fixture
+def managers_group():
+    group, _ = Group.objects.get_or_create(name='Managers')
+    return group
+
+
+@pytest.fixture
+def manager_user(managers_group):
+    User = get_user_model()
+    user = User.objects.create_user('manager', password='manager')
+    user.groups.set([managers_group])
+    return user
+
+
 @pytest.fixture(params=['Europe/Brussels', 'Asia/Kolkata', 'Brazil/East'])
 def time_zone(request, settings):
     settings.TIME_ZONE = request.param
 
 
 # 2017-05-20 -> saturday
 @pytest.fixture(
     params=[
@@ -4573,8 +4588,107 @@ def test_meetings_and_virtual_datetimes_date_filter(app):
     agenda_foo.save()
     resp = app.get(foo_api_url, params=params)
     assert len(resp.json['data']) == 16
     # also on virtual agenda
     virtual_agenda.maximal_booking_delay = 5
     virtual_agenda.save()
     resp = app.get(virtual_api_url, params=params)
     assert len(resp.json['data']) == 16
+
+
+def test_add_event(app, user):
+    agenda = Agenda(label=u'Foo bar')
+    agenda.maximal_booking_delay = 0
+    agenda.save()
+
+    params = {
+        'start_datetime': '2020-12-08 15:38',
+        'places': 10,
+    }
+    api_url = '/api/agenda/%s/add-event/' % (agenda.slug)
+
+    # not an admin user
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(api_url, params=params, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'User "john.doe" cannot access agenda "foo-bar".'
+
+    # add an event
+    user.is_staff = True
+    user.save()
+    resp = app.post(api_url, params=params)
+    assert not resp.json['err']
+    event_id = resp.json['event_id']
+    assert Event.objects.filter(agenda=agenda).count() == 1
+    event = Event.objects.filter(agenda=agenda, id=event_id)[0]
+    assert str(event.start_datetime) == '2020-12-08 14:38:00+00:00'
+    assert str(event.start_datetime.tzinfo) == 'UTC'
+    assert event.places == 10
+    assert event.publication_date is None
+
+    # add with a description
+    params = {
+        'start_datetime': '2020-12-08 15:38',
+        'publication_date': '2020-05-11',
+        'description': 'A description',
+        'places': 11,
+    }
+    resp = app.post(api_url, params=params)
+    event_id = resp.json['event_id']
+    event = Event.objects.filter(agenda=agenda, id=event_id)[0]
+    assert event.description == 'A description'
+    assert event.publication_date == datetime.date(2020, 5, 11)
+
+    # add with errors in datetime parts
+    params = {
+        'start_datetime': '2020-12-08 minuit',
+        'places': 10,
+    }
+    resp = app.post(api_url, params=params, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'invalid payload'
+    assert 'Datetime has wrong format' in resp.json['errors']['start_datetime'][0]
+    assert Event.objects.filter(agenda=agenda).count() == 2
+
+
+def test_add_event_on_missing_agenda(app, user):
+    api_url = '/api/agenda/%s/add-event/' % ('999')
+
+    # no authentication
+    resp = app.post(api_url, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'Authentication credentials were not provided.'
+
+    # wrong password
+    app.authorization = ('Basic', ('john.doe', 'wrong'))
+    resp = app.post(api_url, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'Invalid username/password.'
+
+    # wrong agenda
+    app.authorization = ('Basic', ('john.doe', 'password'))
+    resp = app.post(api_url, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'The agenda "999" does not exist.'
+
+
+def test_add_event_as_manager(app, manager_user):
+    agenda = Agenda(label=u'Foo bar')
+    agenda.view_role = manager_user.groups.all()[0]
+    agenda.save()
+
+    app.authorization = ('Basic', ('manager', 'manager'))
+    params = {
+        'start_datetime': '2020-12-08 15:38',
+        'places': 10,
+    }
+    api_url = '/api/agenda/%s/add-event/' % (agenda.slug)
+    resp = app.post(api_url, params=params, status=200)
+    assert resp.json['err']
+    assert resp.json['err_desc'] == 'User "manager" cannot access agenda "foo-bar".'
+    assert not Event.objects.filter(agenda=agenda).count()
+
+    agenda.edit_role = manager_user.groups.all()[0]
+    agenda.save()
+    resp = app.post(api_url, params=params)
+    assert not resp.json['err']
+    assert Event.objects.filter(agenda=agenda).count()
-- 
2.29.2

