Merge branch 'feature/kanban-board' of https://github.com/TurTaskProject/TurTaskWeb into feature/kanban-board

This commit is contained in:
Pattadon 2023-11-24 22:17:18 +07:00
commit 4e1580f7d1
117 changed files with 5330 additions and 2097 deletions

View File

@ -2,13 +2,12 @@ name: Django CI
on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
services:
@ -24,20 +23,21 @@ jobs:
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: 3.11
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
run: |
cd backend
python manage.py migrate
- name: Run tests
run: |
cd backend
python manage.py test
- uses: actions/checkout@v2
- name: Set up Python 3.11
uses: actions/setup-python@v2
with:
python-version: 3.11
- name: Install dependencies
run: |
cd backend
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run migrations
run: |
cd backend
python manage.py migrate
- name: Run tests
run: |
cd backend
python manage.py test

1
.gitignore vendored
View File

@ -57,7 +57,6 @@ cover/
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal

View File

@ -1,12 +1,11 @@
from django.urls import path
from rest_framework_simplejwt import views as jwt_views
from authentications.views import ObtainTokenPairWithCustomView, GreetingView, GoogleLogin, GoogleRetrieveUserInfo
from authentications.views import ObtainTokenPairWithCustomView, GoogleRetrieveUserInfo, CheckAccessTokenAndRefreshToken
urlpatterns = [
path('token/obtain/', jwt_views.TokenObtainPairView.as_view(), name='token_create'),
path('token/refresh/', jwt_views.TokenRefreshView.as_view(), name='token_refresh'),
path('token/custom_obtain/', ObtainTokenPairWithCustomView.as_view(), name='token_create_custom'),
path('hello/', GreetingView.as_view(), name='hello_world'),
path('dj-rest-auth/google/', GoogleLogin.as_view(), name="google_login"),
path('auth/google/', GoogleRetrieveUserInfo.as_view()),
path('auth/status/', CheckAccessTokenAndRefreshToken.as_view(), name='check_token_status')
]

View File

@ -1,6 +1,3 @@
from django.shortcuts import render
# Create your views here.
"""This module defines API views for authentication, user creation, and a simple hello message."""
import json
@ -10,14 +7,11 @@ from django.conf import settings
from django.contrib.auth.hashers import make_password
from rest_framework import status
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_simplejwt.tokens import RefreshToken
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from dj_rest_auth.registration.views import SocialLoginView
from rest_framework_simplejwt.authentication import JWTAuthentication
from google_auth_oauthlib.flow import InstalledAppFlow
@ -27,6 +21,31 @@ from users.managers import CustomAccountManager
from users.models import CustomUser
class CheckAccessTokenAndRefreshToken(APIView):
permission_classes = (AllowAny,)
JWT_authenticator = JWTAuthentication()
def post(self, request, *args, **kwargs):
access_token = request.data.get('access_token')
refresh_token = request.data.get('refresh_token')
# Check if the access token is valid
if access_token:
response = self.JWT_authenticator.authenticate(request)
if response is not None:
return Response({'status': 'true'}, status=status.HTTP_200_OK)
# Check if the refresh token is valid
if refresh_token:
try:
refresh = RefreshToken(refresh_token)
access_token = str(refresh.access_token)
return Response({'access_token': access_token}, status=status.HTTP_200_OK)
except Exception as e:
return Response({'status': 'false'}, status=status.HTTP_401_UNAUTHORIZED)
return Response({'status': 'false'}, status=status.HTTP_400_BAD_REQUEST)
class ObtainTokenPairWithCustomView(APIView):
"""
Custom Token Obtain Pair View.
@ -45,39 +64,6 @@ class ObtainTokenPairWithCustomView(APIView):
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class GreetingView(APIView):
"""
Hello World View.
Returns a greeting and user information for authenticated users.
"""
permission_classes = (IsAuthenticated,)
def get(self, request):
"""
Retrieve a greeting message and user information.
"""
user = request.user
user_info = {
"username": user.username,
}
response_data = {
"message": "Hello, world!",
"user_info": user_info,
}
return Response(response_data, status=status.HTTP_200_OK)
class GoogleLogin(SocialLoginView):
"""
Google Login View.
Handles Google OAuth2 authentication.
"""
# permission_classes = (AllowAny,)
adapter_class = GoogleOAuth2Adapter
# client_class = OAuth2Client
# callback_url = 'http://localhost:8000/accounts/google/login/callback/'
class GoogleRetrieveUserInfo(APIView):
"""
Retrieve user information from Google and create a user if not exists.
@ -165,4 +151,4 @@ class GoogleRetrieveUserInfo(APIView):
response = requests.get(api_url, headers=headers)
if response.status_code == 200:
return response.json()
raise Exception('Google API Error', response)
raise Exception('Google API Error', response)

View File

17
backend/boards/admin.py Normal file
View File

@ -0,0 +1,17 @@
from django.contrib import admin
from .models import Board, ListBoard, KanbanTaskOrder
@admin.register(Board)
class BoardAdmin(admin.ModelAdmin):
list_display = ['name', 'user']
@admin.register(ListBoard)
class ListBoardAdmin(admin.ModelAdmin):
list_display = ['name', 'position', 'board']
list_filter = ['board', 'position']
@admin.register(KanbanTaskOrder)
class KanbanTaskOrderAdmin(admin.ModelAdmin):
list_display = ['list_board', 'todo_order']
list_filter = ['list_board']

9
backend/boards/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class BoardsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'boards'
def ready(self):
import boards.signals

View File

@ -0,0 +1,35 @@
# Generated by Django 4.2.6 on 2023-11-19 19:19
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Board',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='ListBoard',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255)),
('position', models.IntegerField()),
('board', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='boards.board')),
],
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-20 18:24
import django.contrib.postgres.fields
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boards', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='KanbanTaskOrder',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('todo_order', django.contrib.postgres.fields.ArrayField(base_field=models.PositiveIntegerField(), blank=True, default=list, size=None)),
('list_board', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='boards.listboard')),
],
),
]

View File

56
backend/boards/models.py Normal file
View File

@ -0,0 +1,56 @@
from django.contrib.postgres.fields import ArrayField
from django.db import models
from users.models import CustomUser
class Board(models.Model):
"""
Kanban board model.
:param user: The user who owns the board.
:param name: The name of the board.
:param created_at: The date and time when the board was created.
"""
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self) -> str:
return f"{self.name}"
class KanbanTaskOrder(models.Model):
"""
Model to store the order of Todo tasks in a Kanban board.
:param list_board: The list board that the order belongs to.
:param todo_order: ArrayField to store the order of Todo IDs.
"""
list_board = models.OneToOneField('ListBoard', on_delete=models.CASCADE)
todo_order = ArrayField(models.PositiveIntegerField(), blank=True, default=list)
def __str__(self):
return f"Order for {self.list_board}"
class ListBoard(models.Model):
"""
List inside a Kanban board.
:param board: The board that the list belongs to.
:param name: The name of the list.
:param position: The position of the list in Kanban.
"""
board = models.ForeignKey(Board, on_delete=models.CASCADE)
name = models.CharField(max_length=255)
position = models.IntegerField()
def save(self, *args, **kwargs):
super(ListBoard, self).save(*args, **kwargs)
kanban_order, created = KanbanTaskOrder.objects.get_or_create(list_board=self)
if not created:
return
def __str__(self) -> str:
return f"{self.name}"

View File

@ -0,0 +1,13 @@
from rest_framework import serializers
from boards.models import Board, ListBoard
class BoardSerializer(serializers.ModelSerializer):
class Meta:
model = Board
fields = '__all__'
class ListBoardSerializer(serializers.ModelSerializer):
class Meta:
model = ListBoard
fields = '__all__'

17
backend/boards/signals.py Normal file
View File

@ -0,0 +1,17 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from boards.models import Board, ListBoard
from users.models import CustomUser
@receiver(post_save, sender=CustomUser)
def create_default_board(sender, instance, created, **kwargs):
"""Signal handler to automatically create a default Board for a user upon creation."""
if created:
# Create unique board by user id
user_id = instance.id
board = Board.objects.create(user=instance, name=f"Board of #{user_id}")
ListBoard.objects.create(board=board, name="Backlog", position=1)
ListBoard.objects.create(board=board, name="Doing", position=2)
ListBoard.objects.create(board=board, name="Review", position=3)
ListBoard.objects.create(board=board, name="Done", position=4)

3
backend/boards/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

11
backend/boards/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from boards.views import BoardViewSet, ListBoardViewSet
router = DefaultRouter()
router.register(r'boards', BoardViewSet, basename='board')
router.register(r'lists', ListBoardViewSet, basename='listboard')
urlpatterns = [
path('', include(router.urls)),
]

34
backend/boards/views.py Normal file
View File

@ -0,0 +1,34 @@
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from boards.models import Board, ListBoard
from boards.serializers import BoardSerializer, ListBoardSerializer
class BoardViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
queryset = Board.objects.all()
serializer_class = BoardSerializer
http_method_names = ['get']
def get_queryset(self):
queryset = Board.objects.filter(user_id=self.request.user.id)
return queryset
class ListBoardViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
serializer_class = ListBoardSerializer
def get_queryset(self):
queryset = ListBoard.objects.filter(board__user_id=self.request.user.id)
return queryset
def create(self, request, *args, **kwargs):
board_id = request.data.get('board')
board = Board.objects.get(id=board_id)
if request.user.id != board.user.id:
return Response({"error": "Cannot create ListBoard for another user's board."}, status=status.HTTP_403_FORBIDDEN)
return super().create(request, *args, **kwargs)

View File

