Skip to content

Commit

Permalink
Merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
sravfeyn committed Nov 28, 2024
2 parents dac5557 + cc34026 commit 0a6e359
Show file tree
Hide file tree
Showing 94 changed files with 5,540 additions and 474 deletions.
1 change: 1 addition & 0 deletions .env_template
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SERVICE=
MAPBOX_TOKEN=
OPEN_EXCHANGE_RATES_API_ID=
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Some useful command are available via the `tasks.py` file:

- Install loca.lt
- `npm install -g localtunnel`
- Run `loca.lt --port 8000 --subdomain [my-unique-subdomain]` and copy the generated URL
- Run `lt --port 8000 --subdomain [my-unique-subdomain]` and copy the generated URL
- Update your `.env` file with the host:

DJANGO_ALLOWED_HOSTS=[my-unique-subdomain].loca.lt
Expand All @@ -66,6 +66,7 @@ Some useful command are available via the `tasks.py` file:

**Test the OAuth2 flow**

- Set `COMMCARE_HQ_URL=https://staging.commcarehq.org` in your `.env` file and restart the server.
- Navigate to http://[my-unique-subdomain].loca.lt/accounts/login/
- Click the "Log in with CommCare HQ" button
- You should be redirected to CommCare HQ to log in
Expand Down Expand Up @@ -145,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
29 changes: 26 additions & 3 deletions commcare_connect/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
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,
MobileUserFactory,
OrgWithUsersFactory,
ProgramManagerOrgWithUsersFactory,
UserFactory,
)

Expand Down Expand Up @@ -47,9 +49,15 @@ def user(db) -> User:


@pytest.fixture()
def opportunity():
factory = OpportunityFactory()
OpportunityVerificationFlagsFactory(opportunity=factory)
def opportunity(request):
verification_flags = getattr(request, "param", {}).get("verification_flags", {})
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 Expand Up @@ -105,3 +113,18 @@ def org_user_member(organization) -> User:
@pytest.fixture
def org_user_admin(organization) -> User:
return organization.memberships.filter(role="admin").first().user


@pytest.fixture
def program_manager_org(db) -> Organization:
return ProgramManagerOrgWithUsersFactory()


@pytest.fixture
def program_manager_org_user_member(program_manager_org) -> User:
return program_manager_org.memberships.filter(role="member").first().user


