Skip to content

Refactor: Class-based views - Eligibility index #2913

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

machikoyasuda
Copy link
Member

@machikoyasuda machikoyasuda commented May 13, 2025

closes #2868

This PR converts the Eligibility Index view method from a function-based view into a class-based view (CBV, reference here https://ccbv.co.uk/). This PR uses both mixins from #2875 and #2882.

What this PR does

Notes

  • I kept the comments there for posterity. Do we still want/need them?
  • I updated one test. One part of a spec was failing because it now was returning like this: assert ['eligibility/index.html'] == 'eligibility/index.html', b/c FormView returns a list of template names, instead of a string. I changed the test to match what FormView now does, instead of changing FormView itself to return a string like the test expects.

uses both AgencySessionRequiredMixin and RecaptchaEnabledMixin in class-based index view. updates url to reflect this change.
@machikoyasuda machikoyasuda self-assigned this May 13, 2025
@machikoyasuda machikoyasuda requested a review from a team as a code owner May 13, 2025 21:24
@github-actions github-actions bot added back-end Django views, sessions, middleware, models, migrations etc. deployment-dev [auto] Changes that will trigger a deploy if merged to dev labels May 13, 2025
# clear any prior OAuth token as the user is choosing their desired flow
# this may or may not require OAuth, with a different set of scope/claims than what is already stored
session.logout(request)
template_name = "eligibility/index.html"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this template name be defined on line 19, under TEMPLATE_CONFIRM, for consistency?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these global variables for template names were an artifact of having view functions. With class-based views, one of the main benefits is that everything is more cohesive and together, so I don't think we need to keep using these global variables for template names.

Copy link

github-actions bot commented May 13, 2025

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  benefits/eligibility
  views.py 62
Project Total  

This report was generated by python-coverage-comment-action

@machikoyasuda machikoyasuda marked this pull request as draft May 13, 2025 22:14
@machikoyasuda
Copy link
Member Author

After reading #2870, I started thinking this could be written as a FormView instead. Any thoughts on which way to go? @angela-tran @lalver1 @thekaveman

@angela-tran
Copy link
Member

angela-tran commented May 13, 2025

After reading #2870, I started thinking this could be written as a FormView instead. Any thoughts on which way to go? @angela-tran @lalver1 @thekaveman

I probably have the least hands-on experience with class-based views, so take this with a grain of salt, but I think it's definitely worth looking into using a FormView and whether that makes the code simpler / more readable or not. (There is a form on this view after all.)

@machikoyasuda machikoyasuda force-pushed the refactor/2868-cbv-elig-index branch from f0a290e to c390004 Compare May 14, 2025 00:16
@machikoyasuda machikoyasuda marked this pull request as ready for review May 14, 2025 00:39
def dispatch(self, request, *args, **kwargs):
"""Initialize session state before handling the request."""
if not self.agency:
return TemplateResponse(request, "200-user-error.html")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this CBV inherits from the AgencySessionRequiredMixin, I don't think this logic is needed here in the dispatch() override.

Presumably if the request got this far, it has an agency in the session.

Copy link
Member

@thekaveman thekaveman May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is a little more nuanced than I previously understood.

We have AgencySessionRequiredMixin which defines its logic in the dispatch() method. The original idea was that CBVs inheriting from this mixin would first have the mixin's dispatch() called (which does the check, sets the self.agency variable) and then if that was successful, fall through to the CBV's dispatch() (or return user_error if not successful).

This is sort of how it works.

How MRO affects dispatch calls

When we have a view like class IndexView(AgencySessionRequiredMixin, View), and view_instance.dispatch() is called, Python looks for the dispatch method in this order (simplified):

  1. IndexView
  2. AgencySessionRequiredMixin
  3. View (e.g., django.views.View)
  4. Other parent classes of View (if any)

What this means for mixins

E.g. AgencySessionRequiredMixin relies on its own dispatch method being executed to perform the agency check.

Scenario A: The view using the mixin does NOT define dispatch

If IndexView does not have its own dispatch method, then AgencySessionRequiredMixin.dispatch is found and executed. The mixin runs its check, and if an agency is present, super().dispatch() in the mixin correctly passes control to View.dispatch.

Scenario B: The view using the mixin DOES define dispatch

If IndexView does define its own dispatch method (like in this PR), then IndexView.dispatch will be called instead of AgencySessionRequiredMixin.dispatch.

If IndexView.dispatch simply does its own thing and does not call super().dispatch(), then the mixin's dispatch logic is entirely bypassed.

However, if IndexView.dispatch does call super().dispatch() (like in this PR), then control will be passed to AgencySessionRequiredMixin.dispatch (as it's next in the MRO). The mixin can then perform its check, and its own super().dispatch() call will continue the chain to View.dispatch.

TLDR;

If possible, I think we should avoid implementing dispatch in our CBVs. Can this logic move to the get method or similar?

If not possible (e.g. we absolutely need to implement some logic in the CBVs dispatch method), it MUST call super().dispatch() early, before taking any action that would require the results of the mixin's dispatch (e.g. the session check). I don't think this will always (maybe never?) be possible, since the whole point of our *SessionRequiredMixins is to block the request. If we call super().dispatch() early in the view's dispatch, that is essentially like processing the entire request, generating a response, AND THEN checking for the agency in the session.

def dispatch(self, request, *args, **kwargs):
"""Initialize session state before handling the request."""
if not self.agency:
return TemplateResponse(request, "200-user-error.html")
Copy link
Member

@thekaveman thekaveman May 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is a little more nuanced than I previously understood.

We have AgencySessionRequiredMixin which defines its logic in the dispatch() method. The original idea was that CBVs inheriting from this mixin would first have the mixin's dispatch() called (which does the check, sets the self.agency variable) and then if that was successful, fall through to the CBV's dispatch() (or return user_error if not successful).

This is sort of how it works.

How MRO affects dispatch calls

When we have a view like class IndexView(AgencySessionRequiredMixin, View), and view_instance.dispatch() is called, Python looks for the dispatch method in this order (simplified):

  1. IndexView
  2. AgencySessionRequiredMixin
  3. View (e.g., django.views.View)
  4. Other parent classes of View (if any)

What this means for mixins

E.g. AgencySessionRequiredMixin relies on its own dispatch method being executed to perform the agency check.

Scenario A: The view using the mixin does NOT define dispatch

If IndexView does not have its own dispatch method, then AgencySessionRequiredMixin.dispatch is found and executed. The mixin runs its check, and if an agency is present, super().dispatch() in the mixin correctly passes control to View.dispatch.

Scenario B: The view using the mixin DOES define dispatch

If IndexView does define its own dispatch method (like in this PR), then IndexView.dispatch will be called instead of AgencySessionRequiredMixin.dispatch.

If IndexView.dispatch simply does its own thing and does not call super().dispatch(), then the mixin's dispatch logic is entirely bypassed.

However, if IndexView.dispatch does call super().dispatch() (like in this PR), then control will be passed to AgencySessionRequiredMixin.dispatch (as it's next in the MRO). The mixin can then perform its check, and its own super().dispatch() call will continue the chain to View.dispatch.

TLDR;

If possible, I think we should avoid implementing dispatch in our CBVs. Can this logic move to the get method or similar?

If not possible (e.g. we absolutely need to implement some logic in the CBVs dispatch method), it MUST call super().dispatch() early, before taking any action that would require the results of the mixin's dispatch (e.g. the session check). I don't think this will always (maybe never?) be possible, since the whole point of our *SessionRequiredMixins is to block the request. If we call super().dispatch() early in the view's dispatch, that is essentially like processing the entire request, generating a response, AND THEN checking for the agency in the session.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
back-end Django views, sessions, middleware, models, migrations etc. deployment-dev [auto] Changes that will trigger a deploy if merged to dev
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Class-based views: Eligibility index
3 participants