@ -0,0 +1,269 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'dashboard',
'boards',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"http://localhost:8000",
"http://127.0.0.1:8000",
"http://localhost:5173",
]
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('DB_NAME', default='github_actions'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default='postgres'),
'HOST': config('DB_HOST', default='127.0.0.1'),
'PORT': config('DB_PORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

View File

@ -0,0 +1,267 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'dashboard',
'boards',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"https://turtask.vercel.app",
]
CSRF_TRUSTED_ORIGINS = ["https://turtask.vercel.app"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('PGDATABASE', default='github_actions'),
'USER': config('PGUSER', default='postgres'),
'PASSWORD': config('PGPASSWORD', default='postgres'),
'HOST': config('PGHOST', default='127.0.0.1'),
'PORT': config('PGPORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = "accesstokencache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}

View File

@ -1,267 +1,6 @@
"""
Django settings for core project.
Generated by 'django-admin startproject' using Django 4.2.6.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
from datetime import timedelta
import os
from pathlib import Path
from decouple import config, Csv
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='j5&66&8@b-!3tbq!=w6-dypl($_0zzoi*ilxd1*&$_6s-59il5')
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool)
ALLOWED_HOSTS = config('ALLOWED_HOSTS', default='*', cast=Csv())
# Application definition
SITE_ID = 4
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend',
)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
'tasks',
'users',
'authentications',
'corsheaders',
'drf_spectacular',
'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',
'rest_framework',
'dj_rest_auth',
'dj_rest_auth.registration',
'rest_framework.authtoken',
]
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.TokenAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
'dj_rest_auth.jwt_auth.JWTCookieAuthentication',
],
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
}
SPECTACULAR_SETTINGS = {
'TITLE': 'TurTask API',
'DESCRIPTION': 'API documentation for TurTask',
'VERSION': '1.0.0',
'SERVE_INCLUDE_SCHEMA': False,
}
REST_USE_JWT = True
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=3),
'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
}
GOOGLE_CLIENT_ID = config('GOOGLE_CLIENT_ID', default='fake-client-id')
GOOGLE_CLIENT_SECRET = config('GOOGLE_CLIENT_SECRET', default='fake-client-secret')
SOCIALACCOUNT_PROVIDERS = {
'google': {
'APP': {
'client_id': GOOGLE_CLIENT_ID,
'secret': GOOGLE_CLIENT_SECRET,
'key': ''
},
"SCOPE": [
"profile",
"email",
],
"AUTH_PARAMS": {
"access_type": "online",
}
}
}
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = False
CORS_ALLOWED_ORIGINS = [
"http://localhost:8000",
"http://127.0.0.1:8000",
"http://localhost:5173",
]
CSRF_TRUSTED_ORIGINS = ["http://localhost:5173"]
CORS_ORIGIN_WHITELIST = ["*"]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
"allauth.account.middleware.AccountMiddleware",
]
ROOT_URLCONF = 'core.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates')
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'core.wsgi.application'
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': config('DB_NAME', default='github_actions'),
'USER': config('DB_USER', default='postgres'),
'PASSWORD': config('DB_PASSWORD', default='postgres'),
'HOST': config('DB_HOST', default='127.0.0.1'),
'PORT': config('DB_PORT', default='5432'),
}
}
# Cache
CACHES_LOCATION = f"{config('DB_NAME', default='db_test')}_cache"
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.db.DatabaseCache",
"LOCATION": CACHES_LOCATION,
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.2/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
]
LOGIN_REDIRECT_URL = '/'
LOGOUT_REDIRECT_URL = '/'
AUTH_USER_MODEL = "users.CustomUser"
ACCOUNT_EMAIL_REQUIRED = True
# Storages
AWS_ACCESS_KEY_ID = config('AMAZON_S3_ACCESS_KEY', default='fake-access-key')
AWS_SECRET_ACCESS_KEY = config('AMAZON_S3_SECRET_ACCESS_KEY', default='fake-secret-access-key')
AWS_STORAGE_BUCKET_NAME = config('BUCKET_NAME', default='fake-bucket-name')
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}
MEDIA_URL = '/mediafiles/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'mediafiles')
STORAGES = {
"default": {
"BACKEND": "storages.backends.s3.S3Storage",
"OPTIONS": {
},
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
if os.environ.get("DJANGO_ENV") == "PRODUCTION":
from .production_settings import *
else:
from .local_settings import *

View File

@ -27,4 +27,6 @@ urlpatterns = [
path('api/schema/', SpectacularAPIView.as_view(), name='schema'),
path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
path('api/', include('dashboard.urls')),
path('api/', include('boards.urls')),
]

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings")
application = get_wsgi_application()

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class DashboardConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'dashboard'

View File

View File

@ -0,0 +1,7 @@
from rest_framework import serializers
from .models import UserStats
class UserStatsSerializer(serializers.ModelSerializer):
class Meta:
model = UserStats
fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level']

103
backend/dashboard/tests.py Normal file
View File

@ -0,0 +1,103 @@
from django.test import TestCase
from django.urls import reverse
from tasks.models import Todo
from django.utils import timezone
from datetime import timedelta
from tasks.tests.utils import create_test_user, login_user
class DashboardStatsAndWeeklyViewSetTests(TestCase):
def setUp(self):
self.user = create_test_user()
self.client = login_user(self.user)
def create_task(self, title, completed=False, completion_date=None, end_event=None):
return Todo.objects.create(
user=self.user,
title=title,
completed=completed,
completion_date=completion_date,
end_event=end_event
)
def test_dashboard_stats_view(self):
# Create tasks for testing
self.create_task('Task 1', completed=True)
self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self.create_task('Task 3', end_event=timezone.now())
response = self.client.get(reverse('stats-list'))
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data['completed_this_week'], 1)
self.assertEqual(response.data['tasks_assigned_this_week'], 1)
self.assertEqual(response.data['tasks_assigned_last_week'], 0)
def test_dashboard_weekly_view(self):
# Create tasks for testing
self.create_task('Task 1', completion_date=timezone.now() - timedelta(days=1))
self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8))
self.create_task('Task 3', end_event=timezone.now())
response = self.client.get(reverse('weekly-list'))
self.assertEqual(response.status_code, 200)
# class DashboardStatsAPITestCase(TestCase):
# def setUp(self):
# # Create a test user
# self.user = create_test_user()
# # Create test tasks
# self.todo = Todo.objects.create(user=self.user, title='Test Todo')
# self.recurrence_task = RecurrenceTask.objects.create(user=self.user, title='Test Recurrence Task')
# # Create an API client
# self.client = APIClient()
# def test_dashboard_stats_api(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a GET request to the DashboardStatsAPIView
# response = self.client.get(reverse("dashboard-stats"))
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)
# def test_task_completion_status_update(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a POST request to update the completion status of a task
# data = {'task_id': self.todo.id, 'is_completed': True}
# response = self.client.post(reverse("dashboard-stats"), data, format='json')
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)
# # Assert the message in the response
# self.assertEqual(response.data['message'], 'Task completion status updated successfully')
# # Refresh the todo instance from the database and assert the completion status
# self.todo.refresh_from_db()
# self.assertTrue(self.todo.completed)
# class WeeklyStatsAPITestCase(TestCase):
# def setUp(self):
# # Create a test user
# self.user = create_test_user()
# # Create an API client
# self.client = APIClient()
# def test_weekly_stats_api(self):
# # Authenticate the user
# self.client.force_authenticate(user=self.user)
# # Make a GET request to the WeeklyStatsAPIView
# response = self.client.get(reverse('dashboard-weekly-stats'))
# # Assert the response status code is 200
# self.assertEqual(response.status_code, 200)

11
backend/dashboard/urls.py Normal file
View File

@ -0,0 +1,11 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import DashboardStatsViewSet, DashboardWeeklyViewSet
router = DefaultRouter()
router.register(r'dashboard/stats', DashboardStatsViewSet, basename='stats')
router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly')
urlpatterns = [
path('', include(router.urls)),
]

311
backend/dashboard/views.py Normal file
View File

@ -0,0 +1,311 @@
from datetime import timedelta
from django.utils import timezone
from rest_framework import status
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework import viewsets, mixins
from tasks.models import Todo
class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Todo.objects.all()
def list(self, request, *args, **kwargs):
user = self.request.user
# Calculate the start and end date for the last 7 days
end_date = timezone.now()
start_date = end_date - timedelta(days=7)
# How many tasks were completed in the last 7 days
completed_last_7_days = Todo.objects.filter(
user=user,
completed=True,
completion_date__gte=start_date,
completion_date__lte=end_date
).count()
# Task assign last week compared with this week
tasks_assigned_last_week = Todo.objects.filter(
user=user,
completion_date__gte=start_date - timedelta(days=7),
completion_date__lte=start_date
).count()
tasks_assigned_this_week = Todo.objects.filter(
user=user,
completion_date__gte=start_date,
completion_date__lte=end_date
).count()
# Completed tasks from last week compared with this week
completed_last_week = Todo.objects.filter(
user=user,
completed=True,
completion_date__gte=start_date - timedelta(days=7),
completion_date__lte=start_date
).count()
completed_this_week = Todo.objects.filter(
user=user,
completed=True,
completion_date__gte=start_date,
completion_date__lte=end_date
).count()
overdue_tasks = Todo.objects.filter(
user=user,
completed=False,
end_event__lt=timezone.now()
).count()
# Overall completion rate
total_tasks = Todo.objects.filter(user=user).count()
overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0
data = {
"completed_last_7_days": completed_last_7_days,
"tasks_assigned_last_week": tasks_assigned_last_week,
"tasks_assigned_this_week": tasks_assigned_this_week,
"completed_last_week": completed_last_week,
"completed_this_week": completed_this_week,
"overdue_tasks": overdue_tasks,
"overall_completion_rate": overall_completion_rate,
}
return Response(data, status=status.HTTP_200_OK)
class DashboardWeeklyViewSet(viewsets.GenericViewSet, mixins.ListModelMixin):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Todo.objects.all()
def list(self, request, *args, **kwargs):
user = self.request.user
# Calculate the start and end date for the last 7 days (Monday to Sunday)
today = timezone.now().date()
current_week_start = today - timedelta(days=today.weekday())
current_week_end = current_week_start + timedelta(days=6)
last_week_start = current_week_start - timedelta(days=7)
last_week_end = last_week_start + timedelta(days=6)
# Create a list to store daily statistics
weekly_stats = []
# Iterate over each day of the week
for day in range(7):
current_day = current_week_start + timedelta(days=day)
last_day = last_week_start + timedelta(days=day)
# Calculate stats for this week
tasks_this_week = Todo.objects.filter(
user=user,
completion_date__gte=current_day,
completion_date__lte=current_day + timedelta(days=1)
).count()
completed_this_week = Todo.objects.filter(
user=user,
completed=True,
completion_date__gte=current_day,
completion_date__lte=current_day + timedelta(days=1)
).count()
# Calculate stats for last week
tasks_last_week = Todo.objects.filter(
user=user,
completion_date__gte=last_day,
completion_date__lte=last_day + timedelta(days=1)
).count()
completed_last_week = Todo.objects.filter(
user=user,
completed=True,
completion_date__gte=last_day,
completion_date__lte=last_day + timedelta(days=1)
).count()
daily_stat = {
"date": current_day.strftime("%A"),
"This Week": tasks_this_week,
"Last Week": tasks_last_week,
"Completed This Week": completed_this_week,
"Completed Last Week": completed_last_week,
}
weekly_stats.append(daily_stat)
return Response(weekly_stats, status=status.HTTP_200_OK)
# class DashboardStatsAPIView(APIView):
# permission_classes = [IsAuthenticated]
# def get(self, request):
# user = request.user
# # Calculate task usage statistics
# todo_count = Todo.objects.filter(user=user).count()
# recurrence_task_count = RecurrenceTask.objects.filter(user=user).count()
# # Calculate how many tasks were completed in the last 7 days
# completed_todo_count_last_week = Todo.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count()
# completed_recurrence_task_count_last_week = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count()
# # Calculate subtask completion rate
# total_subtasks = Todo.objects.filter(user=user).aggregate(total=Count('subtask__id'))['total']
# completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True).aggregate(total=Count('subtask__id'))['total']
# # Calculate overall completion rate
# total_tasks = todo_count + recurrence_task_count
# completed_tasks = completed_todo_count_last_week + completed_recurrence_task_count_last_week
# overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0
# # pie chart show
# complete_todo_percent_last_week = (completed_todo_count_last_week / todo_count) * 100 if todo_count > 0 else 0
# complete_recurrence_percent_last_week = (completed_recurrence_task_count_last_week / recurrence_task_count) * 100 if recurrence_task_count > 0 else 0
# incomplete_task_percent_last_week = 100 - complete_recurrence_percent_last_week - complete_todo_percent_last_week
# data = {
# 'todo_count': todo_count,
# 'recurrence_task_count': recurrence_task_count,
# 'completed_todo_count_last_week': completed_todo_count_last_week,
# 'completed_recurrence_task_count_last_week': completed_recurrence_task_count_last_week,
# 'total_subtasks': total_subtasks,
# 'completed_subtasks': completed_subtasks,
# 'overall_completion_rate': overall_completion_rate,
# 'complete_todo_percent_last_week': complete_todo_percent_last_week,
# 'complete_recurrence_percent_last_week' : complete_recurrence_percent_last_week,
# 'incomplete_task_percent_last_week': incomplete_task_percent_last_week,
# }
# return Response(data, status=status.HTTP_200_OK)
# def post(self, request):
# # Handle incoming data from the POST request
# # Update the necessary information based on the data
# task_id = request.data.get('task_id')
# is_completed = request.data.get('is_completed')
# try:
# task = Todo.objects.get(id=task_id, user=request.user)
# task.completed = is_completed
# task.save()
# return Response({'message': 'Task completion status updated successfully'}, status=status.HTTP_200_OK)
# except Todo.DoesNotExist:
# return Response({'error': 'Task not found'}, status=status.HTTP_404_NOT_FOUND)
# class WeeklyStatsAPIView(APIView):
# permission_classes = [IsAuthenticated]
# def get(self, request):
# user = request.user
# today = timezone.now()
# # Calculate the start and end dates for the current week
# current_week_start = today - timezone.timedelta(days=today.weekday())
# current_week_end = current_week_start + timezone.timedelta(days=6)
# # Initialize a list to store daily statistics
# weekly_stats = []
# # Loop through each day of the week
# for i in range(7):
# # Calculate the start and end dates for the current day
# current_day_start = current_week_start + timezone.timedelta(days=i)
# current_day_end = current_day_start + timezone.timedelta(days=1)
# # Calculate the start and end dates for the same day over the last 7 days
# last_7_days_start = current_day_start - timezone.timedelta(days=7)
# last_7_days_end = current_day_end - timezone.timedelta(days=7)
# # Calculate statistics for the current day
# current_day_stats = self.calculate_stats(user, current_day_start, current_day_end)
# # Calculate statistics for the same day over the last 7 days
# last_7_days_stats = self.calculate_stats(user, last_7_days_start, last_7_days_end)
# # Calculate the percentage change
# percent_change_over_all = self.calculate_percent_change(
# current_day_stats['overall_completion_rate'],
# last_7_days_stats['overall_completion_rate']
# )
# # Calculate percentage change for completed_todo_count
# percent_change_todo = self.calculate_percent_change(
# current_day_stats['completed_todo_count'],
# last_7_days_stats['completed_todo_count']
# )
# # Calculate percentage change for completed_recurrence_task_count
# percent_change_recurrence = self.calculate_percent_change(
# current_day_stats['completed_recurrence_task_count'],
# last_7_days_stats['completed_recurrence_task_count']
# )
# # Append the daily statistics to the list
# weekly_stats.append({
# 'day_of_week': current_day_start.strftime('%A'),
# 'current_day_stats': current_day_stats,
# 'last_7_days_stats': last_7_days_stats,
# 'percent_change_over_all': percent_change_over_all,
# 'percent_change_todo': percent_change_todo,
# 'percent_change_recurrence': percent_change_recurrence,
# })
# response_data = {
# 'weekly_stats': weekly_stats,
# }
# return Response(response_data, status=status.HTTP_200_OK)
# def calculate_stats(self, user, start_date, end_date):
# # Calculate task usage statistics for the specified day
# todo_count = Todo.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).count()
# recurrence_task_count = RecurrenceTask.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).count()
# # Calculate how many tasks were completed on the specified day
# completed_todo_count = Todo.objects.filter(user=user, completed=True, last_update__gte=start_date, last_update__lte=end_date).count()
# completed_recurrence_task_count = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=start_date, last_update__lte=end_date).count()
# # Calculate subtask completion rate for the specified day
# total_subtasks = Todo.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).aggregate(total=Count('subtask__id'))['total']
# completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True, created_at__gte=start_date, created_at__lte=end_date).aggregate(total=Count('subtask__id'))['total']
# # Calculate overall completion rate for the specified day
# total_tasks = todo_count + recurrence_task_count
# completed_tasks = completed_todo_count + completed_recurrence_task_count
# overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0
# return {
# 'start_date': start_date.strftime('%Y-%m-%d'),
# 'end_date': end_date.strftime('%Y-%m-%d'),
# 'todo_count': todo_count,
# 'recurrence_task_count': recurrence_task_count,
# 'completed_todo_count': completed_todo_count,
# 'completed_recurrence_task_count': completed_recurrence_task_count,
# 'total_subtasks': total_subtasks,
# 'completed_subtasks': completed_subtasks,
# 'overall_completion_rate': overall_completion_rate,
# }
# def calculate_percent_change(self, current_value, last_value):
# # Calculate the percentage change between current and last values
# if last_value != 0:
# percent_change = ((current_value - last_value) / last_value) * 100
# else:
# percent_change = current_value * 100 # Consider infinite change when the last value is zero
# return round(percent_change, 2)

11
backend/railway.json Normal file
View File

@ -0,0 +1,11 @@
{
"$schema": "https://railway.app/railway.schema.json",
"build": {
"builder": "NIXPACKS"
},
"deploy": {
"startCommand": "python manage.py migrate && python manage.py createcachetable accesstokencache && gunicorn --timeout 500 core.wsgi",
"restartPolicyType": "NEVER",
"restartPolicyMaxRetries": 10
}
}

View File

@ -14,4 +14,7 @@ google_auth_oauthlib>=1.1
google-auth-httplib2>=0.1
django-storages[s3]>=1.14
Pillow>=10.1
drf-spectacular>=0.26
drf-spectacular>=0.26
python-dateutil>=2.8
gunicorn==21.2.0
packaging==23.1

View File

@ -7,4 +7,7 @@ DB_PASSWORD=your_DB_PASSWORD
DB_HOST=your_DB_HOST
DB_PORT=your_DB_PORT
GOOGLE_CLIENT_ID=your_GOOGLE_CLIENT_ID
GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET
GOOGLE_CLIENT_SECRET=your_GOOGLE_CLIENT_SECRET
BUCKET_NAME=your_BUCKET_NAME
AMAZON_S3_ACCESS_KEY=YOUR_S3_ACCESS_KEY
AMAZON_S3_SECRET_ACCESS_KEY=YOUR_S3_SECRET

View File

@ -1,3 +1,30 @@
from django.contrib import admin
from .models import Tag, Todo, RecurrenceTask, RecurrencePattern, Habit, Subtask
# Register your models here.
@admin.register(Tag)
class TagAdmin(admin.ModelAdmin):
list_display = ['name']
@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
list_display = ['title', 'list_board', 'is_active', 'priority', 'completed', 'completion_date']
list_filter = ['list_board', 'is_active', 'priority', 'completed']
exclude = ['completion_date']
@admin.register(RecurrenceTask)
class RecurrenceTaskAdmin(admin.ModelAdmin):
list_display = ['title', 'list_board', 'rrule', 'is_active']
list_filter = ['list_board', 'rrule', 'is_active']
@admin.register(RecurrencePattern)
class RecurrencePatternAdmin(admin.ModelAdmin):
list_display = ['recurrence_task', 'recurring_type', 'day_of_week', 'week_of_month', 'day_of_month', 'month_of_year']
@admin.register(Habit)
class HabitAdmin(admin.ModelAdmin):
list_display = ['title', 'streak', 'current_count']
@admin.register(Subtask)
class SubtaskAdmin(admin.ModelAdmin):
list_display = ['parent_task', 'description', 'completed']
list_filter = ['parent_task', 'completed']

View File

@ -7,45 +7,21 @@ from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from tasks.utils import get_service
from tasks.models import Todo, RecurrenceTask
from tasks.serializers import TodoUpdateSerializer, RecurrenceTaskUpdateSerializer
from tasks.models import Todo
from tasks.serializers import TodoUpdateSerializer
class GoogleCalendarEventViewset(viewsets.ViewSet):
"""Viewset for list or save Google Calendar Events."""
permission_classes = (IsAuthenticated,)
def __init__(self, *args, **kwargs):
super().__init__()
self.current_time = datetime.now(tz=timezone.utc).isoformat()
self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end)'
self.current_time = (datetime.now(tz=timezone.utc) + timedelta(days=-7)).isoformat()
self.max_time = (datetime.now(tz=timezone.utc) + timedelta(days=7)).isoformat()
self.event_fields = 'items(id,summary,description,created,recurringEventId,updated,start,end,originalStartTime)'
def _validate_serializer(self, serializer):
if serializer.is_valid():
serializer.save()
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
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)
return self._validate_serializer(serializer)
except Todo.DoesNotExist:
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=days)).isoformat()
def _get_google_events(self, request):
"""Get all events from Google Calendar. """
service = get_service(request)
events = []
next_page_token = None
@ -54,21 +30,61 @@ class GoogleCalendarEventViewset(viewsets.ViewSet):
query = service.events().list(
calendarId='primary',
timeMin=self.current_time,
timeMax=max_time,
timeMax=self.max_time,
maxResults=200,
singleEvents=True,
orderBy='startTime',
pageToken=next_page_token,
fields='items(id,summary,description,created,recurringEventId,updated,start,end)',
fields=self.event_fields,
)
page_results = query.execute()
page_events = page_results.get('items', [])
events.extend(page_events)
next_page_token = page_results.get('nextPageToken')
if next_page_token is None:
break
return Response(events, status=200)
return events
def _validate_serializer(self, serializer):
"""
Validate serializer and return response.
:param serializer: The serializer to validate.
"""
if serializer.is_valid():
serializer.save()
return Response("Validate Successfully", status=200)
return Response(serializer.errors, status=400)
def create(self, request, *args, **kwargs):
"""Create a new Google Calendar Event."""
events = self._get_google_events(request)
responses = []
for event in events:
start_datetime = event.get('start', {}).get('dateTime')
end_datetime = event.get('end', {}).get('dateTime')
event['start_datetime'] = start_datetime
event['end_datetime'] = end_datetime
event.pop('start')
event.pop('end')
try:
task = Todo.objects.get(google_calendar_id=event['id'])
serializer = TodoUpdateSerializer(instance=task, data=event)
except Todo.DoesNotExist:
serializer = TodoUpdateSerializer(data=event, user=request.user)
responses.append(self._validate_serializer(serializer))
return responses[0] if responses else Response("No events to process", status=200)
def list(self, request):
"""List all Google Calendar Events."""
return Response(self._get_google_events(request), status=200)

View File

@ -0,0 +1,39 @@
# Generated by Django 4.2.6 on 2023-11-13 18:15
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', '0011_recurrencetask'),
]
operations = [
migrations.CreateModel(
name='Habit',
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)),
('streak', models.IntegerField(default=0)),
('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,
},
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-14 15:17
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0012_habit'),
]
operations = [
migrations.AlterField(
model_name='recurrencetask',
name='recurrence_rule',
field=models.CharField(),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-17 16:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0013_alter_recurrencetask_recurrence_rule'),
]
operations = [
migrations.AddField(
model_name='recurrencetask',
name='completed',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='todo',
name='completed',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,107 @@
# Generated by Django 4.2.6 on 2023-11-19 20:15
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boards', '0001_initial'),
('tasks', '0014_recurrencetask_completed_todo_completed'),
]
operations = [
migrations.CreateModel(
name='RecurrencePattern',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recurring_type', models.IntegerField(choices=[(0, 'Daily'), (1, 'Weekly'), (2, 'Monthly'), (3, 'Yearly')])),
('max_occurrences', models.IntegerField(default=0)),
('day_of_week', models.IntegerField(choices=[(0, 'Monday'), (1, 'Tuesday'), (2, 'Wednesday'), (3, 'Thursday'), (4, 'Friday'), (5, 'Saturday'), (6, 'Sunday')])),
('week_of_month', models.IntegerField(choices=[(1, 'First'), (2, 'Second'), (3, 'Third'), (4, 'Fourth'), (5, 'Last')])),
('day_of_month', models.IntegerField(default=0)),
('month_of_year', models.IntegerField(choices=[(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')])),
],
),
migrations.RemoveField(
model_name='transaction',
name='user',
),
migrations.DeleteModel(
name='UserNotification',
),
migrations.RemoveField(
model_name='habit',
name='end_event',
),
migrations.RemoveField(
model_name='habit',
name='google_calendar_id',
),
migrations.RemoveField(
model_name='habit',
name='start_event',
),
migrations.RemoveField(
model_name='recurrencetask',
name='google_calendar_id',
),
migrations.RemoveField(
model_name='recurrencetask',
name='recurrence_rule',
),
migrations.AddField(
model_name='habit',
name='current_count',
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name='recurrencetask',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='recurrencetask',
name='is_full_day_event',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='recurrencetask',
name='list_board',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
migrations.AddField(
model_name='recurrencetask',
name='parent_task',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'),
),
migrations.AddField(
model_name='recurrencetask',
name='rrule',
field=models.CharField(blank=True, max_length=255, null=True),
),
migrations.AddField(
model_name='todo',
name='is_active',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='todo',
name='is_full_day_event',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='todo',
name='list_board',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
migrations.DeleteModel(
name='Transaction',
),
migrations.AddField(
model_name='recurrencepattern',
name='recurrence_task',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.recurrencetask'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-11-19 20:24
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boards', '0001_initial'),
('tasks', '0015_recurrencepattern_remove_transaction_user_and_more'),
]
operations = [
migrations.AlterField(
model_name='recurrencetask',
name='list_board',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
migrations.AlterField(
model_name='todo',
name='list_board',
field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 4.2.6 on 2023-11-19 20:27
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('boards', '0001_initial'),
('tasks', '0016_alter_recurrencetask_list_board_and_more'),
]
operations = [
migrations.AlterField(
model_name='recurrencetask',
name='list_board',
field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
migrations.AlterField(
model_name='todo',
name='list_board',
field=models.ForeignKey(default=1, null=True, on_delete=django.db.models.deletion.CASCADE, to='boards.listboard'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 4.2.6 on 2023-11-20 14:58
from django.db import migrations, models
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('tasks', '0017_alter_recurrencetask_list_board_and_more'),
]
operations = [
migrations.AlterField(
model_name='habit',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='recurrencetask',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
migrations.AlterField(
model_name='todo',
name='creation_date',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False),
),
]

View File

@ -0,0 +1,28 @@
# Generated by Django 4.2.6 on 2023-11-20 15:02
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0018_alter_habit_creation_date_and_more'),
]
operations = [
migrations.AlterField(
model_name='habit',
name='creation_date',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='recurrencetask',
name='creation_date',
field=models.DateTimeField(auto_now_add=True),
),
migrations.AlterField(
model_name='todo',
name='creation_date',
field=models.DateTimeField(auto_now_add=True),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 4.2.6 on 2023-11-20 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('tasks', '0019_alter_habit_creation_date_and_more'),
]
operations = [
migrations.AddField(
model_name='recurrencetask',
name='completion_date',
field=models.DateTimeField(null=True),
),
migrations.AddField(
model_name='todo',
name='completion_date',
field=models.DateTimeField(null=True),
),
]

View File

@ -1,5 +1,8 @@
from django.db import models
from django.conf import settings
from django.utils import timezone
from boards.models import ListBoard, Board
class Tag(models.Model):
"""
@ -12,22 +15,18 @@ class Tag(models.Model):
class Task(models.Model):
"""
Represents a Abstract of task, such as Habit, Daily, Todo, or Reward.
Represents a Abstract of task, such as Habit, Recurrence, Todo.
: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 importance: The importance of the task (range: 1 to 5)
:param difficulty: The difficulty of the task (range: 1 to 5).
:param challenge: Associated challenge (optional).
: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.
:param: google_calendar_id: Google Calendar Event ID of the task.
:param start_event: Start event of the task.
:param end_event: End event(Due Date) of the task.
:param last_update: Last update date of the task.
"""
class Difficulty(models.IntegerChoices):
EASY = 1, 'Easy'
@ -46,33 +45,157 @@ class Task(models.Model):
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(max_length=255, null=True, blank=True)
start_event = models.DateTimeField(null=True)
end_event = models.DateTimeField(null=True)
class Meta:
abstract = True
class Todo(Task):
"""
Represent a Todo task.
:param list_board: The list board that the task belongs to.
:param is_active: A boolean field indicating whether the task is active. (Archive or not)
:param is_full_day_event: A boolean field indicating whether the task is a full day event.
:param start_event: Start date and time of the task.
:param end_event: End date and time of the task.
:param google_calendar_id: The Google Calendar ID of the task.
:param completed: A boolean field indicating whether the task is completed.
:param completion_date: The date and time when the task is completed.
:param priority: The priority of the task (range: 1 to 4).
"""
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'
list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1)
is_active = models.BooleanField(default=True)
is_full_day_event = models.BooleanField(default=False)
start_event = models.DateTimeField(null=True)
end_event = models.DateTimeField(null=True)
google_calendar_id = models.CharField(max_length=255, null=True, blank=True)
completed = models.BooleanField(default=False)
completion_date = models.DateTimeField(null=True)
priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT)
def save(self, *args, **kwargs):
if self.completed and not self.completion_date:
self.completion_date = timezone.now()
elif not self.completed:
self.completion_date = None
super().save(*args, **kwargs)
def __str__(self):
return self.title
class RecurrenceTask(Task):
recurrence_rule = models.TextField()
"""
Represent a Recurrence task. (Occure every day, week, month, year)
:param list_board: The list board that the task belongs to.
:param rrule: The recurrence rule of the task.
:param is_active: A boolean field indicating whether the task is active. (Archive or not)
:param is_full_day_event: A boolean field indicating whether the task is a full day event.
:param start_event: Start date and time of the task.
:param end_event: End date and time of the task.
:param completed: A boolean field indicating whether the task is completed.
:param completion_date: The date and time when the task is completed.
:param parent_task: The parent task of the subtask.
"""
list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1)
rrule = models.CharField(max_length=255, null=True, blank=True)
is_active = models.BooleanField(default=True)
is_full_day_event = models.BooleanField(default=False)
start_event = models.DateTimeField(null=True)
end_event = models.DateTimeField(null=True)
completed = models.BooleanField(default=False)
completion_date = models.DateTimeField(null=True)
parent_task = models.ForeignKey("self", on_delete=models.CASCADE, null=True)
def save(self, *args, **kwargs):
if self.completed and not self.completion_date:
self.completion_date = timezone.now()
elif not self.completed:
self.completion_date = None
super().save(*args, **kwargs)
def __str__(self) -> str:
return f"{self.title} ({self.recurrence_rule})"
class RecurrencePattern(models.Model):
"""
:param recurrence_task: The recurrence task that the pattern belongs to.
:param recurring_type: The type of recurrence.
:param max_occurrences: The maximum number of occurrences.
:param day_of_week: The day of the week that event will occure.
:param week_of_month: The week of the month that event will occure.
:param day_of_month: The day of the month that event will occure.
:param month_of_year: The month of the year that event will occure.
"""
class RecurringType(models.IntegerChoices):
DAILY = 0, 'Daily'
WEEKLY = 1, 'Weekly'
MONTHLY = 2, 'Monthly'
YEARLY = 3, 'Yearly'
class DayOfWeek(models.IntegerChoices):
MONDAY = 0, 'Monday'
TUESDAY = 1, 'Tuesday'
WEDNESDAY = 2, 'Wednesday'
THURSDAY = 3, 'Thursday'
FRIDAY = 4, 'Friday'
SATURDAY = 5, 'Saturday'
SUNDAY = 6, 'Sunday'
class WeekOfMonth(models.IntegerChoices):
FIRST = 1, 'First'
SECOND = 2, 'Second'
THIRD = 3, 'Third'
FOURTH = 4, 'Fourth'
LAST = 5, 'Last'
class MonthOfYear(models.IntegerChoices):
JANUARY = 1, 'January'
FEBRUARY = 2, 'February'
MARCH = 3, 'March'
APRIL = 4, 'April'
MAY = 5, 'May'
JUNE = 6, 'June'
JULY = 7, 'July'
AUGUST = 8, 'August'
SEPTEMBER = 9, 'September'
OCTOBER = 10, 'October'
NOVEMBER = 11, 'November'
DECEMBER = 12, 'December'
recurrence_task = models.ForeignKey(RecurrenceTask, on_delete=models.CASCADE)
recurring_type = models.IntegerField(choices=RecurringType.choices)
max_occurrences = models.IntegerField(default=0)
day_of_week = models.IntegerField(choices=DayOfWeek.choices)
week_of_month = models.IntegerField(choices=WeekOfMonth.choices)
day_of_month = models.IntegerField(default=0)
month_of_year = models.IntegerField(choices=MonthOfYear.choices)
class Habit(Task):
"""
Represent a Habit task with streaks.
:param streak: The streak of the habit.
:param current_count: The current count of the habit.
"""
streak = models.IntegerField(default=0)
current_count = models.IntegerField(default=0)
def __str__(self) -> str:
return f"{self.title} ({self.streak})"
class Subtask(models.Model):
"""
Represents a subtask associated with a task.
@ -82,67 +205,4 @@ class Subtask(models.Model):
"""
parent_task = models.ForeignKey(Todo, on_delete=models.CASCADE)
description = models.TextField()
completed = models.BooleanField(default=False)
class UserNotification(models.Model):
"""
Represents a user notification.
:param type: The type of the notification (e.g., 'NEW_CHAT_MESSAGE').
:param data: JSON data associated with the notification.
:param seen: A boolean field indicating whether the notification has been seen.
"""
NOTIFICATION_TYPES = (
('LEVEL_UP', 'Level Up'),
('DEATH', 'Death'),
)
type = models.CharField(max_length=255, choices=[type for type in NOTIFICATION_TYPES])
data = models.JSONField(default=dict)
seen = models.BooleanField(default=False)
@staticmethod
def clean_notification(notifications):
"""
Cleanup function for removing corrupt notification data:
- Removes notifications with null or missing id or type.
"""
if not notifications:
return notifications
filtered_notifications = []
for notification in notifications:
if notification.id is None or notification.type is None:
continue
return filtered_notifications
class Transaction(models.Model):
"""
Represents a transaction involving currencies in the system.
:param currency: The type of currency used in the transaction
:param transactionType: The type of the transaction
:param description: Additional text.
:param amount: The transaction amount.
:param user: The user involved in the transaction.
"""
CURRENCIES = (('gold', 'Gold'),)
TRANSACTION_TYPES = (
('buy_gold', 'Buy Gold'),
('spend', 'Spend'),
('debug', 'Debug'),
('force_update_gold', 'Force Update Gold'),
)
currency = models.CharField(max_length=12, choices=CURRENCIES)
transaction_type = models.CharField(max_length=24, choices=TRANSACTION_TYPES)
description = models.TextField(blank=True)
amount = models.FloatField(default=0)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
def __str__(self):
return f"Transaction ({self.id})"
completed = models.BooleanField(default=False)

