diff --git a/api/barrier_downloads/views.py b/api/barrier_downloads/views.py index 2a7dc6270..9ae1c060e 100644 --- a/api/barrier_downloads/views.py +++ b/api/barrier_downloads/views.py @@ -15,6 +15,20 @@ from api.barriers.models import Barrier, BarrierFilterSet +class BarrierDownloadFilterBackend(DjangoFilterBackend): + """ + We want to override DjangoFilterBackend to read the filters + from request.data instead of request.query_params + """ + + def get_filterset_kwargs(self, request, queryset, view): + return { + "data": request.data, + "queryset": queryset, + "request": request, + } + + class BarrierDownloadsView(generics.ListCreateAPIView): queryset = ( Barrier.barriers.annotate( @@ -41,7 +55,7 @@ class BarrierDownloadsView(generics.ListCreateAPIView): ) serializer_class = BarrierDownloadSerializer filterset_class = BarrierFilterSet - filter_backends = (DjangoFilterBackend,) + filter_backends = (BarrierDownloadFilterBackend,) pagination_class = PageNumberPagination def post(self, request, *args, **kwargs): diff --git a/api/barriers/serializers/__init__.py b/api/barriers/serializers/__init__.py index 2cde0b1f9..f0a60819e 100644 --- a/api/barriers/serializers/__init__.py +++ b/api/barriers/serializers/__init__.py @@ -1,8 +1,4 @@ -from .barriers import ( # noqa - BarrierDetailSerializer, - BarrierListSerializer, - BarrierRelatedListSerializer, -) +from .barriers import BarrierDetailSerializer, BarrierListSerializer # noqa from .data_workspace import DataWorkspaceSerializer # noqa from .progress_updates import ProgressUpdateSerializer # noqa from .public_barriers import PublicBarrierSerializer, PublishedVersionSerializer # noqa diff --git a/api/barriers/serializers/barriers.py b/api/barriers/serializers/barriers.py index 72f58c9a2..86ffa6e2c 100644 --- a/api/barriers/serializers/barriers.py +++ b/api/barriers/serializers/barriers.py @@ -134,15 +134,3 @@ def get_current_valuation_assessment(instance): return f"{rating}" else: return None - - -# TODO : standard list serialiser may suffice and the following not required base on final designs -class BarrierRelatedListSerializer(serializers.Serializer): - summary = serializers.CharField(read_only=True) - title = serializers.CharField(read_only=True) - id = serializers.UUIDField(read_only=True) - reported_on = serializers.DateTimeField(read_only=True) - modified_on = serializers.DateTimeField(read_only=True) - status = StatusField(required=False) - location = serializers.CharField(read_only=True) - similarity = serializers.FloatField(read_only=True) diff --git a/api/barriers/urls.py b/api/barriers/urls.py index b8d75bffb..a3fc49e3d 100644 --- a/api/barriers/urls.py +++ b/api/barriers/urls.py @@ -3,7 +3,6 @@ from api.barriers.views import ( BarrierActivity, - BarrierDashboardSummary, BarrierDetail, BarrierFullHistory, BarrierHibernate, @@ -171,9 +170,6 @@ name="unknown-barrier", ), path("counts", barrier_count, name="barrier-count"), - path( - "dashboard-summary", BarrierDashboardSummary.as_view(), name="barrier-summary" - ), path("reports", BarrierReportList.as_view(), name="list-reports"), path("reports/", BarrierReportDetail.as_view(), name="get-report"), path( diff --git a/api/barriers/views.py b/api/barriers/views.py index 8a3aa5955..6946d911c 100644 --- a/api/barriers/views.py +++ b/api/barriers/views.py @@ -5,17 +5,7 @@ from dateutil.parser import parse from django.db import transaction -from django.db.models import ( - Case, - CharField, - F, - IntegerField, - Prefetch, - Q, - Sum, - Value, - When, -) +from django.db.models import Case, CharField, F, Prefetch, Value, When from django.http import StreamingHttpResponse from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -58,7 +48,6 @@ from api.metadata.constants import ( BARRIER_INTERACTION_TYPE, BARRIER_SEARCH_ORDERING_CHOICES, - ECONOMIC_ASSESSMENT_IMPACT_MIDPOINTS_NUMERIC_LOOKUP, BarrierStatus, PublicBarrierStatus, ) @@ -188,230 +177,6 @@ class Meta: abstract = True -class BarrierDashboardSummary(generics.GenericAPIView): - """ - View to return high level stats to the dashboard - """ - - serializer_class = BarrierListSerializer - filterset_class = BarrierFilterSet - - filter_backends = (DjangoFilterBackend,) - ordering_fields = ( - "reported_on", - "modified_on", - "estimated_resolution_date", - "status", - "priority", - "country", - ) - ordering = ("-reported_on",) - - def get(self, request): - filtered_queryset = self.filter_queryset( - Barrier.barriers.filter(archived=False) - ) - - current_user = self.request.user - - # Get current financial year - current_year_start = datetime(datetime.now().year, 4, 1) - current_year_end = datetime(datetime.now().year + 1, 3, 31) - previous_year_start = datetime(datetime.now().year - 1, 4, 1) - previous_year_end = datetime(datetime.now().year + 1, 3, 31) - - if not current_user.is_anonymous: - user_barrier_count = Barrier.barriers.filter( - created_by=current_user - ).count() - user_report_count = Barrier.reports.filter(created_by=current_user).count() - user_open_barrier_count = Barrier.barriers.filter( - created_by=current_user, status=2 - ).count() - - when_assessment = [ - When(valuation_assessments__impact=k, then=Value(v)) - for k, v in ECONOMIC_ASSESSMENT_IMPACT_MIDPOINTS_NUMERIC_LOOKUP - ] - - # Resolved vs Estimated barriers values chart - resolved_valuations = ( - filtered_queryset.filter( - Q( - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ] - ) - | Q( - status_date__range=[ - current_year_start, - current_year_end, - ] - ), - Q(status=4) | Q(status=3), - valuation_assessments__archived=False, - ) - .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) - .aggregate(total=Sum("numeric_value")) - ) - - resolved_barrier_value = resolved_valuations["total"] - - estimated_valuations = ( - filtered_queryset.filter( - Q( - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ] - ) - | Q( - status_date__range=[ - current_year_start, - current_year_end, - ] - ), - Q(status=1) | Q(status=2), - valuation_assessments__archived=False, - ) - .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) - .aggregate(total=Sum("numeric_value")) - ) - - estimated_barrier_value = estimated_valuations["total"] - - # Total resolved barriers vs open barriers value chart - - resolved_barriers = ( - filtered_queryset.filter( - Q(status=4) | Q(status=3), - valuation_assessments__archived=False, - ) - .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) - .aggregate(total=Sum("numeric_value")) - ) - - total_resolved_barriers = resolved_barriers["total"] - - open_barriers = ( - filtered_queryset.filter( - Q(status=1) | Q(status=2), - valuation_assessments__archived=False, - ) - .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) - .aggregate(total=Sum("numeric_value")) - ) - - open_barriers_value = open_barriers["total"] - - # Open barriers by status - whens = [When(status=k, then=Value(v)) for k, v in BarrierStatus.choices] - - barrier_by_status = ( - filtered_queryset.filter( - valuation_assessments__archived=False, - ) - .annotate(status_display=Case(*whens, output_field=CharField())) - .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) - .values("status_display") - .annotate(total=Sum("numeric_value")) - .order_by() - ) - - status_labels = [] - status_data = [] - - for series in barrier_by_status: - status_labels.append(series["status_display"]) - status_data.append(series["total"]) - - # TODO for status filter might need to consider status dates as well as ERD - counts = { - "financial_year": { - "current_start": current_year_start, - "current_end": current_year_end, - "previous_start": previous_year_start, - "previous_end": previous_year_end, - }, - "barriers": { - "total": filtered_queryset.count(), - "open": filtered_queryset.filter(status=2).count(), - "paused": filtered_queryset.filter(status=5).count(), - "resolved": filtered_queryset.filter(status=4).count(), - "pb100": filtered_queryset.filter( - top_priority_status__in=["APPROVED", "REMOVAL_PENDING"] - ).count(), - "overseas_delivery": filtered_queryset.filter( - priority_level="OVERSEAS" - ).count(), - }, - "barriers_current_year": { - "total": filtered_queryset.filter( - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ] - ).count(), - "open": filtered_queryset.filter( - status=2, - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ], - ).count(), - "paused": filtered_queryset.filter( - status=5, - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ], - ).count(), - "resolved": filtered_queryset.filter( - status=4, - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ], - ).count(), - "pb100": filtered_queryset.filter( - top_priority_status__in=["APPROVED", "REMOVAL_PENDING"], - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ], - ).count(), - "overseas_delivery": filtered_queryset.filter( - priority_level="OVERSEAS", - estimated_resolution_date__range=[ - current_year_start, - current_year_end, - ], - ).count(), - }, - "user_counts": { - "user_barrier_count": user_barrier_count, - "user_report_count": user_report_count, - "user_open_barrier_count": user_open_barrier_count, - }, - "reports": Barrier.reports.count(), - "barrier_value_chart": { - "resolved_barriers_value": resolved_barrier_value, - "estimated_barriers_value": estimated_barrier_value, - }, - "total_value_chart": { - "resolved_barriers_value": total_resolved_barriers, - "open_barriers_value": open_barriers_value, - }, - "barriers_by_status_chart": { - "series": status_data, - "labels": status_labels, - }, - } - - return Response(counts) - - class BarrierReportList(BarrierReportBase, generics.ListCreateAPIView): # TODO - These report views may now be redundant serializer_class = BarrierReportSerializer diff --git a/api/dashboard/apps.py b/api/dashboard/apps.py new file mode 100644 index 000000000..931402b4f --- /dev/null +++ b/api/dashboard/apps.py @@ -0,0 +1,9 @@ +import logging + +from django.apps import AppConfig + +logger = logging.getLogger(__name__) + + +class RelatedBarriersConfig(AppConfig): + name = "api.dashboard" diff --git a/api/dashboard/constants.py b/api/dashboard/constants.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/dashboard/management/__init__.py b/api/dashboard/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/dashboard/management/commands/__init__.py b/api/dashboard/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/dashboard/serializers.py b/api/dashboard/serializers.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/dashboard/service.py b/api/dashboard/service.py new file mode 100644 index 000000000..c224d8b59 --- /dev/null +++ b/api/dashboard/service.py @@ -0,0 +1,218 @@ +from datetime import datetime +from functools import partial + +from django.db.models import Case, CharField, IntegerField, Q, Sum, Value, When + +from api.barriers.models import Barrier +from api.metadata.constants import ( + ECONOMIC_ASSESSMENT_IMPACT_MIDPOINTS_NUMERIC_LOOKUP, + BarrierStatus, +) + + +def get_financial_year_dates(): + today = datetime.now() + partial_dt = partial(datetime, month=4, day=1) + if today.month < 4: + start_date = partial_dt(year=datetime.now().year - 1) + else: + start_date = partial_dt(year=datetime.now().year) + + end_date = partial_dt(year=start_date.year + 1) + previous_start_date = partial_dt(year=start_date.year - 1) + previous_end_date = start_date + + return start_date, end_date, previous_start_date, previous_end_date + + +def get_counts(qs, user): + current_year_start, current_year_end, previous_year_start, previous_year_end = ( + get_financial_year_dates() + ) + + if not user.is_anonymous: + user_barrier_count = Barrier.barriers.filter(created_by=user).count() + user_report_count = Barrier.reports.filter(created_by=user).count() + user_open_barrier_count = Barrier.barriers.filter( + created_by=user, status=2 + ).count() + + when_assessment = [ + When(valuation_assessments__impact=k, then=Value(v)) + for k, v in ECONOMIC_ASSESSMENT_IMPACT_MIDPOINTS_NUMERIC_LOOKUP + ] + + # Resolved vs Estimated barriers values chart + resolved_valuations = ( + qs.filter( + Q( + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ] + ) + | Q( + status_date__range=[ + current_year_start, + current_year_end, + ] + ), + Q(status=4) | Q(status=3), + valuation_assessments__archived=False, + ) + .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) + .aggregate(total=Sum("numeric_value")) + ) + + resolved_barrier_value = resolved_valuations["total"] + + estimated_valuations = ( + qs.filter( + Q( + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ] + ) + | Q( + status_date__range=[ + current_year_start, + current_year_end, + ] + ), + Q(status=1) | Q(status=2), + valuation_assessments__archived=False, + ) + .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) + .aggregate(total=Sum("numeric_value")) + ) + + estimated_barrier_value = estimated_valuations["total"] + + # Total resolved barriers vs open barriers value chart + + resolved_barriers = ( + qs.filter( + Q(status=4) | Q(status=3), + valuation_assessments__archived=False, + ) + .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) + .aggregate(total=Sum("numeric_value")) + ) + + total_resolved_barriers = resolved_barriers["total"] + + open_barriers = ( + qs.filter( + Q(status=1) | Q(status=2), + valuation_assessments__archived=False, + ) + .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) + .aggregate(total=Sum("numeric_value")) + ) + + open_barriers_value = open_barriers["total"] + + # Open barriers by status + whens = [When(status=k, then=Value(v)) for k, v in BarrierStatus.choices] + + barrier_by_status = ( + qs.filter( + valuation_assessments__archived=False, + ) + .annotate(status_display=Case(*whens, output_field=CharField())) + .annotate(numeric_value=Case(*when_assessment, output_field=IntegerField())) + .values("status_display") + .annotate(total=Sum("numeric_value")) + .order_by() + ) + + status_labels = [] + status_data = [] + + barrier_by_status = sorted(barrier_by_status, key=lambda x: x["total"]) + + for series in barrier_by_status: + status_labels.append(series["status_display"]) + status_data.append(series["total"]) + + # TODO for status filter might need to consider status dates as well as ERD + return { + "financial_year": { + "current_start": current_year_start, + "current_end": current_year_end, + "previous_start": previous_year_start, + "previous_end": previous_year_end, + }, + "barriers": { + "total": qs.count(), + "open": qs.filter(status=2).count(), + "paused": qs.filter(status=5).count(), + "resolved": qs.filter(status=4).count(), + "pb100": qs.filter( + top_priority_status__in=["APPROVED", "REMOVAL_PENDING"] + ).count(), + "overseas_delivery": qs.filter(priority_level="OVERSEAS").count(), + }, + "barriers_current_year": { + "total": qs.filter( + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ] + ).count(), + "open": qs.filter( + status=2, + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ], + ).count(), + "paused": qs.filter( + status=5, + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ], + ).count(), + "resolved": qs.filter( + status=4, + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ], + ).count(), + "pb100": qs.filter( + top_priority_status__in=["APPROVED", "REMOVAL_PENDING"], + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ], + ).count(), + "overseas_delivery": qs.filter( + priority_level="OVERSEAS", + estimated_resolution_date__range=[ + current_year_start, + current_year_end, + ], + ).count(), + }, + "user_counts": { + "user_barrier_count": user_barrier_count, + "user_report_count": user_report_count, + "user_open_barrier_count": user_open_barrier_count, + }, + "reports": Barrier.reports.count(), + "barrier_value_chart": { + "resolved_barriers_value": resolved_barrier_value, + "estimated_barriers_value": estimated_barrier_value, + }, + "total_value_chart": { + "resolved_barriers_value": total_resolved_barriers, + "open_barriers_value": open_barriers_value, + }, + "barriers_by_status_chart": { + "series": status_data, + "labels": status_labels, + }, + } diff --git a/api/dashboard/tasks.py b/api/dashboard/tasks.py new file mode 100644 index 000000000..e69de29bb diff --git a/api/dashboard/urls.py b/api/dashboard/urls.py new file mode 100644 index 000000000..2ee9227f4 --- /dev/null +++ b/api/dashboard/urls.py @@ -0,0 +1,20 @@ +from django.urls import path +from rest_framework.routers import DefaultRouter + +from api.dashboard.views import BarrierDashboardSummary, UserTasksView + +app_name = "dashboard" + +router = DefaultRouter(trailing_slash=False) + + +urlpatterns = router.urls + [ + path( + "dashboard-summary", BarrierDashboardSummary.as_view(), name="barrier-summary" + ), + path( + "dashboard-tasks", + UserTasksView.as_view(), + name="get-dashboard-tasks", + ), +] diff --git a/api/dashboard/views.py b/api/dashboard/views.py new file mode 100644 index 000000000..c21be5028 --- /dev/null +++ b/api/dashboard/views.py @@ -0,0 +1,609 @@ +import logging +from datetime import datetime, time, timedelta + +import dateutil.parser +from django.contrib.auth.models import User +from django.core.paginator import Paginator +from django.db.models import Q +from django.utils import timezone +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework import generics, status +from rest_framework.response import Response + +from api.barriers.models import Barrier, BarrierFilterSet +from api.barriers.serializers import BarrierListSerializer +from api.dashboard import service +from api.interactions.models import Mention + +logger = logging.getLogger(__name__) + + +class BarrierDashboardSummary(generics.GenericAPIView): + """ + View to return high level stats to the dashboard + """ + + serializer_class = BarrierListSerializer + filterset_class = BarrierFilterSet + + filter_backends = (DjangoFilterBackend,) + ordering_fields = ( + "reported_on", + "modified_on", + "estimated_resolution_date", + "status", + "priority", + "country", + ) + ordering = ("-reported_on",) + + def get(self, request): + filtered_queryset = self.filter_queryset( + Barrier.barriers.filter(archived=False) + ) + + counts = service.get_counts(qs=filtered_queryset, user=request.user) + + return Response(counts) + + +class UserTasksView(generics.ListAPIView): + """ + Returns list of dashboard next steps, tasks and progress updates + related to barriers where a given user is either owner or + collaborator. + """ + + # Set variables used in date related calculations + todays_date = datetime.now(timezone.utc) + publishing_overdue = False + countdown = 0 + third_friday_date = None + first_of_month_date = None + + def get(self, request, *args, **kwargs): + # Get the User information from the request + user = request.user + user_groups = user.groups.all() + + # Can use filter from search without excluding barriers that the user owns to + # get the full list of barriers they should be getting task list for + users_barriers = ( + Barrier.objects.filter( + Q(barrier_team__user=user) + & Q(barrier_team__archived=False) + & Q(archived=False) + ) + .distinct() + .order_by("modified_on")[:1000] + .select_related("public_barrier") + .prefetch_related( + "progress_updates", + "barrier_team", + "barrier_commodities", + "next_steps_items", + "tags", + "export_types", + ) + ) + + # Initialise task list + task_list = [] + + # With the list of barriers the user could potentially see, we now need to build a list of + # tasks derived from conditions the barriers in the list are in. + for barrier in users_barriers: + # Check if the barrier is overdue for publishing + self.check_publishing_overdue(barrier) + + # Establish the relationship between the user and barrier + is_owner, is_approver, is_publisher = self.get_user_barrier_relations( + user, user_groups, barrier + ) + + # Only barrier owners should get notifications for public barrier editing + if is_owner: + publishing_editor_task = self.check_public_barrier_editor_tasks(barrier) + if publishing_editor_task: + task_list.append(publishing_editor_task) + + # Only add public barrier approver tasks for users with that role + if is_approver: + publishing_approver_task = self.check_public_barrier_approver_tasks( + barrier + ) + task_list.append(publishing_approver_task) + + # Only add public barrier publisher tasks for users with that role + if is_publisher: + publishing_publisher_task = self.check_public_barrier_publisher_tasks( + barrier + ) + task_list.append(publishing_publisher_task) + + if barrier.status in [1, 2, 3]: + progress_update_tasks = self.check_progress_update_tasks(barrier) + task_list += progress_update_tasks + + missing_barrier_tasks = self.check_missing_barrier_details(barrier) + task_list += missing_barrier_tasks + + estimated_resolution_date_tasks = ( + self.check_estimated_resolution_date_tasks(user, barrier) + ) + task_list += estimated_resolution_date_tasks + + mentions_tasks = self.check_mentions_tasks(user) + # Combine list of tasks with list of mentions + task_list += mentions_tasks + + # Remove empty tasks + filtered_task_list = list(filter(None, task_list)) + + # Paginate + paginator = Paginator(filtered_task_list, 5) + page_number = request.GET.get("page") + page_obj = paginator.get_page(page_number) + + return Response( + status=status.HTTP_200_OK, + data={"results": page_obj.object_list, "count": len(filtered_task_list)}, + ) + + def check_publishing_overdue(self, barrier): + # Get publishing deadline difference for public_barrier related tasks/updates + # Only set publishing deadlines for barriers in either ALLOWED, APPROVAL_PENDING or PUBLISHING_PENDING status + set_to_allowed_date = barrier.public_barrier.set_to_allowed_on + if ( + barrier.public_barrier.public_view_status in [20, 70, 30] + and set_to_allowed_date + ): + publish_deadline = dateutil.parser.parse( + set_to_allowed_date.strftime("%m/%d/%Y") + ) + timedelta(days=30) + diff = publish_deadline - self.todays_date.replace(tzinfo=None) + # Set variables to track if barrier is overdue and by how much + self.publishing_overdue = True if diff.days <= 0 else False + self.countdown = 0 if diff.days <= 0 else diff.days + + def set_date_of_third_friday(self): + # Get the third friday of the month, skip if we've already calculated + if not self.third_friday_date or not self.first_of_month_date: + self.first_of_month_date = self.todays_date.replace(day=1) + # Weekday in int format, 0-6 representing each day + first_day = self.first_of_month_date.weekday() + # Add 3 weeks of days if the first is a sat or sun (and nearest friday is last month) + friday_modifier = 14 if first_day < 4 else 21 + # Add the difference to the nearest friday to 1 then apply friday modifier + third_friday_day = 1 + (4 - first_day) + friday_modifier + self.third_friday_date = self.todays_date.replace(day=third_friday_day) + + def get_user_barrier_relations(self, user, user_groups, barrier): + # Only barrier owners should get notifications for public barrier editing + barrier_team_members = barrier.barrier_team.all() + + is_owner = False + is_approver = False + is_publisher = False + + for team_user in barrier_team_members: + if team_user.user_id == user.id and team_user.role == "Owner": + is_owner = True + + for group in user_groups: + if group.name == "Public barrier approver": + is_approver = True + if group.name == "Publisher": + is_publisher = True + + return (is_owner, is_approver, is_publisher) + + def check_public_barrier_editor_tasks(self, barrier): + if barrier.public_barrier.public_view_status == 20: + if barrier.public_barrier.title and barrier.public_barrier.summary: + # general user needs to send barrier to approver completed detail + # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' + # and (barrier.public_barrier.public_title or barrier.public_barrier.public_summary) and not overdue + + # general user needs to send barrier to approver completed detail and overdue + # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' + # and (barrier.public_barrier.public_title or barrier.public_barrier.public_summary) and overdue + if self.publishing_overdue: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "OVERDUE REVIEW", + "message": f"""Submit this barrier for a review and clearance checks before the GOV.UK content + team to publish it. This needs to be done within {self.countdown} days.""", + } + else: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PUBLICATION REVIEW", + "message": f"""Submit this barrier for a review and clearance checks before the + GOV.UK content team to publish it. This needs to be done within {self.countdown} days.""", + } + elif not barrier.public_barrier.title or not barrier.public_barrier.summary: + # general user needs to send barrier to approver missing detail + # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' + # and (not barrier.public_barrier.public_title or not barrier.public_barrier.public_summary) + # and not overdue + + # general user needs to send barrier to approver missing detail and overdue + # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' + # and (not barrier.public_barrier.public_title or not barrier.public_barrier.public_summary) + # and overdue + if self.publishing_overdue: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "OVERDUE REVIEW", + "message": f"""Add a public title and summary to this barrier before it can be + approved. This needs to be done within {self.countdown} days""", + } + else: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PUBLICATION REVIEW", + "message": f"""Add a public title and summary to this barrier before it can be + approved. This needs to be done within {self.countdown} days""", + } + + def check_public_barrier_approver_tasks(self, barrier): + # approver needs to approve barrier + # logic trigger: barrier.public_barrier.public_view_status == 'APPROVAL_PENDING' and not overdue + + # approver needs to approve barrier overdue + # logic trigger: barrier.public_barrier.public_view_status == 'APPROVAL_PENDING' and overdue + if barrier.public_barrier.public_view_status == 70: + if self.publishing_overdue: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "OVERDUE REVIEW", + "message": f"""Review and check this barrier for clearances before it can be submitted + to the content team. This needs to be done within {self.countdown} days""", + } + else: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PUBLICATION REVIEW", + "message": f"""Review and check this barrier for clearances before it can be submitted + to the content team. This needs to be done within {self.countdown} days""", + } + + def check_public_barrier_publisher_tasks(self, barrier): + # publisher needs to publish barrier + # logic trigger: barrier.public_barrier.public_view_status == 'PUBLISHING_PENDING' and not overdue + + # publisher needs to publish barrier overdue + # logic trigger: barrier.public_barrier.public_view_status == 'PUBLISHING_PENDING' and overdue + + # Publisher needs to publish barrier + if barrier.public_barrier.public_view_status == 30: + if self.publishing_overdue: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "OVERDUE REVIEW", + "message": f"""This barrier has been approved. Complete the final content checks + and publish it. This needs to be done within {self.countdown} days""", + } + else: + return { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PUBLICATION REVIEW", + "message": f"""This barrier has been approved. Complete the final content checks + and publish it. This needs to be done within {self.countdown} days""", + } + + def check_missing_barrier_details(self, barrier): + missing_details_task_list = [] + if barrier.status in [1, 2, 3]: + # hs code missing + # logic trigger: (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') + # and 'goods' in barrier.export_types and barrier.hs_code is null + if ( + barrier.export_types.filter(name="goods") + and not barrier.commodities.all() + ): + missing_details_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "ADD INFORMATION", + "message": """This barrier relates to the export of goods but it does not contain + any HS commodity codes. Check and add the codes now.""", + } + ) + + # other government department missing + # logic trigger: (barrier.status== 'OPEN' or barrier.status == 'RESOLVED IN PART') + # and barrier.government_organisations is null + if not barrier.government_organisations: + missing_details_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "ADD INFORMATION", + "message": """This barrier is not currently linked with any other government + departments (OGD) Check and add any relevant OGDs involved in the + resolution of this barrier""", + } + ) + + # missing delivery confidence for barriers with estimated resolution date in financial year + # logic trigger: (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') + # and barrier.progress_updates is null and barrier.estimated_resolution_date > financial_year_start + # and barrier.estimated_resolution_date < financial_year_end + if barrier.estimated_resolution_date: + + if self.todays_date < datetime(self.todays_date.year, 4, 6).replace( + tzinfo=timezone.utc + ): + # Financial year runs from previous year to current + start_year_value = self.todays_date.year - 1 + end_year_value = self.todays_date.year + else: + # Financial year runs from current year to next + start_year_value = self.todays_date.year + end_year_value = self.todays_date.year + 1 + + financial_year_start = datetime.strptime( + f"06/04/{start_year_value} 00:00:00", "%d/%m/%Y %H:%M:%S" + ) + financial_year_end = datetime.strptime( + f"06/04/{end_year_value} 00:00:00", "%d/%m/%Y %H:%M:%S" + ) + estimated_resolution_datetime = datetime( + barrier.estimated_resolution_date.year, + barrier.estimated_resolution_date.month, + barrier.estimated_resolution_date.day, + ) + + if ( + not barrier.progress_updates.all() + and estimated_resolution_datetime > financial_year_start + and estimated_resolution_datetime < financial_year_end + ): + missing_details_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "ADD INFORMATION", + "message": """This barrier does not have information on how confident you feel + about resolving it this financial year. Add the delivery confidence now.""", + } + ) + + return missing_details_task_list + + def check_estimated_resolution_date_tasks(self, user, barrier): + estimated_resolution_date_task_list = [] + if barrier.status in [1, 2, 3]: + + # outdated estimated resolution date + # logic trigger: (barrier.status == "OPEN" or barrier.status == "RESOLVED IN PART") + # and barrier.estimated_resolution_date < todays_date + if barrier.estimated_resolution_date: + estimated_resolution_datetime = datetime.combine( + barrier.estimated_resolution_date, time(00, 00, 00) + ) + if estimated_resolution_datetime < self.todays_date.replace( + tzinfo=None + ): + estimated_resolution_date_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "CHANGE OVERDUE", + "message": """The estimated resolution date of this barrier is now in the + past. Review and add a new date now.""", + } + ) + + if hasattr(barrier.barrier_team.filter(role="Owner").first(), "user_id"): + if user.id == barrier.barrier_team.filter(role="Owner").first().user_id: + # estimated resolution date missing for high priorities + # stored logic_trigger: "(barrier.is_top_priority or barrier.priority_level == 'overseas delivery') + # and not barrier.estimated_resolution_date and + # (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') + if ( + barrier.is_top_priority or barrier.priority_level == "OVERSEAS" + ) and not barrier.estimated_resolution_date: + estimated_resolution_date_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "ADD DATE", + "message": """As this is a priority barrier you need to add an + estimated resolution date.""", + } + ) + + # progress update estimated resolution date outdated + # stored logic_trigger: "barrier.is_top_priority + # and (barrier.status == "OPEN" or barrier.status == "RESOLVED IN PART" + # and latest_update.modified_on < progress_update_expiry_date" + latest_update = barrier.latest_progress_update + progress_update_expiry_date = self.todays_date - timedelta(days=180) + if latest_update and ( + barrier.is_top_priority + and latest_update.modified_on.replace(tzinfo=None) + < datetime( + progress_update_expiry_date.year, + progress_update_expiry_date.month, + progress_update_expiry_date.day, + ).replace(tzinfo=None) + ): + difference = ( + latest_update.modified_on.year - self.todays_date.year + ) * 12 + ( + latest_update.modified_on.month - self.todays_date.month + ) + estimated_resolution_date_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "REVIEW DATE", + "message": f"""This barriers estimated resolution date has not + been updated in {abs(difference)} months. Check if this date is still accurate.""", + } + ) + + return estimated_resolution_date_task_list + + def check_progress_update_tasks(self, barrier): + progress_update_task_list = [] + progress_update = barrier.latest_progress_update + + # monthly progress update upcoming + # is_top_priority = true + # AND (progress_update_date IS BETWEEN 01 - current month - current year and 3rd Friday of month) + # AND status = OPEN OR status = RESOLVED IN PART + + # monthly progress update overdue + # is_top_priority = true + # AND (progress_update_date NOT BETWEEN 01 - current month - current year and 3rd Friday of month) + # AND status = OPEN OR status = RESOLVED IN PART + if barrier.is_top_priority: + self.set_date_of_third_friday() + if not progress_update or ( + progress_update.modified_on < self.first_of_month_date + and self.todays_date < self.third_friday_date + ): + # Latest update was last month and the current date is before the third friday (the due date) + # Barrier needs an upcoming task added + progress_update_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PROGRESS UPDATE DUE", + "message": f"""This is a PB100 barrier. Add a monthly progress update + by {self.third_friday_date.strftime("%d-%m-%y")}""", + } + ) + elif ( + progress_update.modified_on < self.first_of_month_date + and self.todays_date > self.third_friday_date + ): + # Latest update was last month and the current date is past the third friday (the due date) + # Barrier needs an overdue task added + progress_update_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "OVERDUE PROGRESS UPDATE", + "message": f"""This is a PB100 barrier. Add a monthly progress update + by {self.third_friday_date.strftime("%d-%m-%y")}""", + } + ) + + # overdue next step for pb100 barriers + # is_top_priority = true AND public.market_access_trade_barrier_next_steps (status = IN_PROGRESS) + # AND public.market_access_trade_barrier_next_steps(completion_date 90 days + # AND status = OPEN OR status = RESOLVED IN PART + if barrier.priority_level == "OVERSEAS": + update_date_limit = self.todays_date - timedelta(days=90) + if not progress_update or (progress_update.modified_on < update_date_limit): + progress_update_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PROGRESS UPDATE DUE", + "message": """This is an overseas delivery barrier but there has not been + an update for over 3 months. Add a quarterly progress update now.""", + } + ) + + # quarterly programme fund update + # tag = 'programme fund - facilative regional fund' + # AND programme_fund_progress_update_date > 90 days old AND status = OPEN OR status = RESOLVED IN PART + tags_list = [tag.title for tag in barrier.tags.all()] + if "Programme Fund - Facilitative Regional" in tags_list: + update_date_limit = self.todays_date - timedelta(days=90) + if not barrier.latest_programme_fund_progress_update or ( + barrier.latest_programme_fund_progress_update.modified_on + < update_date_limit + ): + progress_update_task_list.append( + { + "barrier_code": barrier.code, + "barrier_title": barrier.title, + "barrier_id": barrier.id, + "tag": "PROGRESS UPDATE DUE", + "message": """There is an active programme fund for this barrier but + there has not been an update for over 3 months. Add a programme fund update now.""", + } + ) + + return progress_update_task_list + + def check_mentions_tasks(self, user): + mention_tasks_list = [] + # Get the mentions for the given user + user_mentions = Mention.objects.filter( + recipient=user, + created_on__date__gte=(datetime.now() - timedelta(days=30)), + ) + for mention in user_mentions: + # Get name of the mentioner + mentioner = User.objects.get(id=mention.created_by_id) + mention_task = { + "barrier_code": "", + "barrier_title": "", + "barrier_id": "", + "tag": "REVIEW COMMENT", + "message": f"""{mentioner.first_name} {mentioner.last_name} mentioned you + in a comment on {mention.created_on.strftime("%d-%m-%y")} and wants you to reply.""", + } + mention_tasks_list.append(mention_task) + + return mention_tasks_list + # Once a mention is clicked on the frontend, make a call to the + # notification view that will clear the mark the mention as read + # this should be an existing function called by the frontend diff --git a/api/user/urls.py b/api/user/urls.py index 8fe952b39..edb904112 100644 --- a/api/user/urls.py +++ b/api/user/urls.py @@ -2,7 +2,6 @@ from rest_framework.routers import DefaultRouter from .views import ( - DashboardUserTasksView, GroupDetail, GroupList, SavedSearchDetail, @@ -34,9 +33,4 @@ path("groups/", GroupDetail.as_view(), name="group-detail"), path("users", UserList.as_view(), name="user-list"), path("user_activity_log", UserActivityLogList.as_view(), name="user-activity-log"), - path( - "dashboard-tasks", - DashboardUserTasksView.as_view(), - name="get-dashboard-tasks", - ), ] diff --git a/api/user/views.py b/api/user/views.py index 0e37accfc..1ae8da013 100644 --- a/api/user/views.py +++ b/api/user/views.py @@ -1,23 +1,18 @@ import logging -from datetime import datetime, time, timedelta, timezone from http import HTTPStatus -import dateutil.parser from django.contrib.auth import get_user_model -from django.contrib.auth.models import Group, User -from django.core.paginator import Paginator -from django.db.models import F, Q +from django.contrib.auth.models import Group +from django.db.models import F from django.http import HttpResponse from django_filters.rest_framework import DjangoFilterBackend from hawkrest import HawkAuthentication -from rest_framework import generics, status +from rest_framework import generics from rest_framework.decorators import api_view, permission_classes from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from api.barriers.models import Barrier -from api.interactions.models import Mention from api.user.helpers import get_django_user_by_sso_user_id from api.user.models import ( Profile, @@ -150,565 +145,3 @@ class UserActivityLogList(generics.ListAPIView): def get_queryset(self): return UserActvitiyLog.objects.order_by("-event_time").all() - - -class DashboardUserTasksView(generics.ListAPIView): - """ - Returns list of dashboard next steps, tasks and progress updates - related to barriers where a given user is either owner or - collaborator. - """ - - # Set variables used in date related calculations - todays_date = datetime.now(timezone.utc) - publishing_overdue = False - countdown = 0 - third_friday_date = None - first_of_month_date = None - - def get(self, request, *args, **kwargs): - # Get the User information from the request - user = request.user - user_groups = user.groups.all() - - # Can use filter from search without excluding barriers that the user owns to - # get the full list of barriers they should be getting task list for - users_barriers = ( - Barrier.objects.filter( - Q(barrier_team__user=user) - & Q(barrier_team__archived=False) - & Q(archived=False) - ) - .distinct() - .order_by("modified_on")[:1000] - .select_related("public_barrier") - .prefetch_related( - "progress_updates", - "barrier_team", - "barrier_commodities", - "next_steps_items", - "tags", - "export_types", - ) - ) - - # Initialise task list - task_list = [] - - # With the list of barriers the user could potentially see, we now need to build a list of - # tasks derived from conditions the barriers in the list are in. - for barrier in users_barriers: - # Check if the barrier is overdue for publishing - self.check_publishing_overdue(barrier) - - # Establish the relationship between the user and barrier - is_owner, is_approver, is_publisher = self.get_user_barrier_relations( - user, user_groups, barrier - ) - - # Only barrier owners should get notifications for public barrier editing - if is_owner: - publishing_editor_task = self.check_public_barrier_editor_tasks(barrier) - if publishing_editor_task: - task_list.append(publishing_editor_task) - - # Only add public barrier approver tasks for users with that role - if is_approver: - publishing_approver_task = self.check_public_barrier_approver_tasks( - barrier - ) - task_list.append(publishing_approver_task) - - # Only add public barrier publisher tasks for users with that role - if is_publisher: - publishing_publisher_task = self.check_public_barrier_publisher_tasks( - barrier - ) - task_list.append(publishing_publisher_task) - - if barrier.status in [1, 2, 3]: - progress_update_tasks = self.check_progress_update_tasks(barrier) - task_list += progress_update_tasks - - missing_barrier_tasks = self.check_missing_barrier_details(barrier) - task_list += missing_barrier_tasks - - estimated_resolution_date_tasks = ( - self.check_estimated_resolution_date_tasks(user, barrier) - ) - task_list += estimated_resolution_date_tasks - - mentions_tasks = self.check_mentions_tasks(user) - # Combine list of tasks with list of mentions - task_list += mentions_tasks - - # Remove empty tasks - filtered_task_list = list(filter(None, task_list)) - - # Paginate - paginator = Paginator(filtered_task_list, 5) - page_number = request.GET.get("page") - page_obj = paginator.get_page(page_number) - - return Response( - status=status.HTTP_200_OK, - data={"results": page_obj.object_list, "count": len(filtered_task_list)}, - ) - - def check_publishing_overdue(self, barrier): - # Get publishing deadline difference for public_barrier related tasks/updates - # Only set publishing deadlines for barriers in either ALLOWED, APPROVAL_PENDING or PUBLISHING_PENDING status - set_to_allowed_date = barrier.public_barrier.set_to_allowed_on - if ( - barrier.public_barrier.public_view_status in [20, 70, 30] - and set_to_allowed_date - ): - publish_deadline = dateutil.parser.parse( - set_to_allowed_date.strftime("%m/%d/%Y") - ) + timedelta(days=30) - diff = publish_deadline - self.todays_date.replace(tzinfo=None) - # Set variables to track if barrier is overdue and by how much - self.publishing_overdue = True if diff.days <= 0 else False - self.countdown = 0 if diff.days <= 0 else diff.days - - def set_date_of_third_friday(self): - # Get the third friday of the month, skip if we've already calculated - if not self.third_friday_date or not self.first_of_month_date: - self.first_of_month_date = self.todays_date.replace(day=1) - # Weekday in int format, 0-6 representing each day - first_day = self.first_of_month_date.weekday() - # Add 3 weeks of days if the first is a sat or sun (and nearest friday is last month) - friday_modifier = 14 if first_day < 4 else 21 - # Add the difference to the nearest friday to 1 then apply friday modifier - third_friday_day = 1 + (4 - first_day) + friday_modifier - self.third_friday_date = self.todays_date.replace(day=third_friday_day) - - def get_user_barrier_relations(self, user, user_groups, barrier): - # Only barrier owners should get notifications for public barrier editing - barrier_team_members = barrier.barrier_team.all() - - is_owner = False - is_approver = False - is_publisher = False - - for team_user in barrier_team_members: - if team_user.user_id == user.id and team_user.role == "Owner": - is_owner = True - - for group in user_groups: - if group.name == "Public barrier approver": - is_approver = True - if group.name == "Publisher": - is_publisher = True - - return (is_owner, is_approver, is_publisher) - - def check_public_barrier_editor_tasks(self, barrier): - if barrier.public_barrier.public_view_status == 20: - if barrier.public_barrier.title and barrier.public_barrier.summary: - # general user needs to send barrier to approver completed detail - # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' - # and (barrier.public_barrier.public_title or barrier.public_barrier.public_summary) and not overdue - - # general user needs to send barrier to approver completed detail and overdue - # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' - # and (barrier.public_barrier.public_title or barrier.public_barrier.public_summary) and overdue - if self.publishing_overdue: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "OVERDUE REVIEW", - "message": f"""Submit this barrier for a review and clearance checks before the GOV.UK content - team to publish it. This needs to be done within {self.countdown} days.""", - } - else: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PUBLICATION REVIEW", - "message": f"""Submit this barrier for a review and clearance checks before the - GOV.UK content team to publish it. This needs to be done within {self.countdown} days.""", - } - elif not barrier.public_barrier.title or not barrier.public_barrier.summary: - # general user needs to send barrier to approver missing detail - # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' - # and (not barrier.public_barrier.public_title or not barrier.public_barrier.public_summary) - # and not overdue - - # general user needs to send barrier to approver missing detail and overdue - # logic trigger: barrier.public_barrier.public_view_status == 'ALLOWED' - # and (not barrier.public_barrier.public_title or not barrier.public_barrier.public_summary) - # and overdue - if self.publishing_overdue: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "OVERDUE REVIEW", - "message": f"""Add a public title and summary to this barrier before it can be - approved. This needs to be done within {self.countdown} days""", - } - else: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PUBLICATION REVIEW", - "message": f"""Add a public title and summary to this barrier before it can be - approved. This needs to be done within {self.countdown} days""", - } - - def check_public_barrier_approver_tasks(self, barrier): - # approver needs to approve barrier - # logic trigger: barrier.public_barrier.public_view_status == 'APPROVAL_PENDING' and not overdue - - # approver needs to approve barrier overdue - # logic trigger: barrier.public_barrier.public_view_status == 'APPROVAL_PENDING' and overdue - if barrier.public_barrier.public_view_status == 70: - if self.publishing_overdue: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "OVERDUE REVIEW", - "message": f"""Review and check this barrier for clearances before it can be submitted - to the content team. This needs to be done within {self.countdown} days""", - } - else: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PUBLICATION REVIEW", - "message": f"""Review and check this barrier for clearances before it can be submitted - to the content team. This needs to be done within {self.countdown} days""", - } - - def check_public_barrier_publisher_tasks(self, barrier): - # publisher needs to publish barrier - # logic trigger: barrier.public_barrier.public_view_status == 'PUBLISHING_PENDING' and not overdue - - # publisher needs to publish barrier overdue - # logic trigger: barrier.public_barrier.public_view_status == 'PUBLISHING_PENDING' and overdue - - # Publisher needs to publish barrier - if barrier.public_barrier.public_view_status == 30: - if self.publishing_overdue: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "OVERDUE REVIEW", - "message": f"""This barrier has been approved. Complete the final content checks - and publish it. This needs to be done within {self.countdown} days""", - } - else: - return { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PUBLICATION REVIEW", - "message": f"""This barrier has been approved. Complete the final content checks - and publish it. This needs to be done within {self.countdown} days""", - } - - def check_missing_barrier_details(self, barrier): - missing_details_task_list = [] - if barrier.status in [1, 2, 3]: - # hs code missing - # logic trigger: (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') - # and 'goods' in barrier.export_types and barrier.hs_code is null - if ( - barrier.export_types.filter(name="goods") - and not barrier.commodities.all() - ): - missing_details_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "ADD INFORMATION", - "message": """This barrier relates to the export of goods but it does not contain - any HS commodity codes. Check and add the codes now.""", - } - ) - - # other government department missing - # logic trigger: (barrier.status== 'OPEN' or barrier.status == 'RESOLVED IN PART') - # and barrier.government_organisations is null - if not barrier.government_organisations: - missing_details_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "ADD INFORMATION", - "message": """This barrier is not currently linked with any other government - departments (OGD) Check and add any relevant OGDs involved in the - resolution of this barrier""", - } - ) - - # missing delivery confidence for barriers with estimated resolution date in financial year - # logic trigger: (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') - # and barrier.progress_updates is null and barrier.estimated_resolution_date > financial_year_start - # and barrier.estimated_resolution_date < financial_year_end - if barrier.estimated_resolution_date: - - if self.todays_date < datetime(self.todays_date.year, 4, 6).replace( - tzinfo=timezone.utc - ): - # Financial year runs from previous year to current - start_year_value = self.todays_date.year - 1 - end_year_value = self.todays_date.year - else: - # Financial year runs from current year to next - start_year_value = self.todays_date.year - end_year_value = self.todays_date.year + 1 - - financial_year_start = datetime.strptime( - f"06/04/{start_year_value} 00:00:00", "%d/%m/%Y %H:%M:%S" - ) - financial_year_end = datetime.strptime( - f"06/04/{end_year_value} 00:00:00", "%d/%m/%Y %H:%M:%S" - ) - estimated_resolution_datetime = datetime( - barrier.estimated_resolution_date.year, - barrier.estimated_resolution_date.month, - barrier.estimated_resolution_date.day, - ) - - if ( - not barrier.progress_updates.all() - and estimated_resolution_datetime > financial_year_start - and estimated_resolution_datetime < financial_year_end - ): - missing_details_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "ADD INFORMATION", - "message": """This barrier does not have information on how confident you feel - about resolving it this financial year. Add the delivery confidence now.""", - } - ) - - return missing_details_task_list - - def check_estimated_resolution_date_tasks(self, user, barrier): - estimated_resolution_date_task_list = [] - if barrier.status in [1, 2, 3]: - - # outdated estimated resolution date - # logic trigger: (barrier.status == "OPEN" or barrier.status == "RESOLVED IN PART") - # and barrier.estimated_resolution_date < todays_date - if barrier.estimated_resolution_date: - estimated_resolution_datetime = datetime.combine( - barrier.estimated_resolution_date, time(00, 00, 00) - ) - if estimated_resolution_datetime < self.todays_date.replace( - tzinfo=None - ): - estimated_resolution_date_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "CHANGE OVERDUE", - "message": """The estimated resolution date of this barrier is now in the - past. Review and add a new date now.""", - } - ) - - if hasattr(barrier.barrier_team.filter(role="Owner").first(), "user_id"): - if user.id == barrier.barrier_team.filter(role="Owner").first().user_id: - # estimated resolution date missing for high priorities - # stored logic_trigger: "(barrier.is_top_priority or barrier.priority_level == 'overseas delivery') - # and not barrier.estimated_resolution_date and - # (barrier.status == 'OPEN' or barrier.status == 'RESOLVED IN PART') - if ( - barrier.is_top_priority or barrier.priority_level == "OVERSEAS" - ) and not barrier.estimated_resolution_date: - estimated_resolution_date_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "ADD DATE", - "message": """As this is a priority barrier you need to add an - estimated resolution date.""", - } - ) - - # progress update estimated resolution date outdated - # stored logic_trigger: "barrier.is_top_priority - # and (barrier.status == "OPEN" or barrier.status == "RESOLVED IN PART" - # and latest_update.modified_on < progress_update_expiry_date" - latest_update = barrier.latest_progress_update - progress_update_expiry_date = self.todays_date - timedelta(days=180) - if latest_update and ( - barrier.is_top_priority - and latest_update.modified_on.replace(tzinfo=None) - < datetime( - progress_update_expiry_date.year, - progress_update_expiry_date.month, - progress_update_expiry_date.day, - ).replace(tzinfo=None) - ): - difference = ( - latest_update.modified_on.year - self.todays_date.year - ) * 12 + ( - latest_update.modified_on.month - self.todays_date.month - ) - estimated_resolution_date_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "REVIEW DATE", - "message": f"""This barriers estimated resolution date has not - been updated in {abs(difference)} months. Check if this date is still accurate.""", - } - ) - - return estimated_resolution_date_task_list - - def check_progress_update_tasks(self, barrier): - progress_update_task_list = [] - progress_update = barrier.latest_progress_update - - # monthly progress update upcoming - # is_top_priority = true - # AND (progress_update_date IS BETWEEN 01 - current month - current year and 3rd Friday of month) - # AND status = OPEN OR status = RESOLVED IN PART - - # monthly progress update overdue - # is_top_priority = true - # AND (progress_update_date NOT BETWEEN 01 - current month - current year and 3rd Friday of month) - # AND status = OPEN OR status = RESOLVED IN PART - if barrier.is_top_priority: - self.set_date_of_third_friday() - if not progress_update or ( - progress_update.modified_on < self.first_of_month_date - and self.todays_date < self.third_friday_date - ): - # Latest update was last month and the current date is before the third friday (the due date) - # Barrier needs an upcoming task added - progress_update_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PROGRESS UPDATE DUE", - "message": f"""This is a PB100 barrier. Add a monthly progress update - by {self.third_friday_date.strftime("%d-%m-%y")}""", - } - ) - elif ( - progress_update.modified_on < self.first_of_month_date - and self.todays_date > self.third_friday_date - ): - # Latest update was last month and the current date is past the third friday (the due date) - # Barrier needs an overdue task added - progress_update_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "OVERDUE PROGRESS UPDATE", - "message": f"""This is a PB100 barrier. Add a monthly progress update - by {self.third_friday_date.strftime("%d-%m-%y")}""", - } - ) - - # overdue next step for pb100 barriers - # is_top_priority = true AND public.market_access_trade_barrier_next_steps (status = IN_PROGRESS) - # AND public.market_access_trade_barrier_next_steps(completion_date 90 days - # AND status = OPEN OR status = RESOLVED IN PART - if barrier.priority_level == "OVERSEAS": - update_date_limit = self.todays_date - timedelta(days=90) - if not progress_update or (progress_update.modified_on < update_date_limit): - progress_update_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PROGRESS UPDATE DUE", - "message": """This is an overseas delivery barrier but there has not been - an update for over 3 months. Add a quarterly progress update now.""", - } - ) - - # quarterly programme fund update - # tag = 'programme fund - facilative regional fund' - # AND programme_fund_progress_update_date > 90 days old AND status = OPEN OR status = RESOLVED IN PART - tags_list = [tag.title for tag in barrier.tags.all()] - if "Programme Fund - Facilitative Regional" in tags_list: - update_date_limit = self.todays_date - timedelta(days=90) - if not barrier.latest_programme_fund_progress_update or ( - barrier.latest_programme_fund_progress_update.modified_on - < update_date_limit - ): - progress_update_task_list.append( - { - "barrier_code": barrier.code, - "barrier_title": barrier.title, - "barrier_id": barrier.id, - "tag": "PROGRESS UPDATE DUE", - "message": """There is an active programme fund for this barrier but - there has not been an update for over 3 months. Add a programme fund update now.""", - } - ) - - return progress_update_task_list - - def check_mentions_tasks(self, user): - mention_tasks_list = [] - # Get the mentions for the given user - user_mentions = Mention.objects.filter( - recipient=user, - created_on__date__gte=(datetime.now() - timedelta(days=30)), - ) - for mention in user_mentions: - # Get name of the mentioner - mentioner = User.objects.get(id=mention.created_by_id) - mention_task = { - "barrier_code": "", - "barrier_title": "", - "barrier_id": "", - "tag": "REVIEW COMMENT", - "message": f"""{mentioner.first_name} {mentioner.last_name} mentioned you - in a comment on {mention.created_on.strftime("%d-%m-%y")} and wants you to reply.""", - } - mention_tasks_list.append(mention_task) - - return mention_tasks_list - # Once a mention is clicked on the frontend, make a call to the - # notification view that will clear the mark the mention as read - # this should be an existing function called by the frontend diff --git a/config/settings/base.py b/config/settings/base.py index 5795d7cad..afc71cb1b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -72,26 +72,27 @@ ] LOCAL_APPS = [ + "api.action_plans", + "api.assessment", + "api.barrier_downloads", "api.barriers", + "api.collaboration", + "api.commodities", "api.core", + "api.dashboard", + "api.dataset", + "api.documents", + "api.feedback", "api.healthcheck", + "api.history", + "api.interactions", "api.metadata", + "api.pingdom", + "api.related_barriers", "api.user", - "api.documents", - "api.interactions", - "api.assessment", - "api.collaboration", - "api.commodities", - "authbroker_client", "api.user_event_log", - "api.dataset", - "api.history", "api.wto", - "api.action_plans", - "api.feedback", - "api.related_barriers", - "api.barrier_downloads", - "api.pingdom", + "authbroker_client", ] INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS diff --git a/config/urls.py b/config/urls.py index 41c851c57..9a438fdb1 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,6 +9,7 @@ from api.collaboration.urls import urlpatterns as team_urls from api.commodities.urls import urlpatterns as commodities_urls from api.core.views import admin_override +from api.dashboard.urls import urlpatterns as dashboard_urls from api.feedback import urls as feedback_urls from api.interactions.urls import urlpatterns as interaction_urls from api.metadata.views import MetadataView @@ -37,14 +38,15 @@ path("metadata", MetadataView.as_view(), name="metadata"), path("", include("api.dataset.urls", namespace="dataset")), ] + + action_plans_urls + + assessment_urls + barrier_download_urls + barrier_urls + commodities_urls + + dashboard_urls + interaction_urls + + pingdom_urls + + related_barriers_urls + team_urls - + assessment_urls + user_urls - + action_plans_urls - + related_barriers_urls - + pingdom_urls ) diff --git a/tests/barrier_downloads/test_views.py b/tests/barrier_downloads/test_views.py index b760808e4..50d5867e2 100644 --- a/tests/barrier_downloads/test_views.py +++ b/tests/barrier_downloads/test_views.py @@ -27,9 +27,13 @@ def test_barrier_download_post_endpoint_no_results(self): def test_barrier_download_post_endpoint_no_results_filter(self): barrier = BarrierFactory() - url = f'{reverse("barrier-downloads")}?search_term_text=wrong-{barrier.title}' + url = f'{reverse("barrier-downloads")}' - response = self.api_client.post(url) + response = self.api_client.post( + url, + data={"search_term_text": f"wrong-{barrier.title}"}, + format="json", + ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert response.content == b'{"error": "No barriers matching filterset"}' @@ -60,7 +64,7 @@ def test_barrier_download_post_endpoint_success( @patch("api.barrier_downloads.tasks.generate_barrier_download_file") def test_barrier_download_post_endpoint_success_with_filter(self, mock_generate): barrier = BarrierFactory() - url = f'{reverse("barrier-downloads")}?text={barrier.title}' + url = f'{reverse("barrier-downloads")}' response = self.api_client.post( url, data={"filters": {"text": barrier.title}}, format="json" diff --git a/tests/dashboard/test_queries.py b/tests/dashboard/test_queries.py new file mode 100644 index 000000000..11eba5dc0 --- /dev/null +++ b/tests/dashboard/test_queries.py @@ -0,0 +1,378 @@ +from datetime import datetime, timedelta + +import freezegun +import pytest +from factory.fuzzy import FuzzyDate + +from api.barriers.models import Barrier +from api.core.test_utils import create_test_user +from api.dashboard.service import get_counts, get_financial_year_dates +from api.metadata.constants import ( + ECONOMIC_ASSESSMENT_IMPACT, + PRIORITY_LEVELS, + TOP_PRIORITY_BARRIER_STATUS, + BarrierStatus, +) +from tests.assessment.factories import EconomicImpactAssessmentFactory +from tests.barriers.factories import BarrierFactory, ReportFactory + +freezegun.configure(extend_ignore_list=["transformers"]) + + +pytestmark = [pytest.mark.django_db] + + +@pytest.fixture +def users(): + return [ + create_test_user( + first_name="Test1", + last_name="Last1", + email="Test1@Userii.com", + username="Name1", + ), + create_test_user( + first_name="Test2", + last_name="Last2", + email="Test2@Userii.com", + username="Name2", + ), + create_test_user( + first_name="Test3", + last_name="Last3", + email="Test3@Userii.com", + username="Name3", + ), + ] + + +@pytest.fixture +def barrier_factory(users): + start_date, end_date, previous_start_date, previous_end_date = ( + get_financial_year_dates() + ) + + def func(date=None): + if not date: + date = FuzzyDate( + start_date=start_date, + end_date=start_date + timedelta(days=45), + ).evaluate(2, None, False) + + return [ + ReportFactory(created_by=users[0], estimated_resolution_date=date), + BarrierFactory(created_by=users[0], estimated_resolution_date=date), + BarrierFactory(created_by=users[0], estimated_resolution_date=date), + BarrierFactory( + created_by=users[0], + top_priority_status=TOP_PRIORITY_BARRIER_STATUS.APPROVED, + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + top_priority_status=TOP_PRIORITY_BARRIER_STATUS.REMOVAL_PENDING, + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + status=BarrierStatus.OPEN_IN_PROGRESS, + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + status=BarrierStatus.DORMANT, + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + status=BarrierStatus.DORMANT, + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + status=BarrierStatus.RESOLVED_IN_PART, + status_date=datetime.now(), + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + status=BarrierStatus.RESOLVED_IN_FULL, + status_date=datetime.now(), + estimated_resolution_date=date, + ), + BarrierFactory( + created_by=users[0], + priority_level=PRIORITY_LEVELS.OVERSEAS, + estimated_resolution_date=date, + ), + ] + + return func + + +def test_get_counts_full_schema(users): + BarrierFactory(created_by=users[0]) + qs = Barrier.objects.all() + + ts = datetime.now() + with freezegun.freeze_time(ts): + counts = get_counts(qs, users[0]) + + financial_dates = get_financial_year_dates() + + assert counts == { + "barrier_value_chart": { + "estimated_barriers_value": None, + "resolved_barriers_value": None, + }, + "barriers": { + "open": 0, + "overseas_delivery": 0, + "paused": 0, + "pb100": 0, + "resolved": 0, + "total": 1, + }, + "barriers_by_status_chart": {"labels": [], "series": []}, + "barriers_current_year": { + "open": 0, + "overseas_delivery": 0, + "paused": 0, + "pb100": 0, + "resolved": 0, + "total": 1, + }, + "financial_year": { + "current_start": financial_dates[0], + "current_end": financial_dates[1], + "previous_start": financial_dates[2], + "previous_end": financial_dates[3], + }, + "reports": 0, + "total_value_chart": { + "open_barriers_value": None, + "resolved_barriers_value": None, + }, + "user_counts": { + "user_barrier_count": 1, + "user_open_barrier_count": 0, + "user_report_count": 0, + }, + } + + +def test_get_financial_year_meta_before_april(): + ts = datetime(year=2014, month=3, day=31) + with freezegun.freeze_time(ts): + start_date, end_date, previous_start_date, previous_end_date = ( + get_financial_year_dates() + ) + + assert start_date.year == ts.year - 1 + assert end_date.year == ts.year + assert previous_end_date.year == ts.year - 1 + assert previous_start_date.year == ts.year - 2 + + +def test_get_financial_year_meta_after_april(): + ts = datetime(year=2014, month=4, day=1) + with freezegun.freeze_time(ts): + start_date, end_date, previous_start_date, previous_end_date = ( + get_financial_year_dates() + ) + + assert start_date.year == ts.year + assert end_date.year == ts.year + 1 + assert previous_end_date.year == ts.year + assert previous_start_date.year == ts.year - 1 + + +def test_get_user_counts(users): + ReportFactory(created_by=users[0]) + BarrierFactory(created_by=users[0]) + BarrierFactory(created_by=users[0]) + BarrierFactory(created_by=users[0], status=2) + BarrierFactory(created_by=users[1]) + ReportFactory(created_by=users[2]) + + qs = Barrier.objects.all() + + assert get_counts(qs, users[0])["user_counts"] == { + "user_barrier_count": 3, # Report not included? + "user_open_barrier_count": 1, + "user_report_count": 1, + } + + assert get_counts(qs, users[1])["user_counts"] == { + "user_barrier_count": 1, + "user_open_barrier_count": 0, + "user_report_count": 0, + } + + assert get_counts(qs, users[2])["user_counts"] == { + "user_barrier_count": 0, + "user_open_barrier_count": 0, + "user_report_count": 1, + } + + +def test_get_barriers(users, barrier_factory): + barrier_factory() + qs = Barrier.objects.all() + + barriers = get_counts(qs, users[0])["barriers"] + + assert barriers == { + "open": 1, + "overseas_delivery": 1, + "paused": 2, + "pb100": 2, + "resolved": 1, + "total": 11, + } + + +def test_get_barriers_old_date(users, barrier_factory): + barrier_factory(date=datetime.now() - timedelta(days=2 * 365)) + qs = Barrier.objects.all() + + barriers = get_counts(qs, users[0])["barriers"] + + assert barriers == { + "open": 1, + "overseas_delivery": 1, + "paused": 2, + "pb100": 2, + "resolved": 1, + "total": 11, + } + + +def test_get_barriers_current_year(users, barrier_factory): + barrier_factory() + qs = Barrier.objects.all() + + barriers_current_year = get_counts(qs, users[0])["barriers_current_year"] + + assert barriers_current_year == { + "open": 1, + "overseas_delivery": 1, + "paused": 2, + "pb100": 2, + "resolved": 1, + "total": 11, + } + + +@pytest.mark.parametrize( + "date", + [ + datetime(datetime.now().year, 4, 1), + datetime(datetime.now().year + 1, 3, 31) - timedelta(seconds=1), + ], +) +def test_get_barriers_current_financial_year(users, barrier_factory, date): + barrier_factory(date=date) + qs = Barrier.objects.all() + + barriers_current_year = get_counts(qs, users[0])["barriers_current_year"] + + assert barriers_current_year == { + "open": 1, + "overseas_delivery": 1, + "paused": 2, + "pb100": 2, + "resolved": 1, + "total": 11, + } + + +def test_barriers_by_status_chart_empty(users, barrier_factory): + barrier_factory() + qs = Barrier.objects.all() + + barriers_by_status_chart = get_counts(qs, users[0])["barriers_by_status_chart"] + + assert barriers_by_status_chart == {"labels": [], "series": []} + + +def test_barriers_by_status_chart(users, barrier_factory): + barriers = barrier_factory() + assessment_impacts = list(ECONOMIC_ASSESSMENT_IMPACT) + for i, barrier in enumerate(barriers): + EconomicImpactAssessmentFactory( + barrier=barrier, impact=assessment_impacts[i][0] + ) + qs = Barrier.objects.all() + + barriers_by_status_chart = get_counts(qs, users[0])["barriers_by_status_chart"] + + assert barriers_by_status_chart == { + "labels": ["Resolved: In part", "Dormant", "Resolved: In full", "Open"], + "series": [5500000, 8000000, 8500000, 57260000], + } + + +@pytest.mark.parametrize( + "status", + [ + BarrierStatus.OPEN_IN_PROGRESS, + BarrierStatus.DORMANT, + BarrierStatus.RESOLVED_IN_FULL, + BarrierStatus.RESOLVED_IN_PART, + ], +) +def test_barrier_by_status_by_labal(users, status): + assessment_impacts = list(ECONOMIC_ASSESSMENT_IMPACT) + barrier1 = BarrierFactory(status=status, status_date=datetime.now()) + barrier2 = BarrierFactory(status=status, status_date=datetime.now()) + EconomicImpactAssessmentFactory(barrier=barrier1, impact=assessment_impacts[0][0]) + EconomicImpactAssessmentFactory(barrier=barrier2, impact=assessment_impacts[1][0]) + qs = Barrier.objects.all() + + barriers_by_status_chart = get_counts(qs, users[0])["barriers_by_status_chart"] + + index = None + for i, label in enumerate(barriers_by_status_chart["labels"]): + if label == dict(BarrierStatus.choices)[status]: + index = i + break + + assert barriers_by_status_chart["series"][index] == 60000 + assert ( + barriers_by_status_chart["labels"][index] == dict(BarrierStatus.choices)[status] + ) + + +def test_total_value_chart(users, barrier_factory): + barriers = barrier_factory() + assessment_impacts = list(ECONOMIC_ASSESSMENT_IMPACT) + for i, barrier in enumerate(barriers): + EconomicImpactAssessmentFactory( + barrier=barrier, impact=assessment_impacts[i][0] + ) + qs = Barrier.objects.all() + + total_value_chart = get_counts(qs, users[0])["total_value_chart"] + + assert total_value_chart == { + "resolved_barriers_value": 14000000, + "open_barriers_value": 57260000, + } + + +def test_barrier_value_chart(users, barrier_factory): + barriers = barrier_factory() + assessment_impacts = list(ECONOMIC_ASSESSMENT_IMPACT) + for i, barrier in enumerate(barriers): + EconomicImpactAssessmentFactory( + barrier=barrier, impact=assessment_impacts[i][0] + ) + qs = Barrier.objects.all() + + barrier_value_chart = get_counts(qs, users[0])["barrier_value_chart"] + + assert barrier_value_chart == { + "resolved_barriers_value": 14000000, + "estimated_barriers_value": 57260000, + } diff --git a/tests/dashboard/test_views.py b/tests/dashboard/test_views.py new file mode 100644 index 000000000..e30285a03 --- /dev/null +++ b/tests/dashboard/test_views.py @@ -0,0 +1,1181 @@ +from datetime import datetime, timedelta, timezone +from unittest.mock import patch + +from django.contrib.auth.models import Group +from rest_framework.reverse import reverse +from rest_framework.test import APITestCase + +from api.barriers.models import BarrierNextStepItem, BarrierProgressUpdate +from api.collaboration.models import TeamMember +from api.core.test_utils import APITestMixin, create_test_user +from api.interactions.models import Interaction, Mention +from api.metadata.constants import ( + NEXT_STEPS_ITEMS_STATUS_CHOICES, + PROGRESS_UPDATE_CHOICES, + BarrierStatus, + PublicBarrierStatus, +) +from api.metadata.models import BarrierTag, ExportType, Organisation +from tests.barriers.factories import BarrierFactory +from tests.history.factories import ProgrammeFundProgressUpdateFactory + + +class TestDashboardTasksView(APITestMixin, APITestCase): + """Test cases for the Dashboard tasks list retrieval""" + + def setUp(self): + super().setUp() + + """ Create test users, the basic URL and governement orgs used in all tests""" + + self.request_url = reverse("get-dashboard-tasks") + self.test_user = create_test_user( + first_name="Testo", + last_name="Useri", + email="Testo@Useri.com", + username="TestoUseri", + ) + self.test_user_2 = create_test_user( + first_name="Testduo", + last_name="Userii", + email="Testduo@Userii.com", + username="TestduoUserii", + ) + self.test_government_departments = Organisation.objects.all()[:2] + + def basic_barrier_setUp(self, erd_time=None): + """Create basic barrier with an estimated resolution date used in majority of tests""" + if erd_time == "future": + erd = datetime.now() + timedelta(days=200) + elif erd_time == "past": + erd = datetime.now() - timedelta(days=400) + else: + erd = None + self.barrier = BarrierFactory( + estimated_resolution_date=erd, + ) + self.barrier.organisations.add(*self.test_government_departments) + + def owner_user_setUp(self): + """Add user to barrier team""" + tm = TeamMember.objects.create( + barrier=self.barrier, + user=self.test_user, + created_by=self.test_user, + role="Owner", + ) + self.barrier.barrier_team.add(tm) + + def public_barrier_setUp(self): + """Setup method for tasks requiring public barrier data""" + api_client = self.create_api_client(user=self.test_user) + # Create a public barrier with only a title that wont trigger other tasks + self.barrier = BarrierFactory( + estimated_resolution_date=datetime.now() + timedelta(days=500), + ) + self.barrier.organisations.add(*self.test_government_departments) + allowed_to_publish_url = reverse( + "public-barriers-allow-for-publishing-process", + kwargs={"pk": self.barrier.id}, + ) + self.barrier.public_barrier.title = "adding a title" + self.barrier.public_barrier.save() + api_client.post(allowed_to_publish_url, format="json", data={}) + self.barrier.refresh_from_db() + + # Create a public barrier with only a summary that wont trigger other tasks + self.barrier_2 = BarrierFactory( + estimated_resolution_date=datetime.now() + timedelta(days=500), + ) + self.barrier_2.organisations.add(*self.test_government_departments) + allowed_to_publish_url = reverse( + "public-barriers-allow-for-publishing-process", + kwargs={"pk": self.barrier_2.id}, + ) + self.barrier_2.public_barrier.summary = "adding a summary" + self.barrier_2.public_barrier.save() + api_client.post(allowed_to_publish_url, format="json", data={}) + self.barrier_2.refresh_from_db() + + # Attach test_user as the owner to the barriers + tm = TeamMember.objects.create( + barrier=self.barrier, + user=self.test_user, + created_by=self.test_user, + role="Owner", + ) + self.barrier.barrier_team.add(tm) + tm_2 = TeamMember.objects.create( + barrier=self.barrier_2, + user=self.test_user, + created_by=self.test_user, + role="Owner", + ) + self.barrier_2.barrier_team.add(tm_2) + + # Attach test_user_2 as a contributor to the barriers + tm = TeamMember.objects.create( + barrier=self.barrier, user=self.test_user_2, created_by=self.test_user_2 + ) + self.barrier.barrier_team.add(tm) + tm_2 = TeamMember.objects.create( + barrier=self.barrier_2, user=self.test_user_2, created_by=self.test_user_2 + ) + self.barrier_2.barrier_team.add(tm_2) + + def test_user_with_no_barriers(self): + """Users need to be part of a barrier team to retrieve a task list + This test will expect an empty set of response data.""" + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 0 + assert response.data["results"] == [] + + def test_publishing_task_editor_user_info_required_not_owner(self): + """Members of the barrier team do not get tasks related to publishing. + Expect no tasks returned""" + + self.public_barrier_setUp() + + api_client = self.create_api_client(user=self.test_user_2) + response = api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 0 + assert response.data["results"] == [] + + def test_publishing_task_editor_user_info_required(self): + """Owners of the barrier need to enter a public title and public summary. + If either of these fields are missing, we generate a task.""" + + self.public_barrier_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + assert "Add a public title and summary to this barrier" in task["message"] + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + def test_publishing_task_editor_user_info_required_overdue(self): + """This task needs to be completed within a timeframe tracked + on the public barrier. The dashboard task has a seperate tag + for when this countdown is passed.""" + + self.public_barrier_setUp() + + # Modify the public barrier missing the title to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier.public_barrier.save() + + # Modify the public barrier missing the summary to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier_2.public_barrier.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + for task in response.data["results"]: + assert task["tag"] == "OVERDUE REVIEW" + assert "Add a public title and summary to this barrier" in task["message"] + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + + def test_publishing_task_editor_user_send_to_approval(self): + """A second type of task is added in place of the missing information + task if both title and summary are filled in requesting the user to send + the barrier to an approver.""" + + self.public_barrier_setUp() + + # Add summary to barrier with only a title + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + submit_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "Submit this barrier for a review and clearance checks" + in task["message"] + ): + submit_task_count = submit_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + assert submit_task_count == 1 + assert missing_info_task_count == 1 + + def test_publishing_task_editor_user_send_to_approval_overdue(self): + """This task needs to be completed within a timeframe tracked + on the public barrier. The dashboard task has a seperate tag + for when this countdown is passed.""" + + self.public_barrier_setUp() + + # Add summary to barrier with only a title + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + # Modify the public barrier missing the title to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier.public_barrier.save() + + # Modify the public barrier missing the summary to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier_2.public_barrier.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + submit_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "OVERDUE REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "Submit this barrier for a review and clearance checks" + in task["message"] + ): + submit_task_count = submit_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + + assert submit_task_count == 1 + assert missing_info_task_count == 1 + + def test_publishing_task_approver_user_approve_request(self): + """This task is for users with approver permissions only. We need + to check that the task is added when the barrier is in a specific status""" + + self.public_barrier_setUp() + + # Set barrier to AWAITING_APPROVAL status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.APPROVAL_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + # Update user to have the approver permission + self.test_user.groups.add(Group.objects.get(name="Public barrier approver")) + self.test_user.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + approve_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "Review and check this barrier for clearances before it can be submitted" + in task["message"] + ): + approve_task_count = approve_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + assert approve_task_count == 1 + assert missing_info_task_count == 1 + + def test_publishing_task_approver_user_approve_request_no_permission(self): + """This task is for users with approver permissions only. We need + to check that the task is not added when the user is only an editor.""" + + self.public_barrier_setUp() + + # Set barrier to AWAITING_APPROVAL status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.APPROVAL_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + assert "Add a public title and summary to this barrier" in task["message"] + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + def test_publishing_task_approver_user_approve_request_overdue(self): + """This task needs to be completed within a timeframe tracked + on the public barrier. The dashboard task has a seperate tag + for when this countdown is passed.""" + + self.public_barrier_setUp() + + # Set barrier to AWAITING_APPROVAL status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.APPROVAL_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + # Modify the public barrier missing the title to be overdue by making + # set_to_allowed_on more than 30 days in the past + self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier.public_barrier.save() + + # Modify the public barrier missing the summary to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier_2.public_barrier.save() + + # Update user to have the approver permission + self.test_user.groups.add(Group.objects.get(name="Public barrier approver")) + self.test_user.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + approve_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "OVERDUE REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "Review and check this barrier for clearances before it can be submitted" + in task["message"] + ): + approve_task_count = approve_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + + assert approve_task_count == 1 + assert missing_info_task_count == 1 + + def test_publishing_task_publisher_user_approve_request(self): + """This task is for users with publisher permissions only. We need + to check that the task is added when the barrier is in a specific status""" + + self.public_barrier_setUp() + + # Set barrier to PUBLISHING_PENDING status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.PUBLISHING_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + # Update user to have the publisher permission + self.test_user.groups.add(Group.objects.get(name="Publisher")) + self.test_user.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + publish_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "This barrier has been approved. Complete the final content checks" + in task["message"] + ): + publish_task_count = publish_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + assert publish_task_count == 1 + assert missing_info_task_count == 1 + + def test_publishing_task_publisher_user_approve_request_no_permission(self): + """This task is for users with publisher permissions only. We need + to check that the task is added when the barrier is in a specific status""" + + self.public_barrier_setUp() + + # Set barrier to PUBLISHING_PENDING status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.PUBLISHING_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + + for task in response.data["results"]: + assert task["tag"] == "PUBLICATION REVIEW" + assert "Add a public title and summary to this barrier" in task["message"] + # Check countdown has been substituted into message + assert "needs to be done within 29 days" in task["message"] + + def test_publishing_task_publisher_user_approve_request_overdue(self): + """This task is for users with approver permissions only. We need + to check that the task is added when the barrier is in a specific status""" + + self.public_barrier_setUp() + + # Set barrier to PUBLISHING_PENDING status and give it both title and summary + self.barrier.public_barrier.public_view_status = ( + PublicBarrierStatus.PUBLISHING_PENDING + ) + self.barrier.public_barrier.summary = "adding a summary" + self.barrier.public_barrier.save() + + # Modify the public barrier missing the title to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier.public_barrier.save() + + # Modify the public barrier missing the summary to be overdue by + # making set_to_allowed_on more than 30 days in the past + self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( + days=40 + ) + self.barrier_2.public_barrier.save() + + # Update user to have the publisher permission + self.test_user.groups.add(Group.objects.get(name="Publisher")) + self.test_user.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 2 + + # Count the expected task types + missing_info_task_count = 0 + publish_task_count = 0 + for task in response.data["results"]: + assert task["tag"] == "OVERDUE REVIEW" + # Message will be different for barrier with both title and sumary, but we + # also expect for one of the tasks to be the previously tested message + if ( + "This barrier has been approved. Complete the final content checks" + in task["message"] + ): + publish_task_count = publish_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + elif "Add a public title and summary to this barrier" in task["message"]: + missing_info_task_count = missing_info_task_count + 1 + # Check countdown has been substituted into message + assert "needs to be done within 0 days" in task["message"] + + assert publish_task_count == 1 + assert missing_info_task_count == 1 + + def test_missing_barrier_detail_government_orgs(self): + """All owned barriers will generate a task if they do not have + any data stored under 'organisations' attribute""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.organisations.clear() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "ADD INFORMATION" + and "This barrier is not currently linked with any other government" + in task["message"] + ): + task_found = True + assert task_found + + def test_missing_barrier_detail_hs_code(self): + """Barriers that are set as dealing with goods exports + will generate a task if they do not have a product hs_code set""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.export_types.add(ExportType.objects.get(name="goods")) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "ADD INFORMATION" + and "HS commodity codes. Check and add the codes now" in task["message"] + ): + task_found = True + assert task_found + + @patch( + "api.dashboard.views.UserTasksView.todays_date", + datetime(2024, 1, 10).replace(tzinfo=timezone.utc), + ) + def test_missing_barrier_detail_financial_year_delivery_confidence(self): + """A task will be generated for barriers with an estimated resolution date + within the current financial year without any progress updates.""" + + self.barrier = BarrierFactory( + estimated_resolution_date=datetime(2024, 1, 20).replace( + tzinfo=timezone.utc + ), + ) + self.barrier.organisations.add(*self.test_government_departments) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + for task in response.data["results"]: + assert task["tag"] == "ADD INFORMATION" + assert ( + "This barrier does not have information on how confident you feel" + in task["message"] + ) + + def test_missing_barrier_detail_no_tasks_if_resolved(self): + """Test to ensure tasks dealing with missing details will + not be added to the task list if the barrier in question is resolved.""" + self.basic_barrier_setUp(erd_time="future") + self.barrier.export_types.add(ExportType.objects.get(name="goods")) + self.barrier.status = BarrierStatus.RESOLVED_IN_FULL + self.barrier.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 0 + + def test_erd_tasks_outdated_value(self): + """A task will be added if the estimated resolution date for the barrier has passed""" + + self.basic_barrier_setUp(erd_time="past") + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + for task in response.data["results"]: + assert task["tag"] == "CHANGE OVERDUE" + assert "past. Review and add a new date now." in task["message"] + + def test_erd_tasks_resolved_wont_trigger_task(self): + """Ensure that tasks relating to estimated resolution date are not + added when the related barrier is resolved""" + + self.basic_barrier_setUp(erd_time="past") + self.barrier.status = BarrierStatus.RESOLVED_IN_FULL + self.barrier.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 0 + + def test_erd_tasks_missing_for_pb100(self): + """A task should be created for barriers that are top priority (pb100) + that have not had an estimated resolution date set""" + + self.basic_barrier_setUp() + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + # Add progress update to prevent triggering other tasks + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime.now(), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + for task in response.data["results"]: + assert task["tag"] == "ADD DATE" + assert "As this is a priority barrier you need to add an" in task["message"] + + def test_erd_tasks_missing_for_overseas_delivery(self): + """A task should be created for barriers that are overseas delivery priority + that have not had an estimated resolution date set""" + + self.basic_barrier_setUp() + self.barrier.priority_level = "OVERSEAS" + self.barrier.save() + + # Add progress update to prevent triggering other tasks + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime.now(), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 1 + for task in response.data["results"]: + assert task["tag"] == "ADD DATE" + assert "As this is a priority barrier you need to add an" in task["message"] + + def test_erd_tasks_missing_for_overseas_delivery_no_permission(self): + """Only owners should see these estimated resolution date tasks""" + + self.basic_barrier_setUp() + self.barrier.priority_level = "OVERSEAS" + self.barrier.save() + + # Add progress update to prevent triggering other tasks + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime.now(), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 0 + + # pytest tests/user/test_views.py::TestDashboardTasksView::test_erd_tasks_progress_update_erd_outdated + def test_erd_tasks_progress_update_erd_outdated(self): + """Progress updates contain their own estimated resolution date + value seperate from the one on the barrier, we expect to add + a task if this value is outdated.""" + + self.basic_barrier_setUp() + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + # Add progress update with outdated estimated resolution date + expiry_date = datetime.now() - timedelta(days=181) + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime(expiry_date.year, expiry_date.month, expiry_date.day), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + expected_difference = ( + progress_update.modified_on.year - datetime.now().year + ) * 12 + (progress_update.modified_on.month - datetime.now().month) + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "REVIEW DATE" + and "This barriers estimated resolution date has not" in task["message"] + and f"been updated in {abs(expected_difference)} months." + in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_tasks_pb100_mising_progress_update(self): + """It is intended that top priority (pb100) barriers be tracked with progress updates + so we expect a task to be added which checks that the barrier is not missing + a progress update.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "This is a PB100 barrier. Add a monthly progress update" + in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_tasks_none_when_resolved(self): + """Test to ensure no progress-update related tasks are added for resolved barriers""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.top_priority_status = "APPROVED" + self.barrier.status = BarrierStatus.RESOLVED_IN_FULL + self.barrier.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "This is a PB100 barrier. Add a monthly progress update" + in task["message"] + ): + task_found = True + assert not task_found + + @patch( + "api.dashboard.views.UserTasksView.todays_date", + datetime(2024, 1, 10).replace(tzinfo=timezone.utc), + ) + def test_progress_update_tasks_monthly_pb100_update_due(self): + """It is current practice that there are progress updates for top priority (pb100) barriers + which are due before the 3rd friday of every month. A task is expected to trigger + when a pb100 barriers last progress update is between the first of the month and + the date of the third firday to prompt users to make an update.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + # Add progress update + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime(2023, 12, 31), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "This is a PB100 barrier. Add a monthly progress update" + in task["message"] + and "by 19-01-24" in task["message"] + ): + task_found = True + assert task_found + + @patch( + "api.dashboard.views.UserTasksView.todays_date", + datetime(2024, 1, 21).replace(tzinfo=timezone.utc), + ) + def test_progress_update_tasks_monthly_pb100_update_overdue(self): + """It is current practice that there are progress updates for top priority (pb100) barriers + which are due before the 3rd friday of every month. A task indicating the update + is overdue is expected to trigger when a pb100 barriers last progress update is + after the third friday of the month.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + # Add progress update + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime(2023, 12, 31), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "OVERDUE PROGRESS UPDATE" + and "This is a PB100 barrier. Add a monthly progress update" + in task["message"] + and "by 19-01-24" in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_tasks_overdue_next_steps(self): + """A task is generated when a next_step item of a progress update + has a completion_date value in the past.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + # Create next step item + BarrierNextStepItem.objects.create( + barrier=self.barrier, + status=NEXT_STEPS_ITEMS_STATUS_CHOICES.IN_PROGRESS, + next_step_owner="Test Owner", + next_step_item="Test next step item", + completion_date=datetime.now() - timedelta(days=50), + ) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "REVIEW NEXT STEP" + and "The next step for this barrier has not been reviewed" + in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_due_for_overseas_barriers(self): + """A task is generated when a barrier with overseas delivery priority level + had its latest progress update over 90 days ago.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.priority_level = "OVERSEAS" + self.barrier.save() + + # Add progress update + progress_update = BarrierProgressUpdate.objects.create( + barrier=self.barrier, + status=PROGRESS_UPDATE_CHOICES.ON_TRACK, + update="Nothing Specific", + next_steps="First steps", + modified_on=datetime(2023, 12, 31), + ) + progress_update.next_steps = "Edited Steps" + progress_update.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "This is an overseas delivery barrier" in task["message"] + and "Add a quarterly progress update now." in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_missing_for_overseas_barriers(self): + """A task is generated when a barrier with overseas delivery priority level + has not had a progress update.""" + + self.basic_barrier_setUp(erd_time="future") + self.barrier.priority_level = "OVERSEAS" + self.barrier.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "This is an overseas delivery barrier" in task["message"] + and "Add a quarterly progress update now." in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_overdue_for_programme_fund_barriers(self): + """Barriers which are tagged with a Programme Fund tag need + programme fund progress updates, if the latest update is more + than 90 days old we create a task.""" + + self.basic_barrier_setUp(erd_time="future") + + # Add programme fund progress update + pf = ProgrammeFundProgressUpdateFactory(barrier=self.barrier) + pf.modified_on = datetime.now() - timedelta(days=200) + pf.save() + + # Add programme fund tag to barrier + programme_fund_tag = BarrierTag.objects.get( + title="Programme Fund - Facilitative Regional" + ) + self.barrier.tags.add(programme_fund_tag) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "There is an active programme fund for this barrier" + in task["message"] + and "there has not been an update for over 3 months" in task["message"] + ): + task_found = True + assert task_found + + def test_progress_update_missing_for_programme_fund_barriers(self): + """Barriers with the Programme Fund - Faciliative Regional tag + assigned should hace a programme fund progress update, a task is added + if it is missing.""" + + self.basic_barrier_setUp(erd_time="future") + + # Add programme fund tag to barrier + programme_fund_tag = BarrierTag.objects.get( + title="Programme Fund - Facilitative Regional" + ) + self.barrier.tags.add(programme_fund_tag) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "PROGRESS UPDATE DUE" + and "There is an active programme fund for this barrier" + in task["message"] + and "there has not been an update for over 3 months" in task["message"] + ): + task_found = True + assert task_found + + def test_mentions_task_list(self): + """Mentions are when another user uses an @ followed by another users + email in a barrier note to flag to that user that they need to pay attention. + Mentions with a user indicated should create a task that is added to the task list. + """ + + self.basic_barrier_setUp(erd_time="future") + + text = f"test mention @{self.user.email}" + interaction = Interaction( + created_by=self.test_user, + barrier=self.barrier, + kind="kind", + text=text, + pinned=False, + is_active=True, + ) + interaction.save() + + mention = Mention( + barrier=self.barrier, + email_used=self.test_user_2.email, + recipient=self.test_user, + created_by_id=self.test_user_2.id, + ) + mention.save() + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "REVIEW COMMENT" + and f"{self.test_user_2.first_name} {self.test_user_2.last_name} mentioned you" + in task["message"] + and f"in a comment on {datetime.now().strftime('%d-%m-%y')}" + in task["message"] + ): + task_found = True + assert task_found + + def test_mentions_task_list_no_barriers(self): + """Mentions show up in the task list and are unrelated to + barriers owned/related to the user, so we would expect mention tasks + to appear in the task_list if the user is mentioned in a note for + a barrier they have not previously worked on.""" + + self.basic_barrier_setUp(erd_time="future") + + text = f"test mention @{self.user.email}" + interaction = Interaction( + created_by=self.test_user, + barrier=self.barrier, + kind="kind", + text=text, + pinned=False, + is_active=True, + ) + interaction.save() + + mention = Mention( + barrier=self.barrier, + email_used=self.test_user_2.email, + recipient=self.test_user, + created_by_id=self.test_user_2.id, + ) + mention.save() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + task_found = False + for task in response.data["results"]: + if ( + task["tag"] == "REVIEW COMMENT" + and f"{self.test_user_2.first_name} {self.test_user_2.last_name} mentioned you" + in task["message"] + and f"in a comment on {datetime.now().strftime('%d-%m-%y')}" + in task["message"] + ): + task_found = True + assert task_found + + def test_task_list_pagination(self): + """The results are paginated so the frontend does not get swamped with tasks + and overwhelm the user. We expect a maximum of 5 tasks that get returned + at a time, and that if we are on a second or third page, we get the next + block of tasks in the list.""" + + # Create barriers that will trigger more than 5 tasks + self.barrier = BarrierFactory() + self.barrier.top_priority_status = "APPROVED" + self.barrier.save() + + self.barrier_2 = BarrierFactory() + self.barrier_2.priority_level = "OVERSEAS" + self.barrier_2.save() + + # Test requires user to be added to second barrier + tm_2 = TeamMember.objects.create( + barrier=self.barrier_2, + user=self.test_user, + created_by=self.test_user, + role="Owner", + ) + self.barrier_2.barrier_team.add(tm_2) + + self.owner_user_setUp() + + response = self.api_client.get(self.request_url) + + assert response.status_code == 200 + assert response.data["count"] == 8 + assert len(response.data["results"]) == 5 + + self.request_url_page_2 = f'{reverse("get-dashboard-tasks")}?page=2' + response_page_2 = self.api_client.get(self.request_url_page_2) + + assert response_page_2.status_code == 200 + assert response_page_2.data["count"] == 8 + assert len(response_page_2.data["results"]) == 3 diff --git a/tests/user/test_views.py b/tests/user/test_views.py index 7a4921d74..3399f276d 100644 --- a/tests/user/test_views.py +++ b/tests/user/test_views.py @@ -1,27 +1,12 @@ -from datetime import datetime, timedelta, timezone from logging import getLogger from unittest.mock import patch import freezegun import pytest -from django.contrib.auth.models import Group from rest_framework import status from rest_framework.reverse import reverse -from rest_framework.test import APITestCase -from api.barriers.models import BarrierNextStepItem, BarrierProgressUpdate -from api.collaboration.models import TeamMember from api.core.test_utils import APITestMixin, create_test_user -from api.interactions.models import Interaction, Mention -from api.metadata.constants import ( - NEXT_STEPS_ITEMS_STATUS_CHOICES, - PROGRESS_UPDATE_CHOICES, - BarrierStatus, - PublicBarrierStatus, -) -from api.metadata.models import BarrierTag, ExportType, Organisation -from tests.barriers.factories import BarrierFactory -from tests.history.factories import ProgrammeFundProgressUpdateFactory logger = getLogger(__name__) @@ -276,1164 +261,3 @@ def test_user_edit_add_new_profile(self): ) assert edit_response.status_code == status.HTTP_200_OK - - -class TestDashboardTasksView(APITestMixin, APITestCase): - """Test cases for the Dashboard tasks list retrieval""" - - def setUp(self): - super().setUp() - - """ Create test users, the basic URL and governement orgs used in all tests""" - - self.request_url = reverse("get-dashboard-tasks") - self.test_user = create_test_user( - first_name="Testo", - last_name="Useri", - email="Testo@Useri.com", - username="TestoUseri", - ) - self.test_user_2 = create_test_user( - first_name="Testduo", - last_name="Userii", - email="Testduo@Userii.com", - username="TestduoUserii", - ) - self.test_government_departments = Organisation.objects.all()[:2] - - def basic_barrier_setUp(self, erd_time=None): - """Create basic barrier with an estimated resolution date used in majority of tests""" - if erd_time == "future": - erd = datetime.now() + timedelta(days=200) - elif erd_time == "past": - erd = datetime.now() - timedelta(days=400) - else: - erd = None - self.barrier = BarrierFactory( - estimated_resolution_date=erd, - ) - self.barrier.organisations.add(*self.test_government_departments) - - def owner_user_setUp(self): - """Add user to barrier team""" - tm = TeamMember.objects.create( - barrier=self.barrier, - user=self.test_user, - created_by=self.test_user, - role="Owner", - ) - self.barrier.barrier_team.add(tm) - - def public_barrier_setUp(self): - """Setup method for tasks requiring public barrier data""" - api_client = self.create_api_client(user=self.test_user) - # Create a public barrier with only a title that wont trigger other tasks - self.barrier = BarrierFactory( - estimated_resolution_date=datetime.now() + timedelta(days=500), - ) - self.barrier.organisations.add(*self.test_government_departments) - allowed_to_publish_url = reverse( - "public-barriers-allow-for-publishing-process", - kwargs={"pk": self.barrier.id}, - ) - self.barrier.public_barrier.title = "adding a title" - self.barrier.public_barrier.save() - api_client.post(allowed_to_publish_url, format="json", data={}) - self.barrier.refresh_from_db() - - # Create a public barrier with only a summary that wont trigger other tasks - self.barrier_2 = BarrierFactory( - estimated_resolution_date=datetime.now() + timedelta(days=500), - ) - self.barrier_2.organisations.add(*self.test_government_departments) - allowed_to_publish_url = reverse( - "public-barriers-allow-for-publishing-process", - kwargs={"pk": self.barrier_2.id}, - ) - self.barrier_2.public_barrier.summary = "adding a summary" - self.barrier_2.public_barrier.save() - api_client.post(allowed_to_publish_url, format="json", data={}) - self.barrier_2.refresh_from_db() - - # Attach test_user as the owner to the barriers - tm = TeamMember.objects.create( - barrier=self.barrier, - user=self.test_user, - created_by=self.test_user, - role="Owner", - ) - self.barrier.barrier_team.add(tm) - tm_2 = TeamMember.objects.create( - barrier=self.barrier_2, - user=self.test_user, - created_by=self.test_user, - role="Owner", - ) - self.barrier_2.barrier_team.add(tm_2) - - # Attach test_user_2 as a contributor to the barriers - tm = TeamMember.objects.create( - barrier=self.barrier, user=self.test_user_2, created_by=self.test_user_2 - ) - self.barrier.barrier_team.add(tm) - tm_2 = TeamMember.objects.create( - barrier=self.barrier_2, user=self.test_user_2, created_by=self.test_user_2 - ) - self.barrier_2.barrier_team.add(tm_2) - - def test_user_with_no_barriers(self): - """Users need to be part of a barrier team to retrieve a task list - This test will expect an empty set of response data.""" - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 0 - assert response.data["results"] == [] - - def test_publishing_task_editor_user_info_required_not_owner(self): - """Members of the barrier team do not get tasks related to publishing. - Expect no tasks returned""" - - self.public_barrier_setUp() - - api_client = self.create_api_client(user=self.test_user_2) - response = api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 0 - assert response.data["results"] == [] - - def test_publishing_task_editor_user_info_required(self): - """Owners of the barrier need to enter a public title and public summary. - If either of these fields are missing, we generate a task.""" - - self.public_barrier_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - assert "Add a public title and summary to this barrier" in task["message"] - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - def test_publishing_task_editor_user_info_required_overdue(self): - """This task needs to be completed within a timeframe tracked - on the public barrier. The dashboard task has a seperate tag - for when this countdown is passed.""" - - self.public_barrier_setUp() - - # Modify the public barrier missing the title to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier.public_barrier.save() - - # Modify the public barrier missing the summary to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier_2.public_barrier.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - for task in response.data["results"]: - assert task["tag"] == "OVERDUE REVIEW" - assert "Add a public title and summary to this barrier" in task["message"] - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - - def test_publishing_task_editor_user_send_to_approval(self): - """A second type of task is added in place of the missing information - task if both title and summary are filled in requesting the user to send - the barrier to an approver.""" - - self.public_barrier_setUp() - - # Add summary to barrier with only a title - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - submit_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "Submit this barrier for a review and clearance checks" - in task["message"] - ): - submit_task_count = submit_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - assert submit_task_count == 1 - assert missing_info_task_count == 1 - - def test_publishing_task_editor_user_send_to_approval_overdue(self): - """This task needs to be completed within a timeframe tracked - on the public barrier. The dashboard task has a seperate tag - for when this countdown is passed.""" - - self.public_barrier_setUp() - - # Add summary to barrier with only a title - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - # Modify the public barrier missing the title to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier.public_barrier.save() - - # Modify the public barrier missing the summary to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier_2.public_barrier.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - submit_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "OVERDUE REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "Submit this barrier for a review and clearance checks" - in task["message"] - ): - submit_task_count = submit_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - - assert submit_task_count == 1 - assert missing_info_task_count == 1 - - def test_publishing_task_approver_user_approve_request(self): - """This task is for users with approver permissions only. We need - to check that the task is added when the barrier is in a specific status""" - - self.public_barrier_setUp() - - # Set barrier to AWAITING_APPROVAL status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.APPROVAL_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - # Update user to have the approver permission - self.test_user.groups.add(Group.objects.get(name="Public barrier approver")) - self.test_user.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - approve_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "Review and check this barrier for clearances before it can be submitted" - in task["message"] - ): - approve_task_count = approve_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - assert approve_task_count == 1 - assert missing_info_task_count == 1 - - def test_publishing_task_approver_user_approve_request_no_permission(self): - """This task is for users with approver permissions only. We need - to check that the task is not added when the user is only an editor.""" - - self.public_barrier_setUp() - - # Set barrier to AWAITING_APPROVAL status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.APPROVAL_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - assert "Add a public title and summary to this barrier" in task["message"] - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - def test_publishing_task_approver_user_approve_request_overdue(self): - """This task needs to be completed within a timeframe tracked - on the public barrier. The dashboard task has a seperate tag - for when this countdown is passed.""" - - self.public_barrier_setUp() - - # Set barrier to AWAITING_APPROVAL status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.APPROVAL_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - # Modify the public barrier missing the title to be overdue by making - # set_to_allowed_on more than 30 days in the past - self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier.public_barrier.save() - - # Modify the public barrier missing the summary to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier_2.public_barrier.save() - - # Update user to have the approver permission - self.test_user.groups.add(Group.objects.get(name="Public barrier approver")) - self.test_user.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - approve_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "OVERDUE REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "Review and check this barrier for clearances before it can be submitted" - in task["message"] - ): - approve_task_count = approve_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - - assert approve_task_count == 1 - assert missing_info_task_count == 1 - - def test_publishing_task_publisher_user_approve_request(self): - """This task is for users with publisher permissions only. We need - to check that the task is added when the barrier is in a specific status""" - - self.public_barrier_setUp() - - # Set barrier to PUBLISHING_PENDING status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.PUBLISHING_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - # Update user to have the publisher permission - self.test_user.groups.add(Group.objects.get(name="Publisher")) - self.test_user.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - publish_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "This barrier has been approved. Complete the final content checks" - in task["message"] - ): - publish_task_count = publish_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - assert publish_task_count == 1 - assert missing_info_task_count == 1 - - def test_publishing_task_publisher_user_approve_request_no_permission(self): - """This task is for users with publisher permissions only. We need - to check that the task is added when the barrier is in a specific status""" - - self.public_barrier_setUp() - - # Set barrier to PUBLISHING_PENDING status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.PUBLISHING_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - - for task in response.data["results"]: - assert task["tag"] == "PUBLICATION REVIEW" - assert "Add a public title and summary to this barrier" in task["message"] - # Check countdown has been substituted into message - assert "needs to be done within 29 days" in task["message"] - - def test_publishing_task_publisher_user_approve_request_overdue(self): - """This task is for users with approver permissions only. We need - to check that the task is added when the barrier is in a specific status""" - - self.public_barrier_setUp() - - # Set barrier to PUBLISHING_PENDING status and give it both title and summary - self.barrier.public_barrier.public_view_status = ( - PublicBarrierStatus.PUBLISHING_PENDING - ) - self.barrier.public_barrier.summary = "adding a summary" - self.barrier.public_barrier.save() - - # Modify the public barrier missing the title to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier.public_barrier.save() - - # Modify the public barrier missing the summary to be overdue by - # making set_to_allowed_on more than 30 days in the past - self.barrier_2.public_barrier.set_to_allowed_on = datetime.now() - timedelta( - days=40 - ) - self.barrier_2.public_barrier.save() - - # Update user to have the publisher permission - self.test_user.groups.add(Group.objects.get(name="Publisher")) - self.test_user.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 2 - - # Count the expected task types - missing_info_task_count = 0 - publish_task_count = 0 - for task in response.data["results"]: - assert task["tag"] == "OVERDUE REVIEW" - # Message will be different for barrier with both title and sumary, but we - # also expect for one of the tasks to be the previously tested message - if ( - "This barrier has been approved. Complete the final content checks" - in task["message"] - ): - publish_task_count = publish_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - elif "Add a public title and summary to this barrier" in task["message"]: - missing_info_task_count = missing_info_task_count + 1 - # Check countdown has been substituted into message - assert "needs to be done within 0 days" in task["message"] - - assert publish_task_count == 1 - assert missing_info_task_count == 1 - - def test_missing_barrier_detail_government_orgs(self): - """All owned barriers will generate a task if they do not have - any data stored under 'organisations' attribute""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.organisations.clear() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "ADD INFORMATION" - and "This barrier is not currently linked with any other government" - in task["message"] - ): - task_found = True - assert task_found - - def test_missing_barrier_detail_hs_code(self): - """Barriers that are set as dealing with goods exports - will generate a task if they do not have a product hs_code set""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.export_types.add(ExportType.objects.get(name="goods")) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "ADD INFORMATION" - and "HS commodity codes. Check and add the codes now" in task["message"] - ): - task_found = True - assert task_found - - @patch( - "api.user.views.DashboardUserTasksView.todays_date", - datetime(2024, 1, 10).replace(tzinfo=timezone.utc), - ) - def test_missing_barrier_detail_financial_year_delivery_confidence(self): - """A task will be generated for barriers with an estimated resolution date - within the current financial year without any progress updates.""" - - self.barrier = BarrierFactory( - estimated_resolution_date=datetime(2024, 1, 20).replace( - tzinfo=timezone.utc - ), - ) - self.barrier.organisations.add(*self.test_government_departments) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - for task in response.data["results"]: - assert task["tag"] == "ADD INFORMATION" - assert ( - "This barrier does not have information on how confident you feel" - in task["message"] - ) - - def test_missing_barrier_detail_no_tasks_if_resolved(self): - """Test to ensure tasks dealing with missing details will - not be added to the task list if the barrier in question is resolved.""" - self.basic_barrier_setUp(erd_time="future") - self.barrier.export_types.add(ExportType.objects.get(name="goods")) - self.barrier.status = BarrierStatus.RESOLVED_IN_FULL - self.barrier.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 0 - - def test_erd_tasks_outdated_value(self): - """A task will be added if the estimated resolution date for the barrier has passed""" - - self.basic_barrier_setUp(erd_time="past") - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - for task in response.data["results"]: - assert task["tag"] == "CHANGE OVERDUE" - assert "past. Review and add a new date now." in task["message"] - - def test_erd_tasks_resolved_wont_trigger_task(self): - """Ensure that tasks relating to estimated resolution date are not - added when the related barrier is resolved""" - - self.basic_barrier_setUp(erd_time="past") - self.barrier.status = BarrierStatus.RESOLVED_IN_FULL - self.barrier.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 0 - - def test_erd_tasks_missing_for_pb100(self): - """A task should be created for barriers that are top priority (pb100) - that have not had an estimated resolution date set""" - - self.basic_barrier_setUp() - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - # Add progress update to prevent triggering other tasks - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime.now(), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - for task in response.data["results"]: - assert task["tag"] == "ADD DATE" - assert "As this is a priority barrier you need to add an" in task["message"] - - def test_erd_tasks_missing_for_overseas_delivery(self): - """A task should be created for barriers that are overseas delivery priority - that have not had an estimated resolution date set""" - - self.basic_barrier_setUp() - self.barrier.priority_level = "OVERSEAS" - self.barrier.save() - - # Add progress update to prevent triggering other tasks - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime.now(), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 1 - for task in response.data["results"]: - assert task["tag"] == "ADD DATE" - assert "As this is a priority barrier you need to add an" in task["message"] - - def test_erd_tasks_missing_for_overseas_delivery_no_permission(self): - """Only owners should see these estimated resolution date tasks""" - - self.basic_barrier_setUp() - self.barrier.priority_level = "OVERSEAS" - self.barrier.save() - - # Add progress update to prevent triggering other tasks - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime.now(), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 0 - - # pytest tests/user/test_views.py::TestDashboardTasksView::test_erd_tasks_progress_update_erd_outdated - def test_erd_tasks_progress_update_erd_outdated(self): - """Progress updates contain their own estimated resolution date - value seperate from the one on the barrier, we expect to add - a task if this value is outdated.""" - - self.basic_barrier_setUp() - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - # Add progress update with outdated estimated resolution date - expiry_date = datetime.now() - timedelta(days=181) - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime(expiry_date.year, expiry_date.month, expiry_date.day), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - expected_difference = ( - progress_update.modified_on.year - datetime.now().year - ) * 12 + (progress_update.modified_on.month - datetime.now().month) - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "REVIEW DATE" - and "This barriers estimated resolution date has not" in task["message"] - and f"been updated in {abs(expected_difference)} months." - in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_tasks_pb100_mising_progress_update(self): - """It is intended that top priority (pb100) barriers be tracked with progress updates - so we expect a task to be added which checks that the barrier is not missing - a progress update.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "This is a PB100 barrier. Add a monthly progress update" - in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_tasks_none_when_resolved(self): - """Test to ensure no progress-update related tasks are added for resolved barriers""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.top_priority_status = "APPROVED" - self.barrier.status = BarrierStatus.RESOLVED_IN_FULL - self.barrier.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "This is a PB100 barrier. Add a monthly progress update" - in task["message"] - ): - task_found = True - assert not task_found - - @patch( - "api.user.views.DashboardUserTasksView.todays_date", - datetime(2024, 1, 10).replace(tzinfo=timezone.utc), - ) - def test_progress_update_tasks_monthly_pb100_update_due(self): - """It is current practice that there are progress updates for top priority (pb100) barriers - which are due before the 3rd friday of every month. A task is expected to trigger - when a pb100 barriers last progress update is between the first of the month and - the date of the third firday to prompt users to make an update.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - # Add progress update - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime(2023, 12, 31), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "This is a PB100 barrier. Add a monthly progress update" - in task["message"] - and "by 19-01-24" in task["message"] - ): - task_found = True - assert task_found - - @patch( - "api.user.views.DashboardUserTasksView.todays_date", - datetime(2024, 1, 21).replace(tzinfo=timezone.utc), - ) - def test_progress_update_tasks_monthly_pb100_update_overdue(self): - """It is current practice that there are progress updates for top priority (pb100) barriers - which are due before the 3rd friday of every month. A task indicating the update - is overdue is expected to trigger when a pb100 barriers last progress update is - after the third friday of the month.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - # Add progress update - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime(2023, 12, 31), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "OVERDUE PROGRESS UPDATE" - and "This is a PB100 barrier. Add a monthly progress update" - in task["message"] - and "by 19-01-24" in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_tasks_overdue_next_steps(self): - """A task is generated when a next_step item of a progress update - has a completion_date value in the past.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - # Create next step item - BarrierNextStepItem.objects.create( - barrier=self.barrier, - status=NEXT_STEPS_ITEMS_STATUS_CHOICES.IN_PROGRESS, - next_step_owner="Test Owner", - next_step_item="Test next step item", - completion_date=datetime.now() - timedelta(days=50), - ) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "REVIEW NEXT STEP" - and "The next step for this barrier has not been reviewed" - in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_due_for_overseas_barriers(self): - """A task is generated when a barrier with overseas delivery priority level - had its latest progress update over 90 days ago.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.priority_level = "OVERSEAS" - self.barrier.save() - - # Add progress update - progress_update = BarrierProgressUpdate.objects.create( - barrier=self.barrier, - status=PROGRESS_UPDATE_CHOICES.ON_TRACK, - update="Nothing Specific", - next_steps="First steps", - modified_on=datetime(2023, 12, 31), - ) - progress_update.next_steps = "Edited Steps" - progress_update.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "This is an overseas delivery barrier" in task["message"] - and "Add a quarterly progress update now." in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_missing_for_overseas_barriers(self): - """A task is generated when a barrier with overseas delivery priority level - has not had a progress update.""" - - self.basic_barrier_setUp(erd_time="future") - self.barrier.priority_level = "OVERSEAS" - self.barrier.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "This is an overseas delivery barrier" in task["message"] - and "Add a quarterly progress update now." in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_overdue_for_programme_fund_barriers(self): - """Barriers which are tagged with a Programme Fund tag need - programme fund progress updates, if the latest update is more - than 90 days old we create a task.""" - - self.basic_barrier_setUp(erd_time="future") - - # Add programme fund progress update - pf = ProgrammeFundProgressUpdateFactory(barrier=self.barrier) - pf.modified_on = datetime.now() - timedelta(days=200) - pf.save() - - # Add programme fund tag to barrier - programme_fund_tag = BarrierTag.objects.get( - title="Programme Fund - Facilitative Regional" - ) - self.barrier.tags.add(programme_fund_tag) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "There is an active programme fund for this barrier" - in task["message"] - and "there has not been an update for over 3 months" in task["message"] - ): - task_found = True - assert task_found - - def test_progress_update_missing_for_programme_fund_barriers(self): - """Barriers with the Programme Fund - Faciliative Regional tag - assigned should hace a programme fund progress update, a task is added - if it is missing.""" - - self.basic_barrier_setUp(erd_time="future") - - # Add programme fund tag to barrier - programme_fund_tag = BarrierTag.objects.get( - title="Programme Fund - Facilitative Regional" - ) - self.barrier.tags.add(programme_fund_tag) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "PROGRESS UPDATE DUE" - and "There is an active programme fund for this barrier" - in task["message"] - and "there has not been an update for over 3 months" in task["message"] - ): - task_found = True - assert task_found - - def test_mentions_task_list(self): - """Mentions are when another user uses an @ followed by another users - email in a barrier note to flag to that user that they need to pay attention. - Mentions with a user indicated should create a task that is added to the task list. - """ - - self.basic_barrier_setUp(erd_time="future") - - text = f"test mention @{self.user.email}" - interaction = Interaction( - created_by=self.test_user, - barrier=self.barrier, - kind="kind", - text=text, - pinned=False, - is_active=True, - ) - interaction.save() - - mention = Mention( - barrier=self.barrier, - email_used=self.test_user_2.email, - recipient=self.test_user, - created_by_id=self.test_user_2.id, - ) - mention.save() - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "REVIEW COMMENT" - and f"{self.test_user_2.first_name} {self.test_user_2.last_name} mentioned you" - in task["message"] - and f"in a comment on {datetime.now().strftime('%d-%m-%y')}" - in task["message"] - ): - task_found = True - assert task_found - - def test_mentions_task_list_no_barriers(self): - """Mentions show up in the task list and are unrelated to - barriers owned/related to the user, so we would expect mention tasks - to appear in the task_list if the user is mentioned in a note for - a barrier they have not previously worked on.""" - - self.basic_barrier_setUp(erd_time="future") - - text = f"test mention @{self.user.email}" - interaction = Interaction( - created_by=self.test_user, - barrier=self.barrier, - kind="kind", - text=text, - pinned=False, - is_active=True, - ) - interaction.save() - - mention = Mention( - barrier=self.barrier, - email_used=self.test_user_2.email, - recipient=self.test_user, - created_by_id=self.test_user_2.id, - ) - mention.save() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - task_found = False - for task in response.data["results"]: - if ( - task["tag"] == "REVIEW COMMENT" - and f"{self.test_user_2.first_name} {self.test_user_2.last_name} mentioned you" - in task["message"] - and f"in a comment on {datetime.now().strftime('%d-%m-%y')}" - in task["message"] - ): - task_found = True - assert task_found - - def test_task_list_pagination(self): - """The results are paginated so the frontend does not get swamped with tasks - and overwhelm the user. We expect a maximum of 5 tasks that get returned - at a time, and that if we are on a second or third page, we get the next - block of tasks in the list.""" - - # Create barriers that will trigger more than 5 tasks - self.barrier = BarrierFactory() - self.barrier.top_priority_status = "APPROVED" - self.barrier.save() - - self.barrier_2 = BarrierFactory() - self.barrier_2.priority_level = "OVERSEAS" - self.barrier_2.save() - - # Test requires user to be added to second barrier - tm_2 = TeamMember.objects.create( - barrier=self.barrier_2, - user=self.test_user, - created_by=self.test_user, - role="Owner", - ) - self.barrier_2.barrier_team.add(tm_2) - - self.owner_user_setUp() - - response = self.api_client.get(self.request_url) - - assert response.status_code == 200 - assert response.data["count"] == 8 - assert len(response.data["results"]) == 5 - - self.request_url_page_2 = f'{reverse("get-dashboard-tasks")}?page=2' - response_page_2 = self.api_client.get(self.request_url_page_2) - - assert response_page_2.status_code == 200 - assert response_page_2.data["count"] == 8 - assert len(response_page_2.data["results"]) == 3