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 dd9eeb3..c23cdb4 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/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/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..12bea72 100644 --- a/backend/tasks/tasks/serializers.py +++ b/backend/tasks/tasks/serializers.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from users.models import CustomUser from boards.models import ListBoard from tasks.models import Todo, RecurrenceTask, Habit @@ -8,7 +9,14 @@ class TaskSerializer(serializers.ModelSerializer): 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) class TaskCreateSerializer(serializers.ModelSerializer): 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/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/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/frontend/package.json b/frontend/package.json index f057dce..44e4b66 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ "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", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 22fdadd..2bb6ef0 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -101,6 +101,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) @@ -3473,6 +3476,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 +3539,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: @@ -3565,6 +3588,30 @@ 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-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/App.jsx b/frontend/src/App.jsx index 625b9b8..a917ca5 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -11,7 +11,7 @@ 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 { Dashboard } from "./components/dashboard/dashboard"; import { LandingPage } from "./components/landingPage/LandingPage"; import { PublicRoute } from "./PublicRoute"; import { useAuth } from "./hooks/AuthHooks"; diff --git a/frontend/src/components/dashboard/KpiCard.jsx b/frontend/src/components/dashboard/KpiCard.jsx index 85c74c9..b1d02b3 100644 --- a/frontend/src/components/dashboard/KpiCard.jsx +++ b/frontend/src/components/dashboard/KpiCard.jsx @@ -1,4 +1,11 @@ -import { BadgeDelta, Card, Flex, Metric, ProgressBar, Text } from "@tremor/react"; +import { + BadgeDelta, + Card, + Flex, + Metric, + ProgressBar, + Text, +} from "@tremor/react"; import { useEffect, useState } from "react"; import { axiosInstance } from "src/api/AxiosConfig"; @@ -46,10 +53,16 @@ export function KpiCard() {
{kpiCardData.completedThisWeek}
- {kpiCardData.percentage.toFixed(0)}% + + {isNaN(kpiCardData.percentage) || !isFinite(kpiCardData.percentage) + ? "0%" + : `${kpiCardData.percentage.toFixed(0)}%`} + - vs. {kpiCardData.completedLastWeek} (last week) + + vs. {kpiCardData.completedLastWeek} (last week) + diff --git a/frontend/src/components/dashboard/PieChart.jsx b/frontend/src/components/dashboard/PieChart.jsx index ca825bc..754ac64 100644 --- a/frontend/src/components/dashboard/PieChart.jsx +++ b/frontend/src/components/dashboard/PieChart.jsx @@ -13,7 +13,11 @@ export function DonutChartGraph() { const completedTask = response.data.total_completed_tasks || 0; const donutData = [ +<<<<<<< HEAD { name: "Completed task", count: completedTask }, +======= + { name: "Completed task", count: completedTask}, +>>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc { name: "Total task", count: totalTask }, ]; diff --git a/frontend/src/components/dashboard/ProgressCircle.jsx b/frontend/src/components/dashboard/ProgressCircle.jsx index 4a290d9..aa3a0a3 100644 --- a/frontend/src/components/dashboard/ProgressCircle.jsx +++ b/frontend/src/components/dashboard/ProgressCircle.jsx @@ -33,9 +33,18 @@ export function ProgressCircleChart() { return ( - + - {progressData.toFixed(0)} % + {isNaN(progressData) || !isFinite(progressData) + ? "0%" + : `${progressData.toFixed(0)}%`} diff --git a/frontend/src/components/dashboard/dashboard.jsx b/frontend/src/components/dashboard/dashboard.jsx index 1901ebb..005f98a 100644 --- a/frontend/src/components/dashboard/dashboard.jsx +++ b/frontend/src/components/dashboard/dashboard.jsx @@ -154,7 +154,9 @@ export function Dashboard() { color="rose" > - {progressData.toFixed(0)} % + {isNaN(progressData) || !isFinite(progressData) + ? "0%" + : `${progressData.toFixed(0)}%`}

diff --git a/frontend/src/components/kanbanBoard/kanbanBoard.jsx b/frontend/src/components/kanbanBoard/kanbanBoard.jsx index 2bda08b..2635cff 100644 --- a/frontend/src/components/kanbanBoard/kanbanBoard.jsx +++ b/frontend/src/components/kanbanBoard/kanbanBoard.jsx @@ -1,6 +1,12 @@ import { useMemo, useState, useEffect } from "react"; import { ColumnContainerCard } from "./columnContainerWrapper"; -import { DndContext, DragOverlay, PointerSensor, useSensor, useSensors } from "@dnd-kit/core"; +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"; @@ -26,7 +32,9 @@ export function KanbanBoard() { // ---------------- Task Handlers ---------------- const handleTaskUpdate = (tasks, updatedTask) => { - const updatedTasks = tasks.map((task) => (task.id === updatedTask.id ? updatedTask : task)); + const updatedTasks = tasks.map((task) => + task.id === updatedTask.id ? updatedTask : task + ); setTasks(updatedTasks); }; @@ -168,8 +176,14 @@ export function KanbanBoard() { justify-center overflow-x-auto overflow-y-hidden - "> - + " + > +
{!isLoading ? ( @@ -181,7 +195,9 @@ export function KanbanBoard() { createTask={createTask} deleteTask={deleteTask} updateTask={updateTask} - tasks={(tasks || []).filter((task) => task.columnId === col.id)} + tasks={(tasks || []).filter( + (task) => task.columnId === col.id + )} /> ))}{" "} @@ -194,7 +210,11 @@ export function KanbanBoard() { {createPortal( {/* Render the active task as a draggable overlay */} - + , document.body )} @@ -302,7 +322,11 @@ export function KanbanBoard() { 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)) { + if ( + isActiveATask && + isOverAColumn && + tasks.some((task) => task.columnId !== overId) + ) { setTasks((tasks) => { const activeIndex = tasks.findIndex((t) => t.id === activeId); axiosInstance diff --git a/frontend/src/components/kanbanBoard/taskCard.jsx b/frontend/src/components/kanbanBoard/taskCard.jsx index 86a2036..13817a8 100644 --- a/frontend/src/components/kanbanBoard/taskCard.jsx +++ b/frontend/src/components/kanbanBoard/taskCard.jsx @@ -1,12 +1,14 @@ import { useState } from "react"; -import { BsFillTrashFill } from "react-icons/bs"; import { useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { TaskDetailModal } from "./taskDetailModal"; +import { GoChecklist, GoArchive } from "react-icons/go"; export function TaskCard({ task, deleteTask, updateTask }) { + // State to track if the mouse is over the task card const [mouseIsOver, setMouseIsOver] = useState(false); + // DnD Kit hook for sortable items const { setNodeRef, attributes, listeners, transform, transition, isDragging } = useSortable({ id: task.id, data: { @@ -15,50 +17,129 @@ export function TaskCard({ task, deleteTask, updateTask }) { }, }); + // Style for the task card, adjusting for dragging animation const style = { transition, transform: CSS.Transform.toString(transform), }; - { - /* If card is dragged */ - } + // ---- DESC AND TAG ---- */ + + // Tags + const tags = + task.tags.length > 0 ? ( +
+ {task.tags.map((tag, index) => ( +
+ {tag.label} +
+ ))} +
+ ) : null; + + // difficulty? + const difficultyTag = task.difficulty ? ( + + difficulty + + ) : null; + + // Due Date + const dueDateTag = + task.end_event && new Date(task.end_event) > new Date() + ? (() => { + const daysUntilDue = Math.ceil((new Date(task.end_event) - new Date()) / (1000 * 60 * 60 * 24)); + + let colorClass = + daysUntilDue >= 365 + ? "gray-200" + : daysUntilDue >= 30 + ? "blue-200" + : daysUntilDue >= 7 + ? "green-200" + : daysUntilDue > 0 + ? "yellow-200" + : "red-200"; + + const formattedDueDate = + daysUntilDue >= 365 + ? new Date(task.end_event).toLocaleDateString("en-US", { + day: "numeric", + month: "short", + year: "numeric", + }) + : new Date(task.end_event).toLocaleDateString("en-US", { day: "numeric", month: "short" }); + + return ( + + Due: {formattedDueDate} + + ); + })() + : null; + + // Subtask count + const subtaskCountTag = task.subtaskCount ? ( + + {task.subtaskCount} + + ) : null; + + // ---- DRAG STATE ---- */ + + // If the card is being dragged if (isDragging) { return (
); } + // If the card is not being dragged return (
+ {/* Task Detail Modal */} + + {/* -------- Task Card -------- */}
{ setMouseIsOver(true); }} onMouseLeave={() => { setMouseIsOver(false); }}> +<<<<<<< HEAD

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> @@ -74,6 +155,35 @@ export function TaskCard({ task, deleteTask, updateTask }) { )} +======= + {/* -------- Task Content -------- */} + {/* Tags */} + {tags} +

+ {/* Title */} +

document.getElementById(`task_detail_modal_${task.id}`).showModal()}> + {task.content} +

+ {/* -------- Archive Task Button -------- */} + {mouseIsOver && ( + + )} +
+ {/* Description */} +
+ {difficultyTag} + {dueDateTag} + {subtaskCountTag} +
+>>>>>>> 4a3f253e3049f97ef4479dd423642897a56e13fc
); diff --git a/frontend/src/components/kanbanBoard/taskDetailModal.jsx b/frontend/src/components/kanbanBoard/taskDetailModal.jsx index e151d8f..e4317e5 100644 --- a/frontend/src/components/kanbanBoard/taskDetailModal.jsx +++ b/frontend/src/components/kanbanBoard/taskDetailModal.jsx @@ -2,11 +2,19 @@ import { useState } from "react"; import { FaTasks, FaRegListAlt } from "react-icons/fa"; import { FaPlus } from "react-icons/fa6"; import { TbChecklist } from "react-icons/tb"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; -export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId }) { +export function TaskDetailModal({ title, description, tags, difficulty, challenge, importance, taskId, updateTask }) { const [isChallengeChecked, setChallengeChecked] = useState(challenge); const [isImportantChecked, setImportantChecked] = useState(importance); const [currentDifficulty, setCurrentDifficulty] = useState(difficulty); + const [selectedTags, setSelectedTags] = useState([]); + const [dateStart, setDateStart] = useState(new Date()); + const [dateEnd, setDateEnd] = useState(new Date()); + const [startDateEnabled, setStartDateEnabled] = useState(false); + const [endDateEnabled, setEndDateEnabled] = useState(false); + const [isTaskComplete, setTaskComplete] = useState(false); const handleChallengeChange = () => { setChallengeChecked(!isChallengeChecked); @@ -20,6 +28,51 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng setCurrentDifficulty(parseInt(event.target.value, 10)); }; + const handleTagChange = (tag) => { + const isSelected = selectedTags.includes(tag); + setSelectedTags(isSelected ? selectedTags.filter((selectedTag) => selectedTag !== tag) : [...selectedTags, tag]); + }; + + const handleStartDateChange = () => { + if (!isTaskComplete) { + setStartDateEnabled(!startDateEnabled); + } + }; + + const handleEndDateChange = () => { + if (!isTaskComplete) { + setEndDateEnabled(!endDateEnabled); + } + }; + + const handleTaskCompleteChange = () => { + if (isTaskComplete) { + setTaskComplete(false); + } else { + setTaskComplete(true); + setStartDateEnabled(false); + setEndDateEnabled(false); + } + }; + + // Existing tags + const existingTags = tags.map((tag, index) => ( +
+ {tag.label} +
+ )); + + // Selected tags + const selectedTagElements = selectedTags.map((tag, index) => ( +
+ {tag.label} +
+ )); + return (
@@ -35,7 +88,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

{title}

- {/* Tags */}
@@ -43,19 +95,81 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng -
    -
  • - - - Item 2 - -
  • +
      + {tags.map((tag, index) => ( +
    • + +
    • + ))}
-
+
+ {existingTags} + {selectedTagElements} +
+
+ {/* Date Picker */} +
+ {/* Start */} +
+
+

Start At

+
+ +
+ setDateStart(date)} + disabled={!startDateEnabled} + /> +
+
+
+ {/* Complete? */} +
+
+
+

Complete

+ +
+
+
+
+ {/* End */} +
+

End At

+
+ +
+ setDateEnd(date)} disabled={!endDateEnabled} /> +
+
+
- {/* Description */}

@@ -68,7 +182,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng {description}

- {/* Difficulty, Challenge, and Importance */}
@@ -120,7 +233,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng
- {/* Subtask */}

@@ -137,7 +249,6 @@ export function TaskDetailModal({ title, description, tags, difficulty, challeng

-