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
# 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 ..models import Reminder, Tag
from ..models import Tag
class ReminderSerializer(serializers.ModelSerializer):
class Meta:
model = Reminder
fields = '__all__'
class TagSerializer(serializers.ModelSerializer):
class Meta:

View File

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

View File

@ -1,17 +1,7 @@
from django.db import models
from django.conf import settings
from django.core import validators
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)
from django.utils import timezone
class Tag(models.Model):
"""
@ -66,7 +56,7 @@ class Task(models.Model):
validators.MinValueValidator(0.1),
validators.MaxValueValidator(2),
])
difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES)
difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, default=1)
attribute = models.CharField(max_length=15, choices=[
('str', 'Strength'),
('int', 'Intelligence'),
@ -76,10 +66,12 @@ class Task(models.Model):
], default='str')
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
challenge = models.BooleanField(default=False)
reminders = models.ManyToManyField(Reminder, blank=True)
fromSystem = models.BooleanField(default=False)
creation_date = models.DateTimeField(auto_now_add=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):

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:
model = Task
# fields = '__all__'
exclude = ('tags', 'reminders')
exclude = ('tags',)
def create(self, 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.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
class TaskCreateViewTests(APITestCase):

View File

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

View File

@ -1,11 +1,12 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .api import GoogleCalendarEventViewset
from .tasks.views import TaskCreateView, TaskRetrieveView, TaskUpdateView, TaskDeleteView
from .misc.views import TagViewSet, ReminderViewSet
from .misc.views import TagViewSet
router = DefaultRouter()
router.register(r'reminders', ReminderViewSet)
router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
urlpatterns = [
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 .access_token_cache import store_token
from .serializers import MyTokenObtainPairSerializer, CustomUserSerializer
from .managers import CustomAccountManager
from .models import CustomUser
@ -168,6 +169,8 @@ class GoogleRetrieveUserInfo(APIView):
user.email = user_info['email']
user.refresh_token = user_info['refresh_token']
user.save()
store_token(user.id, user_info['access_token'], 'access')
store_token(user.id, user_info['id_token'], 'id')
return user
def call_google_api(self, api_url, access_token):