diff --git a/backend/boards/apps.py b/backend/boards/apps.py index d10d8fa..cdcb8cf 100644 --- a/backend/boards/apps.py +++ b/backend/boards/apps.py @@ -3,7 +3,4 @@ from django.apps import AppConfig class BoardsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' - name = 'boards' - - def ready(self): - import boards.signals \ No newline at end of file + name = 'boards' \ No newline at end of file diff --git a/backend/boards/signals.py b/backend/boards/signals.py deleted file mode 100644 index 2c44daa..0000000 --- a/backend/boards/signals.py +++ /dev/null @@ -1,17 +0,0 @@ -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) \ No newline at end of file diff --git a/backend/core/local_settings.py b/backend/core/local_settings.py index 9e7903c..d801d36 100644 --- a/backend/core/local_settings.py +++ b/backend/core/local_settings.py @@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'API documentation for TurTask', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } REST_USE_JWT = True diff --git a/backend/core/production_settings.py b/backend/core/production_settings.py index 1ea23fb..e55f741 100644 --- a/backend/core/production_settings.py +++ b/backend/core/production_settings.py @@ -88,6 +88,7 @@ SPECTACULAR_SETTINGS = { 'DESCRIPTION': 'API documentation for TurTask', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, + 'SERVE_PERMISSIONS': ['rest_framework.permissions.IsAuthenticated'], } REST_USE_JWT = True diff --git a/backend/dashboard/serializers.py b/backend/dashboard/serializers.py index ddc207b..8563ec4 100644 --- a/backend/dashboard/serializers.py +++ b/backend/dashboard/serializers.py @@ -1,7 +1,7 @@ -from rest_framework import serializers -from .models import UserStats +# 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'] \ No newline at end of file +# class UserStatsSerializer(serializers.ModelSerializer): +# class Meta: +# model = UserStats +# fields = ['health', 'gold', 'experience', 'strength', 'intelligence', 'endurance', 'perception', 'luck', 'level'] \ No newline at end of file diff --git a/backend/dashboard/tests.py b/backend/dashboard/tests.py index 943c3de..9d20ab4 100644 --- a/backend/dashboard/tests.py +++ b/backend/dashboard/tests.py @@ -1,32 +1,35 @@ -from django.test import TestCase +from rest_framework.test import APITestCase 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 +from boards.models import Board +from tasks.tests.utils import create_test_user -class DashboardStatsAndWeeklyViewSetTests(TestCase): +class DashboardStatsAndWeeklyViewSetTests(APITestCase): def setUp(self): self.user = create_test_user() - self.client = login_user(self.user) + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() - def create_task(self, title, completed=False, completion_date=None, end_event=None): + 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 + end_event=end_event, + list_board=self.list_board ) 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()) + 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')) + response = self.client.get(reverse('statstodo-list')) self.assertEqual(response.status_code, 200) self.assertEqual(response.data['completed_this_week'], 1) @@ -35,69 +38,9 @@ class DashboardStatsAndWeeklyViewSetTests(TestCase): 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()) + 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) diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py index 56624ca..af8d154 100644 --- a/backend/dashboard/urls.py +++ b/backend/dashboard/urls.py @@ -1,11 +1,12 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import DashboardStatsViewSet, DashboardWeeklyViewSet +from .views import DashboardStatsTodoViewSet, DashboardWeeklyViewSet router = DefaultRouter() -router.register(r'dashboard/stats', DashboardStatsViewSet, basename='stats') +router.register(r'dashboard/todostats', DashboardStatsTodoViewSet, basename='statstodo') router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly') +router.register(r'dashboard/recstats', DashboardStatsTodoViewSet, basename='statsrec') urlpatterns = [ path('', include(router.urls)), ] diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py index abe0576..b6fd552 100644 --- a/backend/dashboard/views.py +++ b/backend/dashboard/views.py @@ -5,10 +5,13 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from rest_framework import viewsets, mixins -from tasks.models import Todo +from tasks.models import Todo, RecurrenceTask -class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): +class DashboardStatsTodoViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + """ + A viewset for retrieving statistics related to user tasks for the last 7 days. + """ permission_classes = (IsAuthenticated,) def get_queryset(self): @@ -66,6 +69,27 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): # 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 + + total_completed_tasks = Todo.objects.filter(user=user, completed=True).count() + + total_tasks = Todo.objects.filter(user=user).count() + + today_start = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + today_end = timezone.now().replace(hour=23, minute=59, second=59, microsecond=999999) + + tasks_completed_today = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=today_start, + completion_date__lte=today_end + ).count() + + total_tasks_today = Todo.objects.filter( + user=user, + completion_date__gte=today_start, + completion_date__lte=today_end + ).count() + data = { "completed_last_7_days": completed_last_7_days, @@ -75,6 +99,10 @@ class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): "completed_this_week": completed_this_week, "overdue_tasks": overdue_tasks, "overall_completion_rate": overall_completion_rate, + "total_completed_tasks": total_completed_tasks, + "total_tasks" : total_tasks, + "total_tasks_today": total_tasks_today, + "tasks_completed_today": tasks_completed_today, } return Response(data, status=status.HTTP_200_OK) @@ -145,7 +173,142 @@ class DashboardWeeklyViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): return Response(weekly_stats, status=status.HTTP_200_OK) +class DashboardStatsReccurenceViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + """ + A viewset for retrieving statistics related to user tasks for the last 7 days. + """ + permission_classes = (IsAuthenticated,) + def get_queryset(self): + return RecurrenceTask.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 = RecurrenceTask.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 = RecurrenceTask.objects.filter( + user=user, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + tasks_assigned_this_week = RecurrenceTask.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 = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + completed_this_week = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + overdue_tasks = RecurrenceTask.objects.filter( + user=user, + completed=False, + end_event__lt=timezone.now() + ).count() + + # Overall completion rate + total_tasks = RecurrenceTask.objects.filter(user=user).count() + overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0 + + total_completed_tasks = RecurrenceTask.objects.filter( + user=user, + completed=True + ).count() + + total_tasks = RecurrenceTask.objects.filter(user=user).count() + + tasks_completed_today = RecurrenceTask.objects.filter( + user=user, + completed=True, + completion_date__gte=timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) + ).count() + + 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, + "total_completed_tasks": total_completed_tasks, + "total_tasks" : total_tasks, + "tasks_completed_today": tasks_completed_today, + } + + return Response(data, status=status.HTTP_200_OK) + +# class DashboardStatsAllViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): +# permission_classes = [IsAuthenticated] + +# def get_queryset(self): +# return Todo.objects.filter(user=self.request.user) + +# def list(self, request, *args, **kwargs): +# user = request.user + +# # Calculate task usage statistics +# todo_count = self.get_queryset().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) + # class DashboardStatsAPIView(APIView): # permission_classes = [IsAuthenticated] diff --git a/backend/tasks/misc/views.py b/backend/tasks/misc/views.py index 3fbc837..8f00572 100644 --- a/backend/tasks/misc/views.py +++ b/backend/tasks/misc/views.py @@ -1,8 +1,10 @@ from rest_framework import viewsets +from rest_framework.permissions import IsAuthenticated from ..models import Tag from .serializers import TagSerializer class TagViewSet(viewsets.ModelViewSet): queryset = Tag.objects.all() - serializer_class = TagSerializer \ No newline at end of file + serializer_class = TagSerializer + permission_classes = (IsAuthenticated,) \ No newline at end of file diff --git a/backend/tasks/models.py b/backend/tasks/models.py index c848e00..97a2c4b 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -81,6 +81,11 @@ class Todo(Task): priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) def save(self, *args, **kwargs): + done_list_name = "Done" + if self.list_board.name == done_list_name: + self.completed = True + Todo.objects.filter(list_board=self.list_board).update(completed=True) + if self.completed and not self.completion_date: self.completion_date = timezone.now() elif not self.completed: diff --git a/backend/tasks/serializers.py b/backend/tasks/serializers.py index a48330e..cf2046c 100644 --- a/backend/tasks/serializers.py +++ b/backend/tasks/serializers.py @@ -1,5 +1,6 @@ from rest_framework import serializers -from .models import Todo, RecurrenceTask +from boards.models import Board +from tasks.models import Todo, RecurrenceTask class GoogleCalendarEventSerializer(serializers.Serializer): @@ -17,18 +18,22 @@ class TodoUpdateSerializer(serializers.ModelSerializer): updated = serializers.DateTimeField(source="last_update") start_datetime = serializers.DateTimeField(source="start_event", required=False) end_datetime = serializers.DateTimeField(source="end_event", required=False) - + list_board = serializers.SerializerMethodField() class Meta: model = Todo - fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime') + fields = ('id', 'summary', 'description', 'created', 'updated', 'start_datetime', 'end_datetime', 'list_board') def __init__(self, *args, **kwargs): self.user = kwargs.pop('user', None) super(TodoUpdateSerializer, self).__init__(*args, **kwargs) + def get_list_board(self, obj): + return Board.objects.get(user=self.user).listboard_set.first() + def create(self, validated_data): validated_data['user'] = self.user + validated_data['list_board'] = self.get_list_board(self) task = Todo.objects.create(**validated_data) return task diff --git a/backend/tasks/signals.py b/backend/tasks/signals.py index 4c9e6f2..f7436aa 100644 --- a/backend/tasks/signals.py +++ b/backend/tasks/signals.py @@ -1,8 +1,7 @@ -from django.db.models.signals import pre_save, post_save +from django.db.models.signals import pre_save from django.dispatch import receiver from django.utils import timezone -from boards.models import ListBoard, Board from tasks.models import Todo @@ -24,66 +23,4 @@ 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 - - -# @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(), - ) \ No newline at end of file + instance.priority = Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT \ No newline at end of file diff --git a/backend/tasks/tasks/serializers.py b/backend/tasks/tasks/serializers.py index d2863a0..9a56632 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,16 +1,33 @@ from rest_framework import serializers +from users.models import CustomUser from boards.models import ListBoard -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask class TaskSerializer(serializers.ModelSerializer): + tags = serializers.SerializerMethodField() + sub_task_count = serializers.SerializerMethodField() + class Meta: model = Todo fields = '__all__' def create(self, validated_data): - # Create a new task with validated data + user_id = validated_data.get('user') + + try: + user = CustomUser.objects.get(id=user_id) + except CustomUser.DoesNotExist: + raise serializers.ValidationError("User with the provided ID does not exist.") + + validated_data['user'] = user return Todo.objects.create(**validated_data) + def get_tags(self, instance): + return [tag.name for tag in instance.tags.all()] + + def get_sub_task_count(self, instance): + return instance.subtask_set.count() + class TaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Todo @@ -89,4 +106,14 @@ class HabitTaskSerializer(serializers.ModelSerializer): class HabitTaskCreateSerializer(serializers.ModelSerializer): class Meta: model = Habit - exclude = ('tags',) \ No newline at end of file + exclude = ('tags',) + + +class SubTaskSerializer(serializers.ModelSerializer): + class Meta: + model = Subtask + fields = '__all__' + + def create(self, validated_data): + # Create a new task with validated data + return Subtask.objects.create(**validated_data) \ No newline at end of file diff --git a/backend/tasks/tasks/views.py b/backend/tasks/tasks/views.py index fe1c82d..bd5d568 100644 --- a/backend/tasks/tasks/views.py +++ b/backend/tasks/tasks/views.py @@ -4,10 +4,13 @@ from rest_framework import viewsets, status, serializers from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework import mixins -from .serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter + +from tasks.tasks.serializers import ChangeTaskListBoardSerializer, ChangeTaskOrderSerializer, SubTaskSerializer from boards.models import ListBoard, KanbanTaskOrder -from tasks.models import Todo, RecurrenceTask, Habit +from tasks.models import Todo, RecurrenceTask, Habit, Subtask from tasks.tasks.serializers import (TaskCreateSerializer, TaskSerializer, RecurrenceTaskSerializer, @@ -32,6 +35,18 @@ class TodoViewSet(viewsets.ModelViewSet): return TaskCreateSerializer return TaskSerializer + def list(self, request, *args, **kwargs): + """ + list all tasks of the authenticated + user and send tags if those Todo too. + """ + try: + queryset = self.get_queryset() + serializer = TaskSerializer(queryset, many=True) + return Response(serializer.data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + def create(self, request, *args, **kwargs): try: new_task_data = request.data @@ -117,6 +132,73 @@ class TodoViewSet(viewsets.ModelViewSet): return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) +@extend_schema_view( +list=extend_schema( + parameters=[ + OpenApiParameter(name='parent_task', description='Parent Task ID', type=int), + ] + ) +) +class SubTaskViewset(viewsets.GenericViewSet, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.ListModelMixin, + mixins.UpdateModelMixin): + queryset = Subtask.objects.all() + permission_classes = (IsAuthenticated,) + + def get_serializer_class(self): + return SubTaskSerializer + + def list(self, request, *args, **kwargs): + """List only subtask of parent task.""" + try: + parent_task = request.query_params.get('parent_task') + if not parent_task: + raise serializers.ValidationError('parent_task is required.') + queryset = self.get_queryset().filter(parent_task_id=parent_task) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + def create(self, request, *args, **kwargs): + """Create a new subtask, point to some parent tasks.""" + try: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + return Response(serializer.data) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def destroy(self, request, *args, **kwargs): + """Delete a subtask.""" + try: + instance = self.get_object() + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + def partial_update(self, request, *args, **kwargs): + """Update a subtask.""" + try: + instance = self.get_object() + serializer = self.get_serializer(instance, data=request.data, partial=True) + serializer.is_valid(raise_exception=True) + self.perform_update(serializer) + return Response(serializer.data) + + 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 diff --git a/backend/tasks/tests/test_deserializer.py b/backend/tasks/tests/test_deserializer.py index 306185c..758fdd1 100644 --- a/backend/tasks/tests/test_deserializer.py +++ b/backend/tasks/tests/test_deserializer.py @@ -1,18 +1,22 @@ from datetime import datetime from zoneinfo import ZoneInfo -from django.test import TestCase +from rest_framework.test import APITestCase + from django.utils import timezone -from tasks.tests.utils import create_test_user, login_user +from tasks.tests.utils import create_test_user from tasks.serializers import TodoUpdateSerializer from tasks.models import Todo +from boards.models import Board -class TaskUpdateSerializerTest(TestCase): +class TaskUpdateSerializerTest(APITestCase): def setUp(self): self.user = create_test_user() + self.client.force_authenticate(user=self.user) self.current_time = '2020-08-01T00:00:00Z' self.end_time = '2020-08-01T00:00:00Z' + self.list_board = Board.objects.get(user=self.user).listboard_set.first() def test_serializer_create(self): data = { @@ -23,6 +27,7 @@ class TaskUpdateSerializerTest(TestCase): 'updated': self.end_time, 'start_datetime' : self.current_time, 'end_datetie': self.end_time, + 'list_board': self.list_board.id, } serializer = TodoUpdateSerializer(data=data, user=self.user) @@ -32,7 +37,7 @@ class TaskUpdateSerializerTest(TestCase): self.assertIsInstance(task, Todo) def test_serializer_update(self): - task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user) + task = Todo.objects.create(title='Original Task', notes='Original description', user=self.user, list_board=self.list_board) data = { 'id': '32141cwaNcapufh8jq2conw', @@ -42,6 +47,7 @@ class TaskUpdateSerializerTest(TestCase): 'updated': self.end_time, 'start_datetime' : self.current_time, 'end_datetie': self.end_time, + 'list_board': self.list_board.id, } serializer = TodoUpdateSerializer(instance=task, data=data) diff --git a/backend/tasks/tests/test_todo_creation.py b/backend/tasks/tests/test_todo_creation.py index 7c66724..cc7dfc4 100644 --- a/backend/tasks/tests/test_todo_creation.py +++ b/backend/tasks/tests/test_todo_creation.py @@ -2,72 +2,72 @@ from datetime import datetime, timedelta from django.urls import reverse from rest_framework import status from rest_framework.test import APITestCase -from tasks.tests.utils import create_test_user, login_user +from tasks.tests.utils import create_test_user from tasks.models import Todo +from boards.models import ListBoard, Board -# 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.force_authenticate(user=self.user) + self.url = reverse("todo-list") + self.due_date = datetime.now() + timedelta(days=5) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() -# 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', + 'difficulty': 1, + 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + 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_500_INTERNAL_SERVER_ERROR) + 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 = { + 'type': 'habit', + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR) + 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 (OK because we retreive) + id from request. + """ + data = { + 'title': 'Test Task', + 'type': 'habit', + 'exp': 10, + 'priority': 1, + 'difficulty': 1, + 'user': -100, # Invalid user ID + 'end_event': self.due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(self.url, data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + self.assertEqual(Todo.objects.count(), 1) # No task should be created diff --git a/backend/tasks/tests/test_todo_eisenhower.py b/backend/tasks/tests/test_todo_eisenhower.py index 41ee078..2d14877 100644 --- a/backend/tasks/tests/test_todo_eisenhower.py +++ b/backend/tasks/tests/test_todo_eisenhower.py @@ -1,36 +1,39 @@ from datetime import datetime, timedelta, timezone -from django.test import TestCase +from rest_framework.test import APITestCase from tasks.models import Todo from tasks.tests.utils import create_test_user +from boards.models import Board -class TodoPriorityTest(TestCase): +class TodoPriorityTest(APITestCase): def setUp(self): self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() def test_priority_calculation(self): # Important = 2, Till Due = none - todo = Todo(importance=2, end_event=None, user=self.user) + todo = Todo(importance=2, end_event=None, user=self.user, list_board=self.list_board) todo.save() # 'Not Important & Not Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=1) # Important = 4, Till Due = 1 - todo = Todo(importance=4, end_event=due_date, user=self.user) + todo = Todo(importance=4, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Important & Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=10) # Important = 3, Till Due = 10 - todo = Todo(importance=3, end_event=due_date, user=self.user) + todo = Todo(importance=3, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Important & Not Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.IMPORTANT_NOT_URGENT) due_date = datetime.now(timezone.utc) + timedelta(days=2) # Important = 1, Till Due = 2 - todo = Todo(importance=1, end_event=due_date, user=self.user) + todo = Todo(importance=1, end_event=due_date, user=self.user, list_board=self.list_board) todo.save() # 'Not Important & Urgent' self.assertEqual(todo.priority, Todo.EisenhowerMatrix.NOT_IMPORTANT_URGENT) diff --git a/backend/tasks/tests/test_todo_signal.py b/backend/tasks/tests/test_todo_signal.py new file mode 100644 index 0000000..fa7fba5 --- /dev/null +++ b/backend/tasks/tests/test_todo_signal.py @@ -0,0 +1,33 @@ +from datetime import datetime, timedelta +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APITestCase +from tasks.tests.utils import create_test_user +from tasks.models import Todo +from boards.models import ListBoard, Board + + +class TodoSignalHandlersTests(APITestCase): + def setUp(self): + self.user = create_test_user() + self.client.force_authenticate(user=self.user) + self.list_board = Board.objects.get(user=self.user).listboard_set.first() + + def test_update_priority_signal_handler(self): + """ + Test the behavior of the update_priority signal handler. + """ + due_date = datetime.now() + timedelta(days=5) + data = { + 'title': 'Test Task', + 'type': 'habit', + 'difficulty': 1, + 'end_event': due_date.strftime('%Y-%m-%dT%H:%M:%S'), + 'list_board': self.list_board.id, + } + response = self.client.post(reverse("todo-list"), data, format='json') + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + # Retrieve the created task and check if priority is updated + task = Todo.objects.get(title='Test Task') + self.assertIsNotNone(task.priority) # Check if priority is not None \ No newline at end of file diff --git a/backend/tasks/tests/utils.py b/backend/tasks/tests/utils.py index b1bef0b..663cd0a 100644 --- a/backend/tasks/tests/utils.py +++ b/backend/tasks/tests/utils.py @@ -1,26 +1,24 @@ +from rest_framework import status from rest_framework.test import APIClient +from django.urls import reverse from users.models import CustomUser from ..models import Todo -def create_test_user(email="testusertestuser@example.com", username="testusertestuser", - first_name="Test", password="testpassword",): - """create predifined user for testing""" - return CustomUser.objects.create_user( - email=email, - username=username, - first_name=first_name, - password=password, - ) - - -def login_user(user): - """Login a user to API client.""" - +def create_test_user(email="testusertestuser@example.com", + username="testusertestuser", + password="testpassword",) -> CustomUser: + """create predifined user without placeholder task for testing""" client = APIClient() - client.force_authenticate(user=user) - return client + response = client.post(reverse('create_user'), {'email': email, + 'username': username, + 'password': password}) + if response.status_code == status.HTTP_201_CREATED: + user = CustomUser.objects.get(username='testusertestuser') + user.todo_set.all().delete() + return user + return None def create_task_json(user, **kwargs): @@ -29,10 +27,7 @@ def create_task_json(user, **kwargs): "title": "Test Task", "type": "habit", "notes": "This is a test task created via the API.", - "exp": 10, - "priority": 1.5, "difficulty": 1, - "attribute": "str", "challenge": False, "fromSystem": False, "creation_date": None, @@ -51,8 +46,6 @@ def create_test_task(user, **kwargs): 'title': "Test Task", 'task_type': 'habit', 'notes': "This is a test task created via the API.", - 'exp': 10, - 'priority': 1.5, 'difficulty': 1, 'attribute': 'str', 'challenge': False, diff --git a/backend/tasks/urls.py b/backend/tasks/urls.py index d830a65..cf37e23 100644 --- a/backend/tasks/urls.py +++ b/backend/tasks/urls.py @@ -3,7 +3,7 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter from tasks.api import GoogleCalendarEventViewset -from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet +from tasks.tasks.views import TodoViewSet, RecurrenceTaskViewSet, HabitTaskViewSet, SubTaskViewset from tasks.misc.views import TagViewSet @@ -13,6 +13,7 @@ router.register(r'daily', RecurrenceTaskViewSet) router.register(r'habit', HabitTaskViewSet) router.register(r'tags', TagViewSet) router.register(r'calendar-events', GoogleCalendarEventViewset, basename='calendar-events') +router.register(r'subtasks', SubTaskViewset, basename='subtasks') urlpatterns = [ path('', include(router.urls)), diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 962d789..f7ed6fa 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -32,12 +32,32 @@ class UpdateProfileSerializer(serializers.ModelSerializer): Serializer for updating user profile. """ profile_pic = serializers.ImageField(required=False) - first_name = serializers.CharField(max_length=255, required=False) + username = serializers.CharField(max_length=255, required=False) about = serializers.CharField(required=False) class Meta: model = CustomUser - fields = ('profile_pic', 'first_name', 'about') + fields = ('profile_pic', 'username', 'about') + + def update(self, instance, validated_data): + """ + Update an existing user's profile. + """ + for attr, value in validated_data.items(): + setattr(instance, attr, value) + instance.save() + return instance + +class UpdateProfileNopicSerializer(serializers.ModelSerializer): + """ + Serializer for updating user profile. + """ + username = serializers.CharField(max_length=255, required=False) + about = serializers.CharField(required=False) + + class Meta: + model = CustomUser + fields = ('username', 'about') def update(self, instance, validated_data): """ diff --git a/backend/users/signals.py b/backend/users/signals.py index 817986b..22599b1 100644 --- a/backend/users/signals.py +++ b/backend/users/signals.py @@ -1,9 +1,74 @@ +from django.utils import timezone from django.db.models.signals import post_save from django.dispatch import receiver +from tasks.models import Todo from users.models import CustomUser, UserStats +from boards.models import ListBoard, Board + @receiver(post_save, sender=CustomUser) def create_user_stats(sender, instance, created, **kwargs): if created: - UserStats.objects.create(user=instance) \ No newline at end of file + UserStats.objects.create(user=instance) + + +@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) + + +@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(), + ) \ No newline at end of file diff --git a/backend/users/tests.py b/backend/users/tests.py index 7ce503c..5f9bdd6 100644 --- a/backend/users/tests.py +++ b/backend/users/tests.py @@ -1,3 +1,39 @@ -from django.test import TestCase +from rest_framework.test import APITestCase +from rest_framework import status +from django.urls import reverse +from users.models import CustomUser, UserStats +from boards.models import Board, ListBoard +from tasks.models import Todo -# Create your tests here. +class SignalsTest(APITestCase): + def setUp(self): + response = self.client.post(reverse('create_user'), {'email': 'testusertestuser123@mail.com', + 'username': 'testusertestuser123', + 'password': '321testpassword123'}) + # force login If response is 201 OK + if response.status_code == status.HTTP_201_CREATED: + self.user = CustomUser.objects.get(username='testusertestuser123') + self.client.force_login(self.user) + + def test_create_user_with_stas_default_boards_and_lists(self): + # Stats check + self.assertTrue(UserStats.objects.filter(user=self.user).exists()) + + # check if user is created + self.assertEqual(CustomUser.objects.count(), 1) + user = CustomUser.objects.get(username='testusertestuser123') + + # Check for default board + self.assertEqual(Board.objects.filter(user=self.user).count(), 1) + + # Check for default lists in board + default_board = Board.objects.get(user=self.user) + self.assertEqual(ListBoard.objects.filter(board=default_board).count(), 4) + + def test_create_user_with_placeholder_tasks(self): + default_board = Board.objects.get(user=self.user) + + # Check if placeholder tasks are created for each ListBoard + for list_board in ListBoard.objects.filter(board=default_board): + placeholder_tasks_count = Todo.objects.filter(list_board=list_board).count() + self.assertTrue(placeholder_tasks_count > 0) \ No newline at end of file diff --git a/backend/users/urls.py b/backend/users/urls.py index 474da31..88a7238 100644 --- a/backend/users/urls.py +++ b/backend/users/urls.py @@ -1,7 +1,8 @@ from django.urls import path -from users.views import CustomUserCreate, CustomUserProfileUpdate +from users.views import CustomUserCreate, CustomUserProfileUpdate, UserDataRetriveViewset urlpatterns = [ path('user/create/', CustomUserCreate.as_view(), name="create_user"), - path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user') + path('user/update/', CustomUserProfileUpdate.as_view(), name='update_user'), + path('user/data/', UserDataRetriveViewset.as_view({'get': 'retrieve'}), name="get_user_data"), ] \ No newline at end of file diff --git a/backend/users/views.py b/backend/users/views.py index 9d022e3..0157c0c 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -1,6 +1,6 @@ """This module defines API views for user creation""" -from rest_framework import status +from rest_framework import status, viewsets, mixins from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response @@ -9,7 +9,7 @@ from rest_framework.parsers import MultiPartParser from rest_framework_simplejwt.tokens import RefreshToken -from users.serializers import CustomUserSerializer, UpdateProfileSerializer +from users.serializers import CustomUserSerializer, UpdateProfileSerializer, UpdateProfileNopicSerializer from users.models import CustomUser class CustomUserCreate(APIView): @@ -57,8 +57,23 @@ class CustomUserProfileUpdate(APIView): return Response ({ 'error': 'User does not exist' }, status=status.HTTP_404_NOT_FOUND) + serializer = UpdateProfileSerializer(request.user, data=request.data) + if request.data.get('profile_pic') == "null": + serializer = UpdateProfileNopicSerializer(request.user, data=request.data) + if serializer.is_valid(): serializer.save() return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class UserDataRetriveViewset(viewsets.GenericViewSet, mixins.RetrieveModelMixin): + queryset = CustomUser.objects.all() + permission_classes = (IsAuthenticated,) + serializer_class = UpdateProfileSerializer + + def retrieve(self, request, *args, **kwargs): + serializer = self.get_serializer(request.user) + return Response(serializer.data) + diff --git a/frontend/package.json b/frontend/package.json index f057dce..618e30c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "@wojtekmaj/react-daterange-picker": "^5.4.4", "axios": "^1.6.1", "bootstrap": "^5.3.2", + "date-fns": "^2.30.0", "dotenv": "^16.3.1", "framer-motion": "^10.16.4", "gapi-script": "^1.2.0", @@ -42,9 +43,11 @@ "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", "react-bootstrap": "^2.9.1", + "react-datepicker": "^4.23.0", "react-datetime-picker": "^5.5.3", "react-dom": "^18.2.0", "react-icons": "^4.11.0", + "react-ios-time-picker": "^0.2.2", "react-router-dom": "^6.18.0", "react-tsparticles": "^2.12.2", "tsparticles": "^2.12.0" diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 22fdadd..b514b56 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -77,6 +77,9 @@ dependencies: bootstrap: specifier: ^5.3.2 version: 5.3.2(@popperjs/core@2.11.8) + date-fns: + specifier: ^2.30.0 + version: 2.30.0 dotenv: specifier: ^16.3.1 version: 16.3.1 @@ -101,6 +104,9 @@ dependencies: react-bootstrap: specifier: ^2.9.1 version: 2.9.1(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) react-datetime-picker: specifier: ^5.5.3 version: 5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0) @@ -110,6 +116,9 @@ dependencies: react-icons: specifier: ^4.11.0 version: 4.12.0(react@18.2.0) + react-ios-time-picker: + specifier: ^0.2.2 + version: 0.2.2(react-dom@18.2.0)(react@18.2.0) react-router-dom: specifier: ^6.18.0 version: 6.19.0(react-dom@18.2.0)(react@18.2.0) @@ -3473,6 +3482,22 @@ packages: - '@types/react-dom' dev: false + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.3.2 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-datetime-picker@5.5.3(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-bWGEPwGrZjaXTB8P4pbTSDygctLaqTWp0nNibaz8po+l4eTh9gv3yiJ+n4NIcpIJDqZaQJO57Bnij2rAFVQyLw==} peerDependencies: @@ -3520,6 +3545,10 @@ packages: scheduler: 0.23.0 dev: false + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + dev: false + /react-fit@1.7.1(@types/react-dom@18.2.15)(@types/react@18.2.37)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==} peerDependencies: @@ -3550,6 +3579,17 @@ packages: react: 18.2.0 dev: false + /react-ios-time-picker@0.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-bi+K23lK6Pf2xDXmhAlz+RJuy9/onWYi7Ye+ODVhIkis9AVFECOza2ckkZl/4vUypj2+TdTsHn+VZrTNdGIwDQ==} + peerDependencies: + react: ^18.2.0 + react-dom: ^18.2.0 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-portal: 4.2.2(react-dom@18.2.0)(react@18.2.0) + dev: false + /react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -3565,6 +3605,41 @@ packages: resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} dev: false + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + dev: false + + /react-portal@4.2.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-vS18idTmevQxyQpnde0Td6ZcUlv+pD8GTyR42n3CHUQq9OHi1C4jDE4ZWEbEsrbrLRhSECYiao58cvocwMtP7Q==} + peerDependencies: + react: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + react-dom: ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0 + dependencies: + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} peerDependencies: diff --git a/frontend/src/api/SubTaskApi.jsx b/frontend/src/api/SubTaskApi.jsx new file mode 100644 index 0000000..1458153 --- /dev/null +++ b/frontend/src/api/SubTaskApi.jsx @@ -0,0 +1,44 @@ +import { axiosInstance } from "./AxiosConfig"; + +export const getSubtask = async (parentTaskId) => { + try { + const response = await axiosInstance.get(`subtasks?parent_task=${parentTaskId}`); + return response.data; + } catch (error) { + console.error("Error fetching subtasks:", error); + throw error; + } +}; + +export const addSubtasks = async (parentTaskId, text) => { + try { + const response = await axiosInstance.post("subtasks/", { + description: text, + completed: false, + parent_task: parentTaskId, + }); + return response.data; + } catch (error) { + console.error("Error adding subtask:", error); + throw error; + } +}; + +export const deleteSubtasks = async (subtaskId) => { + try { + await axiosInstance.delete(`subtasks/${subtaskId}/`); + } catch (error) { + console.error("Error deleting subtask:", error); + throw error; + } +}; + +export const updateSubtask = async (subtaskId, data) => { + try { + const response = await axiosInstance.patch(`subtasks/${subtaskId}/`, data); + return response.data; + } catch (error) { + console.error("Error updating subtask:", error); + throw error; + } +}; diff --git a/frontend/src/api/TaskApi.jsx b/frontend/src/api/TaskApi.jsx index 098d934..0a56767 100644 --- a/frontend/src/api/TaskApi.jsx +++ b/frontend/src/api/TaskApi.jsx @@ -38,6 +38,15 @@ export const updateTask = (endpoint, id, data) => { }); }; +export const updateTaskPartial = (endpoint, id, data) => { + return axiosInstance + .patch(`${baseURL}${endpoint}/${id}/`, data) + .then((response) => response.data) + .catch((error) => { + throw error; + }); +}; + export const deleteTask = (endpoint, id) => { return axiosInstance .delete(`${baseURL}${endpoint}/${id}/`) @@ -64,6 +73,7 @@ export const readHabitTaskByID = (id) => readTaskByID("habit", id); // Update export const updateTodoTask = (id, data) => updateTask("todo", id, data); +export const updateTodoTaskPartial = (id, data) => updateTaskPartial("todo", id, data); export const updateRecurrenceTask = (id, data) => updateTask("daily", id, data); export const updateHabitTask = (id, data) => updateTask("habit", id, data); diff --git a/frontend/src/api/UserProfileApi.jsx b/frontend/src/api/UserProfileApi.jsx index 6dfc050..1386e33 100644 --- a/frontend/src/api/UserProfileApi.jsx +++ b/frontend/src/api/UserProfileApi.jsx @@ -11,8 +11,6 @@ const ApiUpdateUserProfile = async (formData) => { }, }); - console.log(response.data); - return response.data; } catch (error) { console.error("Error updating user profile:", error); diff --git a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx index cc19aaa..9954950 100644 --- a/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx +++ b/frontend/src/components/EisenhowerMatrix/Eisenhower.jsx @@ -1,5 +1,10 @@ import { useState, useEffect } from "react"; -import { FiAlertCircle, FiClock, FiXCircle, FiCheckCircle } from "react-icons/fi"; +import { + FiAlertCircle, + FiClock, + FiXCircle, + FiCheckCircle, +} from "react-icons/fi"; import { readTodoTasks } from "../../api/TaskApi"; import { axiosInstance } from "src/api/AxiosConfig"; @@ -26,7 +31,9 @@ function EachBlog({ name, colorCode, contentList, icon }) { }; return ( -
{tag}
+document.getElementById(`task_detail_modal_${task.id}`).showModal()}> - {task.content} -
- - {mouseIsOver && ( - - )} + }} + onClick={() => document.getElementById(`task_detail_modal_${task.id}`).showModal()}> + {/* -------- Task Content -------- */} + {/* Tags */} + {tags} +document.getElementById(`task_detail_modal_${task.id}`).showModal()}> + {task.content} +
+ {/* -------- Archive Task Button -------- */} + {mouseIsOver && ( + + )} +
+
+