@pytest.fixture
def program_manager_org_user_admin(program_manager_org) -> User:
return program_manager_org.memberships.filter(role="admin").first().user
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
15 changes: 9 additions & 6 deletions commcare_connect/form_receiver/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
OpportunityClaimLimit,
OpportunityVerificationFlags,
UserVisit,
VisitReviewStatus,
VisitValidationStatus,
)
from commcare_connect.opportunity.tasks import download_user_visit_attachments
Expand Down Expand Up @@ -196,13 +197,14 @@ def clean_form_submission(access: OpportunityAccess, user_visit: UserVisit, xfor
if opportunity_flags.catchment_areas:
areas = access.catchmentarea_set.filter(active=True)
if areas:
cur_lat, cur_lon, *_ = xform.metadata.location.split(" ")
within_catchment = False
for area in areas:
dist = distance((area.latitude, area.longitude), (cur_lat, cur_lon))
if dist.meters < area.radius:
within_catchment = True
break
if xform.metadata.location is not None:
cur_lat, cur_lon, *_ = xform.metadata.location.split(" ")
for area in areas:
dist = distance((area.latitude, area.longitude), (cur_lat, cur_lon))
if dist.meters < area.radius:
within_catchment = True
break
if not within_catchment:
flags.append(["catchment", "Visit outside worker catchment areas"])
if (
Expand Down Expand Up @@ -317,6 +319,7 @@ def process_deliver_unit(user, xform: XForm, app: CommCareApp, opportunity: Oppo
and not user_visit.flagged
):
user_visit.status = VisitValidationStatus.approved
user_visit.review_status = VisitReviewStatus.agree
user_visit.save()
if (
completed_work is not None
Expand Down
153 changes: 95 additions & 58 deletions commcare_connect/form_receiver/tests/test_receiver_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@
OpportunityClaimLimit,
OpportunityVerificationFlags,
UserVisit,
VisitReviewStatus,
VisitValidationStatus,
)
from commcare_connect.opportunity.tasks import bulk_approve_completed_work
from commcare_connect.opportunity.tests.factories import (
CatchmentAreaFactory,
DeliverUnitFactory,
DeliverUnitFlagRulesFactory,
FormJsonValidationRulesFactory,
Expand Down Expand Up @@ -171,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 @@ -459,58 +451,50 @@ def test_auto_approve_visits_and_payments(
assert access.payment_accrued == completed_work.payment_accrued


@pytest.mark.parametrize(
"opportunity",
[
{
"verification_flags": {
"form_submission_start": datetime.time(10, 0),
"form_submission_end": datetime.time(14, 0),
}
}
],
indirect=True,
)
@pytest.mark.parametrize(
"submission_time_hour, expected_message",
[
(11, None),
(9, "Form was submitted before the start time"),
(15, "Form was submitted after the end time"),
],
)
def test_reciever_verification_flags_form_submission(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
user_with_connectid_link: User,
api_client: APIClient,
opportunity: Opportunity,
submission_time_hour,
expected_message,
):
verification_flags = OpportunityVerificationFlags.objects.get(opportunity=opportunity)
verification_flags.form_submission_start = datetime.time(hour=10, minute=0)
verification_flags.form_submission_end = datetime.time(hour=12, minute=0)
verification_flags.save()

form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
time = datetime.datetime(2024, 4, 17, 10, 0, 0)
form_json["metadata"]["timeStart"] = time
form_json["metadata"]["timeEnd"] = time + datetime.timedelta(minutes=10)
make_request(api_client, form_json, user_with_connectid_link)
visit = UserVisit.objects.get(user=user_with_connectid_link)
assert not visit.flagged
submission_time = datetime.datetime(2024, 5, 17, hour=submission_time_hour, minute=0)
form_json["metadata"]["timeStart"] = submission_time


def test_reciever_verification_flags_form_submission_start(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
verification_flags = OpportunityVerificationFlags.objects.get(opportunity=opportunity)
verification_flags.form_submission_start = datetime.time(hour=10, minute=0)
verification_flags.form_submission_end = datetime.time(hour=12, minute=0)
verification_flags.save()

form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
time = datetime.datetime(2024, 4, 17, 9, 0, 0)
form_json["metadata"]["timeStart"] = time
make_request(api_client, form_json, user_with_connectid_link)
visit = UserVisit.objects.get(user=user_with_connectid_link)
assert visit.flagged
assert ["form_submission_period", "Form was submitted before the start time"] in visit.flag_reason.get("flags", [])


def test_reciever_verification_flags_form_submission_end(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
verification_flags = OpportunityVerificationFlags.objects.get(opportunity=opportunity)
verification_flags.form_submission_start = datetime.time(hour=10, minute=0)
verification_flags.form_submission_end = datetime.time(hour=12, minute=0)
verification_flags.save()

form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
time = datetime.datetime(2024, 4, 17, 13, 0, 0)
form_json["metadata"]["timeStart"] = time
make_request(api_client, form_json, user_with_connectid_link)
visit = UserVisit.objects.get(user=user_with_connectid_link)
assert visit.flagged
assert ["form_submission_period", "Form was submitted after the end time"] in visit.flag_reason.get("flags", [])

# Assert based on the expected message
if expected_message is None:
assert not visit.flagged
else:
assert visit.flagged
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 @@ -523,7 +507,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 @@ -536,7 +520,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 @@ -554,7 +538,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 @@ -576,6 +560,59 @@ def test_reciever_verification_flags_form_json_rule_flagged(
] in visit.flag_reason.get("flags", [])


def test_receiver_verification_flags_catchment_areas(
user_with_connectid_link: User, api_client: APIClient, opportunity: Opportunity
):
verification_flags = OpportunityVerificationFlags.objects.get(opportunity=opportunity)
verification_flags.catchment_areas = True
verification_flags.save()

form_json = _create_opp_and_form_json(opportunity, user=user_with_connectid_link)
form_json["metadata"]["location"] = None

access = OpportunityAccess.objects.get(user=user_with_connectid_link, opportunity=opportunity)
CatchmentAreaFactory(opportunity=opportunity, opportunity_access=access, active=True)

make_request(api_client, form_json, user_with_connectid_link)
visit = UserVisit.objects.get(user=user_with_connectid_link)
assert visit.flagged
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
Loading

0 comments on commit 0a6e359

Please sign in to comment.