mirror of
https://github.com/TurTaskProject/TurTaskWeb.git
synced 2025-12-19 14:04:07 +01:00
Merge pull request #19 from TurTaskProject/feature/google-calendar-api
Feature/google calendar api - Improve google api sync
This commit is contained in:
commit
a1b4acefae
@ -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
68
backend/tasks/api.py
Normal 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)
|
||||
18
backend/tasks/migrations/0005_task_google_calendar_id.py
Normal file
18
backend/tasks/migrations/0005_task_google_calendar_id.py
Normal 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),
|
||||
),
|
||||
]
|
||||
18
backend/tasks/migrations/0006_alter_task_difficulty.py
Normal file
18
backend/tasks/migrations/0006_alter_task_difficulty.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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),
|
||||
),
|
||||
]
|
||||
@ -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:
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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):
|
||||
|
||||
36
backend/tasks/serializers.py
Normal file
36
backend/tasks/serializers.py
Normal 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
|
||||
@ -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
|
||||
|
||||
56
backend/tasks/tests/test_deserializer.py
Normal file
56
backend/tasks/tests/test_deserializer.py
Normal 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')))
|
||||
@ -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):
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -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
8
backend/tasks/utils.py
Normal 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)
|
||||
50
backend/users/access_token_cache.py
Normal file
50
backend/users/access_token_cache.py
Normal 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
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user