diff --git a/benefits/in_person/forms.py b/benefits/in_person/forms.py new file mode 100644 index 000000000..f09fabe35 --- /dev/null +++ b/benefits/in_person/forms.py @@ -0,0 +1,32 @@ +""" +The in-person eligibility application: Form definition for the +in-person eligibility verification flow, in which a +transit agency employee manually verifies a rider's eligibility. +""" + +from django import forms +from benefits.routes import routes +from benefits.core import models + + +class InPersonEligibilityForm(forms.Form): + """Form to capture eligibility for in-person verification flow selection.""" + + action_url = routes.IN_PERSON_ELIGIBILITY + id = "form-flow-selection" + method = "POST" + + flow = forms.ChoiceField(label="Choose an eligibility type to qualify this rider.", widget=forms.widgets.RadioSelect) + verified = forms.BooleanField(label="I have verified this person’s eligibility for a transit benefit.", required=True) + + cancel_url = routes.ADMIN_INDEX + + def __init__(self, agency: models.TransitAgency, *args, **kwargs): + super().__init__(*args, **kwargs) + flows = agency.enrollment_flows.all() + + self.classes = "checkbox-parent" + flow_field = self.fields["flow"] + flow_field.choices = [(f.id, f.label) for f in flows] + flow_field.widget.attrs.update({"data-custom-validity": "Please choose a transit benefit."}) + self.use_custom_validity = True diff --git a/benefits/in_person/templates/in_person/eligibility.html b/benefits/in_person/templates/in_person/eligibility.html index e69de29bb..1955233af 100644 --- a/benefits/in_person/templates/in_person/eligibility.html +++ b/benefits/in_person/templates/in_person/eligibility.html @@ -0,0 +1,30 @@ +{% extends "admin/agency-base.html" %} + +{% block title %} + Agency dashboard: In-person enrollment +{% endblock title %} + +{% block content %} +
+
+
+

In-person enrollment

+
+
{% include "core/includes/form.html" with form=form %}
+
+
+ {% url form.cancel_url as url_cancel %} + Cancel +
+
+ +
+
+
+
+{% endblock content %} diff --git a/benefits/in_person/views.py b/benefits/in_person/views.py index e821e2b28..88568a175 100644 --- a/benefits/in_person/views.py +++ b/benefits/in_person/views.py @@ -1,8 +1,39 @@ +from django.contrib.admin import site as admin_site from django.template.response import TemplateResponse +from django.shortcuts import redirect +from django.urls import reverse + + +from benefits.routes import routes +from benefits.core import session +from benefits.core.models import EnrollmentFlow + +from benefits.in_person import forms def eligibility(request): - return TemplateResponse(request, "in_person/eligibility.html") + """View handler for the in-person eligibility flow selection form.""" + + agency = session.agency(request) + context = {**admin_site.each_context(request), "form": forms.InPersonEligibilityForm(agency=agency)} + + if request.method == "POST": + form = forms.InPersonEligibilityForm(data=request.POST, agency=agency) + + if form.is_valid(): + flow_id = form.cleaned_data.get("flow") + flow = EnrollmentFlow.objects.get(id=flow_id) + session.update(request, flow=flow) + + in_person_enrollment = reverse(routes.IN_PERSON_ENROLLMENT) + response = redirect(in_person_enrollment) + else: + context["form"] = form + response = TemplateResponse(request, "in_person/eligibility.html", context) + else: + response = TemplateResponse(request, "in_person/eligibility.html", context) + + return response def enrollment(request): diff --git a/benefits/routes.py b/benefits/routes.py index 8daed2a31..f3743af60 100644 --- a/benefits/routes.py +++ b/benefits/routes.py @@ -121,6 +121,11 @@ def ENROLLMENT_SYSTEM_ERROR(self): """Enrollment error not caused by the user.""" return "enrollment:system_error" + @property + def ADMIN_INDEX(self): + """Admin index page""" + return "admin:index" + @property def IN_PERSON_ELIGIBILITY(self): """In-person (e.g. agency assisted) eligibility""" diff --git a/benefits/static/css/admin/styles.css b/benefits/static/css/admin/styles.css index d0ba275bf..ae33ad12d 100644 --- a/benefits/static/css/admin/styles.css +++ b/benefits/static/css/admin/styles.css @@ -2,6 +2,7 @@ /* Buttons */ /* Primary Button: Use all three classes: btn btn-lg btn-primary */ +/* Outline Primary Button: Use all three classes: btn btn-lg btn-outline-primary */ /* Set button width in parent with Bootstrap column */ /* Height: 60px on Desktop; 72 on mobile*/ @@ -15,8 +16,16 @@ } } +.btn.btn-lg.btn-outline-primary:not(:hover) { + color: var(--primary-color); +} + .btn.btn-lg.btn-primary { background-color: var(--primary-color); +} + +.btn.btn-lg.btn-primary, +.btn.btn-lg.btn-outline-primary { border-color: var(--primary-color); border-width: 2px; font-weight: var(--medium-font-weight); @@ -26,7 +35,8 @@ padding: var(--primary-button-padding); } -.btn.btn-lg.btn-primary:hover { +.btn.btn-lg.btn-primary:hover, +.btn.btn-lg.btn-outline-primary:hover { background-color: var(--hover-color); border-color: var(--hover-color); } @@ -63,3 +73,19 @@ html[data-theme="light"], #logout-form button { text-transform: unset; } + +.checkbox-parent .form-group:last-of-type .col-12 { + display: flex; + flex-direction: row-reverse; + justify-content: start; + column-gap: 0.5rem; + margin-top: 2rem; +} + +.checkbox-parent, +.checkbox-parent .form-group .col-12, +.checkbox-parent .form-group .col-12 #id_flow { + display: flex; + flex-direction: column; + row-gap: 1rem; +} diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index 9aa9e16df..d2179cba5 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -17,6 +17,8 @@ def test_view_not_logged_in(client, viewname): # admin_client is a fixture from pytest # https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-client-django-test-client-logged-in-as-admin +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency") def test_eligibility_logged_in(admin_client): path = reverse(routes.IN_PERSON_ELIGIBILITY) @@ -31,3 +33,27 @@ def test_enrollment_logged_in(admin_client): response = admin_client.get(path) assert response.status_code == 200 assert response.template_name == "in_person/enrollment.html" + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency") +def test_confirm_post_valid_form_eligibility_verified(admin_client): + + path = reverse(routes.IN_PERSON_ELIGIBILITY) + form_data = {"flow": 1, "verified": True} + response = admin_client.post(path, form_data) + + assert response.status_code == 302 + assert response.url == reverse(routes.IN_PERSON_ENROLLMENT) + + +@pytest.mark.django_db +@pytest.mark.usefixtures("mocked_session_agency") +def test_confirm_post_valid_form_eligibility_unverified(admin_client): + + path = reverse(routes.IN_PERSON_ELIGIBILITY) + form_data = {"flow": 1, "verified": False} + response = admin_client.post(path, form_data) + + assert response.status_code == 200 + assert response.template_name == "in_person/eligibility.html"