From d0c1dcf312a76094b89a672dddb8a2bf95ae8f67 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 9 Jul 2024 15:02:39 +0530 Subject: [PATCH 01/19] added update_status method on approval and rejection --- commcare_connect/opportunity/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index 3f470330..178fdad6 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -788,7 +788,7 @@ def visit_verification(request, org_slug=None, pk=None): @org_member_required def approve_visit(request, org_slug=None, pk=None): user_visit = UserVisit.objects.get(pk=pk) - user_visit.status = VisitValidationStatus.approved + user_visit.update_status(VisitValidationStatus.approved) user_visit.save() opp_id = user_visit.opportunity_id access = OpportunityAccess.objects.get(user_id=user_visit.user_id, opportunity_id=opp_id) @@ -799,7 +799,7 @@ def approve_visit(request, org_slug=None, pk=None): @org_member_required def reject_visit(request, org_slug=None, pk=None): user_visit = UserVisit.objects.get(pk=pk) - user_visit.status = VisitValidationStatus.rejected + user_visit.update_status(VisitValidationStatus.rejected) user_visit.save() access = OpportunityAccess.objects.get(user_id=user_visit.user_id, opportunity_id=user_visit.opportunity_id) update_payment_accrued(opportunity=access.opportunity, users=[access.user]) From a44922db5d10ab2a5dba291342c70236ffeb1f14 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Wed, 10 Jul 2024 14:16:57 +0530 Subject: [PATCH 02/19] Override save method to update the date automatically --- commcare_connect/form_receiver/processor.py | 14 ++++++------ commcare_connect/opportunity/models.py | 24 +++++++++++++++----- commcare_connect/opportunity/tasks.py | 4 ++-- commcare_connect/opportunity/views.py | 4 ++-- commcare_connect/opportunity/visit_import.py | 8 +++---- 5 files changed, 33 insertions(+), 21 deletions(-) diff --git a/commcare_connect/form_receiver/processor.py b/commcare_connect/form_receiver/processor.py index 9ad7f727..43021eb9 100644 --- a/commcare_connect/form_receiver/processor.py +++ b/commcare_connect/form_receiver/processor.py @@ -194,17 +194,17 @@ def process_deliver_unit(user, xform: XForm, app: CommCareApp, opportunity: Oppo or counts["total"] >= claim_limit.max_visits or datetime.date.today() > claim.end_date ): - user_visit.update_status(VisitValidationStatus.over_limit) + user_visit.status = VisitValidationStatus.over_limit if not completed_work.status == CompletedWorkStatus.over_limit: - completed_work.update_status(CompletedWorkStatus.over_limit) + completed_work.status = CompletedWorkStatus.over_limit completed_work_needs_save = True elif counts["entity"] > 0: - user_visit.update_status(VisitValidationStatus.duplicate) + user_visit.status = VisitValidationStatus.duplicate flags = [] opportunity_flags, _ = OpportunityVerificationFlags.objects.get_or_create(opportunity=opportunity) if counts["entity"] > 0: - user_visit.update_status(VisitValidationStatus.duplicate) + user_visit.status = VisitValidationStatus.duplicate if opportunity_flags.duplicate: flags.append(["duplicate", "A beneficiary with the same identifier already exists"]) if opportunity_flags.duration > 0 and xform.metadata.duration < datetime.timedelta( @@ -243,7 +243,7 @@ def process_deliver_unit(user, xform: XForm, app: CommCareApp, opportunity: Oppo if access.suspended: flags.append(["user_suspended", "This user is suspended from the opportunity."]) - user_visit.update_status(VisitValidationStatus.rejected) + user_visit.status = VisitValidationStatus.rejected if flags: user_visit.flagged = True user_visit.flag_reason = {"flags": flags} @@ -253,14 +253,14 @@ def process_deliver_unit(user, xform: XForm, app: CommCareApp, opportunity: Oppo and user_visit.status == VisitValidationStatus.pending and not user_visit.flagged ): - user_visit.update_status(VisitValidationStatus.approved) + user_visit.status = VisitValidationStatus.approved user_visit.save() if ( completed_work is not None and completed_work.completed_count > 0 and completed_work.status == CompletedWorkStatus.incomplete ): - completed_work.update_status(CompletedWorkStatus.pending) + completed_work.status = CompletedWorkStatus.pending completed_work_needs_save = True if completed_work_needs_save: completed_work.save() diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 88054dbb..634e5330 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -408,9 +408,15 @@ class CompletedWork(models.Model): reason = models.CharField(max_length=300, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def update_status(self, status): - self.status = status - self.status_modified_date = now() + def save(self, *args, **kwargs): + if self.pk is not None: + old_instance = CompletedWork.objects.get(pk=self.pk) + if old_instance.status != self.status: + self.status_modified_date = now() + else: + self.status_modified_date = now() + + super().save(*args, **kwargs) # TODO: add caching on this property @property @@ -508,9 +514,15 @@ class UserVisit(XFormBaseModel): completed_work = models.ForeignKey(CompletedWork, on_delete=models.DO_NOTHING, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def update_status(self, status): - self.status = status - self.status_modified_date = now() + def save(self, *args, **kwargs): + if self.pk is not None: + old_instance = UserVisit.objects.get(pk=self.pk) + if old_instance.status != self.status: + self.status_modified_date = now() + else: + self.status_modified_date = now() + + super().save(*args, **kwargs) @property def images(self): diff --git a/commcare_connect/opportunity/tasks.py b/commcare_connect/opportunity/tasks.py index 0e8ef2cc..6d840169 100644 --- a/commcare_connect/opportunity/tasks.py +++ b/commcare_connect/opportunity/tasks.py @@ -324,10 +324,10 @@ def bulk_approve_completed_work(): approved_count = completed_work.approved_count visits = completed_work.uservisit_set.values_list("status", "reason") if any(status == "rejected" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.rejected) + completed_work.status = CompletedWorkStatus.rejected completed_work.reason = "\n".join(reason for _, reason in visits if reason) elif all(status == "approved" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.approved) + completed_work.status = CompletedWorkStatus.approved if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: access.payment_accrued += approved_count * completed_work.payment_unit.amount completed_work.save() diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index 178fdad6..3f470330 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -788,7 +788,7 @@ def visit_verification(request, org_slug=None, pk=None): @org_member_required def approve_visit(request, org_slug=None, pk=None): user_visit = UserVisit.objects.get(pk=pk) - user_visit.update_status(VisitValidationStatus.approved) + user_visit.status = VisitValidationStatus.approved user_visit.save() opp_id = user_visit.opportunity_id access = OpportunityAccess.objects.get(user_id=user_visit.user_id, opportunity_id=opp_id) @@ -799,7 +799,7 @@ def approve_visit(request, org_slug=None, pk=None): @org_member_required def reject_visit(request, org_slug=None, pk=None): user_visit = UserVisit.objects.get(pk=pk) - user_visit.update_status(VisitValidationStatus.rejected) + user_visit.status = VisitValidationStatus.rejected user_visit.save() access = OpportunityAccess.objects.get(user_id=user_visit.user_id, opportunity_id=user_visit.opportunity_id) update_payment_accrued(opportunity=access.opportunity, users=[access.user]) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index 4814d6d2..3898d2b3 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -107,7 +107,7 @@ def _bulk_update_visit_status(opportunity: Opportunity, dataset: Dataset): seen_completed_works.add(visit.completed_work_id) status = status_by_visit_id[visit.xform_id] if visit.status != status: - visit.update_status(status) + visit.status = status reason = reasons_by_visit_id.get(visit.xform_id) if visit.status == VisitValidationStatus.rejected and reason: visit.reason = reason @@ -135,10 +135,10 @@ def update_payment_accrued(opportunity: Opportunity, users): if opportunity.auto_approve_payments: visits = completed_work.uservisit_set.values_list("status", "reason") if any(status == "rejected" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.rejected) + completed_work.status = CompletedWorkStatus.rejected completed_work.reason = "\n".join(reason for _, reason in visits if reason) elif all(status == "approved" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.approved) + completed_work.status = CompletedWorkStatus.approved approved_count = completed_work.approved_count if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: access.payment_accrued += approved_count * completed_work.payment_unit.amount @@ -274,7 +274,7 @@ def _bulk_update_completed_work_status(opportunity: Opportunity, dataset: Datase seen_completed_works.add(str(completed_work.id)) status = status_by_work_id[str(completed_work.id)] if completed_work.status != status: - completed_work.update_status(status) + completed_work.status = status reason = reasons_by_work_id.get(str(completed_work.id)) if completed_work.status == CompletedWorkStatus.rejected and reason: completed_work.reason = reason From 82fecf0cc762526c8613799842530418706d08db Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 11 Jul 2024 10:54:35 +0530 Subject: [PATCH 03/19] Override setattr method to update the status_modified_date when status is updated --- commcare_connect/opportunity/models.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 634e5330..77747802 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -408,16 +408,11 @@ class CompletedWork(models.Model): reason = models.CharField(max_length=300, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def save(self, *args, **kwargs): - if self.pk is not None: - old_instance = CompletedWork.objects.get(pk=self.pk) - if old_instance.status != self.status: - self.status_modified_date = now() - else: + def __setattr__(self, name, value): + super().__setattr__(name, value) + if name == "status" and hasattr(self, "status") and getattr(self, "status", None) != value: self.status_modified_date = now() - super().save(*args, **kwargs) - # TODO: add caching on this property @property def completed_count(self): @@ -514,16 +509,11 @@ class UserVisit(XFormBaseModel): completed_work = models.ForeignKey(CompletedWork, on_delete=models.DO_NOTHING, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def save(self, *args, **kwargs): - if self.pk is not None: - old_instance = UserVisit.objects.get(pk=self.pk) - if old_instance.status != self.status: - self.status_modified_date = now() - else: + def __setattr__(self, name, value): + super().__setattr__(name, value) + if name == "status" and hasattr(self, "status") and getattr(self, "status", None) != value: self.status_modified_date = now() - super().save(*args, **kwargs) - @property def images(self): return BlobMeta.objects.filter(parent_id=self.xform_id, content_type__startswith="image/") From 2d63bd2a4e22d281945c6d00a507a2fbf36d8404 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 11 Jul 2024 11:10:56 +0530 Subject: [PATCH 04/19] Revert "Override setattr method to update the status_modified_date when status is updated" This reverts commit 82fecf0cc762526c8613799842530418706d08db. --- commcare_connect/opportunity/models.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 77747802..634e5330 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -408,11 +408,16 @@ class CompletedWork(models.Model): reason = models.CharField(max_length=300, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def __setattr__(self, name, value): - super().__setattr__(name, value) - if name == "status" and hasattr(self, "status") and getattr(self, "status", None) != value: + def save(self, *args, **kwargs): + if self.pk is not None: + old_instance = CompletedWork.objects.get(pk=self.pk) + if old_instance.status != self.status: + self.status_modified_date = now() + else: self.status_modified_date = now() + super().save(*args, **kwargs) + # TODO: add caching on this property @property def completed_count(self): @@ -509,11 +514,16 @@ class UserVisit(XFormBaseModel): completed_work = models.ForeignKey(CompletedWork, on_delete=models.DO_NOTHING, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def __setattr__(self, name, value): - super().__setattr__(name, value) - if name == "status" and hasattr(self, "status") and getattr(self, "status", None) != value: + def save(self, *args, **kwargs): + if self.pk is not None: + old_instance = UserVisit.objects.get(pk=self.pk) + if old_instance.status != self.status: + self.status_modified_date = now() + else: self.status_modified_date = now() + super().save(*args, **kwargs) + @property def images(self): return BlobMeta.objects.filter(parent_id=self.xform_id, content_type__startswith="image/") From 236757036328462326d47f87c5f22fa09c2dfbb8 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 11 Jul 2024 12:07:29 +0530 Subject: [PATCH 05/19] Override init and setattr method --- commcare_connect/opportunity/models.py | 34 ++++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/commcare_connect/opportunity/models.py b/commcare_connect/opportunity/models.py index 634e5330..0d227871 100644 --- a/commcare_connect/opportunity/models.py +++ b/commcare_connect/opportunity/models.py @@ -408,15 +408,16 @@ class CompletedWork(models.Model): reason = models.CharField(max_length=300, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def save(self, *args, **kwargs): - if self.pk is not None: - old_instance = CompletedWork.objects.get(pk=self.pk) - if old_instance.status != self.status: + def __init__(self, *args, **kwargs): + self.status = CompletedWorkStatus.incomplete + self.status_modified_date = now() + super().__init__(*args, **kwargs) + + def __setattr__(self, name, value): + if name == "status": + if getattr(self, "status", None) != value: # Check if status has changed self.status_modified_date = now() - else: - self.status_modified_date = now() - - super().save(*args, **kwargs) + super().__setattr__(name, value) # TODO: add caching on this property @property @@ -514,15 +515,16 @@ class UserVisit(XFormBaseModel): completed_work = models.ForeignKey(CompletedWork, on_delete=models.DO_NOTHING, null=True, blank=True) status_modified_date = models.DateTimeField(null=True) - def save(self, *args, **kwargs): - if self.pk is not None: - old_instance = UserVisit.objects.get(pk=self.pk) - if old_instance.status != self.status: - self.status_modified_date = now() - else: - self.status_modified_date = now() + def __init__(self, *args, **kwargs): + self.status = VisitValidationStatus.pending + self.status_modified_date = now() + super().__init__(*args, **kwargs) - super().save(*args, **kwargs) + def __setattr__(self, name, value): + if name == "status": + if getattr(self, "status", None) != value: + self.status_modified_date = now() + super().__setattr__(name, value) @property def images(self): From c6b1665aa97f77de4e0b0d44a5f41f5133923fb8 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Fri, 12 Jul 2024 09:32:03 +0530 Subject: [PATCH 06/19] Added changes in query set queries --- commcare_connect/opportunity/visit_import.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index 3898d2b3..84d07625 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -114,7 +114,7 @@ def _bulk_update_visit_status(opportunity: Opportunity, dataset: Dataset): to_update.append(visit) user_ids.add(visit.user_id) - UserVisit.objects.bulk_update(to_update, fields=["status", "reason"]) + UserVisit.objects.bulk_update(to_update, fields=["status", "reason", "status_modified_date"]) missing_visits |= set(visit_batch) - seen_visits update_payment_accrued(opportunity, users=user_ids) @@ -280,7 +280,7 @@ def _bulk_update_completed_work_status(opportunity: Opportunity, dataset: Datase completed_work.reason = reason to_update.append(completed_work) user_ids.add(completed_work.opportunity_access.user_id) - CompletedWork.objects.bulk_update(to_update, fields=["status", "reason"]) + CompletedWork.objects.bulk_update(to_update, fields=["status", "reason", "status_modified_date"]) missing_completed_works |= set(work_batch) - seen_completed_works update_payment_accrued(opportunity, users=user_ids) return CompletedWorkImportStatus(seen_completed_works, missing_completed_works) From 3fbdcfb8187353925288d635488345c30f98a268 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 16 Jul 2024 20:38:42 +0530 Subject: [PATCH 07/19] Resolved conflicts --- commcare_connect/opportunity/visit_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index 1b2621f1..11213e41 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -121,7 +121,7 @@ def _bulk_update_visit_status(opportunity: Opportunity, dataset: Dataset): to_update.append(visit) user_ids.add(visit.user_id) - UserVisit.objects.bulk_update(to_update, fields=["status", "reason"]) + UserVisit.objects.bulk_update(to_update, fields=["status", "reason", "status_modified_date"]) missing_visits |= set(visit_batch) - seen_visits update_payment_accrued(opportunity, users=user_ids) @@ -145,7 +145,7 @@ def update_payment_accrued(opportunity: Opportunity, users): completed_work.update_status(CompletedWorkStatus.rejected) completed_work.reason = "\n".join(reason for _, reason in visits if reason) elif all(status == "approved" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.approved) + completed_work.status = CompletedWorkStatus.approved approved_count = completed_work.approved_count if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: access.payment_accrued += approved_count * completed_work.payment_unit.amount @@ -293,7 +293,7 @@ def _bulk_update_completed_work_status(opportunity: Opportunity, dataset: Datase if changed: to_update.append(completed_work) user_ids.add(completed_work.opportunity_access.user_id) - CompletedWork.objects.bulk_update(to_update, fields=["status", "reason"]) + CompletedWork.objects.bulk_update(to_update, fields=["status", "reason", "status_modified_date"]) missing_completed_works |= set(work_batch) - seen_completed_works update_payment_accrued(opportunity, users=user_ids) return CompletedWorkImportStatus(seen_completed_works, missing_completed_works) From 8685db8fedb64ae295d333df8eb6d9944da9652f Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Tue, 16 Jul 2024 20:44:15 +0530 Subject: [PATCH 08/19] Resolved conflicts and updated the conflict as per this pr --- commcare_connect/opportunity/visit_import.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index 11213e41..075b21e8 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -110,7 +110,7 @@ def _bulk_update_visit_status(opportunity: Opportunity, dataset: Dataset): changed = False if visit.status != status: - visit.update_status(status) + visit.status = status changed = True if status == VisitValidationStatus.rejected and reason and reason != visit.reason: @@ -142,7 +142,7 @@ def update_payment_accrued(opportunity: Opportunity, users): if opportunity.auto_approve_payments: visits = completed_work.uservisit_set.values_list("status", "reason") if any(status == "rejected" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.rejected) + completed_work.status = CompletedWorkStatus.rejected completed_work.reason = "\n".join(reason for _, reason in visits if reason) elif all(status == "approved" for status, _ in visits): completed_work.status = CompletedWorkStatus.approved @@ -284,7 +284,7 @@ def _bulk_update_completed_work_status(opportunity: Opportunity, dataset: Datase changed = False if completed_work.status != status: - completed_work.update_status(status) + completed_work.status = status changed = True if status == CompletedWorkStatus.rejected and reason and reason != completed_work.reason: completed_work.reason = reason From ff522efb2847eac3625a13fd8832efd7e7c4adaf Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Wed, 24 Jul 2024 15:59:35 +0530 Subject: [PATCH 09/19] Added management command --- .../commands/auto_approval_opportunities.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 commcare_connect/opportunity/management/commands/auto_approval_opportunities.py diff --git a/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py b/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py new file mode 100644 index 00000000..8c6ac3f2 --- /dev/null +++ b/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py @@ -0,0 +1,27 @@ +from django.core.management import BaseCommand + +from commcare_connect.opportunity.models import Opportunity, OpportunityAccess +from commcare_connect.opportunity.visit_import import update_payment_accrued + + +class Command(BaseCommand): + help = "Run auto-approval logic over opportunity" + + def add_arguments(self, parser): + parser.add_argument( + "--opp", type=int, required=True, help="ID of the opportunity to run auto-approval logic on" + ) + + def handle(self, *args, **options): + opp_id = options["opp"] + try: + opportunity = Opportunity.objects.get(id=opp_id) + access_records = OpportunityAccess.objects.filter(opportunity=opportunity) + users = [access.user for access in access_records] + + update_payment_accrued(opportunity=opportunity, users=users) + + self.stdout.write(self.style.SUCCESS(f"Successfully processed opportunity with id {opp_id}")) + + except Opportunity.DoesNotExist: + self.stdout.write(self.style.ERROR(f"Opportunity with id {opp_id} does not exist.")) From 554bdc6bf86cad550178524ebc641c7ef13d6580 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Thu, 25 Jul 2024 15:55:53 +0530 Subject: [PATCH 10/19] Fix create claim limits on adding new payment units --- commcare_connect/opportunity/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/commcare_connect/opportunity/views.py b/commcare_connect/opportunity/views.py index c51b8bac..0ea1cb77 100644 --- a/commcare_connect/opportunity/views.py +++ b/commcare_connect/opportunity/views.py @@ -494,6 +494,9 @@ def add_payment_unit(request, org_slug=None, pk=None): parent_payment_unit=form.instance.id ) messages.success(request, f"Payment unit {form.instance.name} created.") + claims = OpportunityClaim.objects.filter(opportunity_access__opportunity=opportunity) + for claim in claims: + OpportunityClaimLimit.create_claim_limits(opportunity, claim) return redirect("opportunity:add_payment_units", org_slug=request.org.slug, pk=opportunity.id) elif request.POST: messages.error(request, "Invalid Data") From ed79fc655c0d24e3eacb02f8d82115dbfedf05bc Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 25 Jul 2024 16:02:10 +0530 Subject: [PATCH 11/19] Added test --- .../opportunity/tests/test_visit_import.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/tests/test_visit_import.py b/commcare_connect/opportunity/tests/test_visit_import.py index a707d6cf..8b8971c6 100644 --- a/commcare_connect/opportunity/tests/test_visit_import.py +++ b/commcare_connect/opportunity/tests/test_visit_import.py @@ -3,14 +3,17 @@ from itertools import chain import pytest +from django.utils.timezone import now from tablib import Dataset from commcare_connect.conftest import MobileUserFactory from commcare_connect.opportunity.models import ( + CompletedWork, CompletedWorkStatus, Opportunity, OpportunityAccess, Payment, + PaymentUnit, UserVisit, VisitValidationStatus, ) @@ -23,6 +26,7 @@ ) from commcare_connect.opportunity.visit_import import ( ImportException, + _bulk_update_completed_work_status, _bulk_update_payments, _bulk_update_visit_status, get_status_by_visit_id, @@ -39,10 +43,38 @@ def test_bulk_update_visit_status(opportunity: Opportunity, mobile_user: User): dataset = Dataset(headers=["visit id", "status", "rejected reason"]) dataset.extend([[visit.xform_id, VisitValidationStatus.approved.value, ""] for visit in visits]) + before_update = now() import_status = _bulk_update_visit_status(opportunity, dataset) + after_update = now() + assert not import_status.missing_visits - after_status = set(UserVisit.objects.filter(opportunity=opportunity).values_list("status", flat=True)) - assert after_status == {VisitValidationStatus.approved.value} + + updated_visits = UserVisit.objects.filter(opportunity=opportunity) + for visit in updated_visits: + assert visit.status == VisitValidationStatus.approved.value + assert visit.status_modified_date is not None + assert before_update <= visit.status_modified_date <= after_update + + +@pytest.mark.django_db +def test_bulk_update_completed_work_status(opportunity: Opportunity, mobile_user: User): + access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity) + payment_unit = PaymentUnit.objects.get(opportunity=opportunity) + DeliverUnitFactory(payment_unit=payment_unit, app=opportunity.deliver_app, optional=False) + + completed_works = CompletedWorkFactory.create_batch(5, opportunity_access=access, payment_unit=payment_unit) + dataset = Dataset(headers=["instance id", "payment approval", "rejected reason"]) + dataset.extend([[work.id, CompletedWorkStatus.approved.value, ""] for work in completed_works]) + + before_update = now() + _bulk_update_completed_work_status(opportunity=opportunity, dataset=dataset) + after_update = now() + + updated_work = CompletedWork.objects.filter(opportunity_access=access) + for work in updated_work: + assert work.status == CompletedWorkStatus.approved.value + assert work.status_modified_date is not None + assert before_update <= work.status_modified_date <= after_update @pytest.mark.django_db From ac3e4b9c038c46091245f57024ec42363a964124 Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Thu, 25 Jul 2024 16:11:53 +0530 Subject: [PATCH 12/19] Merged and resolved conflicts --- .../opportunity/tests/test_visit_import.py | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/commcare_connect/opportunity/tests/test_visit_import.py b/commcare_connect/opportunity/tests/test_visit_import.py index bc335f5d..c72844f6 100644 --- a/commcare_connect/opportunity/tests/test_visit_import.py +++ b/commcare_connect/opportunity/tests/test_visit_import.py @@ -4,15 +4,18 @@ from itertools import chain import pytest +from django.utils.timezone import now from tablib import Dataset from commcare_connect.conftest import MobileUserFactory from commcare_connect.opportunity.models import ( CatchmentArea, + CompletedWork, CompletedWorkStatus, Opportunity, OpportunityAccess, Payment, + PaymentUnit, UserVisit, VisitValidationStatus, ) @@ -27,6 +30,7 @@ from commcare_connect.opportunity.visit_import import ( ImportException, _bulk_update_catchments, + _bulk_update_completed_work_status, _bulk_update_payments, _bulk_update_visit_status, get_status_by_visit_id, @@ -43,10 +47,38 @@ def test_bulk_update_visit_status(opportunity: Opportunity, mobile_user: User): dataset = Dataset(headers=["visit id", "status", "rejected reason"]) dataset.extend([[visit.xform_id, VisitValidationStatus.approved.value, ""] for visit in visits]) + before_update = now() import_status = _bulk_update_visit_status(opportunity, dataset) + after_update = now() + assert not import_status.missing_visits - after_status = set(UserVisit.objects.filter(opportunity=opportunity).values_list("status", flat=True)) - assert after_status == {VisitValidationStatus.approved.value} + + updated_visits = UserVisit.objects.filter(opportunity=opportunity) + for visit in updated_visits: + assert visit.status == VisitValidationStatus.approved.value + assert visit.status_modified_date is not None + assert before_update <= visit.status_modified_date <= after_update + + +@pytest.mark.django_db +def test_bulk_update_completed_work_status(opportunity: Opportunity, mobile_user: User): + access = OpportunityAccess.objects.get(user=mobile_user, opportunity=opportunity) + payment_unit = PaymentUnit.objects.get(opportunity=opportunity) + DeliverUnitFactory(payment_unit=payment_unit, app=opportunity.deliver_app, optional=False) + + completed_works = CompletedWorkFactory.create_batch(5, opportunity_access=access, payment_unit=payment_unit) + dataset = Dataset(headers=["instance id", "payment approval", "rejected reason"]) + dataset.extend([[work.id, CompletedWorkStatus.approved.value, ""] for work in completed_works]) + + before_update = now() + _bulk_update_completed_work_status(opportunity=opportunity, dataset=dataset) + after_update = now() + + updated_work = CompletedWork.objects.filter(opportunity_access=access) + for work in updated_work: + assert work.status == CompletedWorkStatus.approved.value + assert work.status_modified_date is not None + assert before_update <= work.status_modified_date <= after_update @pytest.mark.django_db From fb7d9c1836a80f8bbaff5ea17e8d7b0616cd7bdf Mon Sep 17 00:00:00 2001 From: hemant10yadav Date: Fri, 26 Jul 2024 09:43:58 +0530 Subject: [PATCH 13/19] Refactor the common logic and extracted out it in a util func --- .../commands/auto_approval_opportunities.py | 27 +++++++++------- commcare_connect/opportunity/tasks.py | 15 ++------- .../opportunity/utils/__init__.py | 0 .../opportunity/utils/completed_work.py | 31 +++++++++++++++++++ commcare_connect/opportunity/visit_import.py | 17 ++-------- 5 files changed, 50 insertions(+), 40 deletions(-) create mode 100644 commcare_connect/opportunity/utils/__init__.py create mode 100644 commcare_connect/opportunity/utils/completed_work.py diff --git a/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py b/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py index 8c6ac3f2..b36e1b07 100644 --- a/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py +++ b/commcare_connect/opportunity/management/commands/auto_approval_opportunities.py @@ -1,7 +1,7 @@ from django.core.management import BaseCommand -from commcare_connect.opportunity.models import Opportunity, OpportunityAccess -from commcare_connect.opportunity.visit_import import update_payment_accrued +from commcare_connect.opportunity.models import CompletedWorkStatus, Opportunity, OpportunityAccess +from commcare_connect.opportunity.utils.completed_work import update_status_and_compute_payment class Command(BaseCommand): @@ -12,16 +12,19 @@ def add_arguments(self, parser): "--opp", type=int, required=True, help="ID of the opportunity to run auto-approval logic on" ) - def handle(self, *args, **options): - opp_id = options["opp"] + def handle(self, *args, opp: int, **options): try: - opportunity = Opportunity.objects.get(id=opp_id) - access_records = OpportunityAccess.objects.filter(opportunity=opportunity) - users = [access.user for access in access_records] - - update_payment_accrued(opportunity=opportunity, users=users) - - self.stdout.write(self.style.SUCCESS(f"Successfully processed opportunity with id {opp_id}")) + opportunity = Opportunity.objects.get(id=opp) + access_objects = OpportunityAccess.objects.filter( + opportunity=opportunity, suspended=False, opportunity__auto_approve_payments=True + ) + for access in access_objects: + completed_works = access.completedwork_set.exclude( + status__in=[CompletedWorkStatus.rejected, CompletedWorkStatus.over_limit] + ) + update_status_and_compute_payment(completed_works, opportunity, False) + + self.stdout.write(self.style.SUCCESS(f"Successfully processed opportunity with id {opp}")) except Opportunity.DoesNotExist: - self.stdout.write(self.style.ERROR(f"Opportunity with id {opp_id} does not exist.")) + self.stdout.write(self.style.ERROR(f"Opportunity with id {opp} does not exist.")) diff --git a/commcare_connect/opportunity/tasks.py b/commcare_connect/opportunity/tasks.py index d7c495fc..fff45536 100644 --- a/commcare_connect/opportunity/tasks.py +++ b/commcare_connect/opportunity/tasks.py @@ -39,6 +39,7 @@ UserVisit, VisitValidationStatus, ) +from commcare_connect.opportunity.utils.completed_work import update_status_and_compute_payment from commcare_connect.users.models import User from commcare_connect.utils.datetime import is_date_before from commcare_connect.utils.sms import send_sms @@ -333,19 +334,7 @@ def bulk_approve_completed_work(): completed_works = access.completedwork_set.exclude( status__in=[CompletedWorkStatus.rejected, CompletedWorkStatus.over_limit] ) - access.payment_accrued = 0 - for completed_work in completed_works: - if completed_work.completed_count > 0: - approved_count = completed_work.approved_count - visits = completed_work.uservisit_set.values_list("status", "reason") - if any(status == "rejected" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.rejected) - completed_work.reason = "\n".join(reason for _, reason in visits if reason) - elif all(status == "approved" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.approved) - if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: - access.payment_accrued += approved_count * completed_work.payment_unit.amount - completed_work.save() + access.payment_accrued = update_status_and_compute_payment(completed_works, access.opportunity) access.save() diff --git a/commcare_connect/opportunity/utils/__init__.py b/commcare_connect/opportunity/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/commcare_connect/opportunity/utils/completed_work.py b/commcare_connect/opportunity/utils/completed_work.py new file mode 100644 index 00000000..2019f4e3 --- /dev/null +++ b/commcare_connect/opportunity/utils/completed_work.py @@ -0,0 +1,31 @@ +from commcare_connect.opportunity.models import CompletedWorkStatus + + +def update_status_and_compute_payment(completed_works, opportunity, compute_payment=True): + """ + Updates the status of completed works and optionally calculates total payment_accrued. + """ + payment_accrued = 0 + for completed_work in completed_works: + if completed_work.completed_count < 1: + continue + + if opportunity.auto_approve_payments: + update_completed_work_status(completed_work) + + if compute_payment: + approved_count = completed_work.approved_count + if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: + payment_accrued += approved_count * completed_work.payment_unit.amount + return payment_accrued + + +def update_completed_work_status(completed_work): + visits = completed_work.uservisit_set.values_list("status", "reason") + if any(status == "rejected" for status, _ in visits): + completed_work.update_status(CompletedWorkStatus.rejected) + completed_work.reason = "\n".join(reason for _, reason in visits if reason) + elif all(status == "approved" for status, _ in visits): + completed_work.update_status(CompletedWorkStatus.approved) + + completed_work.save() diff --git a/commcare_connect/opportunity/visit_import.py b/commcare_connect/opportunity/visit_import.py index bb98e381..04a645c3 100644 --- a/commcare_connect/opportunity/visit_import.py +++ b/commcare_connect/opportunity/visit_import.py @@ -18,6 +18,7 @@ VisitValidationStatus, ) from commcare_connect.opportunity.tasks import send_payment_notification +from commcare_connect.opportunity.utils.completed_work import update_status_and_compute_payment from commcare_connect.utils.file import get_file_extension from commcare_connect.utils.itertools import batched @@ -154,21 +155,7 @@ def update_payment_accrued(opportunity: Opportunity, users): completed_works = access.completedwork_set.exclude( status__in=[CompletedWorkStatus.rejected, CompletedWorkStatus.over_limit] ).select_related("payment_unit") - access.payment_accrued = 0 - for completed_work in completed_works: - # Auto Approve Payment conditions - if completed_work.completed_count > 0: - if opportunity.auto_approve_payments: - visits = completed_work.uservisit_set.values_list("status", "reason") - if any(status == "rejected" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.rejected) - completed_work.reason = "\n".join(reason for _, reason in visits if reason) - elif all(status == "approved" for status, _ in visits): - completed_work.update_status(CompletedWorkStatus.approved) - approved_count = completed_work.approved_count - if approved_count > 0 and completed_work.status == CompletedWorkStatus.approved: - access.payment_accrued += approved_count * completed_work.payment_unit.amount - completed_work.save() + access.payment_accrued = update_status_and_compute_payment(completed_works, opportunity) access.save() From 31e076f700b7c45473dcde3654e6b6e85f477c0f Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Mon, 29 Jul 2024 11:23:51 +0530 Subject: [PATCH 14/19] Add create new organization on user signup --- commcare_connect/organization/forms.py | 13 +++++++++++ commcare_connect/organization/views.py | 20 +++++++++++++++- .../organization/organization_create.html | 23 +++++++++++++++++++ commcare_connect/users/signals.py | 13 ++--------- commcare_connect/users/views.py | 2 ++ config/urls.py | 3 +++ 6 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 commcare_connect/templates/organization/organization_create.html diff --git a/commcare_connect/organization/forms.py b/commcare_connect/organization/forms.py index d579445f..f22a4b7b 100644 --- a/commcare_connect/organization/forms.py +++ b/commcare_connect/organization/forms.py @@ -77,3 +77,16 @@ def clean_users(self): user_data = self.cleaned_data["users"] split_users = [line.strip() for line in user_data.splitlines() if line.strip()] return split_users + + +OrganizationCreationForm = forms.modelform_factory( + Organization, + fields=("name",), + labels={"name": "Organization Name"}, + help_texts={ + "name": ( + "This would be used to create the Organization URL," + " and you will not be able to change the URL in future." + ) + }, +) diff --git a/commcare_connect/organization/views.py b/commcare_connect/organization/views.py index db961f25..f2e5da30 100644 --- a/commcare_connect/organization/views.py +++ b/commcare_connect/organization/views.py @@ -9,11 +9,29 @@ from commcare_connect import connect_id_client from commcare_connect.connect_id_client.models import Credential from commcare_connect.organization.decorators import org_admin_required -from commcare_connect.organization.forms import AddCredentialForm, MembershipForm, OrganizationChangeForm +from commcare_connect.organization.forms import ( + AddCredentialForm, + MembershipForm, + OrganizationChangeForm, + OrganizationCreationForm, +) from commcare_connect.organization.models import Organization, UserOrganizationMembership from commcare_connect.organization.tasks import add_credential_task, send_org_invite +@login_required +def organization_create(request): + form = OrganizationCreationForm(data=request.POST or None) + + if form.is_valid(): + org = form.save(commit=False) + org.save() + org.members.add(request.user, through_defaults={"role": UserOrganizationMembership.Role.ADMIN}) + return redirect("opportunity:list", org.slug) + + return render(request, "organization/organization_create.html", context={"form": form}) + + @org_admin_required def organization_home(request, org_slug): org = get_object_or_404(Organization, slug=org_slug) diff --git a/commcare_connect/templates/organization/organization_create.html b/commcare_connect/templates/organization/organization_create.html new file mode 100644 index 00000000..1817b30f --- /dev/null +++ b/commcare_connect/templates/organization/organization_create.html @@ -0,0 +1,23 @@ +{% extends "base.html" %} +{% load i18n %} +{% load static %} +{% load crispy_forms_tags %} + +{% block title %}New Organization - CommCare Connect{% endblock title %} + +{% block content %} +
+
+
+
Create a New Organization
+
+ {% csrf_token %} + {{ form|crispy }} +
+ +
+
+
+{% endblock content %} diff --git a/commcare_connect/users/signals.py b/commcare_connect/users/signals.py index 03fa1791..49a449c1 100644 --- a/commcare_connect/users/signals.py +++ b/commcare_connect/users/signals.py @@ -1,19 +1,10 @@ from allauth.account.signals import user_logged_in, user_signed_up from django.dispatch import receiver - -from commcare_connect.organization.models import Organization, UserOrganizationMembership -from commcare_connect.users.models import User +from django.shortcuts import redirect @receiver(user_signed_up) @receiver(user_logged_in) def create_org_for_user(request, user, **kwargs): if not user.memberships.exists(): - _create_default_org_for_user(user) - - -def _create_default_org_for_user(user: User): - organization = Organization.objects.create(name=user.email.split("@")[0]) - organization.members.add(user, through_defaults={"role": UserOrganizationMembership.Role.ADMIN}) - organization.save() - return organization + return redirect("organization_create") diff --git a/commcare_connect/users/views.py b/commcare_connect/users/views.py index 221bd33c..6c14aac2 100644 --- a/commcare_connect/users/views.py +++ b/commcare_connect/users/views.py @@ -57,6 +57,8 @@ class UserRedirectView(LoginRequiredMixin, RedirectView): permanent = False def get_redirect_url(self): + if not self.request.user.memberships.exists(): + return reverse("organization_create") organization = self.request.org if organization: return reverse("opportunity:list", kwargs={"org_slug": organization.slug}) diff --git a/config/urls.py b/config/urls.py index 00f7c955..45fbc63e 100644 --- a/config/urls.py +++ b/config/urls.py @@ -7,6 +7,8 @@ from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView from rest_framework.authtoken.views import obtain_auth_token +from commcare_connect.organization.views import organization_create + urlpatterns = [ path("", TemplateView.as_view(template_name="pages/home.html"), name="home"), path("about/", TemplateView.as_view(template_name="pages/about.html"), name="about"), @@ -17,6 +19,7 @@ path("users/", include("commcare_connect.users.urls", namespace="users")), path("accounts/", include("allauth.urls")), # Your stuff: custom urls includes go here + path("register/organization", organization_create, name="organization_create"), path("a//", include("commcare_connect.organization.urls")), path("a//opportunity/", include("commcare_connect.opportunity.urls", namespace="opportunity")), path("admin_reports/", include("commcare_connect.reports.urls")), From e2277880d5e06b4937fda040e73fecdf8f31bac2 Mon Sep 17 00:00:00 2001 From: Pawan Verma Date: Mon, 29 Jul 2024 11:44:30 +0530 Subject: [PATCH 15/19] Add org create link to org dropdown --- commcare_connect/templates/base.html | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/commcare_connect/templates/base.html b/commcare_connect/templates/base.html index 373f9704..1e3358c9 100644 --- a/commcare_connect/templates/base.html +++ b/commcare_connect/templates/base.html @@ -73,23 +73,27 @@ {% translate "Sign Out" %} - {% if request.org %} - - {% endif %} + {% else %} {% if ACCOUNT_ALLOW_REGISTRATION %}