Skip to content

Commit

Permalink
Admin: In-Person Eligibility View (#2317)
Browse files Browse the repository at this point in the history
This PR adds the in-person eligibility view form, allowing a transit agency user to manually verify a rider's eligibility in person, and select an enrollment pathway for the rider.
  • Loading branch information
machikoyasuda authored Sep 10, 2024
2 parents c5fc5d7 + 19cc65b commit 977dfd4
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 2 deletions.
32 changes: 32 additions & 0 deletions benefits/in_person/forms.py
Original file line number Diff line number Diff line change
@@ -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
30 changes: 30 additions & 0 deletions benefits/in_person/templates/in_person/eligibility.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% extends "admin/agency-base.html" %}

{% block title %}
Agency dashboard: In-person enrollment
{% endblock title %}

{% block content %}
<div class="row justify-content-center">
<div class="col-11 col-md-10 col-lg-6 border border-3 px-0">
<div class="border border-3 border-top-0 border-start-0 border-end-0">
<h2 class="p-3 m-0 text-left">In-person enrollment</h2>
</div>
<div class="p-3">{% include "core/includes/form.html" with form=form %}</div>
<div class="row d-flex justify-content-start p-3 pt-8">
<div class="col-6">
{% url form.cancel_url as url_cancel %}
<a href="{{ url_cancel }}" class="btn btn-lg btn-outline-primary d-block">Cancel</a>
</div>
<div class="col-6">
<button class="btn btn-lg btn-primary d-flex justify-content-center align-items-center"
data-action="submit"
type="submit"
form="{{ form.id }}">
<span class="btn-text">Continue</span>
</button>
</div>
</div>
</div>
</div>
{% endblock content %}
33 changes: 32 additions & 1 deletion benefits/in_person/views.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
5 changes: 5 additions & 0 deletions benefits/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down
28 changes: 27 additions & 1 deletion benefits/static/css/admin/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -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*/

Expand All @@ -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);
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
26 changes: 26 additions & 0 deletions tests/pytest/in_person/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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"

0 comments on commit 977dfd4

Please sign in to comment.