From 2ea6a1e5c8d6d970b9c7dd3f96a0ac1fce9f21ad Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 1 Oct 2024 11:42:31 +0530 Subject: [PATCH] added view for delivery summary --- commcare_connect/opportunity/export.py | 4 +- commcare_connect/opportunity/helpers.py | 71 +--------------- .../migrations/0059_userinvitesummary.py | 85 ++++++++++++++++++- commcare_connect/opportunity/models.py | 17 ++++ commcare_connect/opportunity/tasks.py | 6 +- .../opportunity/tests/test_helpers.py | 14 +-- commcare_connect/opportunity/views.py | 9 +- 7 files changed, 121 insertions(+), 85 deletions(-) diff --git a/commcare_connect/opportunity/export.py b/commcare_connect/opportunity/export.py index b46f3aa8..cea1190d 100644 --- a/commcare_connect/opportunity/export.py +++ b/commcare_connect/opportunity/export.py @@ -6,12 +6,12 @@ from tablib import Dataset from commcare_connect.opportunity.forms import DateRanges -from commcare_connect.opportunity.helpers import get_annotated_opportunity_access_deliver_status from commcare_connect.opportunity.models import ( CatchmentArea, CompletedWork, Opportunity, OpportunityAccess, + OpportunityDeliverySummary, UserInviteSummary, UserVisit, VisitValidationStatus, @@ -112,7 +112,7 @@ def export_user_status_table(opportunity: Opportunity) -> Dataset: def export_deliver_status_table(opportunity: Opportunity) -> Dataset: - access_objects = get_annotated_opportunity_access_deliver_status(opportunity) + access_objects = OpportunityDeliverySummary.objects.filter(opportunity=opportunity) table = DeliverStatusTable(access_objects, exclude=("details", "date_popup")) return get_dataset(table, export_title="Deliver Status export") diff --git a/commcare_connect/opportunity/helpers.py b/commcare_connect/opportunity/helpers.py index b1b89dca..0d36544c 100644 --- a/commcare_connect/opportunity/helpers.py +++ b/commcare_connect/opportunity/helpers.py @@ -1,75 +1,6 @@ from collections import namedtuple -from django.db.models import Count, F, Q, Value - -from commcare_connect.opportunity.models import ( - CompletedWork, - CompletedWorkStatus, - Opportunity, - OpportunityAccess, - PaymentUnit, -) - - -def get_annotated_opportunity_access_deliver_status(opportunity: Opportunity): - access_objects = [] - for payment_unit in opportunity.paymentunit_set.all(): - access_objects += ( - OpportunityAccess.objects.filter(opportunity=opportunity) - .select_related("user") - .annotate( - payment_unit=Value(payment_unit.name), - pending=Count( - "completedwork", - filter=Q( - completedwork__opportunity_access_id=F("pk"), - completedwork__payment_unit=payment_unit, - completedwork__status=CompletedWorkStatus.pending, - ), - distinct=True, - ), - approved=Count( - "completedwork", - filter=Q( - completedwork__opportunity_access_id=F("pk"), - completedwork__payment_unit=payment_unit, - completedwork__status=CompletedWorkStatus.approved, - ), - distinct=True, - ), - rejected=Count( - "completedwork", - filter=Q( - completedwork__opportunity_access_id=F("pk"), - completedwork__payment_unit=payment_unit, - completedwork__status=CompletedWorkStatus.rejected, - ), - distinct=True, - ), - over_limit=Count( - "completedwork", - filter=Q( - completedwork__opportunity_access_id=F("pk"), - completedwork__payment_unit=payment_unit, - completedwork__status=CompletedWorkStatus.over_limit, - ), - distinct=True, - ), - incomplete=Count( - "completedwork", - filter=Q( - completedwork__opportunity_access_id=F("pk"), - completedwork__payment_unit=payment_unit, - completedwork__status=CompletedWorkStatus.incomplete, - ), - distinct=True, - ), - completed=F("approved") + F("rejected") + F("pending") + F("over_limit"), - ) - .order_by("user__name") - ) - access_objects.sort(key=lambda a: a.user.name) - return access_objects +from commcare_connect.opportunity.models import CompletedWork, CompletedWorkStatus, Opportunity, PaymentUnit def get_payment_report_data(opportunity: Opportunity): diff --git a/commcare_connect/opportunity/migrations/0059_userinvitesummary.py b/commcare_connect/opportunity/migrations/0059_userinvitesummary.py index ceaf914d..dc6c1270 100644 --- a/commcare_connect/opportunity/migrations/0059_userinvitesummary.py +++ b/commcare_connect/opportunity/migrations/0059_userinvitesummary.py @@ -33,7 +33,7 @@ class Migration(migrations.Migration): """This migration exists because we created a materialized view for the `UserInviteSummary` model. Django automatically generates migrations for all models present in the application. - Since `managed = False` and `db_table = "opportunity_userinvite_summary"`, Django will not create or modify this table in the database. + Since `managed = False` and `db_table is provided Django will not create or modify this table in the database. However, we still need this migration in the migration script to prevent Django from generating it again when `makemigrations` is run in the future. This ensures that the model is recognized without altering the actual database structure.""" @@ -128,5 +128,88 @@ class Migration(migrations.Migration): """, reverse_sql="DROP MATERIALIZED VIEW IF EXISTS opportunity_userinvite_summary;", ), + migrations.CreateModel( + name="OpportunityDeliverySummary", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("approved", models.IntegerField(default=0)), + ("pending", models.IntegerField(default=0)), + ("rejected", models.IntegerField(default=0)), + ("over_limit", models.IntegerField(default=0)), + ("incomplete", models.IntegerField(default=0)), + ("completed", models.IntegerField(default=0)), + ("payment_unit", models.CharField(max_length=255)), + ( + "opportunity", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="opportunity.opportunity"), + ), + ( + "user", + models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="users.user"), + ), + ( + "opportunity_access", + models.ForeignKey( + on_delete=django.db.models.deletion.DO_NOTHING, + to="opportunity.opportunityaccess", + ), + ), + ], + options={ + "db_table": "opportunity_delivery_summary", + "managed": False, + }, + ), + migrations.RunSQL( + sql=""" + CREATE MATERIALIZED VIEW opportunity_delivery_summary AS + SELECT + access.id AS id, + access.id AS opportunity_access_id, + access.opportunity_id AS opportunity_id, + access.user_id AS user_id, + payment_unit.name AS payment_unit, + + COUNT(DISTINCT CASE WHEN completed_work.status = 'pending' + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END) AS pending, + + COUNT(DISTINCT CASE WHEN completed_work.status = 'approved' + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END) AS approved, + + COUNT(DISTINCT CASE WHEN completed_work.status = 'rejected' + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END) AS rejected, + + COUNT(DISTINCT CASE WHEN completed_work.status = 'over_limit' + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END) AS over_limit, + + COUNT(DISTINCT CASE WHEN completed_work.status = 'incomplete' + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END) AS incomplete, + + COALESCE( + COUNT(DISTINCT CASE WHEN completed_work.status IN ('approved', 'rejected', 'pending', 'over_limit') + AND completed_work.opportunity_access_id = access.id + AND completed_work.payment_unit_id = payment_unit.id THEN completed_work.id END), + 0 + ) AS completed + FROM + opportunity_opportunityaccess AS access + LEFT JOIN + opportunity_completedwork AS completed_work ON access.id = completed_work.opportunity_access_id + LEFT JOIN + opportunity_paymentunit AS payment_unit ON completed_work.payment_unit_id = payment_unit.id + INNER JOIN + users_user AS _user ON access.user_id = _user.id + GROUP BY + access.id, + _user.id, + payment_unit.name; + """, + reverse_sql="DROP MATERIALIZED VIEW IF EXISTS opportunity_delivery_summary;", + ), migrations.RunPython(create_refresh_materialized_view_task, delete_refresh_materialized_view_task), ] diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 755e5a1b..f976f689 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -699,3 +699,20 @@ class UserInviteSummary(models.Model): class Meta: managed = False db_table = "opportunity_userinvite_summary" + + +class OpportunityDeliverySummary(models.Model): + opportunity_access = models.ForeignKey(OpportunityAccess, null=True, on_delete=models.DO_NOTHING) + opportunity = models.ForeignKey(Opportunity, null=True, on_delete=models.CASCADE) + user = models.ForeignKey(User, null=True, on_delete=models.CASCADE) + approved = models.IntegerField(default=0) + pending = models.IntegerField(default=0) + rejected = models.IntegerField(default=0) + over_limit = models.IntegerField(default=0) + incomplete = models.IntegerField(default=0) + completed = models.IntegerField(default=0) + payment_unit = models.CharField(max_length=255) + + class Meta: + managed = False + db_table = "opportunity_delivery_summary" diff --git a/commcare_connect/opportunity/tasks.py b/commcare_connect/opportunity/tasks.py index 21055b26..daf4a09b 100644 --- a/commcare_connect/opportunity/tasks.py +++ b/commcare_connect/opportunity/tasks.py @@ -344,4 +344,8 @@ def generate_catchment_area_export(opportunity_id: int, export_format: str): @celery_app.task() def refresh_materialized_view(): with connection.cursor() as cursor: - cursor.execute("REFRESH MATERIALIZED VIEW opportunity_userinvite_summary;") + cursor.execute( + """REFRESH MATERIALIZED VIEW opportunity_userinvite_summary; + REFRESH MATERIALIZED VIEW opportunity_delivery_summary; + """ + ) diff --git a/commcare_connect/opportunity/tests/test_helpers.py b/commcare_connect/opportunity/tests/test_helpers.py index c0528364..c667c69f 100644 --- a/commcare_connect/opportunity/tests/test_helpers.py +++ b/commcare_connect/opportunity/tests/test_helpers.py @@ -1,7 +1,7 @@ import pytest -from commcare_connect.opportunity.helpers import get_annotated_opportunity_access_deliver_status -from commcare_connect.opportunity.models import Opportunity +from commcare_connect.opportunity.models import Opportunity, OpportunityDeliverySummary +from commcare_connect.opportunity.tasks import refresh_materialized_view from commcare_connect.opportunity.tests.factories import ( CompletedWorkFactory, OpportunityAccessFactory, @@ -15,7 +15,8 @@ def test_deliver_status_query_no_visits(opportunity: Opportunity): mobile_users = MobileUserFactory.create_batch(5) for mobile_user in mobile_users: OpportunityAccessFactory(opportunity=opportunity, user=mobile_user, accepted=True) - access_objects = get_annotated_opportunity_access_deliver_status(opportunity) + refresh_materialized_view() + access_objects = OpportunityDeliverySummary.objects.filter(opportunity=opportunity) usernames = {user.username for user in mobile_users} for access in access_objects: @@ -41,7 +42,8 @@ def test_deliver_status_query(opportunity: Opportunity): count_by_status["completed"] = len(completed_works) - count_by_status["incomplete"] completed_work_counts[(mobile_user.username, pu.name)] = count_by_status - access_objects = get_annotated_opportunity_access_deliver_status(opportunity) + refresh_materialized_view() + access_objects = OpportunityDeliverySummary.objects.filter(opportunity=opportunity) for access in access_objects: username = access.user.username assert (username, access.payment_unit) in completed_work_counts @@ -61,7 +63,9 @@ def test_deliver_status_query_visits_another_opportunity(opportunity: Opportunit for mobile_user in mobile_users: OpportunityAccessFactory(opportunity=opportunity, user=mobile_user, accepted=True) CompletedWorkFactory.create_batch(5) - access_objects = get_annotated_opportunity_access_deliver_status(opportunity) + + refresh_materialized_view() + access_objects = OpportunityDeliverySummary.objects.filter(opportunity=opportunity) usernames = {user.username for user in mobile_users} for access in access_objects: assert access.user.username in usernames diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index fbd9d910..f5b8e6dd 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -43,10 +43,7 @@ SendMessageMobileUsersForm, VisitExportForm, ) -from commcare_connect.opportunity.helpers import ( - get_annotated_opportunity_access_deliver_status, - get_payment_report_data, -) +from commcare_connect.opportunity.helpers import get_payment_report_data from commcare_connect.opportunity.models import ( BlobMeta, CatchmentArea, @@ -59,6 +56,7 @@ OpportunityAccess, OpportunityClaim, OpportunityClaimLimit, + OpportunityDeliverySummary, OpportunityVerificationFlags, Payment, PaymentInvoice, @@ -652,8 +650,7 @@ def get_queryset(self): opportunity_id = self.kwargs["pk"] org_slug = self.kwargs["org_slug"] opportunity = get_opportunity_or_404(pk=opportunity_id, org_slug=org_slug) - access_objects = get_annotated_opportunity_access_deliver_status(opportunity) - return access_objects + return OpportunityDeliverySummary.objects.filter(opportunity=opportunity) @org_member_required