View File

@ -1,5 +1,4 @@
from rest_framework import serializers
from django.utils.dateparse import parse_datetime
from .models import Todo, RecurrenceTask
@ -41,7 +40,7 @@ class RecurrenceTaskUpdateSerializer(serializers.ModelSerializer):
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")
recurrence = serializers.CharField(source="recurrence_rule")
start_datetime = serializers.DateTimeField(source="start_event", required=False)
end_datetime = serializers.DateTimeField(source="end_event", required=False)

View File

@ -1,12 +1,14 @@
from django.db.models.signals import pre_save
from django.db.models.signals import pre_save, post_save
from django.dispatch import receiver
from django.utils import timezone
from boards.models import ListBoard, Board
from tasks.models import Todo
@receiver(pre_save, sender=Todo)
def update_priority(sender, instance, **kwargs):
"""Update the priority of a Todo based on the Eisenhower Matrix"""
if instance.end_event:
time_until_due = (instance.end_event - timezone.now()).days
else:
@ -22,4 +24,66 @@ def update_priority(sender, instance, **kwargs):
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
instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT
# @receiver(post_save, sender=Todo)
# def assign_todo_to_listboard(sender, instance, created, **kwargs):
# """Signal handler to automatically assign a Todo to the first ListBoard in the user's Board upon creation."""
# if created:
# user_board = instance.user.board_set.first()
# if user_board:
# first_list_board = user_board.listboard_set.order_by('position').first()
# if first_list_board:
# instance.list_board = first_list_board
# instance.save()
@receiver(post_save, sender=ListBoard)
def create_placeholder_tasks(sender, instance, created, **kwargs):
"""
Signal handler to create placeholder tasks for each ListBoard.
"""
if created:
list_board_position = instance.position
if list_board_position == 1:
placeholder_tasks = [
{"title": "Normal Task Example"},
{"title": "Task with Extra Information Example", "description": "Description for Task 2"},
]
elif list_board_position == 2:
placeholder_tasks = [
{"title": "Time Task Example #1", "description": "Description for Task 2",
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=5)},
]
elif list_board_position == 3:
placeholder_tasks = [
{"title": "Time Task Example #2", "description": "Description for Task 2",
"start_event": timezone.now(), "end_event": timezone.now() + timezone.timedelta(days=30)},
]
elif list_board_position == 4:
placeholder_tasks = [
{"title": "Completed Task Example", "description": "Description for Task 2",
"start_event": timezone.now(), "completed": True},
]
else:
placeholder_tasks = [
{"title": "Default Task Example"},
]
for task_data in placeholder_tasks:
Todo.objects.create(
list_board=instance,
user=instance.board.user,
title=task_data["title"],
notes=task_data.get("description", ""),
is_active=True,
start_event=task_data.get("start_event"),
end_event=task_data.get("end_event"),
completed=task_data.get("completed", False),
creation_date=timezone.now(),
last_update=timezone.now(),
)

View File

