Merge pull request #19 from TurTaskProject/feature/google-calendar-api

Feature/google calendar api - Improve google api sync
This commit is contained in:
Sirin Puenggun 2023-11-04 03:32:29 +07:00 committed by GitHub
commit a1b4acefae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 324 additions and 29 deletions

View File

@ -162,6 +162,17 @@ DATABASES = {
} }
# Cache
CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators

68
backend/tasks/api.py Normal file
View File

@ -0,0 +1,68 @@
from datetime import datetime, timedelta
from django.utils import timezone
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.models import Task
from tasks.serializers import TaskUpdateSerializer
class GoogleCalendarEventViewset(viewsets.ViewSet):
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,updated,start,end)'
def _validate_serializer(self, serializer):
if serializer.is_valid():
serializer.save()
return Response("Task Sync 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', []):
try:
task = Task.objects.get(google_calendar_id=event['id'])
serializer = TaskUpdateSerializer(instance=task, data=event)
return self._validate_serializer(serializer)
except Task.DoesNotExist:
serializer = TaskUpdateSerializer(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=3)).isoformat()
service = get_service(request)
events = []
next_page_token = None
while True:
query = service.events().list(
calendarId='primary',
timeMin=self.current_time,
timeMax=max_time,
maxResults=20,
singleEvents=True,
orderBy='startTime',
pageToken=next_page_token,
fields='items(id,summary,description,created,updated,start,end)',
)
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)

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-02 07:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0004_rename_time_reminder_alerttime_and_more'),
]
operations = [
migrations.AddField(
model_name='task',
name='google_calendar_id',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-03 05:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0005_task_google_calendar_id'),
]
operations = [
migrations.AlterField(
model_name='task',
name='difficulty',
field=models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.2.6 on 2023-11-03 05:48
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0006_alter_task_difficulty'),
]
operations = [
migrations.RemoveField(
model_name='task',
name='reminders',
),
migrations.DeleteModel(
name='Reminder',
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-03 17:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0007_remove_task_reminders_task_end_event_and_more'),
]
operations = [
migrations.AddField(
model_name='task',
name='end_event',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='task',
name='start_event',
field=models.DateTimeField(null=True),
),
]

View File

@ -1,10 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
from ..models import Reminder, Tag from ..models import Tag
class ReminderSerializer(serializers.ModelSerializer):
class Meta:
model = Reminder
fields = '__all__'
class TagSerializer(serializers.ModelSerializer): class TagSerializer(serializers.ModelSerializer):
class Meta: class Meta:

View File

@ -1,10 +1,7 @@
from rest_framework import viewsets from rest_framework import viewsets
from ..models import Reminder, Tag from ..models import Tag
from .serializers import ReminderSerializer, TagSerializer from .serializers import TagSerializer
class ReminderViewSet(viewsets.ModelViewSet):
queryset = Reminder.objects.all()
serializer_class = ReminderSerializer
class TagViewSet(viewsets.ModelViewSet): class TagViewSet(viewsets.ModelViewSet):
queryset = Tag.objects.all() queryset = Tag.objects.all()

View File

@ -1,17 +1,7 @@
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
from django.core import validators from django.core import validators
from django.utils import timezone
class Reminder(models.Model):
"""
Represents a reminder associated with a task.
Fields:
- startDate: The optional date for which the reminder is set.
- time: The time at which the reminder is triggered.
"""
startDate = models.DateField(auto_now_add=True, null=True, blank=True)
alertTime = models.DateTimeField(null=False, blank=False)
class Tag(models.Model): class Tag(models.Model):
""" """
@ -66,7 +56,7 @@ class Task(models.Model):
validators.MinValueValidator(0.1), validators.MinValueValidator(0.1),
validators.MaxValueValidator(2), validators.MaxValueValidator(2),
]) ])
difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES) difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, default=1)
attribute = models.CharField(max_length=15, choices=[ attribute = models.CharField(max_length=15, choices=[
('str', 'Strength'), ('str', 'Strength'),
('int', 'Intelligence'), ('int', 'Intelligence'),
@ -76,10 +66,12 @@ class Task(models.Model):
], default='str') ], default='str')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
challenge = models.BooleanField(default=False) challenge = models.BooleanField(default=False)
reminders = models.ManyToManyField(Reminder, blank=True)
fromSystem = models.BooleanField(default=False) fromSystem = models.BooleanField(default=False)
creation_date = models.DateTimeField(auto_now_add=True) creation_date = models.DateTimeField(auto_now_add=True)
last_update = models.DateTimeField(auto_now=True) last_update = models.DateTimeField(auto_now=True)
google_calendar_id = models.CharField(blank=True, null=True, max_length=255)
start_event = models.DateTimeField(null=True)
end_event = models.DateTimeField(null=True)
class Subtask(models.Model): class Subtask(models.Model):

