From c3034579605e4c3f5d361b3ee3fa4ca0c5eb21d3 Mon Sep 17 00:00:00 2001 From: Bair Erendzhenov Date: Wed, 21 Feb 2024 01:47:55 +0300 Subject: [PATCH 1/3] Changed analytics.py --- short_tracker/api/v1/analytics/analytics.py | 54 ++++++++++++++----- short_tracker/api/v1/analytics/serializers.py | 2 +- short_tracker/api/v1/analytics/views.py | 26 +++++---- short_tracker/api/v1/filters.py | 11 ++-- ...06_alter_allownotification_notification.py | 18 +++++++ ...07_alter_allownotification_notification.py | 18 +++++++ ...08_alter_allownotification_notification.py | 18 +++++++ ...09_alter_allownotification_notification.py | 18 +++++++ ...10_alter_allownotification_notification.py | 18 +++++++ .../migrations/0005_alter_task_done_date.py | 18 +++++++ .../migrations/0006_alter_task_done_date.py | 18 +++++++ 11 files changed, 192 insertions(+), 27 deletions(-) create mode 100644 short_tracker/bot/migrations/0006_alter_allownotification_notification.py create mode 100644 short_tracker/bot/migrations/0007_alter_allownotification_notification.py create mode 100644 short_tracker/bot/migrations/0008_alter_allownotification_notification.py create mode 100644 short_tracker/bot/migrations/0009_alter_allownotification_notification.py create mode 100644 short_tracker/bot/migrations/0010_alter_allownotification_notification.py create mode 100644 short_tracker/tasks/migrations/0005_alter_task_done_date.py create mode 100644 short_tracker/tasks/migrations/0006_alter_task_done_date.py diff --git a/short_tracker/api/v1/analytics/analytics.py b/short_tracker/api/v1/analytics/analytics.py index 51f7ddd..c6b590d 100644 --- a/short_tracker/api/v1/analytics/analytics.py +++ b/short_tracker/api/v1/analytics/analytics.py @@ -2,20 +2,21 @@ from datetime import timedelta from django.db.models import F +from django.utils import timezone from users.models import CustomUser class TasksAnalyticsFactory: @staticmethod - def calculate_analytics(queryset): + def calculate_analytics(queryset, sort_by=None): analytics = {} analytics.update( TasksAnalyticsFactory.tasks_count(queryset) ) analytics.update( TasksAnalyticsFactory.performers_analytics( - queryset) + queryset, sort_by) ) return analytics @@ -31,45 +32,74 @@ def tasks_count(queryset): } @staticmethod - def performers_analytics(queryset): + def performers_analytics(queryset, sort_by=None): performers_analytics = {} performers_ids = queryset.values_list( - 'performers', flat=True).distinct() + 'performer_id', flat=True).distinct() for performer_id in performers_ids: performer = CustomUser.objects.get(id=performer_id) - filtered_queryset = queryset.filter(performers=performer) + filtered_queryset = queryset.filter(performer=performer) performer_name = f"{performer.first_name} {performer.last_name}" completed_on_time_count = filtered_queryset.filter( done_date__lte=F('deadline_date'), - performers=performer).count() + performer=performer).count() completed_with_delay_count = filtered_queryset.filter( done_date__gt=F('deadline_date'), - performers=performer).count() + performer=performer).count() + total_tasks = completed_on_time_count + completed_with_delay_count performers_analytics[performer_id] = { 'performer_name': performer_name, + 'total_tasks': total_tasks, 'completed_on_time_count': completed_on_time_count, - 'completed_with_delay_count': - completed_with_delay_count, + 'completed_with_delay_count': completed_with_delay_count, 'avg_time_create_date_to_inprogress_date': TasksAnalyticsFactory.avg_time( filtered_queryset, 'create_date', 'inprogress_date'), - 'avg_time_create_date_to_done_date': + 'avg_time_create_date_to_done_date': TasksAnalyticsFactory.avg_time( filtered_queryset, 'create_date', 'done_date'), 'avg_time_inprogress_date_to_done_date': TasksAnalyticsFactory.avg_time( filtered_queryset, 'inprogress_date', 'done_date'), } + performers_analytics = TasksAnalyticsFactory.sort_by( + performers_analytics, sort_by + ) return {'performers_analytics': performers_analytics} + + @staticmethod + def sort_by(performers_analytics, sort_by=None): + key_mapping = { + 'total_tasks': 'total_tasks', + 'completed_on_time_count': 'completed_on_time_count', + 'completed_with_delay_count': 'completed_with_delay_count', + } + + if sort_by: + key = key_mapping.get(sort_by, 'completed_on_time_count') + performers_analytics = dict( + sorted( + performers_analytics.items(), + key=lambda x: x[1][key], reverse=True + ) + ) + return performers_analytics + else: + performers_analytics = dict( + sorted( + performers_analytics.items(), + key=lambda x: x[1]['completed_on_time_count'], + reverse=True)) + return performers_analytics @staticmethod def avg_time(queryset, field1, field2): datetime_list = [ - getattr(task, field2) - getattr(task, field1) + timezone.localtime(getattr(task, field2)) - timezone.localtime(getattr(task, field1)) for task in queryset if getattr(task, field1) and getattr(task, field2) ] sum_of_time = sum(datetime_list, datetime.timedelta()) length = len(datetime_list) avg_time = sum_of_time // length if length > 0 else timedelta() - return str(avg_time).rsplit(':', 1)[0] + return str(avg_time).rsplit(':', 1)[0] \ No newline at end of file diff --git a/short_tracker/api/v1/analytics/serializers.py b/short_tracker/api/v1/analytics/serializers.py index e6a7f8d..cbf6fa0 100644 --- a/short_tracker/api/v1/analytics/serializers.py +++ b/short_tracker/api/v1/analytics/serializers.py @@ -3,13 +3,13 @@ class PerformerAnalyticsSerializer(serializers.Serializer): performer_name = serializers.CharField() + total_tasks = serializers.IntegerField() completed_on_time_count = serializers.IntegerField() completed_with_delay_count = serializers.IntegerField() avg_time_create_date_to_inprogress_date = serializers.CharField() avg_time_create_date_to_done_date = serializers.CharField() avg_time_inprogress_date_to_done_date = serializers.CharField() - class TaskAnalyticsSerializer(serializers.Serializer): total_tasks_on_time = serializers.IntegerField() total_tasks_with_delay = serializers.IntegerField() diff --git a/short_tracker/api/v1/analytics/views.py b/short_tracker/api/v1/analytics/views.py index 6ed05e0..e6d9aae 100644 --- a/short_tracker/api/v1/analytics/views.py +++ b/short_tracker/api/v1/analytics/views.py @@ -1,7 +1,8 @@ -from datetime import datetime, timedelta +from datetime import timedelta -from django.db.models import Prefetch +from django.db.models import F, Prefetch, Q from django_filters.rest_framework import DjangoFilterBackend +from django.utils import timezone from rest_framework import status, viewsets from rest_framework.response import Response @@ -21,32 +22,35 @@ class TaskAnalyticsViewSet(viewsets.ReadOnlyModelViewSet): filterset_class = TaskAnalyticsFilter def get_queryset(self): - queryset = Task.objects.filter( + base_queryset = Task.objects.filter( status=Task.TaskStatus.DONE ).prefetch_related( Prefetch( - 'performers', + 'performer', queryset=CustomUser.objects.filter( is_team_lead=False ) ) ) + # start_date = self.request.query_params.get('start_date') + # end_date = self.request.query_params.get('end_date') if self.request.query_params: filterset = TaskAnalyticsFilter( self.request.query_params, - queryset=queryset, request=self.request) + queryset=base_queryset, request=self.request + ) if filterset.is_valid(): - queryset = filterset.qs + queryset = filterset.qs else: - queryset = Task.objects.filter( - done_date__gte=datetime.today() - timedelta(days=7)) + return base_queryset.filter( + Q(done_date__gte=timezone.now() - timedelta(days=7))) return queryset def list(self, request, *args, **kwargs): + sort_by = request.query_params.get('sort_by') serializer = TaskAnalyticsSerializer( data=TasksAnalyticsFactory.calculate_analytics( - self.get_queryset() - ) - ) + self.get_queryset(), sort_by=sort_by + )) serializer.is_valid(raise_exception=True) return Response(serializer.data, status=status.HTTP_200_OK) \ No newline at end of file diff --git a/short_tracker/api/v1/filters.py b/short_tracker/api/v1/filters.py index a863198..f5e9d32 100644 --- a/short_tracker/api/v1/filters.py +++ b/short_tracker/api/v1/filters.py @@ -1,8 +1,9 @@ import django_filters -from django.db.models import Case, Q, Value, When +from django.db.models import Case, F,Q, Value, When from django.utils import timezone from django_filters.rest_framework import FilterSet, filters +from api.v1.analytics.analytics import TasksAnalyticsFactory from tasks.models import Task @@ -77,12 +78,13 @@ def filter_is_expired(self, queryset, _, value): class TaskAnalyticsFilter(django_filters.FilterSet): performer_id = filters.NumberFilter( - field_name='performers', method='filter_by_performer') + field_name='performer_id', method='filter_by_performer') start_date = django_filters.DateFilter( field_name='done_date',lookup_expr=('gt')) end_date = django_filters.DateFilter( field_name='done_date',lookup_expr=('lt')) date_range = filters.DateRangeFilter(field_name='done_date') + sort_by = filters.CharFilter(method='filter_sort_by') class Meta: model = Task @@ -90,5 +92,8 @@ class Meta: def filter_by_performer(self, queryset, name, value): if value: - return queryset.filter(performers__in=[value]) + return queryset.filter(performer_id=value) return queryset + + def filter_sort_by(self, queryset, name, value): + return queryset diff --git a/short_tracker/bot/migrations/0006_alter_allownotification_notification.py b/short_tracker/bot/migrations/0006_alter_allownotification_notification.py new file mode 100644 index 0000000..3a48756 --- /dev/null +++ b/short_tracker/bot/migrations/0006_alter_allownotification_notification.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-19 14:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0005_alter_allownotification_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='allownotification', + name='notification', + field=models.CharField(choices=[('msg', 'Сообщения'), ('tasks', 'Задачи'), ('status', 'Статусы'), ('deadline', 'Дедлайн')], max_length=10, verbose_name='Уведомление'), + ), + ] diff --git a/short_tracker/bot/migrations/0007_alter_allownotification_notification.py b/short_tracker/bot/migrations/0007_alter_allownotification_notification.py new file mode 100644 index 0000000..7fdc335 --- /dev/null +++ b/short_tracker/bot/migrations/0007_alter_allownotification_notification.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-19 14:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0006_alter_allownotification_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='allownotification', + name='notification', + field=models.CharField(choices=[('status', 'Статусы'), ('tasks', 'Задачи'), ('msg', 'Сообщения'), ('deadline', 'Дедлайн')], max_length=10, verbose_name='Уведомление'), + ), + ] diff --git a/short_tracker/bot/migrations/0008_alter_allownotification_notification.py b/short_tracker/bot/migrations/0008_alter_allownotification_notification.py new file mode 100644 index 0000000..6b0ab9b --- /dev/null +++ b/short_tracker/bot/migrations/0008_alter_allownotification_notification.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-19 14:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0007_alter_allownotification_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='allownotification', + name='notification', + field=models.CharField(choices=[('msg', 'Сообщения'), ('tasks', 'Задачи'), ('deadline', 'Дедлайн'), ('status', 'Статусы')], max_length=10, verbose_name='Уведомление'), + ), + ] diff --git a/short_tracker/bot/migrations/0009_alter_allownotification_notification.py b/short_tracker/bot/migrations/0009_alter_allownotification_notification.py new file mode 100644 index 0000000..fd47f8c --- /dev/null +++ b/short_tracker/bot/migrations/0009_alter_allownotification_notification.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-20 08:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0008_alter_allownotification_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='allownotification', + name='notification', + field=models.CharField(choices=[('status', 'Статусы'), ('tasks', 'Задачи'), ('deadline', 'Дедлайн'), ('msg', 'Сообщения')], max_length=10, verbose_name='Уведомление'), + ), + ] diff --git a/short_tracker/bot/migrations/0010_alter_allownotification_notification.py b/short_tracker/bot/migrations/0010_alter_allownotification_notification.py new file mode 100644 index 0000000..38ca367 --- /dev/null +++ b/short_tracker/bot/migrations/0010_alter_allownotification_notification.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-20 10:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bot', '0009_alter_allownotification_notification'), + ] + + operations = [ + migrations.AlterField( + model_name='allownotification', + name='notification', + field=models.CharField(choices=[('deadline', 'Дедлайн'), ('status', 'Статусы'), ('msg', 'Сообщения'), ('tasks', 'Задачи')], max_length=10, verbose_name='Уведомление'), + ), + ] diff --git a/short_tracker/tasks/migrations/0005_alter_task_done_date.py b/short_tracker/tasks/migrations/0005_alter_task_done_date.py new file mode 100644 index 0000000..24e538b --- /dev/null +++ b/short_tracker/tasks/migrations/0005_alter_task_done_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-20 08:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0004_task_tasks_task_create__81ab0d_idx'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='done_date', + field=models.DateTimeField(auto_now_add=True, help_text='The done date of the task', null=True, verbose_name='done date'), + ), + ] diff --git a/short_tracker/tasks/migrations/0006_alter_task_done_date.py b/short_tracker/tasks/migrations/0006_alter_task_done_date.py new file mode 100644 index 0000000..499ee90 --- /dev/null +++ b/short_tracker/tasks/migrations/0006_alter_task_done_date.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.8 on 2024-02-20 10:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('tasks', '0005_alter_task_done_date'), + ] + + operations = [ + migrations.AlterField( + model_name='task', + name='done_date', + field=models.DateTimeField(blank=True, help_text='The done date of the task', null=True, verbose_name='done date'), + ), + ] From ebffb8ca8710bd44ef1a3657bb0c49b9b7dc72b4 Mon Sep 17 00:00:00 2001 From: Bair Erendzhenov Date: Wed, 21 Feb 2024 13:20:17 +0300 Subject: [PATCH 2/3] saved last changes --- short_tracker/api/v1/analytics/analytics.py | 37 +++++++------------ short_tracker/api/v1/analytics/serializers.py | 4 +- short_tracker/api/v1/analytics/views.py | 14 +++---- short_tracker/api/v1/filters.py | 11 +----- 4 files changed, 24 insertions(+), 42 deletions(-) diff --git a/short_tracker/api/v1/analytics/analytics.py b/short_tracker/api/v1/analytics/analytics.py index c6b590d..e191a30 100644 --- a/short_tracker/api/v1/analytics/analytics.py +++ b/short_tracker/api/v1/analytics/analytics.py @@ -40,18 +40,18 @@ def performers_analytics(queryset, sort_by=None): performer = CustomUser.objects.get(id=performer_id) filtered_queryset = queryset.filter(performer=performer) performer_name = f"{performer.first_name} {performer.last_name}" - completed_on_time_count = filtered_queryset.filter( + on_time_count = filtered_queryset.filter( done_date__lte=F('deadline_date'), performer=performer).count() - completed_with_delay_count = filtered_queryset.filter( + with_delay_count = filtered_queryset.filter( done_date__gt=F('deadline_date'), performer=performer).count() - total_tasks = completed_on_time_count + completed_with_delay_count + total_tasks = on_time_count + with_delay_count performers_analytics[performer_id] = { - 'performer_name': performer_name, + 'performer_name': performer_name, 'total_tasks': total_tasks, - 'completed_on_time_count': completed_on_time_count, - 'completed_with_delay_count': completed_with_delay_count, + 'on_time_count': on_time_count, + 'with_delay_count': with_delay_count, 'avg_time_create_date_to_inprogress_date': TasksAnalyticsFactory.avg_time( filtered_queryset, 'create_date', 'inprogress_date'), @@ -71,26 +71,17 @@ def performers_analytics(queryset, sort_by=None): def sort_by(performers_analytics, sort_by=None): key_mapping = { 'total_tasks': 'total_tasks', - 'completed_on_time_count': 'completed_on_time_count', - 'completed_with_delay_count': 'completed_with_delay_count', + 'on_time_count': 'on_time_count', + 'with_delay_count': 'with_delay_count', } - - if sort_by: - key = key_mapping.get(sort_by, 'completed_on_time_count') - performers_analytics = dict( - sorted( - performers_analytics.items(), - key=lambda x: x[1][key], reverse=True + key = key_mapping.get(sort_by, 'on_time_count') + performers_analytics = dict( + sorted( + performers_analytics.items(), + key=lambda x: x[1][key], reverse=True ) ) - return performers_analytics - else: - performers_analytics = dict( - sorted( - performers_analytics.items(), - key=lambda x: x[1]['completed_on_time_count'], - reverse=True)) - return performers_analytics + return performers_analytics @staticmethod def avg_time(queryset, field1, field2): diff --git a/short_tracker/api/v1/analytics/serializers.py b/short_tracker/api/v1/analytics/serializers.py index cbf6fa0..6a49745 100644 --- a/short_tracker/api/v1/analytics/serializers.py +++ b/short_tracker/api/v1/analytics/serializers.py @@ -4,8 +4,8 @@ class PerformerAnalyticsSerializer(serializers.Serializer): performer_name = serializers.CharField() total_tasks = serializers.IntegerField() - completed_on_time_count = serializers.IntegerField() - completed_with_delay_count = serializers.IntegerField() + on_time_count = serializers.IntegerField() + with_delay_count = serializers.IntegerField() avg_time_create_date_to_inprogress_date = serializers.CharField() avg_time_create_date_to_done_date = serializers.CharField() avg_time_inprogress_date_to_done_date = serializers.CharField() diff --git a/short_tracker/api/v1/analytics/views.py b/short_tracker/api/v1/analytics/views.py index e6d9aae..4a5a7d6 100644 --- a/short_tracker/api/v1/analytics/views.py +++ b/short_tracker/api/v1/analytics/views.py @@ -22,7 +22,7 @@ class TaskAnalyticsViewSet(viewsets.ReadOnlyModelViewSet): filterset_class = TaskAnalyticsFilter def get_queryset(self): - base_queryset = Task.objects.filter( + queryset = Task.objects.filter( status=Task.TaskStatus.DONE ).prefetch_related( Prefetch( @@ -32,18 +32,18 @@ def get_queryset(self): ) ) ) - # start_date = self.request.query_params.get('start_date') - # end_date = self.request.query_params.get('end_date') - if self.request.query_params: + start_date = self.request.query_params.get('start_date') + end_date = self.request.query_params.get('end_date') + if start_date or end_date: filterset = TaskAnalyticsFilter( self.request.query_params, - queryset=base_queryset, request=self.request + queryset=queryset, request=self.request ) if filterset.is_valid(): queryset = filterset.qs else: - return base_queryset.filter( - Q(done_date__gte=timezone.now() - timedelta(days=7))) + return queryset.filter( + Q(done_date__gte=timezone.now() - timedelta(days=7))) return queryset def list(self, request, *args, **kwargs): diff --git a/short_tracker/api/v1/filters.py b/short_tracker/api/v1/filters.py index f5e9d32..44dd5a4 100644 --- a/short_tracker/api/v1/filters.py +++ b/short_tracker/api/v1/filters.py @@ -75,25 +75,16 @@ def filter_is_expired(self, queryset, _, value): ) return queryset - class TaskAnalyticsFilter(django_filters.FilterSet): - performer_id = filters.NumberFilter( - field_name='performer_id', method='filter_by_performer') start_date = django_filters.DateFilter( field_name='done_date',lookup_expr=('gt')) end_date = django_filters.DateFilter( field_name='done_date',lookup_expr=('lt')) - date_range = filters.DateRangeFilter(field_name='done_date') sort_by = filters.CharFilter(method='filter_sort_by') class Meta: model = Task fields=[] - - def filter_by_performer(self, queryset, name, value): - if value: - return queryset.filter(performer_id=value) - return queryset - + def filter_sort_by(self, queryset, name, value): return queryset From 14a34690e40668847f614825ec7b6bfee610eb2b Mon Sep 17 00:00:00 2001 From: Bair Erendzhenov Date: Thu, 22 Feb 2024 12:48:03 +0300 Subject: [PATCH 3/3] changed filters and added time in settings --- short_tracker/api/v1/filters.py | 4 ++-- short_tracker/short_tracker/settings.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/short_tracker/api/v1/filters.py b/short_tracker/api/v1/filters.py index 44dd5a4..ff5d314 100644 --- a/short_tracker/api/v1/filters.py +++ b/short_tracker/api/v1/filters.py @@ -76,9 +76,9 @@ def filter_is_expired(self, queryset, _, value): return queryset class TaskAnalyticsFilter(django_filters.FilterSet): - start_date = django_filters.DateFilter( + start_date = django_filters.IsoDateTimeFilter( field_name='done_date',lookup_expr=('gt')) - end_date = django_filters.DateFilter( + end_date = django_filters.IsoDateTimeFilter( field_name='done_date',lookup_expr=('lt')) sort_by = filters.CharFilter(method='filter_sort_by') diff --git a/short_tracker/short_tracker/settings.py b/short_tracker/short_tracker/settings.py index fd12583..dea59b2 100644 --- a/short_tracker/short_tracker/settings.py +++ b/short_tracker/short_tracker/settings.py @@ -120,6 +120,8 @@ LANGUAGE_CODE = 'ru-ru' +TIME_ZONE = 'Europe/Moscow' + TIME_ZONE = 'UTC' USE_I18N = True