@ -1,21 +1,92 @@
from rest_framework import serializers
from ..models import Todo
from boards.models import ListBoard
from tasks.models import Todo, RecurrenceTask, Habit
class TaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
# fields = '__all__'
exclude = ('tags',)
def create(self, validated_data):
# Create a new task with validated data
return Todo.objects.create(**validated_data)
class TaskGeneralSerializer(serializers.ModelSerializer):
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
return Todo.objects.create(**validated_data)
return Todo.objects.create(**validated_data)
class TaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Todo
exclude = ('tags', 'google_calendar_id', 'creation_date', 'last_update',)
class ChangeTaskOrderSerializer(serializers.Serializer):
list_board_id = serializers.IntegerField(
help_text='ID of the ListBoard for which the task order should be updated.'
)
todo_order = serializers.ListField(
child=serializers.IntegerField(),
required=False,
help_text='New order of Todo IDs in the ListBoard.'
)
def validate(self, data):
list_board_id = data.get('list_board_id')
todo_order = data.get('todo_order', [])
if not ListBoard.objects.filter(id=list_board_id).exists():
raise serializers.ValidationError('ListBoard does not exist.')
existing_tasks = Todo.objects.filter(id__in=todo_order)
existing_task_ids = set(task.id for task in existing_tasks)
non_existing_task_ids = set(todo_order) - existing_task_ids
if non_existing_task_ids:
raise serializers.ValidationError(f'Tasks with IDs {non_existing_task_ids} do not exist.')
return data
class ChangeTaskListBoardSerializer(serializers.Serializer):
todo_id = serializers.IntegerField()
new_list_board_id = serializers.IntegerField()
new_index = serializers.IntegerField(required=False)
def validate(self, data):
todo_id = data.get('todo_id')
new_list_board_id = data.get('new_list_board_id')
new_index = data.get('new_index')
if not Todo.objects.filter(id=todo_id, user=self.context['request'].user).exists():
raise serializers.ValidationError('Todo does not exist for the authenticated user.')
if not ListBoard.objects.filter(id=new_list_board_id).exists():
raise serializers.ValidationError('ListBoard does not exist.')
return data
class RecurrenceTaskSerializer(serializers.ModelSerializer):
class Meta:
model = RecurrenceTask
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
return Todo.objects.create(**validated_data)
class RecurrenceTaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = RecurrenceTask
exclude = ('tags',)
class HabitTaskSerializer(serializers.ModelSerializer):
class Meta:
model = Habit
fields = '__all__'
def create(self, validated_data):
# Create a new task with validated data
return Todo.objects.create(**validated_data)
class HabitTaskCreateSerializer(serializers.ModelSerializer):
class Meta:
model = Habit
exclude = ('tags',)

View File

@ -1,16 +1,141 @@
from rest_framework import viewsets
from django.shortcuts import get_object_or_404
from django.db import IntegrityError
from rest_framework import viewsets, status, serializers
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from tasks.models import Todo
from .serializers import TaskCreateSerializer, TaskGeneralSerializer
from rest_framework.response import Response
from .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer
from boards.models import ListBoard, KanbanTaskOrder
from tasks.models import Todo, RecurrenceTask, Habit
from tasks.tasks.serializers import (TaskCreateSerializer,
TaskSerializer,
RecurrenceTaskSerializer,
RecurrenceTaskCreateSerializer,
HabitTaskSerializer,
HabitTaskCreateSerializer)
class TodoViewSet(viewsets.ModelViewSet):
queryset = Todo.objects.all()
serializer_class = TaskGeneralSerializer
serializer_class = TaskSerializer
permission_classes = [IsAuthenticated]
model = Todo
def get_queryset(self):
queryset = Todo.objects.filter(user=self.request.user)
return queryset
def get_serializer_class(self):
# Can't add ManytoMany at creation time (Tags)
if self.action == 'create':
return TaskCreateSerializer
return TaskGeneralSerializer
return TaskSerializer
def create(self, request, *args, **kwargs):
try:
new_task_data = request.data
new_task_data['user'] = self.request.user.id
serializer = self.get_serializer(data=new_task_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except IntegrityError as e:
return Response({'error': 'IntegrityError - Duplicate Entry'}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['put'])
def change_task_order(self, request):
try:
serializer = ChangeTaskOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
list_board_id = serializer.validated_data['list_board_id']
new_order = serializer.validated_data.get('todo_order', [])
list_board = get_object_or_404(ListBoard, id=list_board_id)
kanban_order, created = KanbanTaskOrder.objects.get_or_create(list_board=list_board)
kanban_order.todo_order = new_order
kanban_order.save()
return Response({'message': 'Task order updated successfully'})
except serializers.ValidationError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@action(detail=False, methods=['put'])
def change_task_list_board(self, request):
try:
serializer = ChangeTaskListBoardSerializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
todo_id = serializer.validated_data['todo_id']
new_list_board_id = serializer.validated_data['new_list_board_id']
new_index = serializer.validated_data.get('new_index')
todo_id = request.data.get('todo_id')
new_list_board_id = request.data.get('new_list_board_id')
todo = get_object_or_404(Todo, id=todo_id, user=self.request.user)
old_list_board = todo.list_board
# Remove todoId from todo_order of the old list board
old_kanban_order, _ = KanbanTaskOrder.objects.get_or_create(list_board=old_list_board)
old_kanban_order.todo_order = [t_id for t_id in old_kanban_order.todo_order if t_id != todo.id]
old_kanban_order.save()
# Get the index to insert the todo in the new list board's todo_order
new_list_board = get_object_or_404(ListBoard, id=new_list_board_id)
new_kanban_order, _ = KanbanTaskOrder.objects.get_or_create(list_board=new_list_board)
# Index where todo need to insert (start from 0)
new_index = request.data.get('new_index', None)
if new_index is not None and 0 <= new_index <= len(new_kanban_order.todo_order):
new_kanban_order.todo_order.insert(new_index, todo.id)
else:
new_kanban_order.todo_order.append(todo.id)
new_kanban_order.save()
todo.list_board = new_list_board
todo.save()
return Response({'message': 'ListBoard updated successfully'})
except serializers.ValidationError as e:
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except Exception as e:
return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
class RecurrenceTaskViewSet(viewsets.ModelViewSet):
queryset = RecurrenceTask.objects.all()
serializer_class = RecurrenceTaskSerializer
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
# Can't add ManytoMany at creation time (Tags)
if self.action == 'create':
return RecurrenceTaskCreateSerializer
return RecurrenceTaskSerializer
class HabitTaskViewSet(viewsets.ModelViewSet):
queryset = Habit.objects.all()
serializer_class = HabitTaskSerializer
permission_classes = [IsAuthenticated]
def get_serializer_class(self):
# Can't add ManytoMany at creation time (Tags)
if self.action == 'create':
return HabitTaskCreateSerializer
return HabitTaskSerializer

View File

@ -6,68 +6,68 @@ from tasks.tests.utils import create_test_user, login_user
from tasks.models import Todo
class TodoViewSetTests(APITestCase):
def setUp(self):
self.user = create_test_user()
self.client = login_user(self.user)
self.url = reverse("todo-list")
self.due_date = datetime.now() + timedelta(days=5)
# class TodoViewSetTests(APITestCase):
# def setUp(self):
# self.user = create_test_user()
# self.client = login_user(self.user)
# 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.
"""
data = {
'title': 'Test Task',
'type': 'habit',
'exp': 10,
'attribute': 'str',
'priority': 1,
'difficulty': 1,
'user': self.user.id,
'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)
self.assertEqual(Todo.objects.count(), 1)
self.assertEqual(Todo.objects.get().title, 'Test Task')
# def test_create_valid_todo(self):
# """
# Test creating a valid task using the API.
# """
# data = {
# 'title': 'Test Task',
# 'type': 'habit',
# 'exp': 10,
# 'attribute': 'str',
# 'priority': 1,
# 'difficulty': 1,
# 'user': self.user.id,
# '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)
# self.assertEqual(Todo.objects.count(), 1)
# self.assertEqual(Todo.objects.get().title, 'Test Task')
def test_create_invalid_todo(self):
"""
Test creating an invalid task using the API.
"""
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
# def test_create_invalid_todo(self):
# """
# Test creating an invalid task using the API.
# """
# 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
def test_missing_required_fields(self):
"""
Test creating a task with missing required fields using the API.
"""
data = {
'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
# def test_missing_required_fields(self):
# """
# Test creating a task with missing required fields using the API.
# """
# data = {
# '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
def test_invalid_user_id(self):
"""
Test creating a task with an invalid user ID using the API.
"""
data = {
'title': 'Test Task',
'type': 'habit',
'exp': 10,
'priority': 1,
'difficulty': 1,
'user': 999, # Invalid user ID
'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
# def test_invalid_user_id(self):
# """
# Test creating a task with an invalid user ID using the API.
# """
# data = {
# 'title': 'Test Task',
# 'type': 'habit',
# 'exp': 10,
# 'priority': 1,
# 'difficulty': 1,
# 'user': 999, # Invalid user ID
# 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'),
# }
# response = self.client.post(self.url, data, format='json')
# self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
# self.assertEqual(Todo.objects.count(), 0) # No task should be created

View File

@ -3,12 +3,14 @@ from django.urls import path, include
from rest_framework.routers import DefaultRouter
from tasks.api import GoogleCalendarEventViewset
from tasks.tasks.views import TodoViewSet
from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet
from tasks.misc.views import TagViewSet
router = DefaultRouter()
router.register(r'todo', TodoViewSet)
router.register(r'daily', RecurrenceTaskViewSet)
router.register(r'habit', HabitTaskViewSet)
router.register(r'tags', TagViewSet)
router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events')

View File

@ -1,8 +1,55 @@
from dateutil import rrule
from datetime import datetime
from googleapiclient.discovery import build
from authentications.access_token_cache import get_credential_from_cache_token
def get_service(request):
"""
Get a service that communicates to a Google API.
:param request: Http request object
:return: A Resource object with methods for interacting with the calendar service
"""
credentials = get_credential_from_cache_token(request.user.id)
return build('calendar', 'v3', credentials=credentials)
return build('calendar', 'v3', credentials=credentials)
def _determine_frequency(time_difference):
if time_difference.days >= 365:
return rrule.YEARLY
elif time_difference.days >= 30:
return rrule.MONTHLY
elif time_difference.days >= 7:
return rrule.WEEKLY
elif time_difference.days >= 1:
return rrule.DAILY
elif time_difference.seconds >= 3600:
return rrule.HOURLY
elif time_difference.seconds >= 60:
return rrule.MINUTELY
else:
return rrule.SECONDLY
def generate_recurrence_rule(datetime1: str, datetime2: str, original_start_time: str) -> str:
"""
Generate recurrence rule from
difference between two datetime string.
:param task1: A task object
:param task2: A task object
:return: A recurrence rule string according to ICAL format
"""
start_time1 = datetime.fromisoformat(datetime1)
start_time2 = datetime.fromisoformat(datetime2)
time_difference = start_time2 - start_time1
recurrence_rule = rrule.rrule(
freq=_determine_frequency(time_difference),
dtstart=datetime.fromisoformat(original_start_time),
interval=time_difference.days if time_difference.days > 0 else 1,
)
return str(recurrence_rule)

View File

@ -0,0 +1,40 @@
# Generated by Django 4.2.6 on 2023-11-13 18:15
import django.core.validators
from django.db import migrations, models
import users.models
class Migration(migrations.Migration):
dependencies = [
('users', '0004_userstats'),
]
operations = [
migrations.AlterField(
model_name='userstats',
name='endurance',
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
),
migrations.AlterField(
model_name='userstats',
name='intelligence',
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
),
migrations.AlterField(
model_name='userstats',
name='luck',
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(50)]),
),
migrations.AlterField(
model_name='userstats',
name='perception',
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
),
migrations.AlterField(
model_name='userstats',
name='strength',
field=models.IntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(100)]),
),
]

View File

@ -0,0 +1,38 @@
# Generated by Django 4.2.6 on 2023-11-19 20:15
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0005_alter_userstats_endurance_and_more'),
]
operations = [
migrations.RemoveField(
model_name='userstats',
name='endurance',
),
migrations.RemoveField(
model_name='userstats',
name='intelligence',
),
migrations.RemoveField(
model_name='userstats',
name='luck',
),
migrations.RemoveField(
model_name='userstats',
name='perception',
),
migrations.RemoveField(
model_name='userstats',
name='strength',
),
migrations.AddField(
model_name='customuser',
name='last_name',
field=models.CharField(blank=True, max_length=150),
),
]

View File

