Merge pull request #27 from TurTaskProject/feature/calendar

Add Calendar and Link with google api
This commit is contained in:
Sirin Puenggun 2023-11-07 03:41:10 +07:00 committed by GitHub
commit dcfc050408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 347 additions and 54 deletions

View File

@ -31,6 +31,10 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
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)

View File

@ -1,37 +1,16 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.generics import CreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, DestroyAPIView
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticated
from ..models import Todo
from tasks.models import Todo
from .serializers import TaskCreateSerializer, TaskGeneralSerializer
class TaskCreateView(CreateAPIView):
queryset = Todo.objects.all()
serializer_class = TaskCreateSerializer
permission_classes = [IsAuthenticated]
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
self.perform_create(serializer)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class TaskRetrieveView(RetrieveAPIView):
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TaskGeneralSerializer
permission_classes = [IsAuthenticated]
class TaskUpdateView(RetrieveUpdateAPIView):
queryset = Todo.objects.all()
serializer_class = TaskGeneralSerializer
permission_classes = [IsAuthenticated]
class TaskDeleteView(DestroyAPIView):
queryset = Todo.objects.all()
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
# Can't add ManytoMany at creation time (Tags)
if self.action == 'create':
return TaskCreateSerializer
return TaskGeneralSerializer

View File

