Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into pkv/synchronous-appro…
Browse files Browse the repository at this point in the history
…ved-status
  • Loading branch information
pxwxnvermx committed Nov 21, 2024
2 parents 88a0f52 + 464c0a0 commit eac4eeb
Show file tree
Hide file tree
Showing 30 changed files with 1,583 additions and 279 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,19 @@ For details on how this actions is configured see:
- https://aws.amazon.com/blogs/security/use-iam-roles-to-connect-github-actions-to-actions-in-aws/
- https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services

### Deploying to the staging environment

The project has a staging environment at [https://connect-staging.dimagi.com/](https://connect-staging.dimagi.com/),
which is connected to the staging environment of CommCare HQ at
[https://staging.commcarehq.org/](https://staging.commcarehq.org/).

By convention, the `pkv/staging` branch is used for changes that are on the staging environment.
To put your own changes on the staging environment, you can create merge your own branch into
`pkv/staging` and then push it to GitHub.

After that, you can deploy to the staging environment by manually running the `deploy`
[workflow from here](https://github.com/dimagi/commcare-connect/actions/workflows/deploy.yml).

### Custom Bootstrap Compilation

The generated CSS is set up with automatic Bootstrap recompilation with variables of your choice.
Expand Down
8 changes: 7 additions & 1 deletion commcare_connect/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
PaymentUnitFactory,
)
from commcare_connect.organization.models import Organization
from commcare_connect.program.tests.factories import ManagedOpportunityFactory
from commcare_connect.users.models import User
from commcare_connect.users.tests.factories import (
ConnectIdUserLinkFactory,
Expand Down Expand Up @@ -50,7 +51,12 @@ def user(db) -> User:
@pytest.fixture()
def opportunity(request):
verification_flags = getattr(request, "param", {}).get("verification_flags", {})
factory = OpportunityFactory(is_test=False)
opp_options = {"is_test": False}
opp_options.update(getattr(request, "param", {}).get("opp_options", {}))
if opp_options.get("managed", False):
factory = ManagedOpportunityFactory(**opp_options)
else:
factory = OpportunityFactory(**opp_options)
OpportunityVerificationFlagsFactory(opportunity=factory, **verification_flags)
return factory

Expand Down
7 changes: 5 additions & 2 deletions commcare_connect/connect_id_client/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,11 @@ def add_credential(organization: Organization, credential: str, users: list[str]
return


def fetch_credentials():
response = _make_request(GET, "/users/fetch_credentials")
def fetch_credentials(org_slug=None):
params = {}
if org_slug:
params["org_slug"] = org_slug
response = _make_request(GET, "/users/fetch_credentials", params=params)
data = response.json()
return [Credential(**c) for c in data["credentials"]]

Expand Down
59 changes: 42 additions & 17 deletions commcare_connect/form_receiver/tests/test_receiver_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
OpportunityClaimLimit,
OpportunityVerificationFlags,
UserVisit,
VisitReviewStatus,
VisitValidationStatus,
)
from commcare_connect.opportunity.tasks import bulk_approve_completed_work
Expand Down Expand Up @@ -172,24 +173,14 @@ def test_receiver_deliver_form_daily_visits_reached(
def test_receiver_deliver_form_max_visits_reached(
mobile_user_with_connect_link: User, api_client: APIClient, opportunity: Opportunity
):
def form_json(payment_unit):
deliver_unit = DeliverUnitFactory(app=opportunity.deliver_app, payment_unit=payment_unit)
stub = DeliverUnitStubFactory(id=deliver_unit.slug)
form_json = get_form_json(
form_block=stub.json,
domain=deliver_unit.app.cc_domain,
app_id=deliver_unit.app.cc_app_id,
)
return form_json

def submit_form_for_random_entity(form_json):
duplicate_json = deepcopy(form_json)
duplicate_json["form"]["deliver"]["entity_id"] = str(uuid4())
make_request(api_client, duplicate_json, mobile_user_with_connect_link)

payment_units = opportunity.paymentunit_set.all()
form_json1 = form_json(payment_units[0])
form_json2 = form_json(payment_units[1])
form_json1 = get_form_json_for_payment_unit(payment_units[0])
form_json2 = get_form_json_for_payment_unit(payment_units[1])
for _ in range(2):
submit_form_for_random_entity(form_json1)
submit_form_for_random_entity(form_json2)
Expand Down Expand Up @@ -465,7 +456,7 @@ def test_reciever_verification_flags_form_submission(
assert ["form_submission_period", expected_message] in visit.flag_reason.get("flags", [])


def test_reciever_verification_flags_duration(
def test_receiver_verification_flags_duration(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
Expand All @@ -478,7 +469,7 @@ def test_reciever_verification_flags_duration(
assert ["duration", "The form was completed too quickly."] in visit.flag_reason.get("flags", [])


def test_reciever_verification_flags_check_attachments(
def test_receiver_verification_flags_check_attachments(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
Expand All @@ -491,7 +482,7 @@ def test_reciever_verification_flags_check_attachments(
assert ["attachment_missing", "Form was submitted without attachements."] in visit.flag_reason.get("flags", [])


def test_reciever_verification_flags_form_json_rule(
def test_receiver_verification_flags_form_json_rule(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
Expand All @@ -509,7 +500,7 @@ def test_reciever_verification_flags_form_json_rule(
assert not visit.flagged


def test_reciever_verification_flags_form_json_rule_flagged(
def test_receiver_verification_flags_form_json_rule_flagged(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
Expand All @@ -531,7 +522,7 @@ def test_reciever_verification_flags_form_json_rule_flagged(
] in visit.flag_reason.get("flags", [])


def test_reciever_verification_flags_catchment_areas(
def test_receiver_verification_flags_catchment_areas(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
verification_flags = OpportunityVerificationFlags.objects.get(opportunity=opportunity)
Expand All @@ -550,6 +541,40 @@ def test_reciever_verification_flags_catchment_areas(
assert ["catchment", "Visit outside worker catchment areas"] in visit.flag_reason.get("flags", [])


@pytest.mark.parametrize("opportunity", [{"opp_options": {"managed": True, "org_pay_per_visit": 2}}], indirect=True)
@pytest.mark.parametrize(
"visit_status, review_status",
[
(VisitValidationStatus.approved, VisitReviewStatus.agree),
(VisitValidationStatus.pending, VisitReviewStatus.pending),
],
)
def test_receiver_visit_review_status(
mobile_user_with_connect_link: User, api_client: APIClient, opportunity: Opportunity, visit_status, review_status
):
assert opportunity.managed
form_json = get_form_json_for_payment_unit(opportunity.paymentunit_set.first())
if visit_status != VisitValidationStatus.approved:
form_json["metadata"]["location"] = None
make_request(api_client, form_json, mobile_user_with_connect_link)
visit = UserVisit.objects.get(user=mobile_user_with_connect_link)
if visit_status != VisitValidationStatus.approved:
assert visit.flagged
assert visit.status == visit_status
assert visit.review_status == review_status


def get_form_json_for_payment_unit(payment_unit):
deliver_unit = DeliverUnitFactory(app=payment_unit.opportunity.deliver_app, payment_unit=payment_unit)
stub = DeliverUnitStubFactory(id=deliver_unit.slug)
form_json = get_form_json(
form_block=stub.json,
domain=deliver_unit.app.cc_domain,
app_id=deliver_unit.app.cc_app_id,
)
return form_json


def _get_form_json(learn_app, module_id, form_block=None):
form_json = get_form_json(
form_block=form_block or LearnModuleJsonFactory(id=module_id).json,
Expand Down
14 changes: 13 additions & 1 deletion commcare_connect/opportunity/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@


admin.site.register(CommCareApp)
admin.site.register(PaymentUnit)
admin.site.register(UserInvite)
admin.site.register(DeliveryType)
admin.site.register(DeliverUnitFlagRules)
Expand All @@ -48,6 +47,7 @@ class OpportunityAccessAdmin(admin.ModelAdmin):
form = OpportunityAccessCreationForm
list_display = ["get_opp_name", "get_username"]
actions = ["clear_user_progress"]
search_fields = ["user__username"]

@admin.display(description="Opportunity Name")
def get_opp_name(self, obj):
Expand Down Expand Up @@ -102,6 +102,7 @@ class CompletedModuleAdmin(admin.ModelAdmin):
@admin.register(UserVisit)
class UserVisitAdmin(admin.ModelAdmin):
list_display = ["deliver_unit", "user", "opportunity", "status"]
search_fields = ["opportunity_access__user__username", "opportunity_access__opportunity__name"]


@admin.register(Assessment)
Expand All @@ -112,6 +113,7 @@ class AssessmentAdmin(admin.ModelAdmin):
@admin.register(CompletedWork)
class CompletedWorkAdmin(admin.ModelAdmin):
list_display = ["get_username", "get_opp_name", "opportunity_access", "payment_unit", "status"]
search_fields = ["opportunity_access__user__username", "opportunity_access__opportunity__name"]

@admin.display(description="Opportunity Name")
def get_opp_name(self, obj):
Expand All @@ -120,3 +122,13 @@ def get_opp_name(self, obj):
@admin.display(description="Username")
def get_username(self, obj):
return obj.opportunity_access.user.username


@admin.register(PaymentUnit)
class PaymentUnitAdmin(admin.ModelAdmin):
list_display = ["name", "get_opp_name"]
search_fields = ["name"]

@admin.display(description="Opportunity Name")
def get_opp_name(self, obj):
return obj.opportunity_access.opportunity.name
8 changes: 6 additions & 2 deletions commcare_connect/opportunity/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@

class OpportunityUserInviteForm(forms.Form):
def __init__(self, *args, **kwargs):
credentials = connect_id_client.fetch_credentials()
org_slug = kwargs.pop("org_slug", None)
credentials = connect_id_client.fetch_credentials(org_slug)
super().__init__(*args, **kwargs)

self.helper = FormHelper(self)
Expand Down Expand Up @@ -73,7 +74,10 @@ def clean_users(self):
return split_users


class OpportunityChangeForm(forms.ModelForm, OpportunityUserInviteForm):
class OpportunityChangeForm(
OpportunityUserInviteForm,
forms.ModelForm,
):
class Meta:
model = Opportunity
fields = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from django.core.management import BaseCommand
from django.db import transaction

from commcare_connect.opportunity.models import OpportunityAccess
from commcare_connect.opportunity.utils.completed_work import update_work_payment_date


class Command(BaseCommand):
help = "Updates paid dates from payments for all opportunity accesses"

def handle(self, *args, **kwargs):
try:
with transaction.atomic():
accesses = OpportunityAccess.objects.all()
self.stdout.write("Starting to process to update the paid date...")

for access in accesses:
update_work_payment_date(access)

self.stdout.write("Process completed")

except Exception as e:
self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}"))
Original file line number Diff line number Diff line change
@@ -1,18 +1,6 @@
# Generated by Django 4.2.5 on 2024-10-07 08:54

from django.db import migrations, models, transaction

from commcare_connect.opportunity.utils.completed_work import update_work_payment_date


@transaction.atomic
def update_paid_date_from_payments(apps, schema_editor):
OpportunityAccess = apps.get_model("opportunity.OpportunityAccess")
Payment = apps.get_model("opportunity.Payment")
CompletedWork = apps.get_model("opportunity.CompletedWork")
accesses = OpportunityAccess.objects.all()
for access in accesses:
update_work_payment_date(access, Payment, CompletedWork)
from django.db import migrations, models


class Migration(migrations.Migration):
Expand All @@ -26,5 +14,4 @@ class Migration(migrations.Migration):
name="payment_date",
field=models.DateTimeField(null=True),
),
migrations.RunPython(update_paid_date_from_payments, migrations.RunPython.noop),
]
14 changes: 13 additions & 1 deletion commcare_connect/opportunity/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,16 @@ def approved_visits(self):

@property
def number_of_users(self):
return self.total_budget / self.budget_per_user
if not self.managed:
return self.total_budget / self.budget_per_user

budget_per_user = 0
payment_units = self.paymentunit_set.all()
org_pay = self.managedopportunity.org_pay_per_visit
for pu in payment_units:
budget_per_user += pu.max_total * (pu.amount + org_pay)

return self.total_budget / budget_per_user

@property
def allotted_visits(self):
Expand Down Expand Up @@ -355,6 +364,9 @@ class PaymentUnit(models.Model):
null=True,
)

def __str__(self):
return self.name


class DeliverUnit(models.Model):
app = models.ForeignKey(
Expand Down
3 changes: 2 additions & 1 deletion commcare_connect/opportunity/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ class UserVisitFactory(DjangoModelFactory):
visit_date = Faker("date_time", tzinfo=timezone.utc)
form_json = Faker("pydict", value_types=[str, int, float, bool])
xform_id = Faker("uuid4")
completed_work = SubFactory(CompletedWorkFactory)

class Meta:
model = "opportunity.UserVisit"
Expand Down Expand Up @@ -233,7 +234,7 @@ class Meta:
class PaymentFactory(DjangoModelFactory):
opportunity_access = SubFactory(OpportunityAccessFactory)
amount = Faker("pyint", min_value=1, max_value=10000)
date_paid = Faker("past_date")
date_paid = Faker("date_time", tzinfo=timezone.utc)

class Meta:
model = "opportunity.Payment"
Loading

0 comments on commit eac4eeb

Please sign in to comment.