diff --git a/backend/tasks/api.py b/backend/tasks/api.py index 7669d76..587f433 100644 --- a/backend/tasks/api.py +++ b/backend/tasks/api.py @@ -6,46 +6,22 @@ from rest_framework import viewsets from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from tasks.utils import get_service +from tasks.utils import get_service, generate_recurrence_rule from tasks.models import Todo, RecurrenceTask from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer - class GoogleCalendarEventViewset(viewsets.ViewSet): + """Viewset for list or save Google Calendar Events.""" permission_classes = (IsAuthenticated,) def __init__(self, *args, **kwargs): super().__init__() - self.current_time = datetime.now(tz=timezone.utc).isoformat() - self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)' + self.current_time = (datetime.now(tz=timezone.utc) + timedelta(days=-7)).isoformat() + self.max_time = (datetime.now(tz=timezone.utc) + timedelta(days=7)).isoformat() + self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end,originalStartTime)' - def _validate_serializer(self, serializer): - if serializer.is_valid(): - serializer.save() - return Response("Validate Successfully", status=200) - return Response(serializer.errors, status=400) - - def post(self, request): - service = get_service(request) - events = service.events().list(calendarId='primary', fields=self.event_fields).execute() - for event in events.get('items', []): - if event.get('recurringEventId'): - continue - event['start_datetime'] = event.get('start').get('dateTime') - event['end_datetime'] = event.get('end').get('dateTime') - event.pop('start') - event.pop('end') - try: - task = Todo.objects.get(google_calendar_id=event['id']) - serializer = TodoUpdateSerializer(instance=task, data=event) - return self._validate_serializer(serializer) - except Todo.DoesNotExist: - serializer = TodoUpdateSerializer(data=event, user=request.user) - return self._validate_serializer(serializer) - - def list(self, request, days=7): - max_time = (datetime.now(tz=timezone.utc) + timedelta(days=days)).isoformat() - + def _get_google_events(self, request): + """Get all events from Google Calendar. """ service = get_service(request) events = [] next_page_token = None @@ -54,21 +30,77 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): query = service.events().list( calendarId='primary', timeMin=self.current_time, - timeMax=max_time, + timeMax=self.max_time, maxResults=200, singleEvents=True, orderBy='startTime', pageToken=next_page_token, - fields='items(id,summary,description,created,recurringEventId,updated,start,end)', + fields=self.event_fields, ) page_results = query.execute() page_events = page_results.get('items', []) - + events.extend(page_events) next_page_token = page_results.get('nextPageToken') if next_page_token is None: break - return Response(events, status=200) \ No newline at end of file + return events + + def _validate_serializer(self, serializer): + """Validate serializer and return response.""" + if serializer.is_valid(): + serializer.save() + return Response("Validate Successfully", status=200) + return Response(serializer.errors, status=400) + + def create(self, request, *args, **kwargs): + """Create a new Google Calendar Event.""" + events = self._get_google_events(request) + + responses = [] + recurrence_task_ids = [] + for event in events: + start_datetime = event.get('start', {}).get('dateTime') + end_datetime = event.get('end', {}).get('dateTime') + + event['start_datetime'] = start_datetime + event['end_datetime'] = end_datetime + event.pop('start') + event.pop('end') + + if (event.get('recurringEventId') in recurrence_task_ids): + continue + + if (event.get('recurringEventId') is not None): + originalStartTime = event.get('originalStartTime', {}).get('dateTime') + rrule_text = generate_recurrence_rule(event['start_datetime'], event['end_datetime'], originalStartTime) + event['recurrence'] = rrule_text + event.pop('originalStartTime') + recurrence_task_ids.append(event['recurringEventId']) + + try: + task = RecurrenceTask.objects.get(google_calendar_id=event['id']) + serializer = RecurrenceTaskUpdateSerializer(instance=task, data=event) + except RecurrenceTask.DoesNotExist: + serializer = RecurrenceTaskUpdateSerializer(data=event, user=request.user) + + responses.append(self._validate_serializer(serializer)) + continue + + try: + task = Todo.objects.get(google_calendar_id=event['id']) + serializer = TodoUpdateSerializer(instance=task, data=event) + except Todo.DoesNotExist: + serializer = TodoUpdateSerializer(data=event, user=request.user) + + responses.append(self._validate_serializer(serializer)) + + return responses[0] if responses else Response("No events to process", status=200) + + def list(self, request): + """List all Google Calendar Events.""" + return Response(self._get_google_events(request), status=200) + \ No newline at end of file diff --git a/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py b/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py new file mode 100644 index 0000000..f629f13 --- /dev/null +++ b/backend/tasks/migrations/0013_alter_recurrencetask_recurrence_rule.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-14 15:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0012_habit'), + ] + + operations = [ + migrations.AlterField( + model_name='recurrencetask', + name='recurrence_rule', + field=models.CharField(), + ), + ] diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 83b9325..a8fc4e5 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -68,7 +68,7 @@ class Todo(Task): return self.title class RecurrenceTask(Task): - recurrence_rule = models.TextField() + recurrence_rule = models.CharField() def __str__(self) -> str: return f"{self.title} ({self.recurrence_rule})" diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index ed02300..408cb55 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -41,7 +41,7 @@ class RecurrenceTaskUpdateSerializer(serializers.ModelSerializer): description = serializers.CharField(source="notes", required=False) created = serializers.DateTimeField(source="creation_date") updated = serializers.DateTimeField(source="last_update") - recurrence = serializers.DateTimeField(source="recurrence_rule") + recurrence = serializers.CharField(source="recurrence_rule") start_datetime = serializers.DateTimeField(source="start_event", required=False) end_datetime = serializers.DateTimeField(source="end_event", required=False) diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index a05f82c..87bbaca 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -14,6 +14,10 @@ class TodoViewSet(viewsets.ModelViewSet): serializer_class = TaskSerializer permission_classes = [IsAuthenticated] + def get_queryset(self): + queryset = Todo.objects.filter(user=self.request.user) + return queryset + def get_serializer_class(self): # Can't add ManytoMany at creation time (Tags) if self.action == 'create': diff --git a/backend/tasks/utils.py b/backend/tasks/utils.py index c55eb4a..de52535 100644 --- a/backend/tasks/utils.py +++ b/backend/tasks/utils.py @@ -1,8 +1,55 @@ +from dateutil import rrule +from datetime import datetime + from googleapiclient.discovery import build from authentications.access_token_cache import get_credential_from_cache_token def get_service(request): + """ + Get a service that communicates to a Google API. + + :param request: Http request object + :return: A Resource object with methods for interacting with the calendar service + """ credentials = get_credential_from_cache_token(request.user.id) - return build('calendar', 'v3', credentials=credentials) \ No newline at end of file + return build('calendar', 'v3', credentials=credentials) + +def _determine_frequency(time_difference): + if time_difference.days >= 365: + return rrule.YEARLY + elif time_difference.days >= 30: + return rrule.MONTHLY + elif time_difference.days >= 7: + return rrule.WEEKLY + elif time_difference.days >= 1: + return rrule.DAILY + elif time_difference.seconds >= 3600: + return rrule.HOURLY + elif time_difference.seconds >= 60: + return rrule.MINUTELY + else: + return rrule.SECONDLY + +def generate_recurrence_rule(datetime1: str, datetime2: str, original_start_time: str) -> str: + """ + Generate recurrence rule from + difference between two datetime string. + + :param task1: A task object + :param task2: A task object + :return: A recurrence rule string according to ICAL format + """ + start_time1 = datetime.fromisoformat(datetime1) + start_time2 = datetime.fromisoformat(datetime2) + + time_difference = start_time2 - start_time1 + + recurrence_rule = rrule.rrule( + freq=_determine_frequency(time_difference), + dtstart=datetime.fromisoformat(original_start_time), + interval=time_difference.days if time_difference.days > 0 else 1, + ) + + return str(recurrence_rule) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 98f2a15..344b980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,5 @@ google_auth_oauthlib>=1.1 google-auth-httplib2>=0.1 django-storages[s3]>=1.14 Pillow>=10.1 -drf-spectacular>=0.26 \ No newline at end of file +drf-spectacular>=0.26 +python-dateutil>=2.8 \ No newline at end of file