Skip to content

Commit

Permalink
added view for delivery summary
Browse files Browse the repository at this point in the history
  • Loading branch information
hemant10yadav committed Oct 1, 2024
1 parent afc0c6e commit 2ea6a1e
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 85 deletions.
4 changes: 2 additions & 2 deletions commcare_connect/opportunity/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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")

Expand Down
71 changes: 1 addition & 70 deletions commcare_connect/opportunity/helpers.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -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),
]
17 changes: 17 additions & 0 deletions commcare_connect/opportunity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
6 changes: 5 additions & 1 deletion commcare_connect/opportunity/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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;
"""
)
14 changes: 9 additions & 5 deletions commcare_connect/opportunity/tests/test_helpers.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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:
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 3 additions & 6 deletions commcare_connect/opportunity/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -59,6 +56,7 @@
OpportunityAccess,
OpportunityClaim,
OpportunityClaimLimit,
OpportunityDeliverySummary,
OpportunityVerificationFlags,
Payment,
PaymentInvoice,
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 2ea6a1e

Please sign in to comment.