View File

@ -0,0 +1,36 @@
from rest_framework import serializers
from django.utils.dateparse import parse_datetime
from .models import Task
from datetime import datetime
class GoogleCalendarEventSerializer(serializers.Serializer):
summary = serializers.CharField()
start = serializers.DateTimeField()
end = serializers.DateTimeField()
description = serializers.CharField(required=False)
class TaskUpdateSerializer(serializers.ModelSerializer):
id = serializers.CharField(source="google_calendar_id")
summary = serializers.CharField(source="title")
description = serializers.CharField(source="notes", required=False)
created = serializers.DateTimeField(source="creation_date")
updated = serializers.DateTimeField(source="last_update")
start_datetime = serializers.DateTimeField(source="start_event", required=False)
end_datetime = serializers.DateTimeField(source="end_event", required=False)
class Meta:
model = Task
fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime')
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super(TaskUpdateSerializer, self).__init__(*args, **kwargs)
def create(self, validated_data):
validated_data['user'] = self.user
task = Task.objects.create(**validated_data)
return task

View File

@ -5,7 +5,7 @@ class TaskCreateSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Task model = Task
# fields = '__all__' # fields = '__all__'
exclude = ('tags', 'reminders') exclude = ('tags',)
def create(self, validated_data): def create(self, validated_data):
# Create a new task with validated data # Create a new task with validated data

View File

@ -0,0 +1,56 @@
from datetime import datetime
from zoneinfo import ZoneInfo
from django.test import TestCase
from django.utils import timezone
from tasks.tests.utils import create_test_user, login_user
from tasks.serializers import TaskUpdateSerializer
from tasks.models import Task
class TaskUpdateSerializerTest(TestCase):
def setUp(self):
self.user = create_test_user()
self.current_time = '2020-08-01T00:00:00Z'
self.end_time = '2020-08-01T00:00:00Z'
def test_serializer_create(self):
data = {
'id': '32141cwaNcapufh8jq2conw',
'summary': 'Updated Task',
'description': 'Updated description',
'created': self.current_time,
'updated': self.end_time,
'start_datetime' : self.current_time,
'end_datetie': self.end_time,
}
serializer = TaskUpdateSerializer(data=data, user=self.user)
self.assertTrue(serializer.is_valid())
serializer.is_valid()
task = serializer.save()
self.assertIsInstance(task, Task)
def test_serializer_update(self):
task = Task.objects.create(title='Original Task', notes='Original description', user=self.user)
data = {
'id': '32141cwaNcapufh8jq2conw',
'summary': 'Updated Task',
'description': 'Updated description',
'created': self.current_time,
'updated': self.end_time,
'start_datetime' : self.current_time,
'end_datetie': self.end_time,
}
serializer = TaskUpdateSerializer(instance=task, data=data)
self.assertTrue(serializer.is_valid())
updated_task = serializer.save()
self.assertEqual(updated_task.title, 'Updated Task')
self.assertEqual(updated_task.notes, 'Updated description')
self.assertEqual(updated_task.start_event,
datetime.strptime(self.current_time,
'%Y-%m-%dT%H:%M:%SZ')
.replace(tzinfo=ZoneInfo(key='UTC')))

View File

