We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Create VerifierSessionRequired middleware that expects a verifier to exist in session
## Background

Once the session tracks the selected verifier in #321, we can make use of that infrastructure to put guards on certain view functions that require a verifier to be selected. The first step is to create a new middleware class that enforces the requirement.

This is similar to how the [`AgencySessionRequired`](https://github.com/cal-itp/benefits/blob/dev/benefits/core/middleware.py#L20) and [`EligibileSessionRequired`](https://github.com/cal-itp/benefits/blob/dev/benefits/core/middleware.py#L68) middleware are used.

## Tasks

- [x] Create a new middleware class like `VerifierSessionRequired` inheriting from `MiddlewareMixin`, see the other `*SessionRequired` as examples
- [x] In `process_request()`, check `session.verifier()` for the request.
  - If `None`, raise an error to stop the request
  - Otherwise return `None` to allow the request to continue
- [x] Apply this middleware to the following views to enforce that a verifier is needed:
  - [x]  [`eligibility:index`](https://github.com/cal-itp/benefits/blob/dev/benefits/eligibility/views.py#L16)
  - [x]  [`eligibility:confirm`](https://github.com/cal-itp/benefits/blob/dev/benefits/eligibility/views.py#L46)


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `benefits/core/middleware.py`
Content:
```
1 """
2 The core application: middleware definitions for request/response cycle.
3 """
4 import logging
5 import time
6 
7 from django.http import HttpResponse, HttpResponseBadRequest
8 from django.template import loader
9 from django.utils.decorators import decorator_from_middleware
10 from django.utils.deprecation import MiddlewareMixin
11 from django.views import i18n
12 
13 from benefits.settings import RATE_LIMIT, RATE_LIMIT_METHODS, RATE_LIMIT_PERIOD, DEBUG
14 from . import analytics, session, viewmodels
15 
16 
17 logger = logging.getLogger(__name__)
18 
19 
20 class AgencySessionRequired(MiddlewareMixin):
21     """Middleware raises an exception for sessions lacking an agency configuration."""
22 
23     def process_request(self, request):
24         if session.active_agency(request):
25             logger.debug("Session configured with agency")
26             return None
27         else:
28             raise AttributeError("Session not configured with agency")
29 
30 
31 class RateLimit(MiddlewareMixin):
32     """Middleware checks settings and session to ensure rate limit is respected."""
33 
34     def process_request(self, request):
35         if any((RATE_LIMIT < 1, len(RATE_LIMIT_METHODS) < 1, RATE_LIMIT_PERIOD < 1)):
36             logger.debug("RATE_LIMIT, RATE_LIMIT_METHODS, or RATE_LIMIT_PERIOD are not configured")
37             return None
38 
39         if request.method in RATE_LIMIT_METHODS:
40             session.increment_rate_limit_counter(request)
41         else:
42             # bail early if the request method doesn't match
43             return None
44 
45         counter = session.rate_limit_counter(request)
46         reset_time = session.rate_limit_time(request)
47         now = int(time.time())
48 
49         if counter > RATE_LIMIT:
50             if reset_time > now:
51                 logger.warn("Rate limit exceeded")
52                 home = viewmodels.Button.home(request)
53                 page = viewmodels.ErrorPage.error(
54                     title="Rate limit error",
55                     content_title="Rate limit error",
56                     paragraphs=["You have reached the rate limit. Please try again."],
57                     button=home,
58                 )
59                 t = loader.get_template("400.html")
60                 return HttpResponseBadRequest(t.render(page.context_dict()))
61             else:
62                 # enough time has passed, reset the rate limit
63                 session.reset_rate_limit(request)
64 
65         return None
66 
67 
68 class EligibleSessionRequired(MiddlewareMixin):
69     """Middleware raises an exception for sessions lacking confirmed eligibility."""
70 
71     def process_request(self, request):
72         if session.eligible(request):
73             logger.debug("Session has confirmed eligibility")
74             return None
75         else:
76             raise AttributeError("Session has no confirmed eligibility")
77 
78 
79 class DebugSession(MiddlewareMixin):
80     """Middleware to configure debug context in the request session."""
81 
82     def process_request(self, request):
83         session.update(request, debug=DEBUG)
84         return None
85 
86 
87 class Healthcheck:
88     """Middleware intercepts and accepts /healthcheck requests."""
89 
90     def __init__(self, get_response):
91         self.get_response = get_response
92 
93     def __call__(self, request):
94         if request.path == "/healthcheck":
95             return HttpResponse("Healthy", content_type="text/plain")
96         return self.get_response(request)
97 
98 
99 class ViewedPageEvent(MiddlewareMixin):
100     """Middleware sends an analytics event for page views."""
101 
102     def process_response(self, request, response):
103         event = analytics.ViewedPageEvent(request)
104         try:
105             analytics.send_event(event)
106         except Exception:
107             logger.warning(f"Failed to send event: {event}")
108         finally:
109             return response
110 
111 
112 pageview_decorator = decorator_from_middleware(ViewedPageEvent)
113 
114 
115 class ChangedLanguageEvent(MiddlewareMixin):
116     """Middleware hooks into django.views.i18n.set_language to send an analytics event."""
117 
118     def process_view(self, request, view_func, view_args, view_kwargs):
119         if view_func == i18n.set_language:
120             new_lang = request.POST["language"]
121             event = analytics.ChangedLanguageEvent(request, new_lang)
122             analytics.send_event(event)
123         return None
124
```

Path: `benefits/eligibility/views.py`
Content:
```
1 """
2 The eligibility application: view definitions for the eligibility verification flow.
3 """
4 from django.contrib import messages
5 from django.shortcuts import redirect
6 from django.urls import reverse
7 from django.utils.decorators import decorator_from_middleware
8 from django.utils.translation import pgettext, gettext as _
9 
10 from benefits.core import middleware, recaptcha, session, viewmodels
11 from benefits.core.views import PageTemplateResponse, _index_image
12 from . import analytics, api, forms
13 
14 
15 @decorator_from_middleware(middleware.AgencySessionRequired)
16 def index(request):
17     """View handler for the eligibility verification getting started screen."""
18 
19     session.update(request, eligibility_types=[], origin=reverse("eligibility:index"))
20 
21     page = viewmodels.Page(
22         title=_("eligibility.pages.index.title"),
23         content_title=_("eligibility.pages.index.content_title"),
24         media=[
25             viewmodels.MediaItem(
26                 icon=viewmodels.Icon("idcardcheck", pgettext("image alt text", "core.icons.idcardcheck")),
27                 heading=_("eligibility.pages.index.items[0].title"),
28                 details=_("eligibility.pages.index.items[0].text"),
29             ),
30             viewmodels.MediaItem(
31                 icon=viewmodels.Icon("bankcardcheck", pgettext("image alt text", "core.icons.bankcardcheck")),
32                 heading=_("eligibility.pages.index.items[1].title"),
33                 details=_("eligibility.pages.index.items[1].text"),
34             ),
35         ],
36         paragraphs=[_("eligibility.pages.index.p[0]")],
37         image=_index_image(),
38         button=viewmodels.Button.primary(text=_("eligibility.pages.index.button"), url=reverse("eligibility:confirm")),
39     )
40 
41     return PageTemplateResponse(request, page)
42 
43 
44 @decorator_from_middleware(middleware.AgencySessionRequired)
45 @decorator_from_middleware(middleware.RateLimit)
46 def confirm(request):
47     """View handler for the eligibility verification form."""
48 
49     page = viewmodels.Page(
50         title=_("eligibility.pages.confirm.title"),
51         content_title=_("eligibility.pages.confirm.content_title"),
52         paragraphs=[_("eligibility.pages.confirm.p[0]")],
53         form=forms.EligibilityVerificationForm(auto_id=True, label_suffix=""),
54         classes="text-lg-center",
55     )
56 
57     if request.method == "POST":
58         analytics.started_eligibility(request)
59 
60         form = forms.EligibilityVerificationForm(request.POST)
61         response = _verify(request, form)
62 
63         if response is None:
64             # form was not valid, allow for correction/resubmission
65             analytics.returned_error(request, form.errors)
66             page.forms = [form]
67             response = PageTemplateResponse(request, page)
68     elif session.eligible(request):
69         eligibility = session.eligibility(request)
70         response = verified(request, [eligibility.name])
71     else:
72         response = PageTemplateResponse(request, page)
73 
74     return response
75 
76 
77 def _verify(request, form):
78     """Helper calls the eligibility verification API with user input."""
79 
80     if not form.is_valid():
81         if recaptcha.has_error(form):
82             messages.error(request, "Recaptcha failed. Please try again.")
83         return None
84 
85     sub, name = form.cleaned_data.get("sub"), form.cleaned_data.get("name")
86 
87     agency = session.agency(request)
88     verifier = session.verifier(request)
89     client = api.Client(agency, verifier)
90 
91     response = client.verify(sub, name)
92 
93     if response.error and any(response.error):
94         form.add_api_errors(response.error)
95         return None
96     elif any(response.eligibility):
97         return verified(request, response.eligibility)
98     else:
99         return unverified(request)
100 
101 
102 @decorator_from_middleware(middleware.AgencySessionRequired)
103 def verified(request, verified_types):
104     """View handler for the verified eligibility page."""
105 
106     analytics.returned_success(request)
107 
108     enrollment_index = reverse("enrollment:index")
109     session.update(request, eligibility_types=verified_types, origin=enrollment_index)
110 
111     return redirect(enrollment_index)
112 
113 
114 @decorator_from_middleware(middleware.AgencySessionRequired)
115 def unverified(request):
116     """View handler for the unverified eligibility page."""
117 
118     analytics.returned_fail(request)
119 
120     # tel: link to agency phone number
121     agency = session.agency(request)
122     buttons = viewmodels.Button.agency_contact_links(agency)
123 
124     page = viewmodels.Page(
125         title=_("eligibility.pages.unverified.title"),
126         content_title=_("eligibility.pages.unverified.content_title"),
127         icon=viewmodels.Icon("idcardquestion", pgettext("image alt text", "core.icons.idcardquestion")),
128         paragraphs=[_("eligibility.pages.unverified.p[0]"), _("eligibility.pages.unverified.p[1]")],
129         buttons=buttons,
130         classes="text-lg-center",
131     )
132 
133     return PageTemplateResponse(request, page)
134
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/benefits/core/middleware.py b/benefits/core/middleware.py
--- a/benefits/core/middleware.py
+++ b/benefits/core/middleware.py
@@ -96,6 +96,17 @@
         return self.get_response(request)
 
 
+class VerifierSessionRequired(MiddlewareMixin):
+    """Middleware raises an exception for sessions lacking an eligibility verifier configuration."""
+
+    def process_request(self, request):
+        if session.verifier(request):
+            logger.debug("Session configured with eligibility verifier")
+            return None
+        else:
+            raise AttributeError("Session not configured with eligibility verifier")
+
+
 class ViewedPageEvent(MiddlewareMixin):
     """Middleware sends an analytics event for page views."""
 
diff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py
--- a/benefits/eligibility/views.py
+++ b/benefits/eligibility/views.py
@@ -13,6 +13,7 @@
 
 
 @decorator_from_middleware(middleware.AgencySessionRequired)
+@decorator_from_middleware(middleware.VerifierSessionRequired)
 def index(request):
     """View handler for the eligibility verification getting started screen."""
 
@@ -43,6 +44,7 @@
 
 @decorator_from_middleware(middleware.AgencySessionRequired)
 @decorator_from_middleware(middleware.RateLimit)
+@decorator_from_middleware(middleware.VerifierSessionRequired)
 def confirm(request):
     """View handler for the eligibility verification form."""

{"golden_diff": "diff --git a/benefits/core/middleware.py b/benefits/core/middleware.py\n--- a/benefits/core/middleware.py\n+++ b/benefits/core/middleware.py\n@@ -96,6 +96,17 @@\n         return self.get_response(request)\n \n \n+class VerifierSessionRequired(MiddlewareMixin):\n+    \"\"\"Middleware raises an exception for sessions lacking an eligibility verifier configuration.\"\"\"\n+\n+    def process_request(self, request):\n+        if session.verifier(request):\n+            logger.debug(\"Session configured with eligibility verifier\")\n+            return None\n+        else:\n+            raise AttributeError(\"Session not configured with eligibility verifier\")\n+\n+\n class ViewedPageEvent(MiddlewareMixin):\n     \"\"\"Middleware sends an analytics event for page views.\"\"\"\n \ndiff --git a/benefits/eligibility/views.py b/benefits/eligibility/views.py\n--- a/benefits/eligibility/views.py\n+++ b/benefits/eligibility/views.py\n@@ -13,6 +13,7 @@\n \n \n @decorator_from_middleware(middleware.AgencySessionRequired)\n+@decorator_from_middleware(middleware.VerifierSessionRequired)\n def index(request):\n     \"\"\"View handler for the eligibility verification getting started screen.\"\"\"\n \n@@ -43,6 +44,7 @@\n \n @decorator_from_middleware(middleware.AgencySessionRequired)\n @decorator_from_middleware(middleware.RateLimit)\n+@decorator_from_middleware(middleware.VerifierSessionRequired)\n def confirm(request):\n     \"\"\"View handler for the eligibility verification form.\"\"\"\n", "issue": "Create VerifierSessionRequired middleware that expects a verifier to exist in session\n## Background\r\n\r\nOnce the session tracks the selected verifier in #321, we can make use of that infrastructure to put guards on certain view functions that require a verifier to be selected. The first step is to create a new middleware class that enforces the requirement.\r\n\r\nThis is similar to how the [`AgencySessionRequired`](https://github.com/cal-itp/benefits/blob/dev/benefits/core/middleware.py#L20) and [`EligibileSessionRequired`](https://github.com/cal-itp/benefits/blob/dev/benefits/core/middleware.py#L68) middleware are used.\r\n\r\n## Tasks\r\n\r\n- [x] Create a new middleware class like `VerifierSessionRequired` inheriting from `MiddlewareMixin`, see the other `*SessionRequired` as examples\r\n- [x] In `process_request()`, check `session.verifier()` for the request.\r\n  - If `None`, raise an error to stop the request\r\n  - Otherwise return `None` to allow the request to continue\r\n- [x] Apply this middleware to the following views to enforce that a verifier is needed:\r\n  - [x]  [`eligibility:index`](https://github.com/cal-itp/benefits/blob/dev/benefits/eligibility/views.py#L16)\r\n  - [x]  [`eligibility:confirm`](https://github.com/cal-itp/benefits/blob/dev/benefits/eligibility/views.py#L46)\r\n\n", "before_files": [{"content": "\"\"\"\nThe core application: middleware definitions for request/response cycle.\n\"\"\"\nimport logging\nimport time\n\nfrom django.http import HttpResponse, HttpResponseBadRequest\nfrom django.template import loader\nfrom django.utils.decorators import decorator_from_middleware\nfrom django.utils.deprecation import MiddlewareMixin\nfrom django.views import i18n\n\nfrom benefits.settings import RATE_LIMIT, RATE_LIMIT_METHODS, RATE_LIMIT_PERIOD, DEBUG\nfrom . import analytics, session, viewmodels\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass AgencySessionRequired(MiddlewareMixin):\n    \"\"\"Middleware raises an exception for sessions lacking an agency configuration.\"\"\"\n\n    def process_request(self, request):\n        if session.active_agency(request):\n            logger.debug(\"Session configured with agency\")\n            return None\n        else:\n            raise AttributeError(\"Session not configured with agency\")\n\n\nclass RateLimit(MiddlewareMixin):\n    \"\"\"Middleware checks settings and session to ensure rate limit is respected.\"\"\"\n\n    def process_request(self, request):\n        if any((RATE_LIMIT < 1, len(RATE_LIMIT_METHODS) < 1, RATE_LIMIT_PERIOD < 1)):\n            logger.debug(\"RATE_LIMIT, RATE_LIMIT_METHODS, or RATE_LIMIT_PERIOD are not configured\")\n            return None\n\n        if request.method in RATE_LIMIT_METHODS:\n            session.increment_rate_limit_counter(request)\n        else:\n            # bail early if the request method doesn't match\n            return None\n\n        counter = session.rate_limit_counter(request)\n        reset_time = session.rate_limit_time(request)\n        now = int(time.time())\n\n        if counter > RATE_LIMIT:\n            if reset_time > now:\n                logger.warn(\"Rate limit exceeded\")\n                home = viewmodels.Button.home(request)\n                page = viewmodels.ErrorPage.error(\n                    title=\"Rate limit error\",\n                    content_title=\"Rate limit error\",\n                    paragraphs=[\"You have reached the rate limit. Please try again.\"],\n                    button=home,\n                )\n                t = loader.get_template(\"400.html\")\n                return HttpResponseBadRequest(t.render(page.context_dict()))\n            else:\n                # enough time has passed, reset the rate limit\n                session.reset_rate_limit(request)\n\n        return None\n\n\nclass EligibleSessionRequired(MiddlewareMixin):\n    \"\"\"Middleware raises an exception for sessions lacking confirmed eligibility.\"\"\"\n\n    def process_request(self, request):\n        if session.eligible(request):\n            logger.debug(\"Session has confirmed eligibility\")\n            return None\n        else:\n            raise AttributeError(\"Session has no confirmed eligibility\")\n\n\nclass DebugSession(MiddlewareMixin):\n    \"\"\"Middleware to configure debug context in the request session.\"\"\"\n\n    def process_request(self, request):\n        session.update(request, debug=DEBUG)\n        return None\n\n\nclass Healthcheck:\n    \"\"\"Middleware intercepts and accepts /healthcheck requests.\"\"\"\n\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def __call__(self, request):\n        if request.path == \"/healthcheck\":\n            return HttpResponse(\"Healthy\", content_type=\"text/plain\")\n        return self.get_response(request)\n\n\nclass ViewedPageEvent(MiddlewareMixin):\n    \"\"\"Middleware sends an analytics event for page views.\"\"\"\n\n    def process_response(self, request, response):\n        event = analytics.ViewedPageEvent(request)\n        try:\n            analytics.send_event(event)\n        except Exception:\n            logger.warning(f\"Failed to send event: {event}\")\n        finally:\n            return response\n\n\npageview_decorator = decorator_from_middleware(ViewedPageEvent)\n\n\nclass ChangedLanguageEvent(MiddlewareMixin):\n    \"\"\"Middleware hooks into django.views.i18n.set_language to send an analytics event.\"\"\"\n\n    def process_view(self, request, view_func, view_args, view_kwargs):\n        if view_func == i18n.set_language:\n            new_lang = request.POST[\"language\"]\n            event = analytics.ChangedLanguageEvent(request, new_lang)\n            analytics.send_event(event)\n        return None\n", "path": "benefits/core/middleware.py"}, {"content": "\"\"\"\nThe eligibility application: view definitions for the eligibility verification flow.\n\"\"\"\nfrom django.contrib import messages\nfrom django.shortcuts import redirect\nfrom django.urls import reverse\nfrom django.utils.decorators import decorator_from_middleware\nfrom django.utils.translation import pgettext, gettext as _\n\nfrom benefits.core import middleware, recaptcha, session, viewmodels\nfrom benefits.core.views import PageTemplateResponse, _index_image\nfrom . import analytics, api, forms\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\ndef index(request):\n    \"\"\"View handler for the eligibility verification getting started screen.\"\"\"\n\n    session.update(request, eligibility_types=[], origin=reverse(\"eligibility:index\"))\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.index.title\"),\n        content_title=_(\"eligibility.pages.index.content_title\"),\n        media=[\n            viewmodels.MediaItem(\n                icon=viewmodels.Icon(\"idcardcheck\", pgettext(\"image alt text\", \"core.icons.idcardcheck\")),\n                heading=_(\"eligibility.pages.index.items[0].title\"),\n                details=_(\"eligibility.pages.index.items[0].text\"),\n            ),\n            viewmodels.MediaItem(\n                icon=viewmodels.Icon(\"bankcardcheck\", pgettext(\"image alt text\", \"core.icons.bankcardcheck\")),\n                heading=_(\"eligibility.pages.index.items[1].title\"),\n                details=_(\"eligibility.pages.index.items[1].text\"),\n            ),\n        ],\n        paragraphs=[_(\"eligibility.pages.index.p[0]\")],\n        image=_index_image(),\n        button=viewmodels.Button.primary(text=_(\"eligibility.pages.index.button\"), url=reverse(\"eligibility:confirm\")),\n    )\n\n    return PageTemplateResponse(request, page)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\n@decorator_from_middleware(middleware.RateLimit)\ndef confirm(request):\n    \"\"\"View handler for the eligibility verification form.\"\"\"\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.confirm.title\"),\n        content_title=_(\"eligibility.pages.confirm.content_title\"),\n        paragraphs=[_(\"eligibility.pages.confirm.p[0]\")],\n        form=forms.EligibilityVerificationForm(auto_id=True, label_suffix=\"\"),\n        classes=\"text-lg-center\",\n    )\n\n    if request.method == \"POST\":\n        analytics.started_eligibility(request)\n\n        form = forms.EligibilityVerificationForm(request.POST)\n        response = _verify(request, form)\n\n        if response is None:\n            # form was not valid, allow for correction/resubmission\n            analytics.returned_error(request, form.errors)\n            page.forms = [form]\n            response = PageTemplateResponse(request, page)\n    elif session.eligible(request):\n        eligibility = session.eligibility(request)\n        response = verified(request, [eligibility.name])\n    else:\n        response = PageTemplateResponse(request, page)\n\n    return response\n\n\ndef _verify(request, form):\n    \"\"\"Helper calls the eligibility verification API with user input.\"\"\"\n\n    if not form.is_valid():\n        if recaptcha.has_error(form):\n            messages.error(request, \"Recaptcha failed. Please try again.\")\n        return None\n\n    sub, name = form.cleaned_data.get(\"sub\"), form.cleaned_data.get(\"name\")\n\n    agency = session.agency(request)\n    verifier = session.verifier(request)\n    client = api.Client(agency, verifier)\n\n    response = client.verify(sub, name)\n\n    if response.error and any(response.error):\n        form.add_api_errors(response.error)\n        return None\n    elif any(response.eligibility):\n        return verified(request, response.eligibility)\n    else:\n        return unverified(request)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\ndef verified(request, verified_types):\n    \"\"\"View handler for the verified eligibility page.\"\"\"\n\n    analytics.returned_success(request)\n\n    enrollment_index = reverse(\"enrollment:index\")\n    session.update(request, eligibility_types=verified_types, origin=enrollment_index)\n\n    return redirect(enrollment_index)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\ndef unverified(request):\n    \"\"\"View handler for the unverified eligibility page.\"\"\"\n\n    analytics.returned_fail(request)\n\n    # tel: link to agency phone number\n    agency = session.agency(request)\n    buttons = viewmodels.Button.agency_contact_links(agency)\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.unverified.title\"),\n        content_title=_(\"eligibility.pages.unverified.content_title\"),\n        icon=viewmodels.Icon(\"idcardquestion\", pgettext(\"image alt text\", \"core.icons.idcardquestion\")),\n        paragraphs=[_(\"eligibility.pages.unverified.p[0]\"), _(\"eligibility.pages.unverified.p[1]\")],\n        buttons=buttons,\n        classes=\"text-lg-center\",\n    )\n\n    return PageTemplateResponse(request, page)\n", "path": "benefits/eligibility/views.py"}], "after_files": [{"content": "\"\"\"\nThe core application: middleware definitions for request/response cycle.\n\"\"\"\nimport logging\nimport time\n\nfrom django.http import HttpResponse, HttpResponseBadRequest\nfrom django.template import loader\nfrom django.utils.decorators import decorator_from_middleware\nfrom django.utils.deprecation import MiddlewareMixin\nfrom django.views import i18n\n\nfrom benefits.settings import RATE_LIMIT, RATE_LIMIT_METHODS, RATE_LIMIT_PERIOD, DEBUG\nfrom . import analytics, session, viewmodels\n\n\nlogger = logging.getLogger(__name__)\n\n\nclass AgencySessionRequired(MiddlewareMixin):\n    \"\"\"Middleware raises an exception for sessions lacking an agency configuration.\"\"\"\n\n    def process_request(self, request):\n        if session.active_agency(request):\n            logger.debug(\"Session configured with agency\")\n            return None\n        else:\n            raise AttributeError(\"Session not configured with agency\")\n\n\nclass RateLimit(MiddlewareMixin):\n    \"\"\"Middleware checks settings and session to ensure rate limit is respected.\"\"\"\n\n    def process_request(self, request):\n        if any((RATE_LIMIT < 1, len(RATE_LIMIT_METHODS) < 1, RATE_LIMIT_PERIOD < 1)):\n            logger.debug(\"RATE_LIMIT, RATE_LIMIT_METHODS, or RATE_LIMIT_PERIOD are not configured\")\n            return None\n\n        if request.method in RATE_LIMIT_METHODS:\n            session.increment_rate_limit_counter(request)\n        else:\n            # bail early if the request method doesn't match\n            return None\n\n        counter = session.rate_limit_counter(request)\n        reset_time = session.rate_limit_time(request)\n        now = int(time.time())\n\n        if counter > RATE_LIMIT:\n            if reset_time > now:\n                logger.warn(\"Rate limit exceeded\")\n                home = viewmodels.Button.home(request)\n                page = viewmodels.ErrorPage.error(\n                    title=\"Rate limit error\",\n                    content_title=\"Rate limit error\",\n                    paragraphs=[\"You have reached the rate limit. Please try again.\"],\n                    button=home,\n                )\n                t = loader.get_template(\"400.html\")\n                return HttpResponseBadRequest(t.render(page.context_dict()))\n            else:\n                # enough time has passed, reset the rate limit\n                session.reset_rate_limit(request)\n\n        return None\n\n\nclass EligibleSessionRequired(MiddlewareMixin):\n    \"\"\"Middleware raises an exception for sessions lacking confirmed eligibility.\"\"\"\n\n    def process_request(self, request):\n        if session.eligible(request):\n            logger.debug(\"Session has confirmed eligibility\")\n            return None\n        else:\n            raise AttributeError(\"Session has no confirmed eligibility\")\n\n\nclass DebugSession(MiddlewareMixin):\n    \"\"\"Middleware to configure debug context in the request session.\"\"\"\n\n    def process_request(self, request):\n        session.update(request, debug=DEBUG)\n        return None\n\n\nclass Healthcheck:\n    \"\"\"Middleware intercepts and accepts /healthcheck requests.\"\"\"\n\n    def __init__(self, get_response):\n        self.get_response = get_response\n\n    def __call__(self, request):\n        if request.path == \"/healthcheck\":\n            return HttpResponse(\"Healthy\", content_type=\"text/plain\")\n        return self.get_response(request)\n\n\nclass VerifierSessionRequired(MiddlewareMixin):\n    \"\"\"Middleware raises an exception for sessions lacking an eligibility verifier configuration.\"\"\"\n\n    def process_request(self, request):\n        if session.verifier(request):\n            logger.debug(\"Session configured with eligibility verifier\")\n            return None\n        else:\n            raise AttributeError(\"Session not configured with eligibility verifier\")\n\n\nclass ViewedPageEvent(MiddlewareMixin):\n    \"\"\"Middleware sends an analytics event for page views.\"\"\"\n\n    def process_response(self, request, response):\n        event = analytics.ViewedPageEvent(request)\n        try:\n            analytics.send_event(event)\n        except Exception:\n            logger.warning(f\"Failed to send event: {event}\")\n        finally:\n            return response\n\n\npageview_decorator = decorator_from_middleware(ViewedPageEvent)\n\n\nclass ChangedLanguageEvent(MiddlewareMixin):\n    \"\"\"Middleware hooks into django.views.i18n.set_language to send an analytics event.\"\"\"\n\n    def process_view(self, request, view_func, view_args, view_kwargs):\n        if view_func == i18n.set_language:\n            new_lang = request.POST[\"language\"]\n            event = analytics.ChangedLanguageEvent(request, new_lang)\n            analytics.send_event(event)\n        return None\n", "path": "benefits/core/middleware.py"}, {"content": "\"\"\"\nThe eligibility application: view definitions for the eligibility verification flow.\n\"\"\"\nfrom django.contrib import messages\nfrom django.shortcuts import redirect\nfrom django.urls import reverse\nfrom django.utils.decorators import decorator_from_middleware\nfrom django.utils.translation import pgettext, gettext as _\n\nfrom benefits.core import middleware, recaptcha, session, viewmodels\nfrom benefits.core.views import PageTemplateResponse, _index_image\nfrom . import analytics, api, forms\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\n@decorator_from_middleware(middleware.VerifierSessionRequired)\ndef index(request):\n    \"\"\"View handler for the eligibility verification getting started screen.\"\"\"\n\n    session.update(request, eligibility_types=[], origin=reverse(\"eligibility:index\"))\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.index.title\"),\n        content_title=_(\"eligibility.pages.index.content_title\"),\n        media=[\n            viewmodels.MediaItem(\n                icon=viewmodels.Icon(\"idcardcheck\", pgettext(\"image alt text\", \"core.icons.idcardcheck\")),\n                heading=_(\"eligibility.pages.index.items[0].title\"),\n                details=_(\"eligibility.pages.index.items[0].text\"),\n            ),\n            viewmodels.MediaItem(\n                icon=viewmodels.Icon(\"bankcardcheck\", pgettext(\"image alt text\", \"core.icons.bankcardcheck\")),\n                heading=_(\"eligibility.pages.index.items[1].title\"),\n                details=_(\"eligibility.pages.index.items[1].text\"),\n            ),\n        ],\n        paragraphs=[_(\"eligibility.pages.index.p[0]\")],\n        image=_index_image(),\n        button=viewmodels.Button.primary(text=_(\"eligibility.pages.index.button\"), url=reverse(\"eligibility:confirm\")),\n    )\n\n    return PageTemplateResponse(request, page)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\n@decorator_from_middleware(middleware.RateLimit)\n@decorator_from_middleware(middleware.VerifierSessionRequired)\ndef confirm(request):\n    \"\"\"View handler for the eligibility verification form.\"\"\"\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.confirm.title\"),\n        content_title=_(\"eligibility.pages.confirm.content_title\"),\n        paragraphs=[_(\"eligibility.pages.confirm.p[0]\")],\n        form=forms.EligibilityVerificationForm(auto_id=True, label_suffix=\"\"),\n        classes=\"text-lg-center\",\n    )\n\n    if request.method == \"POST\":\n        analytics.started_eligibility(request)\n\n        form = forms.EligibilityVerificationForm(request.POST)\n        response = _verify(request, form)\n\n        if response is None:\n            # form was not valid, allow for correction/resubmission\n            analytics.returned_error(request, form.errors)\n            page.forms = [form]\n            response = PageTemplateResponse(request, page)\n    elif session.eligible(request):\n        eligibility = session.eligibility(request)\n        response = verified(request, [eligibility.name])\n    else:\n        response = PageTemplateResponse(request, page)\n\n    return response\n\n\ndef _verify(request, form):\n    \"\"\"Helper calls the eligibility verification API with user input.\"\"\"\n\n    if not form.is_valid():\n        if recaptcha.has_error(form):\n            messages.error(request, \"Recaptcha failed. Please try again.\")\n        return None\n\n    sub, name = form.cleaned_data.get(\"sub\"), form.cleaned_data.get(\"name\")\n\n    agency = session.agency(request)\n    verifier = session.verifier(request)\n    client = api.Client(agency, verifier)\n\n    response = client.verify(sub, name)\n\n    if response.error and any(response.error):\n        form.add_api_errors(response.error)\n        return None\n    elif any(response.eligibility):\n        return verified(request, response.eligibility)\n    else:\n        return unverified(request)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\ndef verified(request, verified_types):\n    \"\"\"View handler for the verified eligibility page.\"\"\"\n\n    analytics.returned_success(request)\n\n    enrollment_index = reverse(\"enrollment:index\")\n    session.update(request, eligibility_types=verified_types, origin=enrollment_index)\n\n    return redirect(enrollment_index)\n\n\n@decorator_from_middleware(middleware.AgencySessionRequired)\ndef unverified(request):\n    \"\"\"View handler for the unverified eligibility page.\"\"\"\n\n    analytics.returned_fail(request)\n\n    # tel: link to agency phone number\n    agency = session.agency(request)\n    buttons = viewmodels.Button.agency_contact_links(agency)\n\n    page = viewmodels.Page(\n        title=_(\"eligibility.pages.unverified.title\"),\n        content_title=_(\"eligibility.pages.unverified.content_title\"),\n        icon=viewmodels.Icon(\"idcardquestion\", pgettext(\"image alt text\", \"core.icons.idcardquestion\")),\n        paragraphs=[_(\"eligibility.pages.unverified.p[0]\"), _(\"eligibility.pages.unverified.p[1]\")],\n        buttons=buttons,\n        classes=\"text-lg-center\",\n    )\n\n    return PageTemplateResponse(request, page)\n", "path": "benefits/eligibility/views.py"}]}