Skip to content

Commit 1699364

Browse files
authored
Merge pull request dimagi#1012 from dimagi/ay/user-visit-export-filter-date
Fixes date filtering for exporting user visits and fetching visits count
2 parents d612f3b + b7f46cc commit 1699364

File tree

5 files changed

+142
-4
lines changed

5 files changed

+142
-4
lines changed

commcare_connect/opportunity/export.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
UserVisitReviewTable,
2929
UserVisitTable,
3030
)
31+
from commcare_connect.utils.datetime import get_start_end_date_range_with_time
3132

3233

3334
class UserVisitExporter:
@@ -95,6 +96,7 @@ def _process_row(self, row):
9596

9697
def get_dataset(self, from_date, to_date, status: list[VisitValidationStatus]) -> Dataset:
9798
"""Get dataset of all user visits for an opportunity."""
99+
from_date, to_date = get_start_end_date_range_with_time(from_date, to_date)
98100
user_visits = UserVisit.objects.filter(
99101
opportunity=self.opportunity, visit_date__gte=from_date, visit_date__lte=to_date
100102
)
@@ -120,6 +122,7 @@ def get_dataset(self, from_date, to_date, status: list[VisitValidationStatus]) -
120122
def export_user_visit_review_data(
121123
opportunity: Opportunity, from_date, to_date, status: list[VisitReviewStatus]
122124
) -> Dataset:
125+
from_date, to_date = get_start_end_date_range_with_time(from_date, to_date)
123126
user_visits = UserVisit.objects.filter(
124127
opportunity=opportunity,
125128
review_created_on__isnull=False,

commcare_connect/opportunity/tests/test_export.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import datetime
12
import random
23
from datetime import timedelta
34

@@ -359,3 +360,93 @@ def test_export_user_visit_review_data(organization, from_date, to_date, expecte
359360
assert row["Program Manager Review"] in [
360361
status.label for status in VisitReviewStatus if status.value in review_status
361362
]
363+
364+
365+
@pytest.mark.django_db
366+
def test_export_user_visit_review_data_boundary_dates(organization):
367+
opp = ManagedOpportunityFactory(organization=organization)
368+
369+
from_date = datetime.date.today() - datetime.timedelta(days=4)
370+
to_date = datetime.date.today() - datetime.timedelta(days=1)
371+
372+
on_from_date = datetime.datetime.combine(from_date, datetime.time.min, tzinfo=datetime.UTC)
373+
on_to_date = datetime.datetime.combine(to_date, datetime.time.max, tzinfo=datetime.UTC)
374+
before_from_date = datetime.datetime.combine(
375+
from_date - datetime.timedelta(days=1), datetime.time.max, tzinfo=datetime.UTC
376+
)
377+
after_to_date = datetime.datetime.combine(
378+
to_date + datetime.timedelta(days=1), datetime.time.min, tzinfo=datetime.UTC
379+
)
380+
381+
UserVisitFactory.create_batch(
382+
3,
383+
opportunity=opp,
384+
visit_date=on_from_date,
385+
review_created_on=on_from_date,
386+
review_status=VisitReviewStatus.pending,
387+
)
388+
UserVisitFactory.create_batch(
389+
2,
390+
opportunity=opp,
391+
visit_date=on_to_date,
392+
review_created_on=on_to_date,
393+
review_status=VisitReviewStatus.pending,
394+
)
395+
UserVisitFactory(
396+
opportunity=opp,
397+
visit_date=before_from_date,
398+
review_created_on=before_from_date,
399+
review_status=VisitReviewStatus.pending,
400+
)
401+
UserVisitFactory(
402+
opportunity=opp,
403+
visit_date=after_to_date,
404+
review_created_on=after_to_date,
405+
review_status=VisitReviewStatus.pending,
406+
)
407+
408+
dataset = export_user_visit_review_data(opp, from_date, to_date, [VisitReviewStatus.pending.value])
409+
410+
assert isinstance(dataset, Dataset)
411+
assert len(dataset) == 5, f"Expected 5 visits (3 on from_date + 2 on to_date), got {len(dataset)}"
412+
413+
414+
@pytest.mark.django_db
415+
def test_user_visit_exporter_get_dataset_boundary_dates(opportunity, mobile_user_with_connect_link):
416+
deliver_unit = DeliverUnitFactory(app=opportunity.deliver_app)
417+
418+
from_date = datetime.date.today() - datetime.timedelta(days=5)
419+
to_date = datetime.date.today() - datetime.timedelta(days=1)
420+
421+
on_from_date = datetime.datetime.combine(from_date, datetime.time.min, tzinfo=datetime.UTC)
422+
on_to_date = datetime.datetime.combine(to_date, datetime.time.max, tzinfo=datetime.UTC)
423+
before_from_date = datetime.datetime.combine(
424+
from_date - datetime.timedelta(days=1), datetime.time.max, tzinfo=datetime.UTC
425+
)
426+
after_to_date = datetime.datetime.combine(
427+
to_date + datetime.timedelta(days=1), datetime.time.min, tzinfo=datetime.UTC
428+
)
429+
430+
UserVisitFactory(
431+
opportunity=opportunity, user=mobile_user_with_connect_link, deliver_unit=deliver_unit, visit_date=on_from_date
432+
)
433+
UserVisitFactory(
434+
opportunity=opportunity, user=mobile_user_with_connect_link, deliver_unit=deliver_unit, visit_date=on_to_date
435+
)
436+
UserVisitFactory(
437+
opportunity=opportunity,
438+
user=mobile_user_with_connect_link,
439+
deliver_unit=deliver_unit,
440+
visit_date=before_from_date,
441+
)
442+
UserVisitFactory(
443+
opportunity=opportunity,
444+
user=mobile_user_with_connect_link,
445+
deliver_unit=deliver_unit,
446+
visit_date=after_to_date,
447+
)
448+
449+
exporter = UserVisitExporter(opportunity, False)
450+
dataset = exporter.get_dataset(from_date, to_date, [])
451+
452+
assert len(dataset) == 2, f"Expected 2 visits (boundary dates), got {len(dataset)}"

commcare_connect/opportunity/tests/test_views.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import inspect
2-
from datetime import date, timedelta
2+
from datetime import UTC, date, datetime, time, timedelta
33
from http import HTTPStatus
44
from unittest import mock
55
from uuid import uuid4
@@ -1943,3 +1943,34 @@ def test_payment_delete_view(client: Client, opportunity: Opportunity, org_user_
19431943

19441944
response = client.post(url)
19451945
assert response.status_code == 404
1946+
1947+
1948+
@pytest.mark.django_db
1949+
def test_visit_export_count_boundary_dates(
1950+
organization: Organization, org_user_member: User, opportunity: Opportunity, client: Client
1951+
):
1952+
from_date = date.today() - timedelta(days=5)
1953+
to_date = date.today() - timedelta(days=1)
1954+
1955+
on_from_date = datetime.combine(from_date, time.min, tzinfo=UTC)
1956+
on_to_date = datetime.combine(to_date, time.max, tzinfo=UTC)
1957+
before_from_date = datetime.combine(from_date - timedelta(days=1), time.max, tzinfo=UTC)
1958+
after_to_date = datetime.combine(to_date + timedelta(days=1), time.min, tzinfo=UTC)
1959+
1960+
UserVisitFactory(opportunity=opportunity, visit_date=on_from_date)
1961+
UserVisitFactory(opportunity=opportunity, visit_date=on_to_date)
1962+
UserVisitFactory(opportunity=opportunity, visit_date=before_from_date)
1963+
UserVisitFactory(opportunity=opportunity, visit_date=after_to_date)
1964+
1965+
url = reverse("opportunity:visit_export_count", args=(organization.slug, opportunity.pk))
1966+
client.force_login(org_user_member)
1967+
response = client.get(
1968+
url,
1969+
data={
1970+
"from_date": from_date.isoformat(),
1971+
"to_date": to_date.isoformat(),
1972+
},
1973+
)
1974+
1975+
assert response.status_code == HTTPStatus.OK
1976+
assert "2 visits match your filters." in response.content.decode()

commcare_connect/opportunity/views.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@
185185
from commcare_connect.users.models import User
186186
from commcare_connect.utils.analytics import GA_CUSTOM_DIMENSIONS, Event, GATrackingInfo, send_event_to_ga
187187
from commcare_connect.utils.celery import download_export_file, render_export_status
188+
from commcare_connect.utils.datetime import get_start_end_date_range_with_time
188189
from commcare_connect.utils.db import get_object_by_uuid_or_int
189190
from commcare_connect.utils.file import get_file_extension
190191
from commcare_connect.utils.flags import FlagLabels, Flags
@@ -3047,15 +3048,18 @@ def download_invoice_line_items(request, org_slug, opp_id):
30473048
@org_member_required
30483049
@opportunity_required
30493050
def visit_export_count(request, org_slug, opp_id):
3050-
from_date = request.GET.get("from_date")
3051-
if not from_date:
3051+
from_date_str = request.GET.get("from_date")
3052+
if not from_date_str:
30523053
return HttpResponse({"error": "Please select a From Date first."}, status=400)
30533054

3054-
to_date = request.GET.get("to_date") or datetime.date.today()
3055+
to_date_str = request.GET.get("to_date")
30553056
status = request.GET.get("status", None)
30563057
review_export = request.GET.get("review_export") == "true"
30573058
format = request.GET.get("format", "csv")
30583059

3060+
from_date = datetime.date.fromisoformat(from_date_str)
3061+
to_date = datetime.date.fromisoformat(to_date_str) if to_date_str else datetime.date.today()
3062+
from_date, to_date = get_start_end_date_range_with_time(from_date, to_date)
30593063
visits = UserVisit.objects.filter(
30603064
opportunity_id=request.opportunity.pk, visit_date__gte=from_date, visit_date__lte=to_date
30613065
)

commcare_connect/utils/datetime.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ def get_month_series(from_date: datetime.date, to_date: datetime.date):
1919
return series
2020

2121

22+
def get_start_end_date_range_with_time(
23+
from_date: datetime.date, to_date: datetime.date
24+
) -> tuple[datetime.datetime, datetime.datetime]:
25+
"""Return (start_datetime, end_datetime) spanning the full days of from_date and to_date in UTC."""
26+
start_time = datetime.datetime.combine(from_date, datetime.time.min, tzinfo=datetime.UTC)
27+
end_time = datetime.datetime.combine(to_date, datetime.time.max, tzinfo=datetime.UTC)
28+
return start_time, end_time
29+
30+
2231
def get_start_end_dates_from_month_range(from_date: datetime.date, to_date: datetime.date):
2332
start_date = datetime.date(from_date.year, from_date.month, 1)
2433
start_time = datetime.datetime.combine(start_date, datetime.time.min, tzinfo=datetime.UTC)

0 commit comments

Comments
 (0)