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/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]} 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) ) diff --git a/benefits/core/session.py b/benefits/core/session.py index 20188e4f4e..e90a2da548 100644 --- a/benefits/core/session.py +++ b/benefits/core/session.py @@ -39,9 +39,17 @@ 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 + + if getattr(request, "_cached_agency", None) and request._cached_agency.id == agency_id: + return request._cached_agency try: - return models.TransitAgency.by_id(request.session[_AGENCY]) - except (KeyError, models.TransitAgency.DoesNotExist): + agency = models.TransitAgency.by_id(agency_id) + request._cached_agency = agency + return agency + except models.TransitAgency.DoesNotExist: return None @@ -124,9 +132,17 @@ 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 + + if getattr(request, "_cached_flow", None) and request._cached_flow.id == flow_id: + return request._cached_flow try: - return models.EnrollmentFlow.by_id(request.session[_FLOW]) - except (KeyError, models.EnrollmentFlow.DoesNotExist): + flow = models.EnrollmentFlow.by_id(flow_id) + request._cached_flow = flow + return flow + except models.EnrollmentFlow.DoesNotExist: return None 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", diff --git a/tests/pytest/core/test_session.py b/tests/pytest/core/test_session.py index fc5a0c7177..19bd834427 100644 --- a/tests/pytest/core/test_session.py +++ b/tests/pytest/core/test_session.py @@ -13,6 +13,29 @@ 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_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) @@ -132,6 +155,29 @@ 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_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"