Skip to content

Commit 35a2d47

Browse files
authored
Fix: set TransitAgency for in-person eligibility URL (#3079)
2 parents 40f8956 + 8ff3c73 commit 35a2d47

4 files changed

Lines changed: 137 additions & 106 deletions

File tree

benefits/in_person/urls.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77

88
app_name = "in_person"
99
urlpatterns = [
10-
path("eligibility/", admin.site.admin_view(views.eligibility), name=routes.name(routes.IN_PERSON_ELIGIBILITY)),
10+
path(
11+
"eligibility/", admin.site.admin_view(views.EligibilityView.as_view()), name=routes.name(routes.IN_PERSON_ELIGIBILITY)
12+
),
1113
path("token/", admin.site.admin_view(views.token), name=routes.name(routes.IN_PERSON_ENROLLMENT_TOKEN)),
1214
path("enrollment/", admin.site.admin_view(views.enrollment), name=routes.name(routes.IN_PERSON_ENROLLMENT)),
1315
path(

benefits/in_person/views.py

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
from django.template.response import TemplateResponse
66
from django.shortcuts import redirect
77
from django.urls import reverse
8+
from django.views.generic import FormView
89
import sentry_sdk
910

1011

12+
from benefits.core.models.transit import TransitAgency
1113
from benefits.routes import routes
1214
from benefits.core import models, session
1315
from benefits.eligibility import analytics as eligibility_analytics
@@ -21,35 +23,51 @@
2123
logger = logging.getLogger(__name__)
2224

2325

24-
def eligibility(request):
25-
"""View handler for the in-person eligibility flow selection form."""
26+
class EligibilityView(FormView):
27+
"""CBV for the in-person eligibility flow selection form."""
2628

27-
agency = session.agency(request)
28-
context = {
29-
**admin_site.each_context(request),
30-
"form": forms.InPersonEligibilityForm(agency=agency),
31-
"title": f"{agency.long_name} | In-person enrollment | {admin_site.site_title}",
32-
}
29+
template_name = "in_person/eligibility.html"
30+
form_class = forms.InPersonEligibilityForm
3331

34-
if request.method == "POST":
35-
form = forms.InPersonEligibilityForm(data=request.POST, agency=agency)
36-
37-
if form.is_valid():
38-
flow_id = form.cleaned_data.get("flow")
39-
flow = models.EnrollmentFlow.objects.get(id=flow_id)
40-
session.update(request, flow=flow)
41-
eligibility_analytics.selected_flow(request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
42-
eligibility_analytics.started_eligibility(request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
43-
44-
in_person_enrollment = reverse(routes.IN_PERSON_ENROLLMENT)
45-
response = redirect(in_person_enrollment)
46-
else:
47-
context["form"] = form
48-
response = TemplateResponse(request, "in_person/eligibility.html", context)
49-
else:
50-
response = TemplateResponse(request, "in_person/eligibility.html", context)
32+
def dispatch(self, request, *args, **kwargs):
33+
"""Initialize session state before handling the request."""
34+
35+
agency = session.agency(request)
36+
if not agency:
37+
agency = TransitAgency.for_user(request.user)
38+
session.update(request, agency=agency)
39+
self.agency = agency
40+
return super().dispatch(request, *args, **kwargs)
41+
42+
def get_context_data(self, **kwargs):
43+
"""Add in-person specific context data."""
44+
45+
context = super().get_context_data(**kwargs)
46+
context.update(
47+
{
48+
**admin_site.each_context(self.request),
49+
"title": f"{self.agency.long_name} | In-person enrollment | {admin_site.site_title}",
50+
}
51+
)
52+
return context
53+
54+
def get_form_kwargs(self):
55+
"""Return the keyword arguments for instantiating the form."""
56+
57+
kwargs = super().get_form_kwargs()
58+
kwargs["agency"] = self.agency
59+
return kwargs
60+
61+
def form_valid(self, form):
62+
"""If the form is valid, set enrollment flow, eligible session, and redirect."""
5163

52-
return response
64+
flow_id = form.cleaned_data.get("flow")
65+
flow = models.EnrollmentFlow.objects.get(id=flow_id)
66+
session.update(self.request, flow=flow, eligible=True)
67+
eligibility_analytics.selected_flow(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
68+
eligibility_analytics.started_eligibility(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
69+
eligibility_analytics.returned_success(self.request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
70+
return redirect(routes.IN_PERSON_ENROLLMENT)
5371

5472

5573
def token(request):
@@ -91,7 +109,6 @@ def enrollment(request):
91109
raise Exception("Invalid card token form")
92110

93111
flow = session.flow(request)
94-
eligibility_analytics.returned_success(request, flow, enrollment_method=models.EnrollmentMethods.IN_PERSON)
95112
card_token = form.cleaned_data.get("card_token")
96113
status, exception = enroll(request, card_token)
97114

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import pytest
2+
3+
from benefits.core import models
4+
from benefits.in_person.forms import InPersonEligibilityForm
5+
6+
7+
@pytest.mark.django_db
8+
def test_eligibility_logged_in_filtering_flows(model_TransitAgency):
9+
digital = models.EnrollmentFlow.objects.create(
10+
transit_agency=model_TransitAgency, supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL], label="Digital"
11+
)
12+
in_person = models.EnrollmentFlow.objects.create(
13+
transit_agency=model_TransitAgency,
14+
supported_enrollment_methods=[models.EnrollmentMethods.IN_PERSON],
15+
label="In-Person",
16+
)
17+
both = models.EnrollmentFlow.objects.create(
18+
transit_agency=model_TransitAgency,
19+
supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL, models.EnrollmentMethods.IN_PERSON],
20+
label="Both",
21+
)
22+
form = InPersonEligibilityForm(agency=model_TransitAgency)
23+
24+
filtered_flow_ids = [choice[0] for choice in form.fields["flow"].choices]
25+
26+
assert in_person.id, both.id in filtered_flow_ids
27+
assert digital.id not in filtered_flow_ids

tests/pytest/in_person/test_views.py

Lines changed: 63 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ def mocked_sentry_sdk_module(mocker):
3939
return mocker.patch.object(benefits.in_person.views, "sentry_sdk")
4040

4141

42+
@pytest.fixture
43+
def mocked_session_module(mocker):
44+
return mocker.patch.object(benefits.in_person.views, "session")
45+
46+
47+
@pytest.fixture
48+
def mocked_transit_agency_class(mocker):
49+
return mocker.patch.object(benefits.in_person.views, "TransitAgency")
50+
51+
4252
@pytest.mark.django_db
4353
@pytest.mark.parametrize("viewname", [routes.IN_PERSON_ELIGIBILITY, routes.IN_PERSON_ENROLLMENT])
4454
def test_view_not_logged_in(client, viewname):
@@ -49,84 +59,60 @@ def test_view_not_logged_in(client, viewname):
4959
assert response.url == "/admin/login/?next=" + path
5060

5161

52-
# admin_client is a fixture from pytest
53-
# https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-client-django-test-client-logged-in-as-admin
54-
@pytest.mark.django_db
55-
@pytest.mark.usefixtures("mocked_session_agency")
56-
def test_eligibility_logged_in(admin_client):
57-
path = reverse(routes.IN_PERSON_ELIGIBILITY)
58-
59-
response = admin_client.get(path)
60-
assert response.status_code == 200
61-
assert response.template_name == "in_person/eligibility.html"
62-
63-
6462
@pytest.mark.django_db
65-
def test_eligibility_logged_in_filtering_flows(mocker, model_TransitAgency, admin_client):
66-
digital = models.EnrollmentFlow.objects.create(
67-
transit_agency=model_TransitAgency, supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL], label="Digital"
68-
)
69-
in_person = models.EnrollmentFlow.objects.create(
70-
transit_agency=model_TransitAgency,
71-
supported_enrollment_methods=[models.EnrollmentMethods.IN_PERSON],
72-
label="In-Person",
73-
)
74-
both = models.EnrollmentFlow.objects.create(
75-
transit_agency=model_TransitAgency,
76-
supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL, models.EnrollmentMethods.IN_PERSON],
77-
label="Both",
78-
)
79-
mocker.patch("benefits.core.session.agency", autospec=True, return_value=model_TransitAgency)
80-
81-
path = reverse(routes.IN_PERSON_ELIGIBILITY)
82-
response = admin_client.get(path)
83-
filtered_flow_ids = [choice[0] for choice in response.context_data["form"].fields["flow"].choices]
84-
85-
assert in_person.id, both.id in filtered_flow_ids
86-
assert digital.id not in filtered_flow_ids
87-
88-
89-
@pytest.mark.django_db
90-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow")
91-
def test_eligibility_post_no_flow_selected(admin_client):
92-
93-
path = reverse(routes.IN_PERSON_ELIGIBILITY)
94-
form_data = {}
95-
response = admin_client.post(path, form_data)
96-
97-
# should return user back to the in-person eligibility index
98-
assert response.status_code == 200
99-
assert response.template_name == "in_person/eligibility.html"
100-
101-
102-
@pytest.mark.django_db
103-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow")
104-
def test_eligibility_post_flow_selected_and_verified(
105-
admin_client, model_EnrollmentFlow, mocked_session_update, mocked_eligibility_analytics_module
106-
):
107-
108-
path = reverse(routes.IN_PERSON_ELIGIBILITY)
109-
form_data = {"flow": 1, "verified_1": True}
110-
response = admin_client.post(path, form_data)
111-
112-
assert response.status_code == 302
113-
assert response.url == reverse(routes.IN_PERSON_ENROLLMENT)
114-
assert mocked_session_update.call_args.kwargs["flow"] == model_EnrollmentFlow
115-
mocked_eligibility_analytics_module.selected_flow.assert_called_once()
116-
mocked_eligibility_analytics_module.started_eligibility.assert_called_once()
117-
118-
119-
@pytest.mark.django_db
120-
@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow")
121-
def test_eligibility_post_flow_selected_and_unverified(admin_client):
122-
123-
path = reverse(routes.IN_PERSON_ELIGIBILITY)
124-
form_data = {"flow": 1, "verified_1": False}
125-
response = admin_client.post(path, form_data)
126-
127-
# should return user back to the in-person eligibility index
128-
assert response.status_code == 200
129-
assert response.template_name == "in_person/eligibility.html"
63+
class TestEligibilityView:
64+
@pytest.fixture
65+
def view(self, model_User, app_request, mocked_session_agency):
66+
# manually attach a logged-in user to the request
67+
app_request.user = model_User
68+
69+
v = benefits.in_person.views.EligibilityView()
70+
v.setup(app_request)
71+
v.agency = mocked_session_agency(app_request)
72+
return v
73+
74+
def test_get_form_kwargs(self, view):
75+
kwargs = view.get_form_kwargs()
76+
assert kwargs["agency"] == view.agency
77+
78+
def test_get_context_data(self, view):
79+
context_data = view.get_context_data()
80+
assert "title" in context_data
81+
82+
def test_dispatch_no_agency_in_session(
83+
self, view, mocked_session_module, mocked_transit_agency_class, model_TransitAgency
84+
):
85+
view.agency = None
86+
mocked_session_module.agency.return_value = None
87+
mocked_transit_agency_class.for_user.return_value = model_TransitAgency
88+
89+
view.dispatch(view.request)
90+
91+
mocked_session_module.update.assert_called_once()
92+
assert view.agency == model_TransitAgency
93+
94+
def test_form_valid(self, view, mocker, model_EnrollmentFlow, mocked_session_module, mocked_eligibility_analytics_module):
95+
mock_enrollment_flow_model = mocker.patch.object(models.EnrollmentFlow.objects, "get")
96+
mock_enrollment_flow_model.return_value = model_EnrollmentFlow
97+
98+
mock_form = mocker.Mock()
99+
mock_form.cleaned_data = {"flow": model_EnrollmentFlow.id}
100+
101+
response = view.form_valid(mock_form)
102+
103+
mock_enrollment_flow_model.assert_called_once_with(id=model_EnrollmentFlow.id)
104+
mocked_session_module.update.assert_called_once_with(view.request, flow=model_EnrollmentFlow, eligible=True)
105+
mocked_eligibility_analytics_module.selected_flow.assert_called_once_with(
106+
view.request, model_EnrollmentFlow, enrollment_method=models.EnrollmentMethods.IN_PERSON
107+
)
108+
mocked_eligibility_analytics_module.started_eligibility.assert_called_once_with(
109+
view.request, model_EnrollmentFlow, enrollment_method=models.EnrollmentMethods.IN_PERSON
110+
)
111+
mocked_eligibility_analytics_module.returned_success.assert_called_once_with(
112+
view.request, model_EnrollmentFlow, enrollment_method=models.EnrollmentMethods.IN_PERSON
113+
)
114+
assert response.status_code == 302
115+
assert response.url == reverse(routes.IN_PERSON_ENROLLMENT)
130116

131117

132118
@pytest.mark.django_db
@@ -343,7 +329,6 @@ def test_enrollment_post_valid_form_success(
343329

344330
assert response.status_code == 302
345331
assert response.url == reverse(routes.IN_PERSON_ENROLLMENT_SUCCESS)
346-
mocked_eligibility_analytics_module.returned_success.assert_called_once()
347332
mocked_enrollment_analytics_module.returned_success.assert_called_once()
348333

349334

0 commit comments

Comments
 (0)