diff --git a/src/api/schedule.py b/src/api/schedule.py index 18beabb0becae360752acf2a7e08091d358313f7..f1acb9e14ba60abaa918d9c30af4f69c39fa3337 100644 --- a/src/api/schedule.py +++ b/src/api/schedule.py @@ -33,20 +33,21 @@ class ScheduleEncoder(json.JSONEncoder): tz = None def encode_event(self, event, tz=None): - start = event.schedule_start.astimezone(tz or self.tz) - duration = event.schedule_duration.seconds + start = event.schedule_start.astimezone(tz or self.tz) if event.schedule_start is not None else None + duration = event.schedule_duration.seconds if event.schedule_start is not None else None + additional_data = event.additional_data or {} return OrderedDict({ **event_template, - 'id': event.additional_data.get('id') or int(re.sub('[^0-9]+', '', event.id)[0:6]), + 'id': additional_data.get('id') or int(re.sub('[^0-9]+', '', str(event.id))[0:6]), 'description': event.description, # TODO: if the description also exists in additional_data it is overwritten due to concatination with abstract - 'slug': event.slug, - **(event.additional_data or {}), + 'slug': event.slug, + **additional_data, 'guid': event.id, - 'date': start.isoformat(), - 'start': start.strftime('%H:%M'), - 'duration': '%d:%02d' % (duration/3600, duration % 3600/60), - 'room': event.room.name, + 'date': start.isoformat() if start is not None else None, + 'start': start.strftime('%H:%M') if start is not None else None, + 'duration': ('%d:%02d' % (duration / 3600, duration % 3600 / 60)) if duration is not None else None, + 'room': event.room.name if event.room is not None else None, 'title': event.name, 'language': event.language, 'track': event.track.name if event.track else None, diff --git a/src/api/tests/__init__.py b/src/api/tests/__init__.py index 63b4d921c6f34af9b38bc3ffc46446c07af9df8d..82daa5fb6d222d49d90eab1eb749750a57920fe0 100644 --- a/src/api/tests/__init__.py +++ b/src/api/tests/__init__.py @@ -1,4 +1,6 @@ +from .bbb import * # noqa: F401, F403 from .engelsystem import * # noqa: F401, F403 +from .schedule import * # noqa: F401, F403 from .workadventure import * # noqa: F401, F403 __all__ = '*' diff --git a/src/api/tests/schedule.py b/src/api/tests/schedule.py new file mode 100644 index 0000000000000000000000000000000000000000..afeea7f0ae3db02b2e122f370fd7ca0e74a82b79 --- /dev/null +++ b/src/api/tests/schedule.py @@ -0,0 +1,61 @@ +import json + +from django.test import TestCase +from django.urls import reverse +from rest_framework.authtoken.models import Token + +from core.models import Assembly, Conference, Event, PlatformUser, Room + + +class ScheduleTest(TestCase): + def test_push_event(self): + conf = Conference(slug='conf', name='TestConf', is_public=True) + conf.save() + conf.tracks.create(name='Community').save() + assembly = Assembly(name='TestAssembly', slug='asmbly', conference=conf) + assembly.save() + room = Room(conference=conf, assembly=assembly, name='Foo Room', room_type=Room.RoomType.STAGE) + room.save() + + user = PlatformUser(username='bernd', is_active=True) + user.save() + + token = Token(user=user) + token.save() + + event = Event(conference=conf, assembly=assembly, name='Example Event') + event.save() + + update = { + "url": "https://fahrplan.events.ccc.de/rc3/2020/Fahrplan/events/11583.html", + "id": 11583, + "guid": str(event.id), + "logo": None, + "date": "2020-12-27T12:20:00+01:00", + "start": "12:20", + "duration": "00:30", + "room": "foo room", + "slug": "rc3-11583-rc3_eroffnung", + "title": "#rC3 Er\u00f6ffnung", + "subtitle": "", + "track": "Community", + "type": "Talk", + "language": "de", + "abstract": "Willkommen zur ersten und hoffentlich einzigen Remote Chaos Experience!", + "description": "", + "recording_license": "", + "do_not_record": False, + "persons": [{"id": 14151, "public_name": "blubbel"}], + "links": [], + "attachments": [] + } + + url = reverse('api:event-schedule', kwargs={'conference': conf.slug, 'pk': event.pk}) + + with self.modify_settings(API_USERS={'append': user.username}): + resp = self.client.post(url, json.dumps(update), content_type='application/json', HTTP_AUTHORIZATION=f'Token {token.key}') + + self.assertEqual(201, resp.status_code, f'Unexpected result from POST: {resp.content}') + + event.refresh_from_db() + self.assertTrue('rC3' in event.name, f'Expected "rC3" in event name "{event.name}".') diff --git a/src/api/views/schedule.py b/src/api/views/schedule.py index 117d8f04f365af6d1d21da499ac4f1720038f22e..d4de2532563e4f6d58a31a7e878f6827251772dc 100644 --- a/src/api/views/schedule.py +++ b/src/api/views/schedule.py @@ -1,18 +1,20 @@ -import json +from datetime import timedelta import logging -from django.conf import settings from django.db.models import F from django.http import JsonResponse, HttpResponse +from django.utils.dateparse import parse_datetime from django.views.generic import View +from rest_framework import authentication from rest_framework.response import Response +from rest_framework.views import APIView import pytz from core.models.events import Event from core.models.rooms import Room from core.models.conference import ConferenceTrack - +from ..permissions import IsApiUserOrReadOnly from ..schedule import Schedule, ScheduleEncoder from .mixins import ConferenceSlugMixin @@ -63,50 +65,76 @@ class RoomSchedule(ConferenceSlugMixin, View): return schedule_response(schedule, kwargs) -class EventSchedule(ConferenceSlugMixin, View): - def get(self, *args, **kwargs): +def schedulexml_time_to_timedelta(s): + if ':' in s: + hours, minutes = s.split(':') + else: + hours, minutes = 0, s + + timedelta(hours=int(hours), minutes=int(minutes)) + + +class EventSchedule(ConferenceSlugMixin, APIView): + authentication_classes = [authentication.TokenAuthentication] + permission_classes = [IsApiUserOrReadOnly] + + def get(self, request, conference, pk, format=None, **kwargs): tz = pytz.timezone(self.conference.timezone) event = Event.objects \ .accessible_by_user(conference=self.conference, user=self.request.user) \ - .get(pk=kwargs.get('pk')) - return Response(ScheduleEncoder.encode_event(event, tz)) + .get(pk=pk) + return Response(ScheduleEncoder().encode_event(event, tz)) - def post(self, *args, **kwargs): - if not self.request.user.is_authenticated and self.request.user.username in settings.API_USERS: - return HttpResponse(status_code=401) + def post(self, request, conference, pk, format=None, **kwargs): + event = request.data + if len(event) == 0: + return Response({'error': 'No data.'}, status=400) - event = json.loads(self.request.body) + try: + obj = Event.objects.get(conference=self.conference, pk=pk) + except Event.DoesNotExist: + obj = Event(conference=self.conference) - obj, created = Event.objects \ - .accessible_by_user(conference=self.conference, user=self.request.user) \ - .get_or_create(pk=event['guid']) + try: + if 'guid' in event: + if event['guid'] != str(obj.pk): + print(f'Attempted update of event {obj.pk} with guid "{event["guid"]}".') + logger.warning(f'Attempted update of event {obj.pk} with guid "{event["guid"]}".') + return JsonResponse({'error': 'GUID mismatch.'}) - if created: - obj.conference = self.conference + if 'slug' in event: + obj.slug = event['slug'] - try: - obj.slug = event['slug'] obj.room = Room.objects.get(conference=self.conference, name__iexact=event['room']) - obj.kind = 'assembly' if not(obj.room.assembly.is_official) else 'official' - obj.name = event['title'] - obj.language = event['language'] - obj.description = str(event['abstract']) + "\n\n" + str(event['description']) + obj.assembly = obj.room.assembly + obj.kind = 'assembly' if not obj.room.assembly.is_official else 'official' obj.is_public = True - obj.schedule_start = event['date'], - obj.schedule_duration = event['duration'] + ':00', - obj.track = ConferenceTrack.objects.get(conference=self.conference, name__iexact=event['track']), + + if 'title' in event: + obj.name = event['title'] + + if 'language' in event: + obj.language = event['language'] + + obj.description = str(event['abstract']) + "\n\n" + str(event['description']) + + obj.schedule_start = parse_datetime(event['date']) + obj.schedule_duration = schedulexml_time_to_timedelta(event['duration']) + + obj.track = ConferenceTrack.objects.get(conference=self.conference, name__iexact=event['track']) + obj.additional_data = filter_additional_data(event) except Room.DoesNotExist: - return JsonResponse({'error': 'Room {} does not exist'.format(event['room'])}, status_code=400) + return Response({'error': 'Room {} does not exist'.format(event['room'])}, status=400) except ConferenceTrack.DoesNotExist: - return JsonResponse({'error': 'Track {} does not exist'.format(event['track'])}, status_code=400) + return Response({'error': 'Track {} does not exist'.format(event['track'])}, status=400) obj.save() - logger.info('Event %s updated via POST by %s', obj, self.request.user) + logger.info('Event %s updated via POST by %s', obj, request.user) - return HttpResponse(status_code=201) + return HttpResponse(status=201) def filter_additional_data(data): diff --git a/src/rc3platform/settings/base.py b/src/rc3platform/settings/base.py index 6566f013a5807f0085b6325df81b6120f7cd71ab..4711ef80478f7526688f64860bfee35eee8cc18e 100644 --- a/src/rc3platform/settings/base.py +++ b/src/rc3platform/settings/base.py @@ -193,6 +193,9 @@ FORBIDDEN_ASSEMBLY_SLUGS = ['visit', 'maps', 'api', 'pusher'] MAIL_REPLY_TO = [] SUPPORT_HTML_MAILS = False +# API access +API_USERS = [] + # SSO SSO_COOKIE_NAME = 'SSO_TOKEN' SSO_COOKIE_DOMAIN = '.localhost'