From 19cf724fbebbcf29e429fc02a4b35d00338bade0 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 6 Jul 2026 22:12:59 +0000 Subject: [PATCH 1/7] chore: add the django-debug-toolbar dev dependency and configure Django to use it. To activate it, set the environment variable DJANGO_DEBUG_TOOLBAR=true. The toolbar interferes with Pytest, so to run tests in VS Code or from the run.sh script, set DJANGO_DEBUG_TOOLBAR=false. --- .env.sample | 1 + benefits/settings.py | 17 +++++++++++++++++ benefits/urls.py | 6 ++++++ pyproject.toml | 1 + 4 files changed, 25 insertions(+) diff --git a/.env.sample b/.env.sample index a7d70a90f0..c82791a9b1 100644 --- a/.env.sample +++ b/.env.sample @@ -6,6 +6,7 @@ DJANGO_SUPERUSER_EMAIL=benefits-admin@calitp.org # Django DJANGO_STORAGE_DIR=. DJANGO_DB_FIXTURES="benefits/core/migrations/local_fixtures.json" +DJANGO_DEBUG_TOOLBAR=false # DJANGO_LOCAL_PORT= USE_POSTGRES=false diff --git a/benefits/settings.py b/benefits/settings.py index 7ac97853ec..5d899ce8e2 100644 --- a/benefits/settings.py +++ b/benefits/settings.py @@ -23,6 +23,9 @@ def _filter_empty(ls): # SECURITY WARNING: don't run with debug turned on in production! DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true" +# SECURITY WARNING: don't run with debug_toolbar turned on in production! +DEBUG_TOOLBAR = os.environ.get("DJANGO_DEBUG_TOOLBAR", "false").lower() == "true" + ALLOWED_HOSTS = _filter_empty(os.environ.get("DJANGO_ALLOWED_HOSTS", "localhost").split(",")) @@ -107,6 +110,20 @@ def RUNTIME_ENVIRONMENT(): if DEBUG: MIDDLEWARE.append("benefits.core.middleware.DebugSession") +if DEBUG_TOOLBAR: + INSTALLED_APPS.append("debug_toolbar") + MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") + INTERNAL_IPS = ["127.0.0.1"] + + def show_toolbar(request): + # Show the toolbar when in local development mode + return DEBUG + + DEBUG_TOOLBAR_CONFIG = { + "SHOW_TOOLBAR_CALLBACK": show_toolbar, + } + + HEALTHCHECK_USER_AGENTS = _filter_empty(os.environ.get("HEALTHCHECK_USER_AGENTS", "").split(",")) CSRF_COOKIE_AGE = None diff --git a/benefits/urls.py b/benefits/urls.py index f69ec8e71f..1ba91a4efa 100644 --- a/benefits/urls.py +++ b/benefits/urls.py @@ -79,6 +79,12 @@ def trigger_csrf(request): urlpatterns.append(path("test500/", trigger_500)) urlpatterns.append(path("testcsrf/", trigger_csrf)) +if settings.DEBUG_TOOLBAR: + from debug_toolbar.toolbar import debug_toolbar_urls + + urlpatterns.extend(debug_toolbar_urls()) + + if settings.RUNTIME_ENVIRONMENT() in (settings.RUNTIME_ENVS.LOCAL, settings.RUNTIME_ENVS.DEV): # simple route to read a pre-defined "secret" # this "secret" does not contain sensitive information diff --git a/pyproject.toml b/pyproject.toml index 51683a91e3..23e2dc7d0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dev = [ "flake8", "isort", "pre-commit", + "django-debug-toolbar" ] test = [ "coverage", From aba832508b29459ca26b01613ffdc34d82d10f83 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 9 Jul 2026 20:18:42 +0000 Subject: [PATCH 2/7] refactor(session): don't query the database if a flow isn't selected If session.flow equals None, this will avoid queries that look like SELECT ... WHERE "core_enrollmentflow"."id" IS NULL and which are unnecessary. If session.flow does not exist (KeyError), using .get on the session ensures safety so we can remove KeyError from the try...except. --- benefits/core/session.py | 7 +++++-- tests/pytest/core/test_session.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/benefits/core/session.py b/benefits/core/session.py index 20188e4f4e..e12e394630 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -124,9 +124,12 @@ def enrollment_reenrollment(request): def flow(request) -> models.EnrollmentFlow | None: """Get the EnrollmentFlow from the request's session, or None""" + flow_id = request.session.get(_FLOW) + if not flow_id: + return None try: - return models.EnrollmentFlow.by_id(request.session[_FLOW]) - except (KeyError, models.EnrollmentFlow.DoesNotExist): + return models.EnrollmentFlow.by_id(flow_id) + except models.EnrollmentFlow.DoesNotExist: return None diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index fc5a0c7177..2b52c698bc 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -132,6 +132,12 @@ def test_enrollment_reenrollment(app_request, model_EnrollmentFlow_supports_expi assert session.enrollment_reenrollment(app_request) == expected_reenrollment +@pytest.mark.django_db +def test_flow_does_not_exist(app_request): + app_request.session[session._FLOW] = -1 + assert not session.flow(app_request) + + @pytest.mark.django_db def test_language_default(app_request): assert session.language(app_request) == "en" From 0363ca764e0a065d495ae4084c56489a048e7f4a Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Thu, 9 Jul 2026 20:30:43 +0000 Subject: [PATCH 3/7] refactor(session): don't query the database if an agency isn't selected If session.agency equals None, this will avoid queries that look like SELECT ... WHERE "core_transitagency"."id" IS NULL and which are unnecessary. If session.agency does not exist (KeyError), using .get on the session ensures safety so we can remove KeyError from the try...except. --- benefits/core/session.py | 7 +++++-- tests/pytest/core/test_session.py | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/benefits/core/session.py b/benefits/core/session.py index e12e394630..83e23fba3a 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -39,9 +39,12 @@ def agency(request): """Get the agency from the request's session, or None""" + agency_id = request.session.get(_AGENCY) + if not agency_id: + return None try: - return models.TransitAgency.by_id(request.session[_AGENCY]) - except (KeyError, models.TransitAgency.DoesNotExist): + return models.TransitAgency.by_id(agency_id) + except models.TransitAgency.DoesNotExist: return None diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index 2b52c698bc..3eecc9017d 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -13,6 +13,12 @@ from benefits.routes import routes +@pytest.mark.django_db +def test_agency_does_not_exist(app_request): + app_request.session[session._AGENCY] = -1 + assert not session.agency(app_request) + + @pytest.mark.django_db def test_active_agency_False(app_request, model_TransitAgency_inactive): session.update(app_request, agency=None) From 078b9d4867cf6d5c8cc06ef45f241fab4cece740 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Fri, 10 Jul 2026 19:19:32 +0000 Subject: [PATCH 4/7] refactor(session): cache result when calling agency The result from getting the transit agency from the request's session is cached to avoid querying the database every time session.agency(request) is called during the same request-response cycle. --- benefits/core/session.py | 7 ++++++- tests/pytest/core/test_session.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/benefits/core/session.py b/benefits/core/session.py index 83e23fba3a..c9190662ad 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -42,8 +42,13 @@ def agency(request): agency_id = request.session.get(_AGENCY) if not agency_id: return None + + if getattr(request, "_cached_agency", None) and request._cached_agency.id == agency_id: + return request._cached_agency try: - return models.TransitAgency.by_id(agency_id) + agency = models.TransitAgency.by_id(agency_id) + request._cached_agency = agency + return agency except models.TransitAgency.DoesNotExist: return None diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index 3eecc9017d..916c15b1d8 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -19,6 +19,23 @@ def test_agency_does_not_exist(app_request): assert not session.agency(app_request) +@pytest.mark.django_db +def test_agency_cache(model_TransitAgency, app_request): + session.update(app_request, agency=model_TransitAgency) + assert not hasattr(app_request, "_cached_agency") + + # set _cached_agency + first_call = session.agency(app_request) + + assert first_call == model_TransitAgency + assert hasattr(app_request, "_cached_agency") + assert app_request._cached_agency == model_TransitAgency + + second_call = session.agency(app_request) + + assert second_call == model_TransitAgency + + @pytest.mark.django_db def test_active_agency_False(app_request, model_TransitAgency_inactive): session.update(app_request, agency=None) From 6d689583f8251ab4f42aa8955126acad78267552 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 13 Jul 2026 16:29:30 +0000 Subject: [PATCH 5/7] refactor(session): cache result when calling flow The result from getting the flow from the request's session is cached to avoid querying the database every time session.flow(request) is called during the same request-response cycle. --- benefits/core/session.py | 7 ++++++- tests/pytest/core/test_session.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/benefits/core/session.py b/benefits/core/session.py index c9190662ad..e90a2da548 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -135,8 +135,13 @@ def flow(request) -> models.EnrollmentFlow | None: flow_id = request.session.get(_FLOW) if not flow_id: return None + + if getattr(request, "_cached_flow", None) and request._cached_flow.id == flow_id: + return request._cached_flow try: - return models.EnrollmentFlow.by_id(flow_id) + flow = models.EnrollmentFlow.by_id(flow_id) + request._cached_flow = flow + return flow except models.EnrollmentFlow.DoesNotExist: return None diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index 916c15b1d8..19bd834427 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -161,6 +161,23 @@ def test_flow_does_not_exist(app_request): assert not session.flow(app_request) +@pytest.mark.django_db +def test_flow_cache(model_EnrollmentFlow, app_request): + session.update(app_request, flow=model_EnrollmentFlow) + assert not hasattr(app_request, "_cached_flow") + + # set _cached_flow + first_call = session.flow(app_request) + + assert first_call == model_EnrollmentFlow + assert hasattr(app_request, "_cached_flow") + assert app_request._cached_flow == model_EnrollmentFlow + + second_call = session.flow(app_request) + + assert second_call == model_EnrollmentFlow + + @pytest.mark.django_db def test_language_default(app_request): assert session.language(app_request) == "en" From 2cae695b4d4ebee80cb49f6affe7b032f4cd61cc Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Mon, 13 Jul 2026 17:14:14 +0000 Subject: [PATCH 6/7] refactor(TransitAgency): evaluate QuerySet lazily which will avoid making extra queries by removing list, since the conversion to a list is not actually needed. --- benefits/core/models/transit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/core/models/transit.py b/benefits/core/models/transit.py index a1b840d731..76c3b453ed 100644 --- a/benefits/core/models/transit.py +++ b/benefits/core/models/transit.py @@ -207,7 +207,7 @@ def group_agencies(self, only_active=True): """ agencies_in_group = ( - TransitAgency.objects.filter(transitagencygroup__in=list(self.transitagencygroup_set.all())) + TransitAgency.objects.filter(transitagencygroup__in=self.transitagencygroup_set.all()) .distinct() .exclude(pk=self.pk) ) From 22deca9b61562966fb76ca277c10e858e5698a0d Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Tue, 14 Jul 2026 11:49:36 +0000 Subject: [PATCH 7/7] refactor(context_processor): fetch related objects for active_agencies by using select_related() which implements a SQL JOIN in a single database query. Limit the related fields to transit_processor_config which is a ForeignKey and accessed by the _agency_context processor. --- benefits/core/context_processors.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 027f69fffb..c6ab24df1a 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -44,7 +44,9 @@ def agency(request): def active_agencies(request): """Context processor adds some information about all active agencies to the request context.""" - agencies = models.TransitAgency.all_active() + agencies = models.TransitAgency.all_active().select_related( + "transit_processor_config", "transit_processor_config__littlepayconfig", "transit_processor_config__switchioconfig" + ) return {"active_agencies": [_agency_context(agency) for agency in agencies]}