@ -1,21 +1,18 @@
from datetime import datetime, timedelta
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from tasks.tests.utils import create_test_user, login_user
from ..models import Todo
from tasks.models import Todo
class TodoCreateViewTests(APITestCase):
class TodoViewSetTests(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client = login_user(self.user)
self.url = reverse("add-task")
self.url = reverse("todo-list")
self.due_date = datetime.now() + timedelta(days=5)
def test_create_valid_todo(self):
"""
Test creating a valid task using the API.
@ -28,7 +25,7 @@ class TodoCreateViewTests(APITestCase):
'priority': 1,
'difficulty': 1,
'user': self.user.id,
'end_event': self.due_date,
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
@ -42,7 +39,6 @@ class TodoCreateViewTests(APITestCase):
data = {
'type': 'invalid', # Invalid task type
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Todo.objects.count(), 0) # No task should be created
@ -55,7 +51,6 @@ class TodoCreateViewTests(APITestCase):
'title': 'Incomplete Task',
'type': 'habit',
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Todo.objects.count(), 0) # No task should be created
@ -71,9 +66,8 @@ class TodoCreateViewTests(APITestCase):
'priority': 1,
'difficulty': 1,
'user': 999, # Invalid user ID
'end_event': self.due_date,
'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
}
response = self.client.post(self.url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertEqual(Todo.objects.count(), 0) # No task should be created

View File

@ -1,17 +1,17 @@
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
from tasks.api import GoogleCalendarEventViewset
from tasks.tasks.views import TodoViewSet
from tasks.misc.views import TagViewSet
router = DefaultRouter()
router.register(r'todo', TodoViewSet)
router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')
urlpatterns = [
path('', include(router.urls)),
path('tasks/create/', TaskCreateView.as_view(), name="add-task"),
path('tasks/<int:pk>/', TaskRetrieveView.as_view(), name='retrieve-task'),
path('tasks/<int:pk>/update/', TaskUpdateView.as_view(), name='update-task'),
path('tasks/<int:pk>/delete/', TaskDeleteView.as_view(), name='delete-task'),
]

View File

@ -12,6 +12,11 @@
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fullcalendar/core": "^6.1.9",
"@fullcalendar/daygrid": "^6.1.9",
"@fullcalendar/interaction": "^6.1.9",
"@fullcalendar/react": "^6.1.9",
"@fullcalendar/timegrid": "^6.1.9",
"@mui/icons-material": "^5.14.15",
"@mui/material": "^5.14.15",
"@mui/system": "^5.14.15",

View File

@ -11,6 +11,21 @@ dependencies:
'@emotion/styled':
specifier: ^11.11.0
version: 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.33)(react@18.2.0)
'@fullcalendar/core':
specifier: ^6.1.9
version: 6.1.9
'@fullcalendar/daygrid':
specifier: ^6.1.9
version: 6.1.9(@fullcalendar/core@6.1.9)
'@fullcalendar/interaction':
specifier: ^6.1.9
version: 6.1.9(@fullcalendar/core@6.1.9)
'@fullcalendar/react':
specifier: ^6.1.9
version: 6.1.9(@fullcalendar/core@6.1.9)(react-dom@18.2.0)(react@18.2.0)
'@fullcalendar/timegrid':
specifier: ^6.1.9
version: 6.1.9(@fullcalendar/core@6.1.9)
'@mui/icons-material':
specifier: ^5.14.15
version: 5.14.15(@mui/material@5.14.15)(@types/react@18.2.33)(react@18.2.0)
@ -728,6 +743,49 @@ packages:
resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
dev: false
/@fullcalendar/core@6.1.9:
resolution: {integrity: sha512-eeG+z9BWerdsU9Ac6j16rpYpPnE0wxtnEHiHrh/u/ADbGTR3hCOjCD9PxQOfhOTHbWOVs7JQunGcksSPu5WZBQ==}
dependencies:
preact: 10.12.1
dev: false
/@fullcalendar/daygrid@6.1.9(@fullcalendar/core@6.1.9):
resolution: {integrity: sha512-o/6joH/7lmVHXAkbaa/tUbzWYnGp/LgfdiFyYPkqQbjKEeivNZWF1WhHqFbhx0zbFONSHtrvkjY2bjr+Ef2quQ==}
peerDependencies:
'@fullcalendar/core': ~6.1.9
dependencies:
'@fullcalendar/core': 6.1.9
dev: false
/@fullcalendar/interaction@6.1.9(@fullcalendar/core@6.1.9):
resolution: {integrity: sha512-I3FGnv0kKZpIwujg3HllbKrciNjTqeTYy3oJG226oAn7lV6wnrrDYMmuGmA0jPJAGN46HKrQqKN7ItxQRDec4Q==}
peerDependencies:
'@fullcalendar/core': ~6.1.9
dependencies:
'@fullcalendar/core': 6.1.9
dev: false
/@fullcalendar/react@6.1.9(@fullcalendar/core@6.1.9)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-ioxu0V++pYz2u/N1LL1V8DkMyiKGRun0gMAll2tQz3Kzi3r9pTwncGKRb1zO8h0e+TrInU08ywk/l5lBwp7eog==}
peerDependencies:
'@fullcalendar/core': ~6.1.9
react: ^16.7.0 || ^17 || ^18
react-dom: ^16.7.0 || ^17 || ^18
dependencies:
'@fullcalendar/core': 6.1.9
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@fullcalendar/timegrid@6.1.9(@fullcalendar/core@6.1.9):
resolution: {integrity: sha512-le7UV05wVE1Trdr054kgJXTwa+A1pEI8nlCBnPWdcyrL+dTLoPvQ4AWEVCnV7So+4zRYaCqnqGXfCJsj0RQa0g==}
peerDependencies:
'@fullcalendar/core': ~6.1.9
dependencies:
'@fullcalendar/core': 6.1.9
'@fullcalendar/daygrid': 6.1.9(@fullcalendar/core@6.1.9)
dev: false
/@humanwhocodes/config-array@0.11.13:
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
engines: {node: '>=10.10.0'}
@ -2779,6 +2837,10 @@ packages:
source-map-js: 1.0.2
dev: true
/preact@10.12.1:
resolution: {integrity: sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==}
dev: false
/prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}

View File

@ -4,9 +4,10 @@ import { BrowserRouter, Route, Routes, Link } from 'react-router-dom';
import TestAuth from './components/testAuth';
import LoginPage from './components/authentication/LoginPage';
import SignUpPage from './components/authentication/SignUpPage';
import NavBar from './components/Nav/Navbar';
import NavBar from './components/nav/Navbar';
import Home from './components/Home';
import ProfileUpdate from './components/ProfileUpdatePage'
import ProfileUpdate from './components/ProfileUpdatePage';
import Calendar from './components/calendar/calendar';
const App = () => {
return (
@ -19,6 +20,7 @@ const App = () => {
<Route path="/signup" element={<SignUpPage/>}/>
<Route path="/testAuth" element={<TestAuth/>}/>
<Route path="/update_profile" element={<ProfileUpdate/>}/>
<Route path="/calendar" element={<Calendar/>}/>
</Routes>
</div>
</BrowserRouter>

View File

@ -0,0 +1,23 @@
import axios from 'axios';
// Create an Axios instance with common configurations
const axiosInstance = axios.create({
baseURL: 'http://127.0.0.1:8000/api/',
timeout: 5000,
headers: {
'Authorization': "Bearer " + localStorage.getItem('access_token'),
'Content-Type': 'application/json',
'accept': 'application/json',
}
});
export const fetchTodoTasks = () => {
return axiosInstance
.get('todo/')
.then((response) => {
return response.data;
})
.catch(error => {
throw error;
});
};

View File

@ -1,7 +1,7 @@
import * as React from 'react';
import { Link, useNavigate } from 'react-router-dom';
import IsAuthenticated from '../authentication/IsAuthenticated';
import axiosapi from '../../api/axiosapi';
import axiosapi from '../../api/AuthenticationApi';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';

View File

@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom";
import { useGoogleLogin } from "@react-oauth/google"
import refreshAccessToken from './refreshAcesstoken';
import axiosapi from '../../api/axiosapi';
import axiosapi from '../../api/AuthenticationApi';
function LoginPage() {
const Navigate = useNavigate();

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axiosapi from '../../api/axiosapi';
import axiosapi from '../../api/AuthenticationApi';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';

View File

@ -0,0 +1,42 @@
import { fetchTodoTasks } from '../../api/TaskApi';
let eventGuid = 0
// function getDateAndTime(dateString) {
// const dateObject = new Date(dateString);
// const year = dateObject.getFullYear();
// const month = (dateObject.getMonth() + 1).toString().padStart(2, '0');
// const day = dateObject.getDate().toString().padStart(2, '0');
// const dateFormatted = `${year}-${month}-${day}`;
// const hours = dateObject.getUTCHours().toString().padStart(2, '0');
// const minutes = dateObject.getUTCMinutes().toString().padStart(2, '0');
// const seconds = dateObject.getUTCSeconds().toString().padStart(2, '0');
// const timeFormatted = `T${hours}:${minutes}:${seconds}`;
// return dateFormatted + timeFormatted;
// }
const mapResponseToEvents = (response) => {
return response.map(item => ({
id: createEventId(),
title: item.title,
start: item.start_event,
end: item.end_event,
}));
}
export async function getEvents() {
try {
const response = await fetchTodoTasks();
return mapResponseToEvents(response);
} catch (error) {
console.error(error);
return [];
}
}
export function createEventId() {
return String(eventGuid++);
}

View File

@ -0,0 +1,127 @@
import React, { useState } from 'react';
import { formatDate } from "@fullcalendar/core";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { getEvents, createEventId } from "./TaskDataHandler";
import './index.css'
export default class Calendar extends React.Component {
state = {
weekendsVisible: true,
currentEvents: [],
};
render() {
return (
<div className="demo-app">
{this.renderSidebar()}
<div className="demo-app-main">
<FullCalendar
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "dayGridMonth,timeGridWeek,timeGridDay",
}}
initialView="dayGridMonth"
editable={true}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
weekends={this.state.weekendsVisible}
initialEvents={getEvents} // alternatively, use the `events` setting to fetch from a feed
select={this.handleDateSelect}
eventContent={renderEventContent} // custom render function
eventClick={this.handleEventClick}
eventsSet={this.handleEvents} // called after events are initialized/added/changed/removed
/* you can update a remote database when these fire:
eventAdd={function(){}}
eventChange={function(){}}
eventRemove={function(){}}
*/
/>
</div>
</div>
);
}
renderSidebar() {
return (
<div className="demo-app-sidebar">
<div className="demo-app-sidebar-section">
<h2>Instructions</h2>
<ul>
<li>Select dates and you will be prompted to create a new event</li>
<li>Drag, drop, and resize events</li>
<li>Click an event to delete it</li>
</ul>
</div>
<div className="demo-app-sidebar-section">
<label>
<input type="checkbox" checked={this.state.weekendsVisible} onChange={this.handleWeekendsToggle}></input>
toggle weekends
</label>
</div>
<div className="demo-app-sidebar-section">
<h2>All Events ({this.state.currentEvents.length})</h2>
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
</div>
</div>
);
}
handleWeekendsToggle = () => {
this.setState({
weekendsVisible: !this.state.weekendsVisible,
});
};
handleDateSelect = selectInfo => {
let title = prompt("Please enter a new title for your event");
let calendarApi = selectInfo.view.calendar;
calendarApi.unselect(); // clear date selection
if (title) {
calendarApi.addEvent({
id: createEventId(),
title,
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay,
});
}
};
handleEventClick = clickInfo => {
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
clickInfo.event.remove();
}
};
handleEvents = events => {
this.setState({
currentEvents: events,
});
};
}
function renderEventContent(eventInfo) {
return (
<>
<b>{eventInfo.timeText}</b>
<i>{eventInfo.event.title}</i>
</>
);
}
function renderSidebarEvent(event) {
return (
<li key={event.id}>
<b>{formatDate(event.start, { year: "numeric", month: "short", day: "numeric" })}</b>
<i>{event.title}</i>
</li>
);
}

View File

@ -0,0 +1,55 @@
html,
body,
body > div { /* the react root */
margin: 0;
padding: 0;
height: 100%;
}
h2 {
margin: 0;
font-size: 16px;
}
ul {
margin: 0;
padding: 0 0 0 1.5em;
}
li {
margin: 1.5em 0;
padding: 0;
}
b { /* used for event dates/times */
margin-right: 3px;
}
.demo-app {
display: flex;
min-height: 100%;
font-family: Arial, Helvetica Neue, Helvetica, sans-serif;
font-size: 14px;
}
.demo-app-sidebar {
width: 300px;
line-height: 1.5;
background: #eaf9ff;
border-right: 1px solid #d3e2e8;
}
.demo-app-sidebar-section {
padding: 2em;
}
.demo-app-main {
flex-grow: 1;
padding: 3em;
}
.fc { /* the calendar root */
max-width: 1100px;
margin: 0 auto;
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import axiosapi from '../api/axiosapi';
import axiosapi from '../api/AuthenticationApi';
import { Button } from '@mui/material';
import { useNavigate } from 'react-router-dom';