@ -5,16 +5,18 @@ 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
class CustomUser(AbstractBaseUser, PermissionsMixin):
# User fields
"""
User model where email is the unique identifier for authentication.
"""
email = models.EmailField(_('email address'), unique=True)
username = models.CharField(max_length=150, unique=True)
first_name = models.CharField(max_length=150, blank=True)
last_name = models.CharField(max_length=150, blank=True)
start_date = models.DateTimeField(default=timezone.now)
about = models.TextField(_('about'), max_length=500, blank=True)
profile_pic = models.ImageField(upload_to='profile_pics', null=True, blank=True, default='profile_pics/default.png')
@ -29,15 +31,12 @@ class CustomUser(AbstractBaseUser, PermissionsMixin):
# Fields for authentication
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username', 'first_name']
REQUIRED_FIELDS = []
def __str__(self):
# String representation of the user
return self.username
def random_luck():
return random.randint(1, 50)
class UserStats(models.Model):
"""
@ -51,17 +50,6 @@ class UserStats(models.Model):
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)],)
@property
def level(self):

View File

@ -7,6 +7,8 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.parsers import MultiPartParser
from rest_framework_simplejwt.tokens import RefreshToken
from users.serializers import CustomUserSerializer, UpdateProfileSerializer
from users.models import CustomUser
@ -25,7 +27,9 @@ class CustomUserCreate(APIView):
if serializer.is_valid():
user = serializer.save()
if user:
return Response(serializer.data, status=status.HTTP_201_CREATED)
refresh = RefreshToken.for_user(user)
return Response(data={'access_token': str(refresh.access_token), 'refresh_token': str(refresh),},
status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

View File

@ -1,20 +1,18 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
env: { browser: true, es2020: true, node: true },
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
"eslint:recommended",
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
ignorePatterns: ["dist", ".eslintrc.cjs"],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
settings: { react: { version: "18.2" } },
plugins: ["react-refresh"],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/prop-types": 0,
},
}
};

View File

@ -4,10 +4,11 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.16/dist/tailwind.min.css" rel="stylesheet"> -->
<title>Vite + React</title>
<title>TurTask</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"]
}
}
}

View File

@ -10,35 +10,44 @@
"preview": "vite preview"
},
"dependencies": {
"@asseinfo/react-kanban": "^2.2.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/free-brands-svg-icons": "^6.4.2",
"@fortawesome/react-fontawesome": "^0.2.0",
"@fullcalendar/core": "^6.1.9",
"@fullcalendar/daygrid": "^6.1.9",
"@fullcalendar/interaction": "^6.1.9",
"@fullcalendar/react": "^6.1.9",
"@fullcalendar/timegrid": "^6.1.9",
"@heroicons/react": "1.0.6",
"@mui/icons-material": "^5.14.16",
"@mui/material": "^5.14.17",
"@mui/system": "^5.14.17",
"@react-oauth/google": "^0.11.1",
"@syncfusion/ej2-base": "^23.1.41",
"@syncfusion/ej2-kanban": "^23.1.36",
"@tremor/react": "^3.11.1",
"@wojtekmaj/react-daterange-picker": "^5.4.4",
"axios": "^1.6.1",
"bootstrap": "^5.3.2",
"dotenv": "^16.3.1",
"framer-motion": "^10.16.4",
"gapi-script": "^1.2.0",
"jwt-decode": "^4.0.0",
"prop-types": "^15.8.1",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-bootstrap": "^2.9.1",
"react-datetime-picker": "^5.5.3",
"react-dom": "^18.2.0",
"react-icons": "^4.11.0",
"react-router-dom": "^6.18.0"
"react-router-dom": "^6.18.0",
"react-tsparticles": "^2.12.2",
"tsparticles": "^2.12.0"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.10",

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,105 @@
import "./App.css";
import { Route, Routes, useLocation } from "react-router-dom";
import axios from "axios";
import { useEffect } from "react";
import { Route, Routes, Navigate } from "react-router-dom";
import { LoginPage } from "./components/authentication/LoginPage";
import { SignUp } from "./components/authentication/SignUpPage";
import { NavBar } from "./components/navigations/Navbar";
import { Calendar } from "./components/calendar/calendar";
import { KanbanPage } from "./components/kanbanBoard/kanbanPage";
import { SideNav } from "./components/navigations/IconSideNav";
import { Eisenhower } from "./components/EisenhowerMatrix/Eisenhower";
import { PrivateRoute } from "./PrivateRoute";
import { ProfileUpdatePage } from "./components/profile/profilePage";
import { Dashboard } from "./components/dashboard/dashboard";
import { LandingPage } from "./components/landingPage/LandingPage";
import { PublicRoute } from "./PublicRoute";
import { useAuth } from "./hooks/AuthHooks";
import TestAuth from "./components/testAuth";
import LoginPage from "./components/authentication/LoginPage";
import SignUpPage from "./components/authentication/SignUpPage";
import NavBar from "./components/navigations/Navbar";
import Home from "./components/Home";
import ProfileUpdate from "./components/ProfileUpdatePage";
import Calendar from "./components/calendar/calendar";
import KanbanBoard from "./components/kanbanBoard/kanbanBoard";
import IconSideNav from "./components/navigations/IconSideNav";
import Eisenhower from "./components/eisenhowerMatrix/Eisenhower";
const baseURL = import.meta.env.VITE_BASE_URL;
const App = () => {
const location = useLocation();
const prevention = ["/login", "/signup"];
const isLoginPageOrSignUpPage = prevention.some(_ => location.pathname.includes(_));
const { isAuthenticated, setIsAuthenticated } = useAuth();
useEffect(() => {
const checkLoginStatus = async () => {
const data = {
access_token: localStorage.getItem("access_token"),
refresh_token: localStorage.getItem("refresh_token"),
};
await axios
.post(`${baseURL}auth/status/`, data, {
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
},
})
.then((response) => {
if (response.status === 200) {
if (response.data.access_token) {
localStorage.setItem("access_token", response.data.access_token);
setIsAuthenticated(true);
} else {
setIsAuthenticated(true);
}
} else {
setIsAuthenticated(false);
}
})
.catch((error) => {});
};
checkLoginStatus();
}, [setIsAuthenticated]);
return <div>{isAuthenticated ? <AuthenticatedComponents /> : <NonAuthenticatedComponents />}</div>;
};
const NonAuthenticatedComponents = () => {
return (
<div className={isLoginPageOrSignUpPage ? "" : "display: flex"}>
{!isLoginPageOrSignUpPage && <IconSideNav />}
<div className={isLoginPageOrSignUpPage ? "" : "flex-1"}>
<NavBar />
<div className={isLoginPageOrSignUpPage ? "" : "flex items-center justify-center"}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/tasks" element={<KanbanBoard />} />
<Route path="/testAuth" element={<TestAuth />} />
<Route path="/update_profile" element={<ProfileUpdate />} />
<Route path="/calendar" element={<Calendar />} />
<Route path="/priority" element={<Eisenhower />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/signup" element={<SignUpPage />} />
</Routes>
</div>
<div>
<Routes>
<Route exact path="/l" element={<PublicRoute />}>
<Route exact path="/l" element={<LandingPage />} />
</Route>
<Route exact path="/login" element={<PublicRoute />}>
<Route exact path="/login" element={<LoginPage />} />
</Route>
<Route exact path="/signup" element={<PublicRoute />}>
<Route exact path="/signup" element={<SignUp />} />
</Route>
<Route path="*" element={<Navigate to="/l" />} />
</Routes>
</div>
);
};
const AuthenticatedComponents = () => {
return (
<div className="display: flex">
<SideNav />
<div className="flex-1 ml-[76px] overflow-hidden">
<NavBar />
<div className="overflow-x-auto">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route exact path="/tasks" element={<PrivateRoute />}>
<Route exact path="/tasks" element={<KanbanPage />} />
</Route>
<Route exact path="/profile" element={<PrivateRoute />}>
<Route exact path="/profile" element={<ProfileUpdatePage />} />
</Route>
<Route exact path="/calendar" element={<PrivateRoute />}>
<Route exact path="/calendar" element={<Calendar />} />
</Route>
<Route exact path="/priority" element={<PrivateRoute />}>
<Route exact path="/priority" element={<Eisenhower />} />
</Route>
<Route path="*" element={<Navigate to="/" />} />
</Routes>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,7 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "src/hooks/AuthHooks";
export const PrivateRoute = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? <Outlet /> : <Navigate to="/" />;
};

View File

@ -0,0 +1,7 @@
import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "src/hooks/AuthHooks";
export const PublicRoute = () => {
const { isAuthenticated } = useAuth();
return isAuthenticated ? <Navigate to="/d" /> : <Outlet />;
};

View File

@ -1,72 +1,43 @@
import axios from "axios";
import axiosInstance from "./configs/AxiosConfig";
import { axiosInstance } from "./AxiosConfig";
const baseURL = import.meta.env.VITE_BASE_URL;
// Function for user login
const apiUserLogin = data => {
export const apiUserLogin = (data) => {
return axiosInstance
.post("token/obtain/", data)
.then(response => {
console.log(response.statusText);
return response;
})
.catch(error => {
console.log("apiUserLogin error: ", error);
return error;
.then((response) => response)
.catch((error) => {
throw error;
});
};
// Function for user logout
const apiUserLogout = () => {
export const apiUserLogout = () => {
axiosInstance.defaults.headers["Authorization"] = ""; // Clear authorization header
localStorage.removeItem("access_token"); // Remove access token
localStorage.removeItem("refresh_token"); // Remove refresh token
};
// Function for Google login
const googleLogin = async token => {
export const googleLogin = async (token) => {
axios.defaults.withCredentials = true;
let res = await axios.post("http://localhost:8000/api/auth/google/", {
let res = await axios.post(`${baseURL}auth/google/`, {
code: token,
});
// console.log('service google login res: ', res);
return await res;
};
// Function to get 'hello' data
const getGreeting = () => {
return axiosInstance
.get("hello")
.then(response => {
return response;
})
.catch(error => {
return error;
});
};
const config = {
headers: {
"Content-Type": "application/json",
},
};
// Function to register
const createUser = async formData => {
export const createUser = async (formData) => {
try {
axios.defaults.withCredentials = true;
const resposne = axios.post("http://localhost:8000/api/user/create/", formData);
// const response = await axiosInstance.post('/user/create/', formData);
const response = await axios.post(`${baseURL}user/create/`, formData);
return response.data;
} catch (error) {
throw error;
} catch (e) {
console.error("Error in createUser function:", e);
throw e;
}
};
// Export the functions and Axios instance
export default {
apiUserLogin,
apiUserLogout,
getGreeting: getGreeting,
googleLogin,
createUser,
};

View File

@ -0,0 +1,53 @@
import axios from "axios";
import { redirect } from "react-router-dom";
const baseURL = import.meta.env.VITE_BASE_URL;
export const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.request.use((config) => {
const access_token = localStorage.getItem("access_token");
if (access_token) {
config.headers.Authorization = `Bearer ${access_token}`;
}
return config;
});
// handling token refresh on 401 Unauthorized errors
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
const originalRequest = error.config;
const refresh_token = localStorage.getItem("refresh_token");
// Check if the error is due to 401 and a refresh token is available
if (error.response && error.response.status === 401) {
if (refresh_token) {
return axiosInstance
.post("/token/refresh/", { refresh: refresh_token })
.then((response) => {
localStorage.setItem("access_token", response.data.access);
axiosInstance.defaults.headers["Authorization"] = "Bearer " + response.data.access;
originalRequest.headers["Authorization"] = "Bearer " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
console.log("Interceptors error: ", err);
});
} else {
redirect("/login");
}
}
return Promise.reject(error);
}
);

View File

@ -1,12 +1,8 @@
import axiosInstance from "./configs/AxiosConfig";
import { createTask, readTasks, readTaskByID, updateTask, deleteTask } from "./TaskApi";
export const fetchTags = () => {
return axiosInstance
.get("tags/")
.then(response => {
return response.data;
})
.catch(error => {
throw error;
});
};
// CRUD functions for "tags" endpoint
export const createTag = data => createTask("tags", data);
export const readTags = () => readTasks("tags");
export const readTagByID = id => readTaskByID("tags", id);
export const updateTag = (id, data) => updateTask("tags", id, data);
export const deleteTag = id => deleteTask("tags", id);

View File

@ -1,23 +1,73 @@
import axiosInstance from "./configs/AxiosConfig";
import { axiosInstance } from "src/api/AxiosConfig";
export const fetchTodoTasks = () => {
const baseURL = import.meta.env.VITE_BASE_URL;
export const createTask = (endpoint, data) => {
return axiosInstance
.get("todo/")
.then(response => {
return response.data;
})
.catch(error => {
.post(`${baseURL}${endpoint}/`, data)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
export const fetchTodoTasksID = id => {
export const readTasks = (endpoint) => {
return axiosInstance
.get(`todo/${id}/`)
.then(response => {
return response.data;
})
.catch(error => {
.get(`${baseURL}${endpoint}/`)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
export const readTaskByID = (endpoint, id) => {
return axiosInstance
.get(`${baseURL}${endpoint}/${id}/`)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
export const updateTask = (endpoint, id, data) => {
return axiosInstance
.put(`${baseURL}${endpoint}/${id}/`, data)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
export const deleteTask = (endpoint, id) => {
return axiosInstance
.delete(`${baseURL}${endpoint}/${id}/`)
.then((response) => response.data)
.catch((error) => {
throw error;
});
};
// Create
export const createTodoTask = (data) => createTask("todo", data);
export const createRecurrenceTask = (data) => createTask("daily", data);
export const createHabitTask = (data) => createTask("habit", data);
// Read
export const readTodoTasks = () => readTasks("todo");
export const readRecurrenceTasks = () => readTasks("daily");
export const readHabitTasks = () => readTasks("habit");
// Read by ID
export const readTodoTaskByID = (id) => readTaskByID("todo", id);
export const readRecurrenceTaskByID = (id) => readTaskByID("daily", id);
export const readHabitTaskByID = (id) => readTaskByID("habit", id);
// Update
export const updateTodoTask = (id, data) => updateTask("todo", id, data);
export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data);
export const updateHabitTask = (id, data) => updateTask("habit", id, data);
// Delete
export const deleteTodoTask = (id) => deleteTask("todo", id);
export const deleteRecurrenceTask = (id) => deleteTask("daily", id);
export const deleteHabitTask = (id) => deleteTask("habit", id);

View File

@ -1,11 +1,13 @@
import axios from 'axios';
import axios from "axios";
const baseURL = import.meta.env.VITE_BASE_URL;
const ApiUpdateUserProfile = async (formData) => {
try {
const response = await axios.post('http://127.0.1:8000/api/user/update/', formData, {
const response = await axios.post(`${baseURL}user/update/`, formData, {
headers: {
'Authorization': "Bearer " + localStorage.getItem('access_token'),
'Content-Type': 'multipart/form-data',
Authorization: "Bearer " + localStorage.getItem("access_token"),
"Content-Type": "multipart/form-data",
},
});
@ -13,7 +15,7 @@ const ApiUpdateUserProfile = async (formData) => {
return response.data;
} catch (error) {
console.error('Error updating user profile:', error);
console.error("Error updating user profile:", error);
throw error;
}
};

View File

@ -1,42 +0,0 @@
import axios from 'axios';
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',
},
});
// handling token refresh on 401 Unauthorized errors
axiosInstance.interceptors.response.use(
response => response,
error => {
const originalRequest = error.config;
const refresh_token = localStorage.getItem('refresh_token');
// Check if the error is due to 401 and a refresh token is available
if (error.response.status === 401 && error.response.statusText === "Unauthorized" && refresh_token !== "undefined") {
return axiosInstance
.post('/token/refresh/', { refresh: refresh_token })
.then((response) => {
localStorage.setItem('access_token', response.data.access);
axiosInstance.defaults.headers['Authorization'] = "Bearer " + response.data.access;
originalRequest.headers['Authorization'] = "Bearer " + response.data.access;
return axiosInstance(originalRequest);
})
.catch(err => {
console.log('Interceptors error: ', err);
});
}
return Promise.reject(error);
}
);
export default axiosInstance;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,30 +1,110 @@
import React from 'react';
import { useState, useEffect } from "react";
import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi";
import { readTodoTasks } from "../../api/TaskApi";
import { axiosInstance } from "src/api/AxiosConfig";
function EachBlog({ name, colorCode, contentList, icon }) {
const [tasks, setTasks] = useState(contentList);
const handleCheckboxChange = async (index) => {
try {
setTasks(contentList);
const updatedTasks = [...tasks];
const taskId = updatedTasks[index].id;
const response = await axiosInstance.patch(`todo/${taskId}/`, {
completed: !updatedTasks[index].completed,
});
updatedTasks[index].completed = response.data.completed;
setTasks(updatedTasks);
} catch (error) {
console.error("Error updating task:", error);
}
};
function EachBlog({ name, colorCode }) {
return (
<div className={`grid grid-rows-2 gap-4 text-left p-4 rounded-lg bg-white border border-gray-300 shadow-md`}>
<div className={`text-xl font-bold`} style={{ color: colorCode }}>
{name}
<div className={`h-full text-left p-4 rounded-lg bg-white border border-gray-300 overflow-y-auto`}>
<div className="flex" style={{ color: colorCode }}>
<span className="mx-2 mt-1">{icon}</span>
<span>{name}</span>
</div>
<div className='h-36'>
Content goes here
<hr className="my-3 h-0.5 border-t-0 bg-gray-300 opacity-100 dark:opacity-50" />
<div className="space-y-2">
{contentList && contentList.length > 0 ? (
contentList.map((item, index) => (
<div key={index} className="flex items-start">
<input
type="checkbox"
checked={item.completed}
className="checkbox mt-1 mr-2"
onChange={() => handleCheckboxChange(index)}
/>
<label className={`cursor-pointer ${item.completed ? "line-through text-gray-500" : ""}`}>
{item.title}
</label>
</div>
))
) : (
<p className="text-gray-500 text-center">No tasks</p>
)}
</div>
</div>
);
}
function Eisenhower() {
export function Eisenhower() {
const [tasks, setTasks] = useState([]);
useEffect(() => {
readTodoTasks()
.then((data) => {
console.log(data);
const contentList_ui = data.filter((task) => task.priority === 1);
const contentList_uni = data.filter((task) => task.priority === 2);
const contentList_nui = data.filter((task) => task.priority === 3);
const contentList_nuni = data.filter((task) => task.priority === 4);
setTasks({
contentList_ui,
contentList_uni,
contentList_nui,
contentList_nuni,
});
})
.catch((error) => console.error("Error fetching tasks:", error));
}, []);
return (
<div className='bg-slate-100 text-left p-4 m-auto'>
<h1 className="text-3xl font-bold mb-4">The Eisenhower Matrix</h1>
<div className='grid grid-rows-2 grid-cols-2 gap-2'>
<EachBlog name="Urgent & Important" colorCode="#FF5733" />
<EachBlog name="Urgent & Not important" colorCode="#FDDD5C" />
<EachBlog name="Not urgent & Important" colorCode="#189AB4" />
<EachBlog name="Not urgent & Not important" colorCode="#94FA92" />
<div className="bg-slate-100 text-left p-4 w-full h-max">
<div className="grid grid-rows-2 grid-cols-2 gap-2">
<EachBlog
name="Urgent & Important"
colorCode="#ff5f68"
icon={<FiAlertCircle />}
contentList={tasks.contentList_ui}
/>
<EachBlog
name="Urgent & Not important"
colorCode="#ffb000"
icon={<FiClock />}
contentList={tasks.contentList_uni}
/>
<EachBlog
name="Not urgent & Important"
colorCode="#4772fa"
icon={<FiCheckCircle />}
contentList={tasks.contentList_nui}
/>
<EachBlog
name="Not urgent & Not important"
colorCode="#0cce9c"
icon={<FiXCircle />}
contentList={tasks.contentList_nuni}
/>
</div>
</div>
);
}
export default Eisenhower;

View File

@ -0,0 +1,84 @@
import { useCallback } from "react";
import Particles from "react-tsparticles";
import { loadFull } from "tsparticles";
export function FloatingParticles() {
const particlesInit = useCallback(async (engine) => {
await loadFull(engine);
}, []);
return (
<div style={{ width: "0%", height: "100vh" }}>
<Particles
id="particles"
init={particlesInit}
className="-z-10"
options={{
fpsLimit: 240,
smooth: true,
interactivity: {
events: {
onClick: {
enable: true,
mode: "push",
},
onHover: {
enable: true,
mode: "repulse",
},
resize: true,
},
modes: {
push: {
quantity: 4,
},
repulse: {
distance: 200,
duration: 0.4,
},
},
},
particles: {
color: {
value: "#4f46e5",
},
links: {
color: "#818cf8",
distance: 150,
enable: true,
opacity: 0.5,
width: 1,
},
move: {
direction: "none",
enable: true,
outModes: {
default: "bounce",
},
random: false,
speed: 2,
straight: false,
},
number: {
density: {
enable: true,
area: 800,
},
value: 40,
},
opacity: {
value: 0.5,
},
shape: {
type: "square",
},
size: {
value: { min: 4, max: 5 },
},
},
detectRetina: true,
}}
/>
</div>
);
}

View File

@ -1,11 +0,0 @@
import React from 'react';
function HomePage() {
return (
<div>
<h1>Welcome to My Website</h1>
</div>
);
}
export default HomePage;

View File

@ -1,19 +0,0 @@
import { useState, useEffect } from 'react';
function IsAuthenticated() {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const access_token = localStorage.getItem('access_token');
if (access_token) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
}, []);
return isAuthenticated;
}
export default IsAuthenticated;

View File

@ -1,141 +1,153 @@
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { useState } from "react";
import { useNavigate, redirect } from "react-router-dom";
import { useGoogleLogin } from "@react-oauth/google";
import { FcGoogle } from "react-icons/fc";
import { useAuth } from "src/hooks/AuthHooks";
import { FloatingParticles } from "../FlaotingParticles";
import { NavPreLogin } from "../navigations/NavPreLogin";
import { apiUserLogin, googleLogin } from "src/api/AuthenticationApi";
import refreshAccessToken from "./refreshAcesstoken";
import axiosapi from "../../api/AuthenticationApi";
function LoginPage() {
export function LoginPage() {
const { setIsAuthenticated } = useAuth();
const Navigate = useNavigate();
useEffect(() => {
if (!refreshAccessToken()) {
Navigate("/");
}
}, []);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState(null);
const handleEmailChange = event => {
const handleEmailChange = (event) => {
setEmail(event.target.value);
};
const handlePasswordChange = event => {
const handlePasswordChange = (event) => {
setPassword(event.target.value);
};
const handleSubmit = event => {
const handleSubmit = (event) => {
event.preventDefault();
// Send a POST request to the authentication API
axiosapi
.apiUserLogin({
email: email,
password: password,
})
.then(res => {
// On successful login, store tokens and set the authorization header
apiUserLogin({
email: email,
password: password,
})
.then((res) => {
localStorage.setItem("access_token", res.data.access);
localStorage.setItem("refresh_token", res.data.refresh);
axiosapi.axiosInstance.defaults.headers["Authorization"] = "Bearer " + res.data.access;
Navigate("/");
setIsAuthenticated(true);
redirect("/");
})
.catch(err => {
console.log("Login failed");
console.log(err);
.catch((err) => {
setError("Incorrect username or password");
});
};
const googleLoginImplicit = useGoogleLogin({
flow: "auth-code",
redirect_uri: "postmessage",
onSuccess: async response => {
onSuccess: async (response) => {
try {
const loginResponse = await axiosapi.googleLogin(response.code);
const loginResponse = await googleLogin(response.code);
if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
setIsAuthenticated(true);
Navigate("/");
}
} catch (error) {
console.error("Error with the POST request:", error);
}
},
onError: errorResponse => console.log(errorResponse),
onError: (errorResponse) => console.log(errorResponse),
});
return (
<div data-theme="night" className="min-h-screen flex">
{/* Left Section (Login Box) */}
<div className="w-1/2 flex items-center justify-center">
<div className="w-96 bg-neutral rounded-lg p-8 shadow-md space-y-4">
<h2 className="text-2xl font-semibold text-left">Log in to your account</h2>
{/* Email Input */}
<div className="form-control">
<label className="label" htmlFor="email">
<p className="text-bold">
Email<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="email"
id="email"
placeholder="Enter your email"
onChange={handleEmailChange}
/>
</div>
{/* Password Input */}
<div className="form-control">
<label className="label" htmlFor="password">
<p className="text-bold">
Password<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="password"
id="password"
placeholder="Enter your password"
onChange={handlePasswordChange}
/>
</div>
{/* Login Button */}
<button className="btn btn-primary w-full" onClick={handleSubmit}>
Login
</button>
<div className="divider">OR</div>
{/* Login with Google Button */}
<button className="btn btn-outline btn-secondary w-full" onClick={() => googleLoginImplicit()}>
Login with Google
</button>
{/* Forgot Password Link */}
<div className="justify-left">
<a href="#" className="text-blue-500 text-sm text-left">
Forgot your password?
</a>
</div>
</div>
</div>
<div>
<NavPreLogin
text="Don't have account?"
btn_text="Sign Up"
link="/signup"
/>
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
{/* Particles Container */}
{/* Right Section (Blurred Image Background) */}
<div className="w-1/2 relative">
<div
className="w-full h-full bg-cover bg-center"
style={{
backgroundImage: 'url("https://th.bing.com/th/id/OIG.9byG0pWUCcbGL7Kly9tA?pid=ImgGn&w=1024&h=1024&rs=1")',
filter: "blur(2px) brightness(.5)",
}}></div>
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 text-white text-2xl font-semibold">
Text Overlay
<FloatingParticles />
{/* Login Box */}
<div className="flex items-center justify-center flex-1 z-50">
<div className="w-100 bg-white border-solid rounded-lg p-8 shadow space-y-4">
<h2 className="text-3xl font-bold">Log in to your account</h2>
{/* Error Message */}
{error && (
<div role="alert" className="alert alert-error">
<svg
xmlns="http://www.w3.org/2000/svg"
className="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<span>{error}</span>
</div>
)}
{/* Email Input */}
<div className="form-control ">
<label className="label" htmlFor="email">
<p className="text-bold">
Email<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="email"
id="email"
placeholder="Enter your email"
value={email}
onChange={handleEmailChange}
/>
</div>
{/* Password Input */}
<div className="form-control">
<label className="label" htmlFor="password">
<p className="text-bold">
Password<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="password"
id="password"
placeholder="Enter your password"
value={password}
onChange={handlePasswordChange}
/>
</div>
{/* Login Button */}
<button
className="btn bg-blue-700 hover:bg-blue-900 w-full text-white font-bold"
onClick={handleSubmit}
>
Login
</button>
<div className="divider">OR</div>
{/* Login with Google Button */}
<button
className="btn bg-gray-200 btn-outline w-full "
onClick={() => googleLoginImplicit()}
>
<FcGoogle className="rounded-full bg-white" />
Login with Google
</button>
</div>
</div>
</div>
</div>
);
}
export default LoginPage;

View File

@ -1,151 +1,162 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import axiosapi from '../../api/AuthenticationApi';
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { FcGoogle } from "react-icons/fc";
import { useGoogleLogin } from "@react-oauth/google";
import { NavPreLogin } from "../navigations/NavPreLogin";
import { useAuth } from "src/hooks/AuthHooks";
import { createUser, googleLogin } from "src/api/AuthenticationApi";
import { FloatingParticles } from "../FlaotingParticles";
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import FormControlLabel from '@mui/material/FormControlLabel';
import Checkbox from '@mui/material/Checkbox';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
export function SignUp() {
const Navigate = useNavigate();
const { setIsAuthenticated } = useAuth();
const [formData, setFormData] = useState({
email: "",
username: "",
password: "",
});
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
try {
const data = await createUser(formData);
localStorage.setItem("access_token", data.access_token);
localStorage.setItem("refresh_token", data.refresh_token);
await delay(200);
setIsAuthenticated(true);
Navigate("/");
} catch (error) {
console.error("Error creating user:", error);
setError("Registration failed. Please try again.");
} finally {
setIsSubmitting(false);
}
};
const handleEmailChange = (e) => {
setFormData({ ...formData, email: e.target.value });
};
const handleUsernameChange = (e) => {
setFormData({ ...formData, username: e.target.value });
};
const handlePasswordChange = (e) => {
setFormData({ ...formData, password: e.target.value });
};
const googleLoginImplicit = useGoogleLogin({
flow: "auth-code",
redirect_uri: "postmessage",
scope:
"https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/calendar.acls.readonly https://www.googleapis.com/auth/calendar.events.readonly",
onSuccess: async (response) => {
try {
const loginResponse = await googleLogin(response.code);
if (loginResponse && loginResponse.data) {
const { access_token, refresh_token } = loginResponse.data;
localStorage.setItem("access_token", access_token);
localStorage.setItem("refresh_token", refresh_token);
setIsAuthenticated(true);
Navigate("/profile");
}
} catch (error) {
console.error("Error with the POST request:", error);
setIsAuthenticated(false);
}
},
onError: (errorResponse) => console.log(errorResponse),
});
function Copyright(props) {
return (
<Typography variant="body2" color="text.secondary" align="center" {...props}>
{'Copyright © '}
<Link color="inherit" href="https://github.com/TurTaskProject/TurTaskWeb">
TurTask
</Link>{' '}
{new Date().getFullYear()}
{'.'}
</Typography>
<div>
<NavPreLogin
text="Already have an account?"
btn_text="Log In"
link="/login"
/>
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
<FloatingParticles />
<div className="w-1/4 h-1 flex items-center justify-center z-10">
<div className="w-96 bg-white rounded-lg p-8 space-y-4 z-10">
{/* Register Form */}
<h2 className="text-3xl font-bold text-center">Signup</h2>
{/* Email Input */}
<div className="form-control ">
<label className="label" htmlFor="email">
<p className="text-bold">
Email<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="email"
id="email"
placeholder="Enter your email"
onChange={handleEmailChange}
/>
</div>
{/* Username Input */}
<div className="form-control">
<label className="label" htmlFor="Username">
<p className="text-bold">
Username<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="text"
id="Username"
placeholder="Enter your username"
onChange={handleUsernameChange}
/>
</div>
{/* Password Input */}
<div className="form-control">
<label className="label" htmlFor="password">
<p className="text-bold">
Password<span className="text-red-500 text-bold">*</span>
</p>
</label>
<input
className="input"
type="password"
id="password"
placeholder="Enter your password"
onChange={handlePasswordChange}
/>
</div>
<br></br>
{/* Signups Button */}
<button className="btn btn-success w-full " onClick={handleSubmit}>
Signup
</button>
<div className="divider">OR</div>
{/* Login with Google Button */}
<button
className="btn btn-outline btn-secondary w-full "
onClick={() => googleLoginImplicit()}
>
<FcGoogle className="rounded-full bg-white" />
Login with Google
</button>
{/* Already have an account? */}
<div className="text-blue-500 flex justify-center text-sm">
<a href="login">Already have an account?</a>
</div>
</div>
</div>
</div>
</div>
);
}
const defaultTheme = createTheme();
export default function SignUp() {
const Navigate = useNavigate();
const [formData, setFormData] = useState({
email: '',
username: '',
password: '',
});
const [error, setError] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setError(null);
try {
axiosapi.createUser(formData);
} catch (error) {
console.error('Error creating user:', error);
setError('Registration failed. Please try again.');
} finally {
setIsSubmitting(false);
}
Navigate('/login');
};
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
return (
<ThemeProvider theme={defaultTheme}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
autoComplete="username"
name="Username"
required
fullWidth
id="Username"
label="Username"
autoFocus
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
onChange={handleChange}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox value="allowExtraEmails" color="primary" />}
label="I want to receive inspiration, marketing promotions and updates via email."
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign Up
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
<Link href="#" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</Box>
</Box>
<Copyright sx={{ mt: 5 }} />
</Container>
</ThemeProvider>
);
}

View File

@ -1,37 +0,0 @@
import axios from 'axios';
async function refreshAccessToken() {
const refresh_token = localStorage.getItem('refresh_token');
const access_token = localStorage.getItem('access_token');
if (access_token) {
return true;
}
if (!refresh_token) {
return false;
}
const refreshUrl = 'http://127.0.0.1:8000/api/token/refresh/';
try {
const response = await axios.post(refreshUrl, { refresh: refresh_token });
if (response.status === 200) {
// Successful refresh - save the new access token and refresh token
const newAccessToken = response.data.access;
const newRefreshToken = response.data.refresh;
localStorage.setItem('access_token', newAccessToken);
localStorage.setItem('refresh_token', newRefreshToken);
return true;
} else {
return false;
}
} catch (error) {
return false;
}
}
export default refreshAccessToken;

View File

@ -1,42 +1,26 @@
import { fetchTodoTasks } from '../../api/TaskApi';
import { readTodoTasks } from "src/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;
// }
let eventGuid = 0;
const mapResponseToEvents = (response) => {
return response.map(item => ({
id: createEventId(),
title: item.title,
start: item.start_event,
end: item.end_event,
}));
}
return response.map((item) => ({
id: item.id,
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 [];
}
try {
const response = await readTodoTasks();
return mapResponseToEvents(response);
} catch (error) {
console.error(error);
return [];
}
}
export function createEventId() {
return String(eventGuid++);
}
return String(eventGuid++);
}

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState } from "react";
import { formatDate } from "@fullcalendar/core";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
@ -6,7 +6,7 @@ import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import { getEvents, createEventId } from "./TaskDataHandler";
export default class Calendar extends React.Component {
export class Calendar extends React.Component {
state = {
weekendsVisible: true,
currentEvents: [],
@ -43,7 +43,8 @@ export default class Calendar extends React.Component {
renderSidebar() {
return (
<div className="w-72 bg-blue-100 border-r border-blue-200 p-8 flex-shrink-0">
<div className="w-72 bg-blue-100 border-r border-blue-200 p-8 flex flex-col">
{/* Description Zone */}
<div className="mb-8">
<h2 className="text-xl font-bold">Instructions</h2>
<ul className="list-disc pl-4">
@ -53,19 +54,24 @@ export default class Calendar extends React.Component {
</ul>
</div>
{/* Toggle */}
<div className="mb-8">
<label className="flex items-center">
<input
type="checkbox"
checked={this.state.weekendsVisible}
onChange={this.handleWeekendsToggle}
className="mr-2"
className="mr-2 mb-4"
/>
Toggle weekends
</label>
<button className="btn btn-info" onClick={() => alert("Commit soon🥺")}>
Load Data from Google
</button>
</div>
<div>
{/* Show all task */}
<div className="overflow-y-auto">
<h2 className="text-xl font-bold">All Events ({this.state.currentEvents.length})</h2>
<ul>{this.state.currentEvents.map(renderSidebarEvent)}</ul>
</div>
@ -98,7 +104,14 @@ export default class Calendar extends React.Component {
handleEventClick = (clickInfo) => {
if (confirm(`Are you sure you want to delete the event '${clickInfo.event.title}'`)) {
clickInfo.event.remove();
axiosInstance
.delete(`todo/${clickInfo.event.id}/`)
.then((response) => {
clickInfo.event.remove();
})
.catch((error) => {
console.error("Error deleting Task:", error);
});
}
};

View File

@ -0,0 +1,36 @@
import { AreaChart, Title } from "@tremor/react";
import { useState, useEffect } from "react";
import { axiosInstance } from "src/api/AxiosConfig";
export const AreaChartGraph = () => {
const [areaChartDataArray, setAreaChartDataArray] = useState([]);
useEffect(() => {
const fetchAreaChartData = async () => {
try {
const response = await axiosInstance.get("/dashboard/weekly/");
const areaChartData = response.data;
setAreaChartDataArray(areaChartData);
} catch (error) {
console.error("Error fetching area chart data:", error);
}
};
fetchAreaChartData();
}, []);
return (
<>
<Title>Number of tasks statistics vs. last week</Title>
<AreaChart
className="mt-6"
data={areaChartDataArray}
index="date"
categories={["This Week", "Last Week"]}
colors={["neutral", "indigo"]}
yAxisWidth={30}
showAnimation
/>
</>
);
};

View File

@ -0,0 +1,36 @@
import { BarChart, Title } from "@tremor/react";
import { useState, useEffect } from "react";
import { axiosInstance } from "src/api/AxiosConfig";
export const BarChartGraph = () => {
const [barchartDataArray, setBarChartDataArray] = useState([]);
useEffect(() => {
const fetchAreaChartData = async () => {
try {
const response = await axiosInstance.get("/dashboard/weekly/");
const barchartDataArray = response.data;
setBarChartDataArray(barchartDataArray);
} catch (error) {
console.error("Error fetching area chart data:", error);
}
};
fetchAreaChartData();
}, []);
return (
<>
<Title>Task completed statistics vs. last week</Title>
<BarChart
className="mt-6"
data={barchartDataArray}
index="date"
categories={["This Week", "Last Week"]}
colors={["neutral", "indigo"]}
yAxisWidth={30}
showAnimation
/>
</>
);
};

View File

@ -0,0 +1,39 @@
import { DonutChart } from "@tremor/react";
import { axiosInstance } from "src/api/AxiosConfig";
import { useState, useEffect } from "react";
export function DonutChartGraph() {
const [donutData, setDonutData] = useState([]);
useEffect(() => {
const fetchDonutData = async () => {
try {
const response = await axiosInstance.get("/dashboard/stats/");
const todoCount = response.data.todo_count || 0;
const recurrenceCount = response.data.recurrence_count || 0;
const donutData = [
{ name: "Todo", count: todoCount },
{ name: "Recurrence", count: recurrenceCount },
];
setDonutData(donutData);
} catch (error) {
console.error("Error fetching donut data:", error);
}
};
fetchDonutData();
}, []);
return (
<DonutChart
className="mt-6"
data={donutData}
category="count"
index="name"
colors={["rose", "yellow", "orange"]}
showAnimation
radius={25}
/>
);
}

View File

@ -0,0 +1,57 @@
import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react";
import { useEffect, useState } from "react";
import { axiosInstance } from "src/api/AxiosConfig";
export function KpiCard() {
const [kpiCardData, setKpiCardData] = useState({
completedThisWeek: 0,
completedLastWeek: 0,
incOrdec: undefined,
percentage: 0,
});
useEffect(() => {
const fetchKpiCardData = async () => {
try {
const response = await axiosInstance.get("/dashboard/stats/");
const completedThisWeek = response.data.completed_this_week || 0;
const completedLastWeek = response.data.completed_last_week || 0;
const percentage = (completedThisWeek / completedLastWeek) * 100;
let incOrdec = undefined;
if (completedThisWeek <= completedLastWeek) {
incOrdec = "moderateDecrease";
}
if (completedThisWeek > completedLastWeek) {
incOrdec = "moderateIncrease";
}
setKpiCardData({
completedThisWeek,
completedLastWeek,
incOrdec,
percentage,
});
} catch (error) {
console.error("Error fetching KPI card data:", error);
}
};
fetchKpiCardData();
}, []);
return (
<Card className="max-w-lg mx-auto">
<Flex alignItems="start">
<div>
<Metric>{kpiCardData.completedThisWeek}</Metric>
</div>
<BadgeDelta deltaType={kpiCardData.incOrdec}>{kpiCardData.percentage.toFixed(0)}%</BadgeDelta>
</Flex>
<Flex className="mt-4">
<Text className="truncate">vs. {kpiCardData.completedLastWeek} (last week)</Text>
</Flex>
<ProgressBar value={kpiCardData.percentage} className="mt-2" />
</Card>
);
}

View File

@ -0,0 +1,44 @@
import { Card, Flex, ProgressCircle } from "@tremor/react";
import { useState, useEffect } from "react";
import { axiosInstance } from "src/api/AxiosConfig";
export function ProgressCircleChart() {
const [progressData, setProgressData] = useState(0);
useEffect(() => {
const fetchProgressData = async () => {
try {
const response = await axiosInstance.get("/dashboard/stats/");
let completedLastWeek = response.data.completed_last_week || 0;
let assignLastWeek = response.data.tasks_assigned_last_week || 0;
if (completedLastWeek === undefined) {
completedLastWeek = 0;
}
if (assignLastWeek === undefined) {
assignLastWeek = 0;
}
const progress = (completedLastWeek / assignLastWeek) * 100;
setProgressData(progress);
} catch (error) {
console.error("Error fetching progress data:", error);
}
};
fetchProgressData();
}, []);
return (
<Card className="max-w-lg mx-auto">
<Flex className="flex-col items-center">
<ProgressCircle className="mt-6" value={progressData} size={200} strokeWidth={10} radius={60} color="indigo">
<span className="h-12 w-12 rounded-full bg-indigo-100 flex items-center justify-center text-sm text-indigo-500 font-medium">
{progressData.toFixed(0)} %
</span>
</ProgressCircle>
</Flex>
</Card>
);
}

View File

@ -0,0 +1,72 @@
import { Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title, Legend } from "@tremor/react";
import { KpiCard } from "./KpiCard";
import { BarChartGraph } from "./Barchart";
import { DonutChartGraph } from "./DonutChart";
import { AreaChartGraph } from "./Areachart";
import { ProgressCircleChart } from "./ProgressCircle";
import { useState } from "react";
export function Dashboard() {
const [value, setValue] = useState({
from: new Date(2021, 0, 1),
to: new Date(2023, 0, 7),
});
return (
<div className="flex flex-col p-12">
<div>
<Title>Dashboard</Title>
<Text>All of your progress will be shown right here.</Text>
<br />
</div>
<div>
<TabGroup className="mt-6">
<TabList>
<Tab>Weekly</Tab>
<Tab>Overview</Tab>
</TabList>
<TabPanels>
{/*Weekly Tab*/}
<TabPanel>
<Grid numItemsMd={2} numItemsLg={3} className="gap-6 mt-6">
<Card>
<Title>Highlights vs. last week</Title>
<br />
<KpiCard />
<br />
<Title>Last week progress rate</Title>
<br />
<ProgressCircleChart />
<Legend
className="mt-3 mx-auto w-1/2"
categories={["Completed Tasks", "Assigned Tasks"]}
colors={["indigo"]}></Legend>
</Card>
<Card>
<BarChartGraph />
</Card>
<Card>
<AreaChartGraph />
</Card>
</Grid>
</TabPanel>
<TabPanel>
<div className="h-31">
<Card className="mx-auto h-full">
<Title>Tasks</Title>
<DonutChartGraph />
<br />
<Legend
className="mt-3 mx-auto w-1/2"
categories={["Todo Task", "Recurrence Task"]}
colors={["rose", "yellow"]}
/>
</Card>
</div>
</TabPanel>
</TabPanels>
</TabGroup>
</div>
</div>
);
}

View File

@ -1,22 +0,0 @@
import React from 'react';
function PlusIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 9v6m3-3H9m12 0a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
);
}
export default PlusIcon;

View File

@ -1,23 +0,0 @@
import React from 'react';
function TrashIcon() {
return (
React.createElement(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
viewBox: "0 0 24 24",
strokeWidth: 1.5,
className: "w-6 h-6"
},
React.createElement("path", {
strokeLinecap: "round",
strokeLinejoin: "round",
d: "M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
})
)
);
}
export default TrashIcon;

View File

@ -1,173 +1,64 @@
import { SortableContext, useSortable } from "@dnd-kit/sortable";
import TrashIcon from "../icons/trashIcon";
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);
import { AiOutlinePlusCircle } from "react-icons/ai";
import { useMemo } from "react";
import { TaskCard } from "./taskCard";
export function ColumnContainer({ column, createTask, tasks, deleteTask, updateTask }) {
// Memoize task IDs to prevent unnecessary recalculations
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 (
<div
ref={setNodeRef}
style={style}
className="
bg-columnBackgroundColor
w-[350px]
h-[500px]
max-h-[500px]
rounded-md
flex
flex-col
"
></div>
);
}
return (
<div
ref={setNodeRef}
style={style}
className="
bg-columnBackgroundColor
w-[350px]
h-[500px]
max-h-[500px]
bg-[#f1f2f4]
w-[280px]
max-h-[400px]
rounded-md
flex
flex-col
"
>
">
{/* Column title */}
<div
{...attributes}
{...listeners}
onClick={() => {
setEditMode(true);
}}
className="
bg-mainBackgroundColor
ml-3
text-md
h-[60px]
cursor-grab
rounded-md
rounded-b-none
p-3
font-bold
border-columnBackgroundColor
border-4
flex
items-center
justify-between
"
>
<div className="flex gap-2">
<div
className="
flex
justify-center
items-center
bg-columnBackgroundColor
px-2
py-1
text-sm
rounded-full
"
></div>
{!editMode && column.title}
{editMode && (
<input
className="bg-white focus:border-rose-500 border rounded outline-none px-2"
value={column.title}
onChange={(e) => updateColumn(column.id, e.target.value)}
autoFocus
onBlur={() => {
setEditMode(false);
}}
onKeyDown={(e) => {
if (e.key !== "Enter") return;
setEditMode(false);
}}
/>
)}
</div>
<button
onClick={() => {
deleteColumn(column.id);
}}
className="
stroke-gray-500
hover:stroke-white
hover:bg-columnBackgroundColor
rounded
px-1
py-2
"
>
<TrashIcon />
</button>
">
<div className="flex gap-2">{column.title}</div>
</div>
{/* Column task container */}
<div className="flex flex-grow flex-col gap-4 p-2 overflow-x-hidden overflow-y-auto">
<div className="flex flex-grow flex-col gap-2 p-1 overflow-x-hidden overflow-y-auto">
{/* Provide a SortableContext for the tasks within the column */}
<SortableContext items={tasksIds}>
{/* Render TaskCard for each task in the column */}
{tasks.map((task) => (
<TaskCard
key={task.id}
task={task}
deleteTask={deleteTask} // Pass deleteTask to TaskCard
deleteTask={deleteTask}
updateTask={updateTask}
// Adjust the useSortable hook for tasks to enable dragging
useSortable={(props) => useSortable({ ...props, disabled: false })}
/>
))}
</SortableContext>
</div>
{/* Column footer */}
<button
className="flex gap-2 items-center border-columnBackgroundColor border-2 rounded-md p-4 border-x-columnBackgroundColor hover:bg-mainBackgroundColor hover:text-rose-500 active:bg-white"
className="flex gap-2 items-center rounded-md p-2 my-2 hover:bg-zinc-200 active:bg-zinc-400"
onClick={() => {
createTask(column.id);
}}
>
<PlusIcon />
}}>
<AiOutlinePlusCircle />
Add task
</button>
</div>
);
}
export default ColumnContainer;

View File

@ -0,0 +1,15 @@
import { ColumnContainer } from "./columnContainer";
export function ColumnContainerCard({ column, createTask, tasks, deleteTask, updateTask }) {
return (
<div className="card bg-[#f1f2f4] shadow border p-1 my-2">
<ColumnContainer
column={column}
createTask={createTask}
tasks={tasks}
deleteTask={deleteTask}
updateTask={updateTask}
/>
</div>
);
}

View File

@ -1,110 +1,20 @@
import PlusIcon from "../icons/plusIcon";
import { useMemo, useState } from "react";
import ColumnContainer from "./columnContainer";
import {
DndContext,
DragOverlay,
PointerSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import { useMemo, useState, useEffect } from "react";
import { ColumnContainerCard } from "./columnContainerWrapper";
import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
import { SortableContext, arrayMove } from "@dnd-kit/sortable";
import { createPortal } from "react-dom";
import TaskCard from "./taskCard";
import { TaskCard } from "./taskCard";
import { axiosInstance } from "src/api/AxiosConfig";
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);
export function KanbanBoard() {
const [columns, setColumns] = useState([]);
const [boardId, setBoardData] = useState();
const [isLoading, setLoading] = useState(false);
const [tasks, setTasks] = useState([]);
const [activeTask, setActiveTask] = useState(null);
const columnsId = useMemo(() => columns.map((col) => col.id), [columns]);
const [tasks, setTasks] = useState(defaultTasks);
const [activeColumn, setActiveColumn] = useState(null);
const [activeTask, setActiveTask] = useState(null);
// ---------------- END STATE INITIATE ----------------
const sensors = useSensors(
useSensor(PointerSensor, {
@ -114,88 +24,177 @@ function KanbanBoard() {
})
);
// ---------------- Task Handlers ----------------
const handleTaskUpdate = (tasks, updatedTask) => {
const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task));
setTasks(updatedTasks);
};
const handleApiError = (error, action) => {
console.error(`Error ${action}:`, error);
};
const createTask = async (columnId) => {
try {
const response = await axiosInstance.post("todo/", {
title: `New Task`,
importance: 1,
difficulty: 1,
challenge: false,
fromSystem: false,
is_active: false,
is_full_day_event: false,
completed: false,
priority: 1,
list_board: columnId,
});
const newTask = {
id: response.data.id,
columnId,
content: response.data.title,
};
setTasks((prevTasks) => [...prevTasks, newTask]);
} catch (error) {
handleApiError(error, "creating task");
}
};
const deleteTask = async (id) => {
try {
await axiosInstance.delete(`todo/${id}/`);
const newTasks = tasks.filter((task) => task.id !== id);
setTasks(newTasks);
} catch (error) {
handleApiError(error, "deleting task");
}
};
const updateTask = async (id, content, tasks) => {
try {
if (content === "") {
await deleteTask(id);
} else {
const response = await axiosInstance.put(`todo/${id}/`, { content });
const updatedTask = {
id,
columnId: response.data.list_board,
content: response.data.title,
};
handleTaskUpdate(tasks, updatedTask);
}
} catch (error) {
handleApiError(error, "updating task");
}
};
// ---------------- END Task Handlers ----------------
// ---------------- Fetch Data ----------------
useEffect(() => {
const fetchData = async () => {
try {
const tasksResponse = await axiosInstance.get("/todo");
// Transform
const transformedTasks = tasksResponse.data.map((task) => ({
id: task.id,
columnId: task.list_board,
content: task.title,
difficulty: task.difficulty,
notes: task.notes,
importance: task.importance,
challenge: task.challenge,
fromSystem: task.fromSystem,
creation_date: task.creation_date,
last_update: task.last_update,
is_active: task.is_active,
is_full_day_event: task.is_full_day_event,
start_event: task.start_event,
end_event: task.end_event,
google_calendar_id: task.google_calendar_id,
completed: task.completed,
completion_date: task.completion_date,
priority: task.priority,
user: task.user,
list_board: task.list_board,
tags: task.tags,
}));
setTasks(transformedTasks);
const columnsResponse = await axiosInstance.get("/lists");
// Transform
const transformedColumns = columnsResponse.data.map((column) => ({
id: column.id,
title: column.name,
}));
setColumns(transformedColumns);
} catch (error) {
console.error("Error fetching data from API:", error);
}
};
fetchData();
}, []);
useEffect(() => {
const fetchBoardData = async () => {
try {
setLoading(true);
const response = await axiosInstance.get("boards/");
if (response.data && response.data.length > 0) {
setBoardData(response.data[0]);
}
} catch (error) {
console.error("Error fetching board data:", error);
setLoading(false);
}
setLoading(false);
};
fetchBoardData();
}, []);
// ---------------- END Fetch Data ----------------
return (
<div
className="
m-auto
flex
w-full
items-center
overflow-x-auto
overflow-y-hidden
"
>
<DndContext
sensors={sensors}
onDragStart={onDragStart}
onDragEnd={onDragEnd}
onDragOver={onDragOver}
>
<div className="m-auto flex gap-4">
<div className="flex gap-4">
<SortableContext items={columnsId}>
{columns.map((col) => (
<ColumnContainer
key={col.id}
column={col}
deleteColumn={deleteColumn}
updateColumn={updateColumn}
createTask={createTask}
deleteTask={deleteTask}
updateTask={updateTask}
tasks={tasks.filter((task) => task.columnId === col.id)}
/>
))}
</SortableContext>
</div>
{/* create new column */}
<button
onClick={() => {
createNewColumn();
}}
className="
h-[60px]
w-[350px]
min-w-[350px]
cursor-pointer
rounded-lg
bg-mainBackgroundColor
border-2
border-columnBackgroundColor
p-4
ring-rose-500
hover:ring-2
m-auto
flex
gap-2
"
>
<PlusIcon />
Add Column
</button>
w-full
items-center
justify-center
overflow-x-auto
overflow-y-hidden
">
<DndContext sensors={sensors} onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver}>
<div className="flex gap-4">
<div className="flex gap-4">
{!isLoading ? (
<SortableContext items={columnsId}>
{columns.map((col) => (
<ColumnContainerCard
key={col.id}
column={col}
createTask={createTask}
deleteTask={deleteTask}
updateTask={updateTask}
tasks={(tasks || []).filter((task) => task.columnId === col.id)}
/>
))}{" "}
</SortableContext>
) : (
<span className="loading loading-dots loading-lg"></span>
)}
</div>
</div>
{createPortal(
<DragOverlay>
{activeColumn && (
<ColumnContainer
column={activeColumn}
deleteColumn={deleteColumn}
updateColumn={updateColumn}
createTask={createTask}
deleteTask={deleteTask}
updateTask={updateTask}
tasks={tasks.filter(
(task) => task.columnId === activeColumn.id
)}
/>
)}
{activeTask && (
<TaskCard
task={activeTask}
deleteTask={deleteTask}
updateTask={updateTask}
/>
)}
<DragOverlay className="bg-white" dropAnimation={null} zIndex={20}>
{/* Render the active task as a draggable overlay */}
<TaskCard task={activeTask} deleteTask={deleteTask} updateTask={updateTask} />
</DragOverlay>,
document.body
)}
@ -203,135 +202,122 @@ function KanbanBoard() {
</div>
);
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 };
});
if (content === "") return deleteTask(id);
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);
}
// Handle the start of a drag event
function onDragStart(event) {
if (event.active.data.current?.type === "Column") {
setActiveColumn(event.active.data.current.column);
return;
}
// Check if the dragged item is a Task
if (event.active.data.current?.type === "Task") {
setActiveTask(event.active.data.current.task);
return;
}
}
// Handle the end of a drag event
function onDragEnd(event) {
setActiveColumn(null);
// Reset active column and task after the drag ends
setActiveTask(null);
const { active, over } = event;
if (!over) return;
if (!over) return; // If not dropped over anything, exit
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";
// Move tasks between columns and update columnId
if (isActiveATask && isOverAColumn) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
tasks[activeIndex].columnId = overId;
// Extract the column ID from overId
const columnId = extractColumnId(overId);
tasks[activeIndex].columnId = columnId;
// API call to update task's columnId
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
return arrayMove(tasks, activeIndex, activeIndex);
});
}
}
function generateId() {
return Math.floor(Math.random() * 10001);
// Helper function to extract the column ID from the element ID
function extractColumnId(elementId) {
// Implement logic to extract the column ID from elementId
// For example, if elementId is in the format "column-123", you can do:
const parts = elementId.split("-");
return parts.length === 2 ? parseInt(parts[1], 10) : null;
}
// Handle the drag-over event
function onDragOver(event) {
const { active, over } = event;
if (!over) return; // If not over anything, exit
const activeId = active.id;
const overId = over.id;
if (activeId === overId) return; // If over the same element, exit
const isActiveATask = active.data.current?.type === "Task";
const isOverATask = over.data.current?.type === "Task";
if (!isActiveATask) return; // If not dragging a Task, exit
// Reorder logic for Tasks within the same column
if (isActiveATask && isOverATask) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
const overIndex = tasks.findIndex((t) => t.id === overId);
// If moving to a different column, update columnId
if (tasks[activeIndex].columnId !== tasks[overIndex].columnId) {
tasks[activeIndex].columnId = tasks[overIndex].columnId;
return arrayMove(tasks, activeIndex, overIndex - 1);
}
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
return arrayMove(tasks, activeIndex, overIndex);
});
}
const isOverAColumn = over.data.current?.type === "Column";
// Move the Task to a different column and update columnId
if (isActiveATask && isOverAColumn && tasks.some((task) => task.columnId !== overId)) {
setTasks((tasks) => {
const activeIndex = tasks.findIndex((t) => t.id === activeId);
axiosInstance
.put(`todo/change_task_list_board/`, {
todo_id: activeId,
new_list_board_id: over.data.current.task.columnId,
new_index: 0,
})
.then((response) => {})
.catch((error) => {
console.error("Error updating task columnId:", error);
});
tasks[activeIndex].columnId = overId;
return arrayMove(tasks, activeIndex, activeIndex);
});
}
}
}
export default KanbanBoard;

View File

@ -0,0 +1,34 @@
import { KanbanBoard } from "./kanbanBoard";
import { useState } from "react";
export const KanbanPage = () => {
const [activeTab, setActiveTab] = useState("kanban");
const handleTabClick = (tabId) => {
setActiveTab(tabId);
};
return (
<div className="flex flex-col">
<div className="flex justify-center border-2 py-3 mb-1">
<div>
<div className="tabs tabs-boxed">
<a
id="kanban"
className={`tab ${activeTab === "kanban" ? "tab-active" : ""}`}
onClick={() => handleTabClick("kanban")}>
Kanban
</a>
{/* <a
id="table"
className={`tab ${activeTab === "table" ? "tab-active" : ""}`}
onClick={() => handleTabClick("table")}>
Table
</a> */}
</div>
</div>
</div>
<KanbanBoard />
</div>
);
};

View File

@ -1,26 +1,18 @@
import { useState } from "react";
import TrashIcon from "../icons/trashIcon";
import { BsFillTrashFill } from "react-icons/bs";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { TaskDetailModal } from "./taskDetailModal";
function TaskCard({ task, deleteTask, updateTask }) {
export function TaskCard({ task, deleteTask, updateTask }) {
const [mouseIsOver, setMouseIsOver] = useState(false);
const [editMode, setEditMode] = useState(true);
const {
setNodeRef,
attributes,
listeners,
transform,
transition,
isDragging,
} = useSortable({
const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({
id: task.id,
data: {
type: "Task",
task,
},
disabled: editMode,
});
const style = {
@ -28,11 +20,9 @@ function TaskCard({ task, deleteTask, updateTask }) {
transform: CSS.Transform.toString(transform),
};
const toggleEditMode = () => {
setEditMode((prev) => !prev);
setMouseIsOver(false);
};
{
/* If card is dragged */
}
if (isDragging) {
return (
<div
@ -40,72 +30,51 @@ function TaskCard({ task, deleteTask, updateTask }) {
style={style}
className="
opacity-30
bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl border-2 border-rose-500 cursor-grab relative
bg-mainBackgroundColor p-2.5 items-center flex text-left rounded-xl border-2 border-gray-400 cursor-grab relative
"
/>
);
}
if (editMode) {
return (
return (
<div>
<TaskDetailModal
taskId={task.id}
title={task.content}
description={task.description}
tags={task.tags}
difficulty={task.difficulty}
f challenge={task.challenge}
importance={task.importance}
/>
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
className="outline-double outline-1 outline-offset-2 hover:outline-double hover:outline-1 hover:outline-offset-2 bg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative"
>
<textarea
className="
h-[90%]
w-full resize-none border-none rounded bg-transparent text-black focus:outline-none
"
value={task.content}
autoFocus
placeholder="Task content here"
onBlur={toggleEditMode}
onKeyDown={(e) => {
if (e.key === "Enter" && e.shiftKey) {
toggleEditMode();
}
}}
onChange={(e) => updateTask(task.id, e.target.value)}
/>
style={style}
className="justify-center items-center flex text-left rounded-xl cursor-grab relative hover:border-2 hover:border-blue-400 shadow bg-white"
onMouseEnter={() => {
setMouseIsOver(true);
}}
onMouseLeave={() => {
setMouseIsOver(false);
}}>
<p
className="p-2.5 my-auto w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap rounded-xl shadow bg-white"
onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}>
{task.content}
</p>
{mouseIsOver && (
<button
onClick={() => {
deleteTask(task.id);
}}
className="stroke-white absolute right-0 top-1/2 rounded-full bg-white -translate-y-1/2 bg-columnBackgroundColor p-2 hover:opacity-100 ">
<BsFillTrashFill />
</button>
)}
</div>
);
}
return (
<div
ref={setNodeRef}
style={style}
{...attributes}
{...listeners}
onClick={toggleEditMode}
className="outline-double outline-1 outline-offset-2 hover:outline-double hover:outline-1 hover:outline-offset-2 sbg-mainBackgroundColor p-2.5 h-[100px] min-h-[100px] items-center flex text-left rounded-xl hover:ring-2 hover:ring-inset hover:ring-rose-500 cursor-grab relative task"
onMouseEnter={() => {
setMouseIsOver(true);
}}
onMouseLeave={() => {
setMouseIsOver(false);
}}
>
<p className="my-auto h-[90%] w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap">
{task.content}
</p>
{mouseIsOver && (
<button
onClick={() => {
deleteTask(task.id);
}}
className="stroke-white absolute right-4 top-1/2 -translate-y-1/2 bg-columnBackgroundColor p-2 rounded opacity-60 hover:opacity-100"
>
<TrashIcon />
</button>
)}
</div>
);
}
export default TaskCard;

View File

@ -0,0 +1,147 @@
import { useState } from "react";
import { FaTasks, FaRegListAlt } from "react-icons/fa";
import { FaPlus } from "react-icons/fa6";
import { TbChecklist } from "react-icons/tb";
export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) {
const [isChallengeChecked, setChallengeChecked] = useState(challenge);
const [isImportantChecked, setImportantChecked] = useState(importance);
const [currentDifficulty, setCurrentDifficulty] = useState(difficulty);
const handleChallengeChange = () => {
setChallengeChecked(!isChallengeChecked);
};
const handleImportantChange = () => {
setImportantChecked(!isImportantChecked);
};
const handleDifficultyChange = (event) => {
setCurrentDifficulty(parseInt(event.target.value, 10));
};
return (
<dialog id={`task_detail_modal_${taskId}`} className="modal">
<div className="modal-box w-4/5 max-w-3xl">
{/* Title */}
<div className="flex flex-col py-2">
<div className="flex flex-col">
<h3 className="font-bold text-lg">
<span className="flex gap-2">
{<FaTasks className="my-2" />}
{title}
</span>
</h3>
<p className="text-xs">{title}</p>
</div>
</div>
{/* Tags */}
<div className="flex flex-col py-2 pb-4">
<div className="flex flex-row space-x-5">
<div className="dropdown">
<label tabIndex={0} className="btn-md border-2 rounded-xl m-1 py-1">
+ Add Tags
</label>
<ul tabIndex={0} className="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
<li>
<a>
<input type="checkbox" checked="checked" className="checkbox checkbox-sm" />
Item 2
</a>
</li>
</ul>
</div>
</div>
<div className="flex flex-nowrap overflow-x-auto"></div>
</div>
{/* Description */}
<div className="flex flex-col gap-2">
<h2 className="font-bold">
<span className="flex gap-2">
<FaRegListAlt className="my-1" />
Description
</span>
</h2>
<textarea className="textarea w-full" disabled>
{description}
</textarea>
</div>
{/* Difficulty, Challenge, and Importance */}
<div className="flex flex-row space-x-3 my-4">
<div className="flex-1 card shadow border-2 p-2">
<input
type="range"
id="difficultySelector"
min={0}
max="100"
value={currentDifficulty}
className="range"
step="25"
onChange={handleDifficultyChange}
/>
<div className="w-full flex justify-between text-xs px-2 space-x-2">
<span>Easy</span>
<span>Normal</span>
<span>Hard</span>
<span>Very Hard</span>
<span>Devil</span>
</div>
</div>
{/* Challenge Checkbox */}
<div className="card shadow border-2 p-2">
<div className="form-control">
<label className="label cursor-pointer space-x-2">
<span className="label-text">Challenge</span>
<input
type="checkbox"
checked={isChallengeChecked}
className="checkbox"
onChange={handleChallengeChange}
/>
</label>
</div>
</div>
{/* Important Checkbox */}
<div className="card shadow border-2 p-2">
<div className="form-control">
<label className="label cursor-pointer space-x-2">
<span className="label-text">Important</span>
<input
type="checkbox"
checked={isImportantChecked}
className="checkbox"
onChange={handleImportantChange}
/>
</label>
</div>
</div>
</div>
{/* Subtask */}
<div className="flex flex-col pt-2">
<h2 className="font-bold">
<span className="flex gap-1">
<TbChecklist className="my-1" />
Subtasks
</span>
</h2>
<div className="flex space-x-3 pt-2">
<input type="text" placeholder="subtask topic" className="input input-bordered flex-1 w-full" />
<button className="btn">
<FaPlus />
Add Subtask
</button>
</div>
</div>
<form method="dialog">
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">X</button>
</form>
</div>
</dialog>
);
}

View File

@ -0,0 +1,44 @@
import { FloatingParticles } from "../FlaotingParticles";
export function LandingPage() {
return (
<div className="h-screen flex items-center justify-center bg-gradient-to-r from-zinc-100 via-gray-200 to-zinc-100">
{/* Particles Container */}
<FloatingParticles />
{/* Navbar */}
<div className="relative" id="home">
<div className="max-w-7xl mx-auto px-6 md:px-12 xl:px-6">
<div className="relative pt-36 ml-auto">
<div className="lg:w-2/3 text-center mx-auto">
<h1 className="text-#143D6C font-bold text-5xl md:text-6xl xl:text-7xl">
Manage your task with{" "}
<span className="text-primary">
TurTask
<label className="swap swap-flip text-6xl">
<input type="checkbox" />
<div className="swap-on">😇</div>
<div className="swap-off">🥳</div>
</label>
</span>
</h1>
<p className="mt-8 text-#143D6C">
Unleash productivity with our personal task and project
management.
</p>
<div className="mt-8 flex flex-wrap justify-center gap-y-4 gap-x-6">
<a
href="/login"
className="relative flex h-11 w-full items-center justify-center px-6 before:absolute before:inset-0 before:rounded-full before:bg-primary before:transition before:duration-300 hover:before:scale-105 active:duration-75 active:before:scale-95 sm:w-max"
>
<span className="relative text-base font-semibold text-white">
Get started
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -1,36 +1,24 @@
import { useState } from "react";
import {
AiOutlineHome,
AiOutlineSchedule,
AiOutlineUnorderedList,
AiOutlinePieChart,
AiOutlinePlus,
} from "react-icons/ai";
import { AiOutlineHome, AiOutlineSchedule, AiOutlineUnorderedList } from "react-icons/ai";
import { PiStepsDuotone } from "react-icons/pi";
import { IoSettingsOutline } from "react-icons/io5";
import { AnimatePresence, motion } from "framer-motion";
import { Link, useNavigate } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const menuItems = [
{ id: 0, path: "/", icon: <AiOutlineHome /> },
{ id: 1, path: "/tasks", icon: <AiOutlineUnorderedList /> },
{ id: 2, path: "/calendar", icon: <AiOutlineSchedule /> },
{ id: 3, path: "/analytic", icon: <AiOutlinePieChart /> },
{ id: 4, path: "/priority", icon: <AiOutlinePlus /> },
{ id: 3, path: "/priority", icon: <PiStepsDuotone /> },
];
// { id: 3, path: "/settings", icon: <IoSettingsOutline /> },
const IconSideNav = () => {
return (
<div className="bg-slate-900 text-slate-100 flex">
<SideNav />
</div>
);
};
const SideNav = () => {
export const SideNav = () => {
const [selected, setSelected] = useState(0);
return (
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-screen">
{menuItems.map(item => (
<nav className="bg-slate-950 p-4 flex flex-col items-center gap-2 h-full fixed top-0 left-0 z-50">
{menuItems.map((item) => (
<NavItem
key={item.id}
icon={item.icon}
@ -44,12 +32,12 @@ const SideNav = () => {
);
};
const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
const NavItem = ({ icon, selected, id, setSelected, path }) => {
const navigate = useNavigate();
return (
<motion.button
className="p-3 text-xl bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
className="p-3 text-xl text-white bg-slate-800 hover-bg-slate-700 rounded-md transition-colors relative"
onClick={() => {
setSelected(id);
navigate(path);
@ -69,5 +57,3 @@ const NavItem = ({ icon, selected, id, setSelected, logo, path }) => {
</motion.button>
);
};
export default IconSideNav;

Some files were not shown because too many files have changed in this diff Show More