@ -2,7 +2,7 @@ from django.urls import reverse
from rest_framework import status from rest_framework import status
from rest_framework.test import APITestCase from rest_framework.test import APITestCase
from .utils import create_test_user, login_user from tasks.tests.utils import create_test_user, login_user
from ..models import Task from ..models import Task
class TaskCreateViewTests(APITestCase): class TaskCreateViewTests(APITestCase):

View File

@ -34,7 +34,6 @@ def create_task_json(user, **kwargs):
"difficulty": 1, "difficulty": 1,
"attribute": "str", "attribute": "str",
"challenge": False, "challenge": False,
"reminders": False,
"fromSystem": False, "fromSystem": False,
"creation_date": None, "creation_date": None,
"last_update": None, "last_update": None,
@ -57,7 +56,6 @@ def create_test_task(user, **kwargs):
'difficulty': 1, 'difficulty': 1,
'attribute': 'str', 'attribute': 'str',
'challenge': False, 'challenge': False,
'reminders': False,
'fromSystem': False, 'fromSystem': False,
} }

View File

@ -1,11 +1,12 @@
from django.urls import path, include from django.urls import path, include
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from .api import GoogleCalendarEventViewset
from .tasks.views import TaskCreateView, TaskRetrieveView, TaskUpdateView, TaskDeleteView from .tasks.views import TaskCreateView, TaskRetrieveView, TaskUpdateView, TaskDeleteView
from .misc.views import TagViewSet, ReminderViewSet from .misc.views import TagViewSet
router = DefaultRouter() router = DefaultRouter()
router.register(r'reminders', ReminderViewSet)
router.register(r'tags', TagViewSet) router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
urlpatterns = [ urlpatterns = [
path('', include(router.urls)), path('', include(router.urls)),

8
backend/tasks/utils.py Normal file
View File

@ -0,0 +1,8 @@
from googleapiclient.discovery import build
from users.access_token_cache import get_credential_from_cache_token
def get_service(request):
credentials = get_credential_from_cache_token(request.user.id)
return build('calendar', 'v3', credentials=credentials)

View File

@ -0,0 +1,50 @@
from django.core.cache import cache
from django.conf import settings
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
from .models import CustomUser
def store_token(user_id, token, token_type):
cache_key = f"user_{token_type}_token:{user_id}"
cache.set(cache_key, token, timeout=3600)
def get_credential_from_cache_token(user_id):
access_token = cache.get(f"user_access_token:{user_id}")
id_token = cache.get(f"user_id_token:{user_id}")
refresh_token = CustomUser.objects.get(id=user_id).refresh_token
scopes = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/calendar.readonly',
]
# credentials = Credentials.from_authorized_user_info(
# {
# 'access_token': access_token,
# 'token_uri': 'https://oauth2.googleapis.com/token',
# 'refresh_token': refresh_token,
# 'client_id': settings.GOOGLE_CLIENT_ID,
# 'client_secret': settings.GOOGLE_CLIENT_SECRET,
# 'id_token': id_token,
# }
credentials = Credentials(token=access_token,
refresh_token=refresh_token,
token_uri='https://oauth2.googleapis.com/token',
client_id=settings.GOOGLE_CLIENT_ID,
client_secret=settings.GOOGLE_CLIENT_SECRET,
scopes=scopes,
id_token=id_token
)
# If can refresh, refresh
if credentials.expired and credentials.refresh_token:
credentials.refresh(Request())
store_token(user_id, credentials.token, 'access')
store_token(user_id, credentials.id_token, 'id')
return credentials

View File

@ -19,6 +19,7 @@ from dj_rest_auth.registration.views import SocialLoginView
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from .access_token_cache import store_token
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
from .managers import CustomAccountManager from .managers import CustomAccountManager
from .models import CustomUser from .models import CustomUser
@ -168,6 +169,8 @@ class GoogleRetrieveUserInfo(APIView):
user.email = user_info['email'] user.email = user_info['email']
user.refresh_token = user_info['refresh_token'] user.refresh_token = user_info['refresh_token']
user.save() user.save()
store_token(user.id, user_info['access_token'], 'access')
store_token(user.id, user_info['id_token'], 'id')
return user return user
def call_google_api(self, api_url, access_token): def call_google_api(self, api_url, access_token):