From f189b8ea75dca79465235ba35d108eed9cb4633f Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Sun, 5 Nov 2023 11:59:27 +0700 Subject: [PATCH 01/26] Constructing kanban board. --- frontend/pnpm-lock.yaml | 93 +++++++++++++++++++ frontend/src/App.jsx | 6 +- .../components/kanbanBoard/kanbanBoard.jsx | 30 ++++++ 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/kanbanBoard/kanbanBoard.jsx diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 18613e9..73b01f7 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1202,6 +1202,13 @@ packages: '@babel/types': 7.23.0 dev: true + /@types/hoist-non-react-statics@3.3.4: + resolution: {integrity: sha512-ZchYkbieA+7tnxwX/SCBySx9WwvWR8TaP5tb2jRAzwvLb/rWchGw3v0w3pqUbUvj0GCwW2Xz/AVPSk6kUGctXQ==} + dependencies: + '@types/react': 18.2.33 + hoist-non-react-statics: 3.3.2 + dev: false + /@types/parse-json@4.0.1: resolution: {integrity: sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng==} dev: false @@ -1215,6 +1222,15 @@ packages: '@types/react': 18.2.33 dev: true + /@types/react-redux@7.1.28: + resolution: {integrity: sha512-EQr7cChVzVUuqbA+J8ArWK1H0hLAHKOs21SIMrskKZ3nHNeE+LFYA+IsoZGhVOT8Ktjn3M20v4rnZKN3fLbypw==} + dependencies: + '@types/hoist-non-react-statics': 3.3.4 + '@types/react': 18.2.33 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + /@types/react-transition-group@4.4.8: resolution: {integrity: sha512-QmQ22q+Pb+HQSn04NL3HtrqHwYMf4h3QKArOy5F8U5nEVMaihBs3SR10WiOM1iwPz5jIo8x/u11al+iEGZZrvg==} dependencies: @@ -1598,6 +1614,12 @@ packages: which: 2.0.2 dev: true + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.1 + dev: false + /css-vendor@2.0.8: resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} dependencies: @@ -2653,6 +2675,10 @@ packages: yallist: 3.1.1 dev: true + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + /merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2981,6 +3007,29 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.23.2 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + /react-bootstrap@2.9.1(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ezgmh/ARCYp18LbZEqPp0ppvy+ytCmycDORqc8vXSKYV3cer4VH7OReV8uMOoKXmYzivJTxgzGHalGrHamryHA==} peerDependencies: @@ -3042,6 +3091,10 @@ packages: /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + /react-is@18.2.0: resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: false @@ -3050,6 +3103,28 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.23.2 + '@types/react-redux': 7.1.28 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + /react-refresh@0.14.0: resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} engines: {node: '>=0.10.0'} @@ -3112,6 +3187,12 @@ packages: picomatch: 2.3.1 dev: true + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.23.2 + dev: false + /reflect.getprototypeof@1.0.4: resolution: {integrity: sha512-ECkTw8TmJwW60lOTR+ZkODISW6RQ8+2CL3COqtiJKLd6MmB45hN51HprHFziKLGkAuTGQhBb91V8cy+KHlaCjw==} engines: {node: '>= 0.4'} @@ -3396,6 +3477,10 @@ packages: any-promise: 1.3.0 dev: true + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + /tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} dev: false @@ -3515,6 +3600,14 @@ packages: punycode: 2.3.0 dev: true + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + /util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} dev: true diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7333c4a..76c6abc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,6 +7,7 @@ import AuthenticantionPage from './components/authentication/AuthenticationPage' import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; import Home from './components/Home'; +import UncontrolledBoard from './components/kanbanBoard/kanbanBoard'; const App = () => { @@ -21,9 +22,8 @@ const App = () => { }/> - {/*
- -
*/} + + ); } diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx new file mode 100644 index 0000000..852fd7d --- /dev/null +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -0,0 +1,30 @@ +import React, { useState } from "react"; +import Column from "./Column"; + +const KanbanBoard = () => { + const [tasks, setTasks] = useState([ + { id: 1, content: "Task 1" }, + { id: 2, content: "Task 2" }, + { id: 3, content: "Task 3" }, + ]); + + const [columns, setColumns] = useState({ + backlog: tasks, + inProgress: [], + done: [], + }); + + const handleTaskDelete = (taskId) => { + const updatedTasks = tasks.filter((task) => task.id !== taskId); + setTasks(updatedTasks); + + const updatedColumns = { + ...columns, + backlog: updatedTasks, + inProgress: columns.inProgress.filter((task) => task.id !== taskId), + done: columns.done.filter((task) => task.id !== taskId), + }; + setColumns(updatedColumns); + }; + + From 074aeb70bc64fbdf8a1e61bafd43904fe9ab00e7 Mon Sep 17 00:00:00 2001 From: Wissarut Kanasub Date: Mon, 6 Nov 2023 11:08:44 +0700 Subject: [PATCH 02/26] Calander component and branch --- frontend/package.json | 4 ++ frontend/pnpm-lock.yaml | 59 +++++++++++++++++++ frontend/src/components/calendar/calendar.jsx | 28 +++++++++ 3 files changed, 91 insertions(+) create mode 100644 frontend/src/components/calendar/calendar.jsx diff --git a/frontend/package.json b/frontend/package.json index 793cfe0..1a78f51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,10 @@ "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", + "@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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 67325cd..271029a 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,18 @@ 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/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 +740,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 +2834,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'} diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx new file mode 100644 index 0000000..00cdc9c --- /dev/null +++ b/frontend/src/components/calendar/calendar.jsx @@ -0,0 +1,28 @@ +import React from 'react'; +import FullCalendar from '@fullcalendar/react'; +import dayGridPlugin from '@fullcalendar/daygrid'; +import timeGridPlugin from '@fullcalendar/timegrid'; + +const Calendar = () => { + return ( +
+ +
+ ); +}; + +export default Calendar; From cb2b235e20fdcf4e1953b80563dfb30c45830663 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 12:37:10 +0700 Subject: [PATCH 03/26] Add UserProfile --- backend/users/migrations/0004_userstats.py | 30 ++++++++++++++++ backend/users/models.py | 42 +++++++++++++++------- 2 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 backend/users/migrations/0004_userstats.py diff --git a/backend/users/migrations/0004_userstats.py b/backend/users/migrations/0004_userstats.py new file mode 100644 index 0000000..2780aae --- /dev/null +++ b/backend/users/migrations/0004_userstats.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.6 on 2023-11-06 05:30 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_customuser_profile_pic'), + ] + + operations = [ + migrations.CreateModel( + name='UserStats', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('health', models.IntegerField(default=100)), + ('gold', models.FloatField(default=0.0)), + ('experience', models.FloatField(default=0)), + ('strength', models.IntegerField(default=1)), + ('intelligence', models.IntegerField(default=1)), + ('endurance', models.IntegerField(default=1)), + ('perception', models.IntegerField(default=1)), + ('luck', models.IntegerField(default=1)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/users/models.py b/backend/users/models.py index 5b5b176..ce906f9 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,7 +1,10 @@ +import random + from django.db import models from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.core.validators import MinValueValidator, MaxValueValidator from .managers import CustomAccountManager @@ -32,15 +35,30 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): return self.username -# class UserStats(models.Model): -# """ -# Represents User Profiles and Attributes. -# Fields: -# - health: health points of the user. -# - gold: gold points of the user. -# - experience: experience points of the user. -# """ -# user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) -# health = models.IntegerField(default=100) -# gold = models.IntegerField(default=0) -# experience = models.FloatField(default=0) \ No newline at end of file +def random_luck(): + return random.randint(1, 50) + +class UserStats(models.Model): + """ + Represents User Profiles and Attributes. + Fields: + - health: health points of the user. + - gold: gold points of the user. + - experience: experience points of the user. + """ + user = models.OneToOneField(CustomUser, on_delete=models.CASCADE) + health = models.IntegerField(default=100) + gold = models.FloatField(default=0.0) + experience = models.FloatField(default=0) + strength = models.IntegerField(default=1, + validators=[MinValueValidator(1), + MaxValueValidator(100)]) + intelligence = models.IntegerField(default=1, validators=[MinValueValidator(1), + MaxValueValidator(100)]) + endurance = models.IntegerField(default=1, validators=[MinValueValidator(1), + MaxValueValidator(100)]) + perception = models.IntegerField(default=1, validators=[MinValueValidator(1), + MaxValueValidator(100)]) + luck = models.IntegerField(default=random_luck, validators=[MinValueValidator(1), + MaxValueValidator(50)],) + \ No newline at end of file From 661b178715fdb83813d61fc215bc47d915252bc1 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 13:02:25 +0700 Subject: [PATCH 04/26] Add Level in UserStats --- backend/tasks/models.py | 11 +++++++++++ backend/users/models.py | 10 +++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 7677acb..06be2f1 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -110,6 +110,17 @@ class Task(models.Model): else: return 4 + def get_exp(self): + return self.user.level * (0.2*self.difficulty) * (0.3*self.user.userstats.luck) + + def get_reward(self): + pass + + def get_penalty(self): + pass + + + def save(self, *args, **kwargs): self.priority = self.calculate_eisenhower_matrix_category() super(Task, self).save(*args, **kwargs) diff --git a/backend/users/models.py b/backend/users/models.py index ce906f9..56d6432 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -1,4 +1,5 @@ import random +import math from django.db import models from django.utils import timezone @@ -30,6 +31,10 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'first_name'] + def save(self, *args, **kwargs): + UserStats.objects.get_or_create(user=self) + super(CustomUser, self).save(*args, **kwargs) + def __str__(self): # String representation of the user return self.username @@ -61,4 +66,7 @@ class UserStats(models.Model): MaxValueValidator(100)]) luck = models.IntegerField(default=random_luck, validators=[MinValueValidator(1), MaxValueValidator(50)],) - \ No newline at end of file + + @property + def level(self): + return (math.pow(self.experience, 2) // 225) + 1 \ No newline at end of file From 568f739b559522cc34d9d101025f4a9433974492 Mon Sep 17 00:00:00 2001 From: Wissarut Kanasub Date: Mon, 6 Nov 2023 13:02:30 +0700 Subject: [PATCH 05/26] Calendar page adding --- frontend/src/components/calendar/calendar.jsx | 2 +- frontend/src/components/calendar/calendarPage.jsx | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/calendar/calendarPage.jsx diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index 00cdc9c..2c6dbf7 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -12,7 +12,7 @@ const Calendar = () => { events={[ { title: 'Event 1', date: '2023-11-10' }, { title: 'Event 2', date: '2023-11-15' }, - // Add more events as needed + { title: 'Event 2', date: '2023-11-15' }, ]} headerToolbar={{ start: 'prev,next', diff --git a/frontend/src/components/calendar/calendarPage.jsx b/frontend/src/components/calendar/calendarPage.jsx new file mode 100644 index 0000000..7b5dbd7 --- /dev/null +++ b/frontend/src/components/calendar/calendarPage.jsx @@ -0,0 +1,11 @@ +import React from 'react' + +function calendarPage() { + return ( +
+ +
+ ) +} + +export default calendarPage From 0c256bf003f2b9ee104e39600d1476b14ee511ab Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 13:04:28 +0700 Subject: [PATCH 06/26] Remove unready stat calculation --- backend/tasks/models.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 06be2f1..7677acb 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -110,17 +110,6 @@ class Task(models.Model): else: return 4 - def get_exp(self): - return self.user.level * (0.2*self.difficulty) * (0.3*self.user.userstats.luck) - - def get_reward(self): - pass - - def get_penalty(self): - pass - - - def save(self, *args, **kwargs): self.priority = self.calculate_eisenhower_matrix_category() super(Task, self).save(*args, **kwargs) From 89fb6186988ec89291e16792deb767178123b44e Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 18:34:41 +0700 Subject: [PATCH 07/26] Use signal to create UserStats Instead --- backend/users/apps.py | 3 +++ backend/users/models.py | 4 ---- backend/users/signals.py | 9 +++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 backend/users/signals.py diff --git a/backend/users/apps.py b/backend/users/apps.py index 72b1401..434524e 100644 --- a/backend/users/apps.py +++ b/backend/users/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class UsersConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'users' + + def ready(self): + import users.signals \ No newline at end of file diff --git a/backend/users/models.py b/backend/users/models.py index 56d6432..c2eb9fd 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -31,10 +31,6 @@ class CustomUser(AbstractBaseUser, PermissionsMixin): USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username', 'first_name'] - def save(self, *args, **kwargs): - UserStats.objects.get_or_create(user=self) - super(CustomUser, self).save(*args, **kwargs) - def __str__(self): # String representation of the user return self.username diff --git a/backend/users/signals.py b/backend/users/signals.py new file mode 100644 index 0000000..817986b --- /dev/null +++ b/backend/users/signals.py @@ -0,0 +1,9 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from users.models import CustomUser, UserStats + +@receiver(post_save, sender=CustomUser) +def create_user_stats(sender, instance, created, **kwargs): + if created: + UserStats.objects.create(user=instance) \ No newline at end of file From 66f1b7dccc36c99d9cffb093d540378e717c2c89 Mon Sep 17 00:00:00 2001 From: THIS ONE IS A LITTLE BIT TRICKY KRUB Date: Mon, 6 Nov 2023 20:14:17 +0700 Subject: [PATCH 08/26] Working on kanban table. --- frontend/package.json | 4 + frontend/pnpm-lock.yaml | 130 ++++++++++++++++++ frontend/src/App.jsx | 4 +- .../components/kanbanBoard/kanbanBoard.jsx | 66 +++++---- 4 files changed, 179 insertions(+), 25 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 7ff9906..6f616fb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "preview": "vite preview" }, "dependencies": { + "@asseinfo/react-kanban": "^2.2.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@material-ui/core": "^4.12.4", @@ -18,6 +19,8 @@ "@mui/material": "^5.14.15", "@mui/system": "^5.14.15", "@react-oauth/google": "^0.11.1", + "@syncfusion/ej2-base": "^23.1.41", + "@syncfusion/ej2-kanban": "^23.1.36", "axios": "^1.5.1", "bootstrap": "^5.3.2", "dotenv": "^16.3.1", @@ -25,6 +28,7 @@ "gapi-script": "^1.2.0", "jwt-decode": "^4.0.0", "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", "react-dom": "^18.2.0", "react-google-login": "^5.2.2", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 73b01f7..27511eb 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@asseinfo/react-kanban': + specifier: ^2.2.0 + version: 2.2.0(react-dom@18.2.0)(react@18.2.0) '@emotion/react': specifier: ^11.11.1 version: 11.11.1(@types/react@18.2.33)(react@18.2.0) @@ -29,6 +32,12 @@ dependencies: '@react-oauth/google': specifier: ^0.11.1 version: 0.11.1(react-dom@18.2.0)(react@18.2.0) + '@syncfusion/ej2-base': + specifier: ^23.1.41 + version: 23.1.41 + '@syncfusion/ej2-kanban': + specifier: ^23.1.36 + version: 23.1.36 axios: specifier: ^1.5.1 version: 1.5.1 @@ -50,6 +59,9 @@ dependencies: react: specifier: ^18.2.0 version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) react-bootstrap: specifier: ^2.9.1 version: 2.9.1(@types/react@18.2.33)(react-dom@18.2.0)(react@18.2.0) @@ -121,6 +133,19 @@ packages: '@jridgewell/trace-mapping': 0.3.20 dev: true + /@asseinfo/react-kanban@2.2.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-/gCigrNXRHeP9VCo8RipTOrA0vAPRIOThJhR4ibVxe6BLkaWFUEuJ1RMT4fODpRRsE3XsdrfVGKkfpRBKgvxXg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 + react-dom: ^16.8.0 || ^17.0.0 + dependencies: + react: 18.2.0 + react-beautiful-dnd: 13.1.1(react-dom@18.2.0)(react@18.2.0) + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + /@babel/code-frame@7.22.13: resolution: {integrity: sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==} engines: {node: '>=6.9.0'} @@ -1173,6 +1198,111 @@ packages: tslib: 2.6.2 dev: false + /@syncfusion/ej2-base@23.1.41: + resolution: {integrity: sha512-ROtvuLIVkKl4eL+ubQjQQLleRMY98nYlxlBaFw4axtiDLoBbzOYtiFXmdP/KE+uNrMquZAwl+aduPX0loG3EAw==} + hasBin: true + dependencies: + '@syncfusion/ej2-icons': 23.1.36 + dev: false + + /@syncfusion/ej2-buttons@23.1.43: + resolution: {integrity: sha512-Jg1cC/2o6ds+xDwQSlAF7cEYP4O4C9wojKFllcVvZTwTKWVxZ2KfYeAFM1kV1GR/WuJd+TmvNhwkvmkWz8UgQw==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + dev: false + + /@syncfusion/ej2-data@23.1.43: + resolution: {integrity: sha512-kiuyuKsVogLvIP72Bd2IwETk9mOpZrbtfbOx5/y7Q1A05FQkaxlJqKFoOjkzbfMtKV5iDjAxz/Q3VHqq+hZoYA==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + dev: false + + /@syncfusion/ej2-dropdowns@23.1.43: + resolution: {integrity: sha512-75tWTEoEJU/fJ8V3CvwOeQ7svZoyftlVp7DO3oSl6nR4LuyG0Uf7BP+bSMvKVXuEyulyZmHo1/xeqjYoVp4M7g==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-data': 23.1.43 + '@syncfusion/ej2-inputs': 23.1.43 + '@syncfusion/ej2-lists': 23.1.43 + '@syncfusion/ej2-navigations': 23.1.43 + '@syncfusion/ej2-notifications': 23.1.40 + '@syncfusion/ej2-popups': 23.1.43 + dev: false + + /@syncfusion/ej2-icons@23.1.36: + resolution: {integrity: sha512-Q7S50bOzXL9X46doNIGSGr61RCY/1RW9iz1U7yyARr2XBQhWnijk+t/FVBk1piR0nioRXbKQcPZOLiEo6zf1xw==} + dev: false + + /@syncfusion/ej2-inputs@23.1.43: + resolution: {integrity: sha512-G4HhneF2HXe6Tcig5pDlTaEgR46vQJgshcEym6WEVm0G39xiUSR5NIvyvS54IldGRyWwoyWEucbpM1kWGJC+7g==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + '@syncfusion/ej2-popups': 23.1.43 + '@syncfusion/ej2-splitbuttons': 23.1.43 + dev: false + + /@syncfusion/ej2-kanban@23.1.36: + resolution: {integrity: sha512-qkp7ZS+o40I9oWj/lDJ4D20ygsOuDmjJ1CEkpz2xQgzXuHcA+/89OGeKS8WGFtK2uPSl+U2ub2PdopXF4OqL+w==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + '@syncfusion/ej2-data': 23.1.43 + '@syncfusion/ej2-dropdowns': 23.1.43 + '@syncfusion/ej2-inputs': 23.1.43 + '@syncfusion/ej2-layouts': 23.1.36 + '@syncfusion/ej2-navigations': 23.1.43 + '@syncfusion/ej2-notifications': 23.1.40 + '@syncfusion/ej2-popups': 23.1.43 + dev: false + + /@syncfusion/ej2-layouts@23.1.36: + resolution: {integrity: sha512-vS1KpUxLcrFNjRgkaotWWMmja9x1XjY06r70P/1JbxT66oJZwv5YRfBBqT1rFeYEWUGm0ciMEMehyTsPE3Pefw==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + dev: false + + /@syncfusion/ej2-lists@23.1.43: + resolution: {integrity: sha512-AoEpXn7F8AmXq74PmGCqQjURHnriyhimVreenDWrB1HlMUPcAw7Lkep4gYQdJXYyfWlT+GE+lr6lfS7hgAFT+A==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + '@syncfusion/ej2-data': 23.1.43 + dev: false + + /@syncfusion/ej2-navigations@23.1.43: + resolution: {integrity: sha512-3eiMnciQx8X5FSQd4CONd5yZfTvphcgH3U+tQNFecntCh/3hfoYoj0yGkayakIR0DP08YE32y3/8yztwK+GwUQ==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + '@syncfusion/ej2-data': 23.1.43 + '@syncfusion/ej2-inputs': 23.1.43 + '@syncfusion/ej2-lists': 23.1.43 + '@syncfusion/ej2-popups': 23.1.43 + dev: false + + /@syncfusion/ej2-notifications@23.1.40: + resolution: {integrity: sha512-xB0U/THNVQN09AK+5DV6f9e7YbvWi5UqhdObCdpZarAMOFUEZBtAIVQRxc1LrINRmRapGU7NMFQj1F1xU7DVyw==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + '@syncfusion/ej2-popups': 23.1.43 + dev: false + + /@syncfusion/ej2-popups@23.1.43: + resolution: {integrity: sha512-I3CT7LmJHJ4r+4kRrVMgOTUrV7xpVHkjk67ApbJFucpN++Qo18b9Qy/QetRera5mS26ZVG78T3TST3g9dvO5Zw==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-buttons': 23.1.43 + dev: false + + /@syncfusion/ej2-splitbuttons@23.1.43: + resolution: {integrity: sha512-NbT5XMCU87QPRr4svcEn/zEwIFAmwZiAYYLz2WjiuKVOnFw6htAKT81FF7X4CLl4HN0o1S7w09N9r5HQxv3mgQ==} + dependencies: + '@syncfusion/ej2-base': 23.1.41 + '@syncfusion/ej2-popups': 23.1.43 + dev: false + /@types/babel__core@7.20.3: resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==} dependencies: diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 76c6abc..47a3ef4 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -7,7 +7,7 @@ import AuthenticantionPage from './components/authentication/AuthenticationPage' import SignUpPage from './components/authentication/SignUpPage'; import NavBar from './components/Nav/Navbar'; import Home from './components/Home'; -import UncontrolledBoard from './components/kanbanBoard/kanbanBoard'; +import KanbanBoard from './components/kanbanBoard/kanbanBoard'; const App = () => { @@ -23,7 +23,7 @@ const App = () => { - + ); } diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 852fd7d..5ddfc7d 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -1,30 +1,50 @@ -import React, { useState } from "react"; -import Column from "./Column"; +import React, { useEffect } from 'react'; +import * as ej from '@syncfusion/ej2-base'; // Import necessary Syncfusion modules and styles +import '@syncfusion/ej2-base/styles/material.css'; +import '@syncfusion/ej2-buttons/styles/material.css'; +import '@syncfusion/ej2-lists/styles/material.css'; +import '@syncfusion/ej2-inputs/styles/material.css'; +import '@syncfusion/ej2-popups/styles/material.css'; +import '@syncfusion/ej2-dropdowns/styles/material.css'; +import '@syncfusion/ej2-splitbuttons/styles/material.css'; +import '@syncfusion/ej2-navigations/styles/material.css'; +import '@syncfusion/ej2-kanban/styles/material.css'; +import '@syncfusion/ej2-react-kanban/styles/material.css'; const KanbanBoard = () => { - const [tasks, setTasks] = useState([ - { id: 1, content: "Task 1" }, - { id: 2, content: "Task 2" }, - { id: 3, content: "Task 3" }, - ]); + useEffect(() => { + const kanbanData = [ + // Your Kanban data here + ]; - const [columns, setColumns] = useState({ - backlog: tasks, - inProgress: [], - done: [], - }); + const kanbanObj = new ej.kanban.Kanban({ + dataSource: kanbanData, + keyField: 'Status', + columns: [ + { headerText: 'Backlog', keyField: 'Open' }, + { headerText: 'In Progress', keyField: 'InProgress' }, + { headerText: 'Testing', keyField: 'Testing' }, + { headerText: 'Done', keyField: 'Close' } + ], + cardSettings: { + contentField: 'Summary', + headerField: 'Id', + }, + swimlaneSettings: { + keyField: 'Assignee' + } + }); - const handleTaskDelete = (taskId) => { - const updatedTasks = tasks.filter((task) => task.id !== taskId); - setTasks(updatedTasks); - - const updatedColumns = { - ...columns, - backlog: updatedTasks, - inProgress: columns.inProgress.filter((task) => task.id !== taskId), - done: columns.done.filter((task) => task.id !== taskId), + kanbanObj.appendTo('#Kanban'); + + return () => { + kanbanObj.destroy(); }; - setColumns(updatedColumns); - }; + }, []); + return ( +
+ ); +}; +export default KanbanBoard; From 4139f9be482aa36ef01c85e3c200f2dd3462f7af Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 20:35:44 +0700 Subject: [PATCH 09/26] Rename axiosapi to AuthenticationApi --- frontend/src/api/{axiosapi.jsx => AuthenticationApi.jsx} | 0 frontend/src/components/Nav/Navbar.jsx | 2 +- frontend/src/components/authentication/LoginPage.jsx | 2 +- frontend/src/components/authentication/SignUpPage.jsx | 2 +- frontend/src/components/testAuth.jsx | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/api/{axiosapi.jsx => AuthenticationApi.jsx} (100%) diff --git a/frontend/src/api/axiosapi.jsx b/frontend/src/api/AuthenticationApi.jsx similarity index 100% rename from frontend/src/api/axiosapi.jsx rename to frontend/src/api/AuthenticationApi.jsx diff --git a/frontend/src/components/Nav/Navbar.jsx b/frontend/src/components/Nav/Navbar.jsx index f88d821..5fee6cf 100644 --- a/frontend/src/components/Nav/Navbar.jsx +++ b/frontend/src/components/Nav/Navbar.jsx @@ -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'; diff --git a/frontend/src/components/authentication/LoginPage.jsx b/frontend/src/components/authentication/LoginPage.jsx index 242a9f1..b28e274 100644 --- a/frontend/src/components/authentication/LoginPage.jsx +++ b/frontend/src/components/authentication/LoginPage.jsx @@ -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(); diff --git a/frontend/src/components/authentication/SignUpPage.jsx b/frontend/src/components/authentication/SignUpPage.jsx index 7739016..2712f97 100644 --- a/frontend/src/components/authentication/SignUpPage.jsx +++ b/frontend/src/components/authentication/SignUpPage.jsx @@ -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'; diff --git a/frontend/src/components/testAuth.jsx b/frontend/src/components/testAuth.jsx index 9a822c7..abd7ba1 100644 --- a/frontend/src/components/testAuth.jsx +++ b/frontend/src/components/testAuth.jsx @@ -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'; From 39601926ffd35a5a6b0922ae8a996040c99cada5 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 22:02:11 +0700 Subject: [PATCH 10/26] Use Task Abstract model instead of Big Task model --- backend/tasks/api.py | 6 +- ...o_alter_subtask_parent_task_delete_task.py | 47 +++++++++ backend/tasks/models.py | 97 +++++++------------ backend/tasks/serializers.py | 6 +- backend/tasks/tasks/serializers.py | 10 +- backend/tasks/tasks/views.py | 10 +- backend/tasks/tests/test_deserializer.py | 6 +- backend/tasks/tests/test_task_creation.py | 12 +-- backend/tasks/tests/test_task_eisenhower.py | 10 +- backend/tasks/tests/utils.py | 4 +- 10 files changed, 113 insertions(+), 95 deletions(-) create mode 100644 backend/tasks/migrations/0010_todo_alter_subtask_parent_task_delete_task.py diff --git a/backend/tasks/api.py b/backend/tasks/api.py index 4736582..9f5d95e 100644 --- a/backend/tasks/api.py +++ b/backend/tasks/api.py @@ -7,7 +7,7 @@ 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.models import Todo from tasks.serializers import TaskUpdateSerializer @@ -30,10 +30,10 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): 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']) + task = Todo.objects.get(google_calendar_id=event['id']) serializer = TaskUpdateSerializer(instance=task, data=event) return self._validate_serializer(serializer) - except Task.DoesNotExist: + except Todo.DoesNotExist: serializer = TaskUpdateSerializer(data=event, user=request.user) return self._validate_serializer(serializer) diff --git a/backend/tasks/migrations/0010_todo_alter_subtask_parent_task_delete_task.py b/backend/tasks/migrations/0010_todo_alter_subtask_parent_task_delete_task.py new file mode 100644 index 0000000..2e6b65d --- /dev/null +++ b/backend/tasks/migrations/0010_todo_alter_subtask_parent_task_delete_task.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.6 on 2023-11-06 15:01 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0009_alter_task_options_task_importance_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Todo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField()), + ('notes', models.TextField(default='')), + ('importance', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=1)), + ('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1)), + ('challenge', models.BooleanField(default=False)), + ('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, max_length=255, null=True)), + ('start_event', models.DateTimeField(null=True)), + ('end_event', models.DateTimeField(null=True)), + ('priority', models.PositiveSmallIntegerField(choices=[(1, 'Important & Urgent'), (2, 'Important & Not Urgent'), (3, 'Not Important & Urgent'), (4, 'Not Important & Not Urgent')], default=4)), + ('tags', models.ManyToManyField(blank=True, to='tasks.tag')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='subtask', + name='parent_task', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.todo'), + ), + migrations.DeleteModel( + name='Task', + ), + ] diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 7677acb..9f79e0b 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -14,23 +14,18 @@ class Tag(models.Model): name = models.CharField(max_length=255) -class Task(models.Model): +class Todo(models.Model): """ - Represents a task, such as Habit, Daily, Todo, or Reward. - - :param type: The type of the tasks + Represents a Abstract of task, such as Habit, Daily, Todo, or Reward. + + :param user: The user who owns the task. :param title: Title of the task. :param notes: Optional additional notes for the task. :param tags: Associated tags for the task. :param completed: A boolean field indicating whether the task is completed. - :param exp: The experience values user will get from the task. - :param priority: The priority of the task (1, 2, .., 4), using Eisenhower Matrix Idea. :param importance: The importance of the task (range: 1 to 5) :param difficulty: The difficulty of the task (range: 1 to 5). - :param attribute: The attribute linked to the task - :param user: The user who owns the task. :param challenge: Associated challenge (optional). - :param reminders: A Many-to-Many relationship with Reminder. :param fromSystem: A boolean field indicating if the task is from System. :param creation_date: Creation date of the task. :param last_update: Last updated date of the task. @@ -38,63 +33,44 @@ class Task(models.Model): :param start_event: Start event of the task. :param end_event: End event(Due Date) of the task. """ - TASK_TYPES = [ - ('daily', 'Daily'), - ('habit', 'Habit'), - ('todo', 'Todo'), - ('Long Term Goal', 'Long Term Goal'), - ] + class Difficulty(models.IntegerChoices): + EASY = 1, 'Easy' + NORMAL = 2, 'Normal' + HARD = 3, 'Hard' + VERY_HARD = 4, 'Very Hard' + DEVIL = 5, 'Devil' - DIFFICULTY_CHOICES = [ - (1, 'Easy'), - (2, 'Normal'), - (3, 'Hard'), - (4, 'Very Hard'), - (5, 'Devil'), - ] - - ATTRIBUTE = [ - ('str', 'Strength'), - ('int', 'Intelligence'), - ('end', 'Endurance'), - ('per', 'Perception'), - ('luck', 'Luck'), - ] - - EISENHOWER_MATRIX = [ - (1, 'Important & Urgent'), - (2, 'Important & Not Urgent'), - (3, 'Not Important & Urgent'), - (4, 'Not Important & Not Urgent'), - ] - - type = models.CharField(max_length=15, choices=TASK_TYPES, default='habit') + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) title = models.TextField() notes = models.TextField(default='') tags = models.ManyToManyField(Tag, blank=True) - completed = models.BooleanField(default=False) - exp = models.FloatField(default=0) - priority = models.PositiveSmallIntegerField(choices=EISENHOWER_MATRIX, default=4) importance = models.PositiveSmallIntegerField(choices=[(i, str(i)) for i in range(1, 6)], default=1) - difficulty = models.PositiveSmallIntegerField(choices=DIFFICULTY_CHOICES, default=1) - attribute = models.CharField(max_length=15, choices=ATTRIBUTE, default='str') - user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) + difficulty = models.PositiveSmallIntegerField(choices=Difficulty.choices, default=Difficulty.EASY) challenge = models.BooleanField(default=False) 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) + google_calendar_id = models.CharField(max_length=255, null=True, blank=True) start_event = models.DateTimeField(null=True) end_event = models.DateTimeField(null=True) - def calculate_eisenhower_matrix_category(self): - """ - Classify the task into one of the four categories in the Eisenhower Matrix. + class Meta: + abstract = True - :return: The category of the task (1, 2, 3, or 4). - """ + +class Todo(Todo): + + class EisenhowerMatrix(models.IntegerChoices): + IMPORTANT_URGENT = 1, 'Important & Urgent' + IMPORTANT_NOT_URGENT = 2, 'Important & Not Urgent' + NOT_IMPORTANT_URGENT = 3, 'Not Important & Urgent' + NOT_IMPORTANT_NOT_URGENT = 4, 'Not Important & Not Urgent' + + priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) + + def calculate_eisenhower_matrix_category(self): if self.end_event: - time_until_due = (self.end_event - datetime.now(timezone.utc)).days + time_until_due = (self.end_event - timezone.now()).days else: time_until_due = float('inf') @@ -102,22 +78,17 @@ class Task(models.Model): importance_threshold = 3 if time_until_due <= urgency_threshold and self.importance >= importance_threshold: - return 1 + return Todo.EisenhowerMatrix.IMPORTANT_URGENT elif time_until_due > urgency_threshold and self.importance >= importance_threshold: - return 2 + return Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT elif time_until_due <= urgency_threshold and self.importance < importance_threshold: - return 3 + return Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT else: - return 4 + return Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT def save(self, *args, **kwargs): self.priority = self.calculate_eisenhower_matrix_category() - super(Task, self).save(*args, **kwargs) - - class Meta: - verbose_name = 'Task' - verbose_name_plural = 'Tasks' - + super(Todo, self).save(*args, **kwargs) class Subtask(models.Model): @@ -127,9 +98,9 @@ class Subtask(models.Model): :param completed: A boolean field indicating whether the subtask is completed. :param parent_task: The parent task of the subtask. """ + parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE) description = models.TextField() completed = models.BooleanField(default=False) - parent_task = models.ForeignKey(Task, on_delete=models.CASCADE) class UserNotification(models.Model): diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index 494920f..9204c1f 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from django.utils.dateparse import parse_datetime -from .models import Task +from .models import Todo class GoogleCalendarEventSerializer(serializers.Serializer): @@ -21,7 +21,7 @@ class TaskUpdateSerializer(serializers.ModelSerializer): class Meta: - model = Task + model = Todo fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime') def __init__(self, *args, **kwargs): @@ -30,6 +30,6 @@ class TaskUpdateSerializer(serializers.ModelSerializer): def create(self, validated_data): validated_data['user'] = self.user - task = Task.objects.create(**validated_data) + task = Todo.objects.create(**validated_data) return task \ No newline at end of file diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index 7876263..85f0281 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,21 +1,21 @@ from rest_framework import serializers -from ..models import Task +from ..models import Todo class TaskCreateSerializer(serializers.ModelSerializer): class Meta: - model = Task + model = Todo # fields = '__all__' exclude = ('tags',) def create(self, validated_data): # Create a new task with validated data - return Task.objects.create(**validated_data) + return Todo.objects.create(**validated_data) class TaskGeneralSerializer(serializers.ModelSerializer): class Meta: - model = Task + model = Todo fields = '__all__' def create(self, validated_data): # Create a new task with validated data - return Task.objects.create(**validated_data) \ No newline at end of file + return Todo.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 0f75ced..4077315 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -2,11 +2,11 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.generics import CreateAPIView, RetrieveAPIView, RetrieveUpdateAPIView, DestroyAPIView from rest_framework.permissions import IsAuthenticated -from ..models import Task +from ..models import Todo from .serializers import TaskCreateSerializer, TaskGeneralSerializer class TaskCreateView(CreateAPIView): - queryset = Task.objects.all() + queryset = Todo.objects.all() serializer_class = TaskCreateSerializer permission_classes = [IsAuthenticated] @@ -21,17 +21,17 @@ class TaskCreateView(CreateAPIView): class TaskRetrieveView(RetrieveAPIView): - queryset = Task.objects.all() + queryset = Todo.objects.all() serializer_class = TaskGeneralSerializer permission_classes = [IsAuthenticated] class TaskUpdateView(RetrieveUpdateAPIView): - queryset = Task.objects.all() + queryset = Todo.objects.all() serializer_class = TaskGeneralSerializer permission_classes = [IsAuthenticated] class TaskDeleteView(DestroyAPIView): - queryset = Task.objects.all() + queryset = Todo.objects.all() permission_classes = [IsAuthenticated] \ No newline at end of file diff --git a/backend/tasks/tests/test_deserializer.py b/backend/tasks/tests/test_deserializer.py index aa07480..492e3d2 100644 --- a/backend/tasks/tests/test_deserializer.py +++ b/backend/tasks/tests/test_deserializer.py @@ -6,7 +6,7 @@ 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 +from tasks.models import Todo class TaskUpdateSerializerTest(TestCase): def setUp(self): @@ -29,10 +29,10 @@ class TaskUpdateSerializerTest(TestCase): self.assertTrue(serializer.is_valid()) serializer.is_valid() task = serializer.save() - self.assertIsInstance(task, Task) + self.assertIsInstance(task, Todo) def test_serializer_update(self): - task = Task.objects.create(title='Original Task', notes='Original description', user=self.user) + task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user) data = { 'id': '32141cwaNcapufh8jq2conw', diff --git a/backend/tasks/tests/test_task_creation.py b/backend/tasks/tests/test_task_creation.py index af7986f..0fee97d 100644 --- a/backend/tasks/tests/test_task_creation.py +++ b/backend/tasks/tests/test_task_creation.py @@ -5,7 +5,7 @@ from rest_framework import status from rest_framework.test import APITestCase from tasks.tests.utils import create_test_user, login_user -from ..models import Task +from ..models import Todo class TaskCreateViewTests(APITestCase): def setUp(self): @@ -32,8 +32,8 @@ class TaskCreateViewTests(APITestCase): } response = self.client.post(self.url, data, format='json') self.assertEqual(response.status_code, status.HTTP_201_CREATED) - self.assertEqual(Task.objects.count(), 1) - self.assertEqual(Task.objects.get().title, 'Test Task') + self.assertEqual(Todo.objects.count(), 1) + self.assertEqual(Todo.objects.get().title, 'Test Task') def test_create_invalid_task(self): """ @@ -45,7 +45,7 @@ class TaskCreateViewTests(APITestCase): response = self.client.post(self.url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Task.objects.count(), 0) # No task should be created + self.assertEqual(Todo.objects.count(), 0) # No task should be created def test_missing_required_fields(self): """ @@ -58,7 +58,7 @@ class TaskCreateViewTests(APITestCase): response = self.client.post(self.url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Task.objects.count(), 0) # No task should be created + self.assertEqual(Todo.objects.count(), 0) # No task should be created def test_invalid_user_id(self): """ @@ -76,4 +76,4 @@ class TaskCreateViewTests(APITestCase): response = self.client.post(self.url, data, format='json') self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - self.assertEqual(Task.objects.count(), 0) # No task should be created + self.assertEqual(Todo.objects.count(), 0) # No task should be created diff --git a/backend/tasks/tests/test_task_eisenhower.py b/backend/tasks/tests/test_task_eisenhower.py index b8b3c22..ca30e5d 100644 --- a/backend/tasks/tests/test_task_eisenhower.py +++ b/backend/tasks/tests/test_task_eisenhower.py @@ -2,7 +2,7 @@ from datetime import datetime, timedelta, timezone from django.test import TestCase -from tasks.models import Task +from tasks.models import Todo from tasks.tests.utils import create_test_user class TaskModelTest(TestCase): @@ -10,28 +10,28 @@ class TaskModelTest(TestCase): self.user = create_test_user() def test_eisenhower_matrix_category(self): - task = Task(importance=2, end_event=None, user=self.user) + task = Todo(importance=2, end_event=None, user=self.user) task.save() # 'Not Important & Not Urgent' category (category 4) self.assertEqual(task.calculate_eisenhower_matrix_category(), 4) due_date = datetime.now(timezone.utc) + timedelta(days=1) - task = Task(importance=4, end_event=due_date, user=self.user) + task = Todo(importance=4, end_event=due_date, user=self.user) task.save() # 'Important & Urgent' category (category 1) self.assertEqual(task.calculate_eisenhower_matrix_category(), 1) due_date = datetime.now(timezone.utc) + timedelta(days=10) - task = Task(importance=3, end_event=due_date, user=self.user) + task = Todo(importance=3, end_event=due_date, user=self.user) task.save() # 'Important & Not Urgent' category (category 2) self.assertEqual(task.calculate_eisenhower_matrix_category(), 2) due_date = datetime.now(timezone.utc) + timedelta(days=4) - task = Task(importance=1, end_event=due_date, user=self.user) + task = Todo(importance=1, end_event=due_date, user=self.user) task.save() # 'Not Important & Urgent' category (category 3) diff --git a/backend/tasks/tests/utils.py b/backend/tasks/tests/utils.py index f33b828..b1bef0b 100644 --- a/backend/tasks/tests/utils.py +++ b/backend/tasks/tests/utils.py @@ -1,7 +1,7 @@ from rest_framework.test import APIClient from users.models import CustomUser -from ..models import Task +from ..models import Todo def create_test_user(email="testusertestuser@example.com", username="testusertestuser", @@ -61,4 +61,4 @@ def create_test_task(user, **kwargs): task_attributes = {**defaults, **kwargs} - return Task.objects.create(user=user, **task_attributes) \ No newline at end of file + return Todo.objects.create(user=user, **task_attributes) \ No newline at end of file From 3baf716c6f4a75a46c317e94e0eaa2533b68a85a Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 22:18:20 +0700 Subject: [PATCH 11/26] Use Signal to calculate Priority --- backend/tasks/apps.py | 3 +++ backend/tasks/models.py | 23 ++--------------------- backend/tasks/signals.py | 25 +++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 21 deletions(-) create mode 100644 backend/tasks/signals.py diff --git a/backend/tasks/apps.py b/backend/tasks/apps.py index 3ff3ab3..0d81307 100644 --- a/backend/tasks/apps.py +++ b/backend/tasks/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class TasksConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'tasks' + + def ready(self): + import tasks.signals \ No newline at end of file diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 9f79e0b..7dcdbc8 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -68,27 +68,8 @@ class Todo(Todo): priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) - def calculate_eisenhower_matrix_category(self): - if self.end_event: - time_until_due = (self.end_event - timezone.now()).days - else: - time_until_due = float('inf') - - urgency_threshold = 3 - importance_threshold = 3 - - if time_until_due <= urgency_threshold and self.importance >= importance_threshold: - return Todo.EisenhowerMatrix.IMPORTANT_URGENT - elif time_until_due > urgency_threshold and self.importance >= importance_threshold: - return Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT - elif time_until_due <= urgency_threshold and self.importance < importance_threshold: - return Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT - else: - return Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT - - def save(self, *args, **kwargs): - self.priority = self.calculate_eisenhower_matrix_category() - super(Todo, self).save(*args, **kwargs) + def __str__(self): + return self.title class Subtask(models.Model): diff --git a/backend/tasks/signals.py b/backend/tasks/signals.py new file mode 100644 index 0000000..af17e57 --- /dev/null +++ b/backend/tasks/signals.py @@ -0,0 +1,25 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver +from django.utils import timezone + +from tasks.models import Todo + + +@receiver(pre_save, sender=Todo) +def update_priority(sender, instance, **kwargs): + if instance.end_event: + time_until_due = (instance.end_event - timezone.now()).days + else: + time_until_due = float('inf') + + urgency_threshold = 3 + importance_threshold = 3 + + if time_until_due <= urgency_threshold and instance.importance >= importance_threshold: + instance.priority = Todo.EisenhowerMatrix.IMPORTANT_URGENT + elif time_until_due > urgency_threshold and instance.importance >= importance_threshold: + instance.priority = Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT + elif time_until_due <= urgency_threshold and instance.importance < importance_threshold: + instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT + else: + instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT \ No newline at end of file From 792e1f5276b7026271b5ea70790dfad6e2b1460b Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 22:18:28 +0700 Subject: [PATCH 12/26] Rewrite test for Todo --- backend/tasks/tests/test_task_eisenhower.py | 38 ------------------- ...task_creation.py => test_todo_creation.py} | 6 +-- backend/tasks/tests/test_todo_eisenhower.py | 36 ++++++++++++++++++ 3 files changed, 39 insertions(+), 41 deletions(-) delete mode 100644 backend/tasks/tests/test_task_eisenhower.py rename backend/tasks/tests/{test_task_creation.py => test_todo_creation.py} (95%) create mode 100644 backend/tasks/tests/test_todo_eisenhower.py diff --git a/backend/tasks/tests/test_task_eisenhower.py b/backend/tasks/tests/test_task_eisenhower.py deleted file mode 100644 index ca30e5d..0000000 --- a/backend/tasks/tests/test_task_eisenhower.py +++ /dev/null @@ -1,38 +0,0 @@ -from datetime import datetime, timedelta, timezone - -from django.test import TestCase - -from tasks.models import Todo -from tasks.tests.utils import create_test_user - -class TaskModelTest(TestCase): - def setUp(self): - self.user = create_test_user() - - def test_eisenhower_matrix_category(self): - task = Todo(importance=2, end_event=None, user=self.user) - task.save() - - # 'Not Important & Not Urgent' category (category 4) - self.assertEqual(task.calculate_eisenhower_matrix_category(), 4) - - due_date = datetime.now(timezone.utc) + timedelta(days=1) - task = Todo(importance=4, end_event=due_date, user=self.user) - task.save() - - # 'Important & Urgent' category (category 1) - self.assertEqual(task.calculate_eisenhower_matrix_category(), 1) - - due_date = datetime.now(timezone.utc) + timedelta(days=10) - task = Todo(importance=3, end_event=due_date, user=self.user) - task.save() - - # 'Important & Not Urgent' category (category 2) - self.assertEqual(task.calculate_eisenhower_matrix_category(), 2) - - due_date = datetime.now(timezone.utc) + timedelta(days=4) - task = Todo(importance=1, end_event=due_date, user=self.user) - task.save() - - # 'Not Important & Urgent' category (category 3) - self.assertEqual(task.calculate_eisenhower_matrix_category(), 3) \ No newline at end of file diff --git a/backend/tasks/tests/test_task_creation.py b/backend/tasks/tests/test_todo_creation.py similarity index 95% rename from backend/tasks/tests/test_task_creation.py rename to backend/tasks/tests/test_todo_creation.py index 0fee97d..0852fbe 100644 --- a/backend/tasks/tests/test_task_creation.py +++ b/backend/tasks/tests/test_todo_creation.py @@ -7,7 +7,7 @@ from rest_framework.test import APITestCase from tasks.tests.utils import create_test_user, login_user from ..models import Todo -class TaskCreateViewTests(APITestCase): +class TodoCreateViewTests(APITestCase): def setUp(self): self.user = create_test_user() @@ -16,7 +16,7 @@ class TaskCreateViewTests(APITestCase): self.due_date = datetime.now() + timedelta(days=5) - def test_create_valid_task(self): + def test_create_valid_todo(self): """ Test creating a valid task using the API. """ @@ -35,7 +35,7 @@ class TaskCreateViewTests(APITestCase): self.assertEqual(Todo.objects.count(), 1) self.assertEqual(Todo.objects.get().title, 'Test Task') - def test_create_invalid_task(self): + def test_create_invalid_todo(self): """ Test creating an invalid task using the API. """ diff --git a/backend/tasks/tests/test_todo_eisenhower.py b/backend/tasks/tests/test_todo_eisenhower.py new file mode 100644 index 0000000..41ee078 --- /dev/null +++ b/backend/tasks/tests/test_todo_eisenhower.py @@ -0,0 +1,36 @@ +from datetime import datetime, timedelta, timezone +from django.test import TestCase +from tasks.models import Todo +from tasks.tests.utils import create_test_user + +class TodoPriorityTest(TestCase): + def setUp(self): + self.user = create_test_user() + + def test_priority_calculation(self): + # Important = 2, Till Due = none + todo = Todo(importance=2, end_event=None, user=self.user) + todo.save() + # 'Not Important & Not Urgent' + self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) + + due_date = datetime.now(timezone.utc) + timedelta(days=1) + # Important = 4, Till Due = 1 + todo = Todo(importance=4, end_event=due_date, user=self.user) + todo.save() + # 'Important & Urgent' + self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT) + + due_date = datetime.now(timezone.utc) + timedelta(days=10) + # Important = 3, Till Due = 10 + todo = Todo(importance=3, end_event=due_date, user=self.user) + todo.save() + # 'Important & Not Urgent' + self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT) + + due_date = datetime.now(timezone.utc) + timedelta(days=2) + # Important = 1, Till Due = 2 + todo = Todo(importance=1, end_event=due_date, user=self.user) + todo.save() + # 'Not Important & Urgent' + self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT) From 4100b101670bafb8e19a8d927511a4707adb5b49 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 23:14:53 +0700 Subject: [PATCH 13/26] Add recurrence tasks use rrule field --- .../tasks/migrations/0011_recurrencetask.py | 39 +++++++++++++++++++ backend/tasks/models.py | 13 ++++--- 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 backend/tasks/migrations/0011_recurrencetask.py diff --git a/backend/tasks/migrations/0011_recurrencetask.py b/backend/tasks/migrations/0011_recurrencetask.py new file mode 100644 index 0000000..cc7225a --- /dev/null +++ b/backend/tasks/migrations/0011_recurrencetask.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.6 on 2023-11-06 16:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('tasks', '0010_todo_alter_subtask_parent_task_delete_task'), + ] + + operations = [ + migrations.CreateModel( + name='RecurrenceTask', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.TextField()), + ('notes', models.TextField(default='')), + ('importance', models.PositiveSmallIntegerField(choices=[(1, '1'), (2, '2'), (3, '3'), (4, '4'), (5, '5')], default=1)), + ('difficulty', models.PositiveSmallIntegerField(choices=[(1, 'Easy'), (2, 'Normal'), (3, 'Hard'), (4, 'Very Hard'), (5, 'Devil')], default=1)), + ('challenge', models.BooleanField(default=False)), + ('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, max_length=255, null=True)), + ('start_event', models.DateTimeField(null=True)), + ('end_event', models.DateTimeField(null=True)), + ('recurrence_rule', models.TextField()), + ('tags', models.ManyToManyField(blank=True, to='tasks.tag')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/backend/tasks/models.py b/backend/tasks/models.py index 7dcdbc8..5da815e 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -1,9 +1,5 @@ -from datetime import datetime - from django.db import models from django.conf import settings -from django.core import validators -from django.utils import timezone class Tag(models.Model): """ @@ -14,7 +10,7 @@ class Tag(models.Model): name = models.CharField(max_length=255) -class Todo(models.Model): +class Task(models.Model): """ Represents a Abstract of task, such as Habit, Daily, Todo, or Reward. @@ -58,7 +54,7 @@ class Todo(models.Model): abstract = True -class Todo(Todo): +class Todo(Task): class EisenhowerMatrix(models.IntegerChoices): IMPORTANT_URGENT = 1, 'Important & Urgent' @@ -71,6 +67,11 @@ class Todo(Todo): def __str__(self): return self.title +class RecurrenceTask(Task): + recurrence_rule = models.TextField() + + def __str__(self) -> str: + return f"{self.title} ({self.recurrence_rule})" class Subtask(models.Model): """ From 6f47b107495309504d48a4ffdd69df7bd9849518 Mon Sep 17 00:00:00 2001 From: sosokker Date: Mon, 6 Nov 2023 23:58:40 +0700 Subject: [PATCH 14/26] Rename Serializer and Skip Reccurence task mapping for now --- backend/tasks/api.py | 20 ++++++++------- backend/tasks/serializers.py | 32 +++++++++++++++++++++--- backend/tasks/tests/test_deserializer.py | 6 ++--- 3 files changed, 43 insertions(+), 15 deletions(-) diff --git a/backend/tasks/api.py b/backend/tasks/api.py index 9f5d95e..5352f9a 100644 --- a/backend/tasks/api.py +++ b/backend/tasks/api.py @@ -7,8 +7,8 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from tasks.utils import get_service -from tasks.models import Todo -from tasks.serializers import TaskUpdateSerializer +from tasks.models import Todo, RecurrenceTask +from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer class GoogleCalendarEventViewset(viewsets.ViewSet): @@ -17,28 +17,30 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): 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)' + self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)' def _validate_serializer(self, serializer): if serializer.is_valid(): serializer.save() - return Response("Task Sync Successfully", status=200) + 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 try: task = Todo.objects.get(google_calendar_id=event['id']) - serializer = TaskUpdateSerializer(instance=task, data=event) + serializer = TodoUpdateSerializer(instance=task, data=event) return self._validate_serializer(serializer) except Todo.DoesNotExist: - serializer = TaskUpdateSerializer(data=event, user=request.user) + 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=3)).isoformat() + max_time = (datetime.now(tz=timezone.utc) + timedelta(days=days)).isoformat() service = get_service(request) events = [] @@ -49,11 +51,11 @@ class GoogleCalendarEventViewset(viewsets.ViewSet): calendarId='primary', timeMin=self.current_time, timeMax=max_time, - maxResults=20, + maxResults=200, singleEvents=True, orderBy='startTime', pageToken=next_page_token, - fields='items(id,summary,description,created,updated,start,end)', + fields='items(id,summary,description,created,recurringEventId,updated,start,end)', ) page_results = query.execute() diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index 9204c1f..ed02300 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers from django.utils.dateparse import parse_datetime -from .models import Todo +from .models import Todo, RecurrenceTask class GoogleCalendarEventSerializer(serializers.Serializer): @@ -10,7 +10,7 @@ class GoogleCalendarEventSerializer(serializers.Serializer): description = serializers.CharField(required=False) -class TaskUpdateSerializer(serializers.ModelSerializer): +class TodoUpdateSerializer(serializers.ModelSerializer): id = serializers.CharField(source="google_calendar_id") summary = serializers.CharField(source="title") description = serializers.CharField(source="notes", required=False) @@ -26,10 +26,36 @@ class TaskUpdateSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) - super(TaskUpdateSerializer, self).__init__(*args, **kwargs) + super(TodoUpdateSerializer, self).__init__(*args, **kwargs) def create(self, validated_data): validated_data['user'] = self.user task = Todo.objects.create(**validated_data) + return task + + +class RecurrenceTaskUpdateSerializer(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") + recurrence = serializers.DateTimeField(source="recurrence_rule") + start_datetime = serializers.DateTimeField(source="start_event", required=False) + end_datetime = serializers.DateTimeField(source="end_event", required=False) + + + class Meta: + model = RecurrenceTask + fields = ('id', 'summary', 'description', 'created', 'updated', 'recurrence', 'start_datetime', 'end_datetime') + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) + super(RecurrenceTaskUpdateSerializer, self).__init__(*args, **kwargs) + + def create(self, validated_data): + validated_data['user'] = self.user + task = RecurrenceTask.objects.create(**validated_data) + return task \ No newline at end of file diff --git a/backend/tasks/tests/test_deserializer.py b/backend/tasks/tests/test_deserializer.py index 492e3d2..306185c 100644 --- a/backend/tasks/tests/test_deserializer.py +++ b/backend/tasks/tests/test_deserializer.py @@ -5,7 +5,7 @@ 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.serializers import TodoUpdateSerializer from tasks.models import Todo class TaskUpdateSerializerTest(TestCase): @@ -25,7 +25,7 @@ class TaskUpdateSerializerTest(TestCase): 'end_datetie': self.end_time, } - serializer = TaskUpdateSerializer(data=data, user=self.user) + serializer = TodoUpdateSerializer(data=data, user=self.user) self.assertTrue(serializer.is_valid()) serializer.is_valid() task = serializer.save() @@ -44,7 +44,7 @@ class TaskUpdateSerializerTest(TestCase): 'end_datetie': self.end_time, } - serializer = TaskUpdateSerializer(instance=task, data=data) + serializer = TodoUpdateSerializer(instance=task, data=data) self.assertTrue(serializer.is_valid()) updated_task = serializer.save() From a403e45f9fc9f0ca2fd41a49ed5880bf4f00fa58 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 7 Nov 2023 03:23:20 +0700 Subject: [PATCH 15/26] Use ModelViewset for Todo and Add some fields before validate --- backend/tasks/api.py | 4 ++++ backend/tasks/tasks/views.py | 37 ++++++++---------------------------- backend/tasks/urls.py | 14 +++++++------- 3 files changed, 19 insertions(+), 36 deletions(-) diff --git a/backend/tasks/api.py b/backend/tasks/api.py index 5352f9a..7669d76 100644 --- a/backend/tasks/api.py +++ b/backend/tasks/api.py @@ -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) diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index 4077315..d884bbe 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -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] \ No newline at end of file + def get_serializer_class(self): + # Can't add ManytoMany at creation time (Tags) + if self.action == 'create': + return TaskCreateSerializer + return TaskGeneralSerializer \ No newline at end of file diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py index c04bd68..b44ddd9 100644 --- a/backend/tasks/urls.py +++ b/backend/tasks/urls.py @@ -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//', TaskRetrieveView.as_view(), name='retrieve-task'), - path('tasks//update/', TaskUpdateView.as_view(), name='update-task'), - path('tasks//delete/', TaskDeleteView.as_view(), name='delete-task'), ] \ No newline at end of file From 02e76f88c58c37828ec4e552d3e274b8ecd0754e Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 7 Nov 2023 03:24:58 +0700 Subject: [PATCH 16/26] Fetch event data from api and show on fullcalendar --- frontend/package.json | 1 + frontend/pnpm-lock.yaml | 3 + frontend/src/App.jsx | 4 +- frontend/src/api/TaskApi.jsx | 23 +++ .../components/calendar/TaskDataHandler.jsx | 42 +++++ frontend/src/components/calendar/calendar.jsx | 147 +++++++++++++++--- .../src/components/calendar/calendarPage.jsx | 11 -- frontend/src/components/calendar/index.css | 55 +++++++ 8 files changed, 250 insertions(+), 36 deletions(-) create mode 100644 frontend/src/api/TaskApi.jsx create mode 100644 frontend/src/components/calendar/TaskDataHandler.jsx delete mode 100644 frontend/src/components/calendar/calendarPage.jsx create mode 100644 frontend/src/components/calendar/index.css diff --git a/frontend/package.json b/frontend/package.json index 1a78f51..a7f5f04 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 271029a..a23ba2c 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,6 +11,9 @@ 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) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e9bb13b..cfa3a5a 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -6,7 +6,8 @@ import LoginPage from './components/authentication/LoginPage'; import SignUpPage from './components/authentication/SignUpPage'; 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 = () => { }/> }/> }/> + }/> diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx new file mode 100644 index 0000000..e56662b --- /dev/null +++ b/frontend/src/api/TaskApi.jsx @@ -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; + }); +}; \ No newline at end of file diff --git a/frontend/src/components/calendar/TaskDataHandler.jsx b/frontend/src/components/calendar/TaskDataHandler.jsx new file mode 100644 index 0000000..3c123e9 --- /dev/null +++ b/frontend/src/components/calendar/TaskDataHandler.jsx @@ -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++); +} \ No newline at end of file diff --git a/frontend/src/components/calendar/calendar.jsx b/frontend/src/components/calendar/calendar.jsx index 2c6dbf7..443d4bc 100644 --- a/frontend/src/components/calendar/calendar.jsx +++ b/frontend/src/components/calendar/calendar.jsx @@ -1,28 +1,127 @@ -import React from 'react'; -import FullCalendar from '@fullcalendar/react'; -import dayGridPlugin from '@fullcalendar/daygrid'; -import timeGridPlugin from '@fullcalendar/timegrid'; +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' -const Calendar = () => { +export default class Calendar extends React.Component { + state = { + weekendsVisible: true, + currentEvents: [], + }; + + render() { + return ( +
+ {this.renderSidebar()} +
+ +
+
+ ); + } + + renderSidebar() { + return ( +
+
+

Instructions

+
    +
  • Select dates and you will be prompted to create a new event
  • +
  • Drag, drop, and resize events
  • +
  • Click an event to delete it
  • +
+
+
+ +
+
+

All Events ({this.state.currentEvents.length})

+
    {this.state.currentEvents.map(renderSidebarEvent)}
+
+
+ ); + } + + 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 ( -
- -
+ <> + {eventInfo.timeText} + {eventInfo.event.title} + ); -}; +} -export default Calendar; +function renderSidebarEvent(event) { + return ( +
  • + {formatDate(event.start, { year: "numeric", month: "short", day: "numeric" })} + {event.title} +
  • + ); +} diff --git a/frontend/src/components/calendar/calendarPage.jsx b/frontend/src/components/calendar/calendarPage.jsx deleted file mode 100644 index 7b5dbd7..0000000 --- a/frontend/src/components/calendar/calendarPage.jsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' - -function calendarPage() { - return ( -
    - -
    - ) -} - -export default calendarPage diff --git a/frontend/src/components/calendar/index.css b/frontend/src/components/calendar/index.css new file mode 100644 index 0000000..e5086bd --- /dev/null +++ b/frontend/src/components/calendar/index.css @@ -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; +} From 5e0b4012f5f9f085503c67ba41d145b5d49648a8 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 7 Nov 2023 03:32:24 +0700 Subject: [PATCH 17/26] rename nav folder --- frontend/src/App.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index cfa3a5a..67360d3 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -4,7 +4,7 @@ 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 Calendar from './components/calendar/calendar'; From 67a30ce114f02ac1d2e73870da167ca0e1623e88 Mon Sep 17 00:00:00 2001 From: sosokker Date: Tue, 7 Nov 2023 03:38:51 +0700 Subject: [PATCH 18/26] Update test to match Todoviewset --- backend/tasks/tests/test_todo_creation.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/backend/tasks/tests/test_todo_creation.py b/backend/tasks/tests/test_todo_creation.py index 0852fbe..9912126 100644 --- a/backend/tasks/tests/test_todo_creation.py +++ b/backend/tasks/tests/test_todo_creation.py @@ -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 From fc66908d701352135228dabb0919bf969a2d7fe8 Mon Sep 17 00:00:00 2001 From: Sirin Puenggun Date: Tue, 7 Nov 2023 23:15:25 +0700 Subject: [PATCH 19/26] Update wiki page link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 803f355..70ab41a 100644 --- a/README.md +++ b/README.md @@ -6,4 +6,4 @@ TurTask is a task and project management tool that incorporates gamification elements. -[Wiki Repository](https://github.com/TurTaskProject/TurTaskWiki) +[Wiki Page](https://github.com/TurTaskProject/TurTaskWeb/wiki) From 2898668579d2766bc7a170a19c87cce62aa68fa9 Mon Sep 17 00:00:00 2001 From: Sirin Puenggun Date: Wed, 8 Nov 2023 23:55:45 +0700 Subject: [PATCH 20/26] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 70ab41a..a583ea1 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,5 @@ TurTask is a task and project management tool that incorporates gamification elements. [Wiki Page](https://github.com/TurTaskProject/TurTaskWeb/wiki) + +[Project Board](https://github.com/orgs/TurTaskProject/projects/1) From c8a6d66ea11a08cd23144657b82c909f3e0b4297 Mon Sep 17 00:00:00 2001 From: Pattadon Date: Fri, 10 Nov 2023 12:57:28 +0700 Subject: [PATCH 21/26] Constructing kanban system. --- .../kanbanBoard/columnContainer.jsx | 179 +++++++++ .../components/kanbanBoard/kanbanBoard.jsx | 342 ++++++++++++++++++ .../src/components/kanbanBoard/taskCard.jsx | 111 ++++++ 3 files changed, 632 insertions(+) create mode 100644 frontend/src/components/kanbanBoard/columnContainer.jsx create mode 100644 frontend/src/components/kanbanBoard/kanbanBoard.jsx create mode 100644 frontend/src/components/kanbanBoard/taskCard.jsx diff --git a/frontend/src/components/kanbanBoard/columnContainer.jsx b/frontend/src/components/kanbanBoard/columnContainer.jsx new file mode 100644 index 0000000..03bc011 --- /dev/null +++ b/frontend/src/components/kanbanBoard/columnContainer.jsx @@ -0,0 +1,179 @@ +import { SortableContext, useSortable } from "@dnd-kit/sortable"; +import TrashIcon from "../icons/TrashIcon"; +import { Column, Id, Task } from "../types"; +import { CSS } from "@dnd-kit/utilities"; +import { useMemo, useState } from "react"; +import PlusIcon from "../icons/PlusIcon"; +import TaskCard from "./taskCard"; + +function ColumnContainer({ + column, + deleteColumn, + updateColumn, + createTask, + tasks, + deleteTask, + updateTask, +}) { + const [editMode, setEditMode] = useState(false); + + const tasksIds = useMemo(() => { + return tasks.map((task) => task.id); + }, [tasks]); + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging, + } = useSortable({ + id: column.id, + data: { + type: "Column", + column, + }, + disabled: editMode, + }); + + const style = { + transition, + transform: CSS.Transform.toString(transform), + }; + + if (isDragging) { + return ( +
    + ); + } + + return ( +
    + {/* Column title */} +
    { + setEditMode(true); + }} + className=" + bg-mainBackgroundColor + text-md + h-[60px] + cursor-grab + rounded-md + rounded-b-none + p-3 + font-bold + border-columnBackgroundColor + border-4 + flex + items-center + justify-between + " + > +
    +
    + 0 +
    + {!editMode && column.title} + {editMode && ( + updateColumn(column.id, e.target.value)} + autoFocus + onBlur={() => { + setEditMode(false); + }} + onKeyDown={(e) => { + if (e.key !== "Enter") return; + setEditMode(false); + }} + /> + )} +
    + +
    + + {/* Column task container */} +
    + + {tasks.map((task) => ( + + ))} + +
    + {/* Column footer */} + +
    + ); +} + +export default ColumnContainer; diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx new file mode 100644 index 0000000..a289652 --- /dev/null +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -0,0 +1,342 @@ +import PlusIcon from "../icons/PlusIcon"; +import { useMemo, useState } from "react"; +import { Column, Id, Task } from "../types"; +import ColumnContainer from "./columnContainer"; +import { + DndContext, + DragEndEvent, + DragOverEvent, + DragOverlay, + DragStartEvent, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { SortableContext, arrayMove } from "@dnd-kit/sortable"; +import { createPortal } from "react-dom"; +import TaskCard from "./taskCard"; + +const defaultCols = [ + { + id: "todo", + title: "Todo", + }, + { + id: "doing", + title: "Work in progress", + }, + { + id: "done", + title: "Done", + }, +]; + +const defaultTasks = [ + { + id: "1", + columnId: "todo", + content: "List admin APIs for dashboard", + }, + { + id: "2", + columnId: "todo", + content: + "Develop user registration functionality with OTP delivered on SMS after email confirmation and phone number confirmation", + }, + { + id: "3", + columnId: "doing", + content: "Conduct security testing", + }, + { + id: "4", + columnId: "doing", + content: "Analyze competitors", + }, + { + id: "5", + columnId: "done", + content: "Create UI kit documentation", + }, + { + id: "6", + columnId: "done", + content: "Dev meeting", + }, + { + id: "7", + columnId: "done", + content: "Deliver dashboard prototype", + }, + { + id: "8", + columnId: "todo", + content: "Optimize application performance", + }, + { + id: "9", + columnId: "todo", + content: "Implement data validation", + }, + { + id: "10", + columnId: "todo", + content: "Design database schema", + }, + { + id: "11", + columnId: "todo", + content: "Integrate SSL web certificates into workflow", + }, + { + id: "12", + columnId: "doing", + content: "Implement error logging and monitoring", + }, + { + id: "13", + columnId: "doing", + content: "Design and implement responsive UI", + }, +]; + +function KanbanBoard() { + const [columns, setColumns] = useState(defaultCols); + const columnsId = useMemo(() => columns.map((col) => col.id), [columns]); + + const [tasks, setTasks] = useState(defaultTasks); + + const [activeColumn, setActiveColumn] = useState(null); + + const [activeTask, setActiveTask] = useState(null); + + const sensors = useSensors( + useSensor(PointerSensor, { + activationConstraint: { + distance: 10, + }, + }) + ); + + return ( +
    + +
    +
    + + {columns.map((col) => ( + task.columnId === col.id)} + /> + ))} + +
    + +
    + + {createPortal( + + {activeColumn && ( + task.columnId === activeColumn.id + )} + /> + )} + {activeTask && ( + + )} + , + document.body + )} +
    +
    + ); + + function createTask(columnId) { + const newTask = { + id: generateId(), + columnId, + content: `Task ${tasks.length + 1}`, + }; + + setTasks([...tasks, newTask]); + } + + function deleteTask(id) { + const newTasks = tasks.filter((task) => task.id !== id); + setTasks(newTasks); + } + + function updateTask(id, content) { + const newTasks = tasks.map((task) => { + if (task.id !== id) return task; + return { ...task, content }; + }); + + setTasks(newTasks); + } + + function createNewColumn() { + const columnToAdd = { + id: generateId(), + title: `Column ${columns.length + 1}`, + }; + + setColumns([...columns, columnToAdd]); + } + + function deleteColumn(id) { + const filteredColumns = columns.filter((col) => col.id !== id); + setColumns(filteredColumns); + + const newTasks = tasks.filter((t) => t.columnId !== id); + setTasks(newTasks); + } + + function updateColumn(id, title) { + const newColumns = columns.map((col) => { + if (col.id !== id) return col; + return { ...col, title }; + }); + + setColumns(newColumns); + } + + function onDragStart(event) { + if (event.active.data.current?.type === "Column") { + setActiveColumn(event.active.data.current.column); + return; + } + + if (event.active.data.current?.type === "Task") { + setActiveTask(event.active.data.current.task); + return; + } + } + + function onDragEnd(event) { + setActiveColumn(null); + setActiveTask(null); + + const { active, over } = event; + if (!over) return; + + const activeId = active.id; + const overId = over.id; + + if (activeId === overId) return; + + const isActiveAColumn = active.data.current?.type === "Column"; + if (!isActiveAColumn) return; + + setColumns((columns) => { + const activeColumnIndex = columns.findIndex((col) => col.id === activeId); + + const overColumnIndex = columns.findIndex((col) => col.id === overId); + + return arrayMove(columns, activeColumnIndex, overColumnIndex); + }); + } + + function onDragOver(event) { + const { active, over } = event; + if (!over) return; + + const activeId = active.id; + const overId = over.id; + + if (activeId === overId) return; + + const isActiveATask = active.data.current?.type === "Task"; + const isOverATask = over.data.current?.type === "Task"; + + if (!isActiveATask) return; + + if (isActiveATask && isOverATask) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + const overIndex = tasks.findIndex((t) => t.id === overId); + + if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) { + tasks[activeIndex].columnId = tasks[overIndex].columnId; + return arrayMove(tasks, activeIndex, overIndex - 1); + } + + return arrayMove(tasks, activeIndex, overIndex); + }); + } + + const isOverAColumn = over.data.current?.type === "Column"; + + if (isActiveATask && isOverAColumn) { + setTasks((tasks) => { + const activeIndex = tasks.findIndex((t) => t.id === activeId); + + tasks[activeIndex].columnId = overId; + return arrayMove(tasks, activeIndex, activeIndex); + }); + } + } + + function generateId() { + return Math.floor(Math.random() * 10001); + } +} + +export default KanbanBoard; diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx new file mode 100644 index 0000000..226b319 --- /dev/null +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -0,0 +1,111 @@ +import { useState } from "react"; +import TrashIcon from "../icons/TrashIcon"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +function TaskCard({ task, deleteTask, updateTask }) { + const [mouseIsOver, setMouseIsOver] = useState(false); + const [editMode, setEditMode] = useState(true); + + const { + setNodeRef, + attributes, + listeners, + transform, + transition, + isDragging, + } = useSortable({ + id: task.id, + data: { + type: "Task", + task, + }, + disabled: editMode, + }); + + const style = { + transition, + transform: CSS.Transform.toString(transform), + }; + + const toggleEditMode = () => { + setEditMode((prev) => !prev); + setMouseIsOver(false); + }; + + if (isDragging) { + return ( +
    + ); + } + + if (editMode) { + return ( +
    +