diff --git a/backend/dashboard/tests.py b/backend/dashboard/tests.py index 7ce503c..943c3de 100644 --- a/backend/dashboard/tests.py +++ b/backend/dashboard/tests.py @@ -1,3 +1,103 @@ from django.test import TestCase +from django.urls import reverse +from tasks.models import Todo +from django.utils import timezone +from datetime import timedelta -# Create your tests here. +from tasks.tests.utils import create_test_user, login_user + +class DashboardStatsAndWeeklyViewSetTests(TestCase): + def setUp(self): + self.user = create_test_user() + self.client = login_user(self.user) + + def create_task(self, title, completed=False, completion_date=None, end_event=None): + return Todo.objects.create( + user=self.user, + title=title, + completed=completed, + completion_date=completion_date, + end_event=end_event + ) + + def test_dashboard_stats_view(self): + # Create tasks for testing + self.create_task('Task 1', completed=True) + self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) + self.create_task('Task 3', end_event=timezone.now()) + + response = self.client.get(reverse('stats-list')) + self.assertEqual(response.status_code, 200) + + self.assertEqual(response.data['completed_this_week'], 1) + self.assertEqual(response.data['tasks_assigned_this_week'], 1) + self.assertEqual(response.data['tasks_assigned_last_week'], 0) + + def test_dashboard_weekly_view(self): + # Create tasks for testing + self.create_task('Task 1', completion_date=timezone.now() - timedelta(days=1)) + self.create_task('Task 2', end_event=timezone.now() - timedelta(days=8)) + self.create_task('Task 3', end_event=timezone.now()) + + response = self.client.get(reverse('weekly-list')) + self.assertEqual(response.status_code, 200) + + +# class DashboardStatsAPITestCase(TestCase): +# def setUp(self): +# # Create a test user +# self.user = create_test_user() + +# # Create test tasks +# self.todo = Todo.objects.create(user=self.user, title='Test Todo') +# self.recurrence_task = RecurrenceTask.objects.create(user=self.user, title='Test Recurrence Task') + +# # Create an API client +# self.client = APIClient() + +# def test_dashboard_stats_api(self): +# # Authenticate the user +# self.client.force_authenticate(user=self.user) + +# # Make a GET request to the DashboardStatsAPIView +# response = self.client.get(reverse("dashboard-stats")) + +# # Assert the response status code is 200 +# self.assertEqual(response.status_code, 200) + +# def test_task_completion_status_update(self): +# # Authenticate the user +# self.client.force_authenticate(user=self.user) + +# # Make a POST request to update the completion status of a task +# data = {'task_id': self.todo.id, 'is_completed': True} +# response = self.client.post(reverse("dashboard-stats"), data, format='json') + +# # Assert the response status code is 200 +# self.assertEqual(response.status_code, 200) + +# # Assert the message in the response +# self.assertEqual(response.data['message'], 'Task completion status updated successfully') + +# # Refresh the todo instance from the database and assert the completion status +# self.todo.refresh_from_db() +# self.assertTrue(self.todo.completed) + + +# class WeeklyStatsAPITestCase(TestCase): +# def setUp(self): +# # Create a test user +# self.user = create_test_user() + +# # Create an API client +# self.client = APIClient() + +# def test_weekly_stats_api(self): +# # Authenticate the user +# self.client.force_authenticate(user=self.user) + +# # Make a GET request to the WeeklyStatsAPIView +# response = self.client.get(reverse('dashboard-weekly-stats')) + +# # Assert the response status code is 200 +# self.assertEqual(response.status_code, 200) diff --git a/backend/dashboard/urls.py b/backend/dashboard/urls.py index beb8f1b..56624ca 100644 --- a/backend/dashboard/urls.py +++ b/backend/dashboard/urls.py @@ -1,6 +1,11 @@ -from django.urls import path -from .views import DashboardStatsAPIView +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import DashboardStatsViewSet, DashboardWeeklyViewSet + +router = DefaultRouter() +router.register(r'dashboard/stats', DashboardStatsViewSet, basename='stats') +router.register(r'dashboard/weekly', DashboardWeeklyViewSet, basename='weekly') urlpatterns = [ - path('dashboard/stats/', DashboardStatsAPIView.as_view(), name='dashboard-stats'), + path('', include(router.urls)), ] diff --git a/backend/dashboard/views.py b/backend/dashboard/views.py index 4475d77..abe0576 100644 --- a/backend/dashboard/views.py +++ b/backend/dashboard/views.py @@ -1,58 +1,311 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from django.db.models import Count +from datetime import timedelta from django.utils import timezone +from rest_framework import status +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from rest_framework import viewsets, mixins -from tasks.models import Todo, RecurrenceTask +from tasks.models import Todo -class DashboardStatsAPIView(APIView): - permission_classes = [IsAuthenticated] - def get(self, request): - user = request.user +class DashboardStatsViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + permission_classes = (IsAuthenticated,) - # Calculate task usage statistics - todo_count = Todo.objects.filter(user=user).count() - recurrence_task_count = RecurrenceTask.objects.filter(user=user).count() + def get_queryset(self): + return Todo.objects.all() - # 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() + def list(self, request, *args, **kwargs): + user = self.request.user - # 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 the start and end date for the last 7 days + end_date = timezone.now() + start_date = end_date - timedelta(days=7) - # 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 + # How many tasks were completed in the last 7 days + completed_last_7_days = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + # Task assign last week compared with this week + tasks_assigned_last_week = Todo.objects.filter( + user=user, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + tasks_assigned_this_week = Todo.objects.filter( + user=user, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + # Completed tasks from last week compared with this week + completed_last_week = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date - timedelta(days=7), + completion_date__lte=start_date + ).count() + + completed_this_week = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=start_date, + completion_date__lte=end_date + ).count() + + overdue_tasks = Todo.objects.filter( + user=user, + completed=False, + end_event__lt=timezone.now() + ).count() + + # Overall completion rate + total_tasks = Todo.objects.filter(user=user).count() + overall_completion_rate = (completed_last_7_days / total_tasks) * 100 if total_tasks > 0 else 0 data = { - '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, + "completed_last_7_days": completed_last_7_days, + "tasks_assigned_last_week": tasks_assigned_last_week, + "tasks_assigned_this_week": tasks_assigned_this_week, + "completed_last_week": completed_last_week, + "completed_this_week": completed_this_week, + "overdue_tasks": overdue_tasks, + "overall_completion_rate": overall_completion_rate, } return Response(data, status=status.HTTP_200_OK) + + +class DashboardWeeklyViewSet(viewsets.GenericViewSet, mixins.ListModelMixin): + permission_classes = (IsAuthenticated,) + + def get_queryset(self): + return Todo.objects.all() + + def list(self, request, *args, **kwargs): + user = self.request.user + + # Calculate the start and end date for the last 7 days (Monday to Sunday) + today = timezone.now().date() + current_week_start = today - timedelta(days=today.weekday()) + current_week_end = current_week_start + timedelta(days=6) + + last_week_start = current_week_start - timedelta(days=7) + last_week_end = last_week_start + timedelta(days=6) + + # Create a list to store daily statistics + weekly_stats = [] + + # Iterate over each day of the week + for day in range(7): + current_day = current_week_start + timedelta(days=day) + last_day = last_week_start + timedelta(days=day) + + # Calculate stats for this week + tasks_this_week = Todo.objects.filter( + user=user, + completion_date__gte=current_day, + completion_date__lte=current_day + timedelta(days=1) + ).count() + + completed_this_week = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=current_day, + completion_date__lte=current_day + timedelta(days=1) + ).count() + + # Calculate stats for last week + tasks_last_week = Todo.objects.filter( + user=user, + completion_date__gte=last_day, + completion_date__lte=last_day + timedelta(days=1) + ).count() + + completed_last_week = Todo.objects.filter( + user=user, + completed=True, + completion_date__gte=last_day, + completion_date__lte=last_day + timedelta(days=1) + ).count() + + daily_stat = { + "date": current_day.strftime("%A"), + "This Week": tasks_this_week, + "Last Week": tasks_last_week, + "Completed This Week": completed_this_week, + "Completed Last Week": completed_last_week, + } + + weekly_stats.append(daily_stat) + + return Response(weekly_stats, status=status.HTTP_200_OK) + + +# class DashboardStatsAPIView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# user = request.user + +# # Calculate task usage statistics +# todo_count = Todo.objects.filter(user=user).count() +# recurrence_task_count = RecurrenceTask.objects.filter(user=user).count() + +# # Calculate how many tasks were completed in the last 7 days +# completed_todo_count_last_week = Todo.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() +# completed_recurrence_task_count_last_week = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=timezone.now() - timezone.timedelta(days=7)).count() + +# # Calculate subtask completion rate +# total_subtasks = Todo.objects.filter(user=user).aggregate(total=Count('subtask__id'))['total'] +# completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True).aggregate(total=Count('subtask__id'))['total'] + +# # Calculate overall completion rate +# total_tasks = todo_count + recurrence_task_count +# completed_tasks = completed_todo_count_last_week + completed_recurrence_task_count_last_week +# overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0 + +# # pie chart show +# complete_todo_percent_last_week = (completed_todo_count_last_week / todo_count) * 100 if todo_count > 0 else 0 + +# complete_recurrence_percent_last_week = (completed_recurrence_task_count_last_week / recurrence_task_count) * 100 if recurrence_task_count > 0 else 0 + +# incomplete_task_percent_last_week = 100 - complete_recurrence_percent_last_week - complete_todo_percent_last_week + +# data = { +# 'todo_count': todo_count, +# 'recurrence_task_count': recurrence_task_count, +# 'completed_todo_count_last_week': completed_todo_count_last_week, +# 'completed_recurrence_task_count_last_week': completed_recurrence_task_count_last_week, +# 'total_subtasks': total_subtasks, +# 'completed_subtasks': completed_subtasks, +# 'overall_completion_rate': overall_completion_rate, +# 'complete_todo_percent_last_week': complete_todo_percent_last_week, +# 'complete_recurrence_percent_last_week' : complete_recurrence_percent_last_week, +# 'incomplete_task_percent_last_week': incomplete_task_percent_last_week, + +# } + +# return Response(data, status=status.HTTP_200_OK) - def post(self, request): - # Handle incoming data from the POST request - # Update the necessary information based on the data +# def post(self, request): +# # Handle incoming data from the POST request +# # Update the necessary information based on the data - task_id = request.data.get('task_id') - is_completed = request.data.get('is_completed') +# task_id = request.data.get('task_id') +# is_completed = request.data.get('is_completed') - try: - task = Todo.objects.get(id=task_id, user=request.user) - task.completed = is_completed - task.save() - return Response({'message': 'Task completion status updated successfully'}, status=status.HTTP_200_OK) - except Todo.DoesNotExist: - return Response({'error': 'Task not found'}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file +# try: +# task = Todo.objects.get(id=task_id, user=request.user) +# task.completed = is_completed +# task.save() +# return Response({'message': 'Task completion status updated successfully'}, status=status.HTTP_200_OK) +# except Todo.DoesNotExist: +# return Response({'error': 'Task not found'}, status=status.HTTP_404_NOT_FOUND) + +# class WeeklyStatsAPIView(APIView): +# permission_classes = [IsAuthenticated] + +# def get(self, request): +# user = request.user +# today = timezone.now() + +# # Calculate the start and end dates for the current week +# current_week_start = today - timezone.timedelta(days=today.weekday()) +# current_week_end = current_week_start + timezone.timedelta(days=6) + +# # Initialize a list to store daily statistics +# weekly_stats = [] + +# # Loop through each day of the week +# for i in range(7): +# # Calculate the start and end dates for the current day +# current_day_start = current_week_start + timezone.timedelta(days=i) +# current_day_end = current_day_start + timezone.timedelta(days=1) + +# # Calculate the start and end dates for the same day over the last 7 days +# last_7_days_start = current_day_start - timezone.timedelta(days=7) +# last_7_days_end = current_day_end - timezone.timedelta(days=7) + +# # Calculate statistics for the current day +# current_day_stats = self.calculate_stats(user, current_day_start, current_day_end) + +# # Calculate statistics for the same day over the last 7 days +# last_7_days_stats = self.calculate_stats(user, last_7_days_start, last_7_days_end) + +# # Calculate the percentage change +# percent_change_over_all = self.calculate_percent_change( +# current_day_stats['overall_completion_rate'], +# last_7_days_stats['overall_completion_rate'] +# ) + +# # Calculate percentage change for completed_todo_count +# percent_change_todo = self.calculate_percent_change( +# current_day_stats['completed_todo_count'], +# last_7_days_stats['completed_todo_count'] +# ) + +# # Calculate percentage change for completed_recurrence_task_count +# percent_change_recurrence = self.calculate_percent_change( +# current_day_stats['completed_recurrence_task_count'], +# last_7_days_stats['completed_recurrence_task_count'] +# ) + +# # Append the daily statistics to the list +# weekly_stats.append({ +# 'day_of_week': current_day_start.strftime('%A'), +# 'current_day_stats': current_day_stats, +# 'last_7_days_stats': last_7_days_stats, +# 'percent_change_over_all': percent_change_over_all, +# 'percent_change_todo': percent_change_todo, +# 'percent_change_recurrence': percent_change_recurrence, +# }) + +# response_data = { +# 'weekly_stats': weekly_stats, +# } + +# return Response(response_data, status=status.HTTP_200_OK) + +# def calculate_stats(self, user, start_date, end_date): +# # Calculate task usage statistics for the specified day +# todo_count = Todo.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).count() +# recurrence_task_count = RecurrenceTask.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).count() + +# # Calculate how many tasks were completed on the specified day +# completed_todo_count = Todo.objects.filter(user=user, completed=True, last_update__gte=start_date, last_update__lte=end_date).count() +# completed_recurrence_task_count = RecurrenceTask.objects.filter(user=user, completed=True, last_update__gte=start_date, last_update__lte=end_date).count() + +# # Calculate subtask completion rate for the specified day +# total_subtasks = Todo.objects.filter(user=user, created_at__gte=start_date, created_at__lte=end_date).aggregate(total=Count('subtask__id'))['total'] +# completed_subtasks = Todo.objects.filter(user=user, subtask__completed=True, created_at__gte=start_date, created_at__lte=end_date).aggregate(total=Count('subtask__id'))['total'] + +# # Calculate overall completion rate for the specified day +# total_tasks = todo_count + recurrence_task_count +# completed_tasks = completed_todo_count + completed_recurrence_task_count +# overall_completion_rate = (completed_tasks / total_tasks) * 100 if total_tasks > 0 else 0 + +# return { +# 'start_date': start_date.strftime('%Y-%m-%d'), +# 'end_date': end_date.strftime('%Y-%m-%d'), +# 'todo_count': todo_count, +# 'recurrence_task_count': recurrence_task_count, +# 'completed_todo_count': completed_todo_count, +# 'completed_recurrence_task_count': completed_recurrence_task_count, +# 'total_subtasks': total_subtasks, +# 'completed_subtasks': completed_subtasks, +# 'overall_completion_rate': overall_completion_rate, +# } + +# def calculate_percent_change(self, current_value, last_value): +# # Calculate the percentage change between current and last values +# if last_value != 0: +# percent_change = ((current_value - last_value) / last_value) * 100 +# else: +# percent_change = current_value * 100 # Consider infinite change when the last value is zero + +# return round(percent_change, 2) diff --git a/backend/tasks/admin.py b/backend/tasks/admin.py index 5c5bbb3..cabf594 100644 --- a/backend/tasks/admin.py +++ b/backend/tasks/admin.py @@ -7,8 +7,9 @@ class TagAdmin(admin.ModelAdmin): @admin.register(Todo) class TodoAdmin(admin.ModelAdmin): - list_display = ['title', 'list_board', 'is_active', 'priority'] - list_filter = ['list_board', 'is_active', 'priority'] + list_display = ['title', 'list_board', 'is_active', 'priority', 'completed', 'completion_date'] + list_filter = ['list_board', 'is_active', 'priority', 'completed'] + exclude = ['completion_date'] @admin.register(RecurrenceTask) class RecurrenceTaskAdmin(admin.ModelAdmin): diff --git a/backend/tasks/migrations/0018_alter_habit_creation_date_and_more.py b/backend/tasks/migrations/0018_alter_habit_creation_date_and_more.py new file mode 100644 index 0000000..bbb1714 --- /dev/null +++ b/backend/tasks/migrations/0018_alter_habit_creation_date_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.6 on 2023-11-20 14:58 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0017_alter_recurrencetask_list_board_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='habit', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AlterField( + model_name='recurrencetask', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + migrations.AlterField( + model_name='todo', + name='creation_date', + field=models.DateTimeField(default=django.utils.timezone.now, editable=False), + ), + ] diff --git a/backend/tasks/migrations/0019_alter_habit_creation_date_and_more.py b/backend/tasks/migrations/0019_alter_habit_creation_date_and_more.py new file mode 100644 index 0000000..0ba0307 --- /dev/null +++ b/backend/tasks/migrations/0019_alter_habit_creation_date_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.6 on 2023-11-20 15:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0018_alter_habit_creation_date_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='habit', + name='creation_date', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='recurrencetask', + name='creation_date', + field=models.DateTimeField(auto_now_add=True), + ), + migrations.AlterField( + model_name='todo', + name='creation_date', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/backend/tasks/migrations/0020_recurrencetask_completion_date_todo_completion_date.py b/backend/tasks/migrations/0020_recurrencetask_completion_date_todo_completion_date.py new file mode 100644 index 0000000..f1ffe13 --- /dev/null +++ b/backend/tasks/migrations/0020_recurrencetask_completion_date_todo_completion_date.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.6 on 2023-11-20 15:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0019_alter_habit_creation_date_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='recurrencetask', + name='completion_date', + field=models.DateTimeField(null=True), + ), + migrations.AddField( + model_name='todo', + name='completion_date', + field=models.DateTimeField(null=True), + ), + ] diff --git a/backend/tasks/models.py b/backend/tasks/models.py index fc29fe6..c848e00 100644 --- a/backend/tasks/models.py +++ b/backend/tasks/models.py @@ -1,5 +1,6 @@ from django.db import models from django.conf import settings +from django.utils import timezone from boards.models import ListBoard, Board @@ -25,6 +26,7 @@ class Task(models.Model): :param challenge: Associated challenge (optional). :param fromSystem: A boolean field indicating if the task is from System. :param creation_date: Creation date of the task. + :param last_update: Last update date of the task. """ class Difficulty(models.IntegerChoices): EASY = 1, 'Easy' @@ -59,6 +61,7 @@ class Todo(Task): :param end_event: End date and time of the task. :param google_calendar_id: The Google Calendar ID of the task. :param completed: A boolean field indicating whether the task is completed. + :param completion_date: The date and time when the task is completed. :param priority: The priority of the task (range: 1 to 4). """ class EisenhowerMatrix(models.IntegerChoices): @@ -74,8 +77,17 @@ class Todo(Task): end_event = models.DateTimeField(null=True) google_calendar_id = models.CharField(max_length=255, null=True, blank=True) completed = models.BooleanField(default=False) + completion_date = models.DateTimeField(null=True) priority = models.PositiveSmallIntegerField(choices=EisenhowerMatrix.choices, default=EisenhowerMatrix.NOT_IMPORTANT_NOT_URGENT) + def save(self, *args, **kwargs): + if self.completed and not self.completion_date: + self.completion_date = timezone.now() + elif not self.completed: + self.completion_date = None + + super().save(*args, **kwargs) + def __str__(self): return self.title @@ -90,6 +102,7 @@ class RecurrenceTask(Task): :param start_event: Start date and time of the task. :param end_event: End date and time of the task. :param completed: A boolean field indicating whether the task is completed. + :param completion_date: The date and time when the task is completed. :param parent_task: The parent task of the subtask. """ list_board = models.ForeignKey(ListBoard, on_delete=models.CASCADE, null=True, default=1) @@ -99,8 +112,17 @@ class RecurrenceTask(Task): start_event = models.DateTimeField(null=True) end_event = models.DateTimeField(null=True) completed = models.BooleanField(default=False) + completion_date = models.DateTimeField(null=True) parent_task = models.ForeignKey("self", on_delete=models.CASCADE, null=True) + def save(self, *args, **kwargs): + if self.completed and not self.completion_date: + self.completion_date = timezone.now() + elif not self.completed: + self.completion_date = None + + super().save(*args, **kwargs) + def __str__(self) -> str: return f"{self.title} ({self.recurrence_rule})"