From 8a8988edf469623d7c8065b0edfb496dd0a83a89 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Mon, 18 Mar 2024 17:18:19 +0000 Subject: [PATCH 1/9] feat: implement CalFresh include template for Help page --- .../core/includes/help--calfresh.html | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 benefits/core/templates/core/includes/help--calfresh.html diff --git a/benefits/core/templates/core/includes/help--calfresh.html b/benefits/core/templates/core/includes/help--calfresh.html new file mode 100644 index 000000000..3a44909ff --- /dev/null +++ b/benefits/core/templates/core/includes/help--calfresh.html @@ -0,0 +1,41 @@ +{% load i18n %} + +

+ {% translate "How do I know if I'm eligible for the transit benefit for CalFresh Cardholders?" %} +

+

+ {% blocktranslate trimmed %} + We verify your eligibility as a CalFresh Cardholder by confirming you have received funds in your + CalFresh account at any point in the last three months. This means you are eligible for a transit + benefit even if you did not receive funds in your CalFresh account this month or last month. + {% endblocktranslate %} +

+ +

+ {% translate "Will this transit benefit change my CalFresh account?" %} +

+

+ {% blocktranslate trimmed %} + No. Your monthly CalFresh allotment will not change. + {% endblocktranslate %} +

+ +

+ {% translate "Do I need my Golden State Advantage card to enroll?" %} +

+

+ {% blocktranslate trimmed %} + No, you do not need your physical EBT card to enroll. We use information from Login.gov and the California + Department of Social Services to enroll you in the benefit. + {% endblocktranslate %} +

+ +

+ {% translate "Can I use my Golden State Advantage card to pay for transit rides?" %} +

+

+ {% blocktranslate trimmed %} + No. You can not use your EBT or P-EBT card to pay for public transportation. When you tap to ride, use your personal + contactless debit or credit card to pay for public transportation. + {% endblocktranslate %} +

From b1a688362458bcdfb678d8e49a2a0ee4f01011da Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 21 Mar 2024 01:03:29 +0000 Subject: [PATCH 2/9] refactor: move help_template to EligibilityVerifier and use unique ones we need the templates to be specified on verifiers so that the help page reflects configuration changes rather than being hard-coded on an agency's template. --- benefits/core/context_processors.py | 9 +++++++- ...ve_help_template_to_eligibilityverifier.py | 22 +++++++++++++++++++ benefits/core/migrations/local_fixtures.json | 9 ++++---- benefits/core/models.py | 2 +- benefits/core/templates/core/help.html | 6 +++-- 5 files changed, 39 insertions(+), 9 deletions(-) create mode 100644 benefits/core/migrations/0005_move_help_template_to_eligibilityverifier.py diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 958ac6d57..96d3879ea 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -7,10 +7,17 @@ from . import models, session +def unique_values(original_list): + # dict.fromkeys gets the unique values and preserves order + return list(dict.fromkeys(original_list)) + + def _agency_context(agency): return { "eligibility_index_url": agency.eligibility_index_url, - "help_template": agency.help_template, + "help_templates": unique_values( + [v.help_template for v in agency.eligibility_verifiers.all() if v.help_template is not None] + ), "info_url": agency.info_url, "long_name": agency.long_name, "phone": agency.phone, diff --git a/benefits/core/migrations/0005_move_help_template_to_eligibilityverifier.py b/benefits/core/migrations/0005_move_help_template_to_eligibilityverifier.py new file mode 100644 index 000000000..8f26ac8ea --- /dev/null +++ b/benefits/core/migrations/0005_move_help_template_to_eligibilityverifier.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-03-21 00:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0004_alter_eligibilityverifier_display_order"), + ] + + operations = [ + migrations.RemoveField( + model_name="transitagency", + name="help_template", + ), + migrations.AddField( + model_name="eligibilityverifier", + name="help_template", + field=models.TextField(null=True), + ), + ] diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 511c95095..5f11f1a72 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -186,7 +186,8 @@ "auth_provider": null, "selection_label_template": "eligibility/includes/selection-label--mst-courtesy-card.html", "start_template": "eligibility/start--mst-courtesy-card.html", - "form_class": "benefits.eligibility.forms.MSTCourtesyCard" + "form_class": "benefits.eligibility.forms.MSTCourtesyCard", + "help_template": "core/includes/help--mst.html" } }, { @@ -249,7 +250,8 @@ "auth_provider": null, "selection_label_template": "eligibility/includes/selection-label--sbmtd-mobility-pass.html", "start_template": "eligibility/start--sbmtd-mobility-pass.html", - "form_class": "benefits.eligibility.forms.SBMTDMobilityPass" + "form_class": "benefits.eligibility.forms.SBMTDMobilityPass", + "help_template": "core/includes/help--sbmtd.html" } }, { @@ -334,7 +336,6 @@ "index_template": "core/index--mst.html", "eligibility_index_template": "eligibility/index--mst.html", "enrollment_success_template": "enrollment/success--mst.html", - "help_template": "core/includes/help--mst.html", "eligibility_types": [1, 7, 2, 3], "eligibility_verifiers": [1, 7, 2, 3] } @@ -358,7 +359,6 @@ "index_template": "core/index--sacrt.html", "eligibility_index_template": "eligibility/index--sacrt.html", "enrollment_success_template": "enrollment/success--sacrt.html", - "help_template": null, "eligibility_types": [4], "eligibility_verifiers": [4] } @@ -382,7 +382,6 @@ "index_template": "core/index--sbmtd.html", "eligibility_index_template": "eligibility/index--sbmtd.html", "enrollment_success_template": "enrollment/success--sbmtd.html", - "help_template": "core/includes/help--sbmtd.html", "eligibility_types": [5, 6], "eligibility_verifiers": [5, 6] } diff --git a/benefits/core/models.py b/benefits/core/models.py index 36010414a..b68df626d 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -177,6 +177,7 @@ class EligibilityVerifier(models.Model): start_template = models.TextField(null=True) # reference to a form class used by this Verifier, e.g. benefits.app.forms.FormClass form_class = models.TextField(null=True) + help_template = models.TextField(null=True) class Meta: ordering = ["display_order"] @@ -269,7 +270,6 @@ class TransitAgency(models.Model): index_template = models.TextField() eligibility_index_template = models.TextField() enrollment_success_template = models.TextField() - help_template = models.TextField(null=True) def __str__(self): return self.long_name diff --git a/benefits/core/templates/core/help.html b/benefits/core/templates/core/help.html index 53adc1569..ef3325904 100644 --- a/benefits/core/templates/core/help.html +++ b/benefits/core/templates/core/help.html @@ -110,8 +110,10 @@

{% translate "How do I veri {% endblocktranslate %}

- {% if agency and agency.help_template %} - {% include agency.help_template %} + {% if agency and agency.help_templates %} + {% for help_template in agency.help_templates %} + {% include help_template %} + {% endfor %} {% endif %}

{% translate "What is Littlepay?" %}

From dc95f50b9d8e1807eb1928fee3ec5b8636b85e23 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 21 Mar 2024 01:19:46 +0000 Subject: [PATCH 3/9] refactor(help): rename templates to reflect their eligibility type --- benefits/core/migrations/local_fixtures.json | 4 ++-- .../includes/{help--mst.html => help--mst-courtesy-card.html} | 0 .../{help--sbmtd.html => help--sbmtd-mobility-pass.html} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename benefits/core/templates/core/includes/{help--mst.html => help--mst-courtesy-card.html} (100%) rename benefits/core/templates/core/includes/{help--sbmtd.html => help--sbmtd-mobility-pass.html} (100%) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 5f11f1a72..5b21ccda7 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -187,7 +187,7 @@ "selection_label_template": "eligibility/includes/selection-label--mst-courtesy-card.html", "start_template": "eligibility/start--mst-courtesy-card.html", "form_class": "benefits.eligibility.forms.MSTCourtesyCard", - "help_template": "core/includes/help--mst.html" + "help_template": "core/includes/help--mst-courtesy-card.html" } }, { @@ -251,7 +251,7 @@ "selection_label_template": "eligibility/includes/selection-label--sbmtd-mobility-pass.html", "start_template": "eligibility/start--sbmtd-mobility-pass.html", "form_class": "benefits.eligibility.forms.SBMTDMobilityPass", - "help_template": "core/includes/help--sbmtd.html" + "help_template": "core/includes/help--sbmtd-mobility-pass.html" } }, { diff --git a/benefits/core/templates/core/includes/help--mst.html b/benefits/core/templates/core/includes/help--mst-courtesy-card.html similarity index 100% rename from benefits/core/templates/core/includes/help--mst.html rename to benefits/core/templates/core/includes/help--mst-courtesy-card.html diff --git a/benefits/core/templates/core/includes/help--sbmtd.html b/benefits/core/templates/core/includes/help--sbmtd-mobility-pass.html similarity index 100% rename from benefits/core/templates/core/includes/help--sbmtd.html rename to benefits/core/templates/core/includes/help--sbmtd-mobility-pass.html From 13335a8803016c5dba2fd2d56ed807210cf736b1 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 21 Mar 2024 01:43:02 +0000 Subject: [PATCH 4/9] chore(fixture): add help template to CalFresh eligibility verifier --- benefits/core/migrations/local_fixtures.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 5b21ccda7..4e047041d 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -272,7 +272,8 @@ "auth_provider": 1, "selection_label_template": "eligibility/includes/selection-label--calfresh.html", "start_template": "eligibility/start--calfresh.html", - "form_class": null + "form_class": null, + "help_template": "core/includes/help--calfresh.html" } }, { From ce14b73d9ff072f81f61dd69a1637dfb722fe6d8 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 21 Mar 2024 18:39:24 +0000 Subject: [PATCH 5/9] fix: context processor should use same filter as verifier selection form fixes failing eligibility view tests --- benefits/core/context_processors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 96d3879ea..3a9ec296a 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -16,7 +16,7 @@ def _agency_context(agency): return { "eligibility_index_url": agency.eligibility_index_url, "help_templates": unique_values( - [v.help_template for v in agency.eligibility_verifiers.all() if v.help_template is not None] + [v.help_template for v in agency.eligibility_verifiers.filter(active=True) if v.help_template is not None] ), "info_url": agency.info_url, "long_name": agency.long_name, From cc0a6d3b962197bbbc977598f30fb25e7ef98f15 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Thu, 21 Mar 2024 18:39:44 +0000 Subject: [PATCH 6/9] fix(test): update core view tests to ensure mocked value is iterable --- tests/pytest/core/test_views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index 74b05fbe1..1a0aa9220 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -24,6 +24,11 @@ def session_reset_spy(mocker): @pytest.fixture def mocked_active_agency(mocker): mock_agency = mocker.Mock() + + # ensure agency.eligibility_verifiers is iterable + eligibility_verifiers = mocker.MagicMock() + mock_agency.eligibility_verifiers = eligibility_verifiers + mock_agency.index_url = "/agency" mocker.patch("benefits.core.session.agency", return_value=mock_agency) mocker.patch("benefits.core.session.active_agency", return_value=True) From 0cd9a1e59aecf5997c043107bfc181d76a5af25b Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 22 Mar 2024 18:12:30 +0000 Subject: [PATCH 7/9] chore(conftest): move test fixtures into conftest for consistency --- tests/pytest/conftest.py | 39 ++++++++++++++++++++++++++++++++ tests/pytest/core/test_models.py | 39 -------------------------------- 2 files changed, 39 insertions(+), 39 deletions(-) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 0dcf3aa8b..c10093d6d 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -105,6 +105,45 @@ def model_EligibilityType(): return eligibility +@pytest.fixture +def model_EligibilityType_does_not_support_expiration(model_EligibilityType): + model_EligibilityType.supports_expiration = False + model_EligibilityType.expiration_days = 0 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +def model_EligibilityType_zero_expiration_days(model_EligibilityType): + model_EligibilityType.supports_expiration = True + model_EligibilityType.expiration_days = 0 + model_EligibilityType.expiration_reenrollment_days = 14 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +def model_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType): + model_EligibilityType.supports_expiration = True + model_EligibilityType.expiration_days = 14 + model_EligibilityType.expiration_reenrollment_days = 0 + model_EligibilityType.save() + + return model_EligibilityType + + +@pytest.fixture +def model_EligibilityType_supports_expiration(model_EligibilityType): + model_EligibilityType.supports_expiration = True + model_EligibilityType.expiration_days = 365 + model_EligibilityType.expiration_reenrollment_days = 14 + model_EligibilityType.save() + + return model_EligibilityType + + @pytest.fixture def model_EligibilityVerifier(model_PemData, model_EligibilityType): verifier = EligibilityVerifier.objects.create( diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index dbf4f44bd..4a888a14a 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -13,45 +13,6 @@ def mock_requests_get_pem_data(mocker): return mocker.patch("benefits.core.models.requests.get", return_value=mocker.Mock(text="PEM text")) -@pytest.fixture -def model_EligibilityType_does_not_support_expiration(model_EligibilityType): - model_EligibilityType.supports_expiration = False - model_EligibilityType.expiration_days = 0 - model_EligibilityType.save() - - return model_EligibilityType - - -@pytest.fixture -def model_EligibilityType_zero_expiration_days(model_EligibilityType): - model_EligibilityType.supports_expiration = True - model_EligibilityType.expiration_days = 0 - model_EligibilityType.expiration_reenrollment_days = 14 - model_EligibilityType.save() - - return model_EligibilityType - - -@pytest.fixture -def model_EligibilityType_zero_expiration_reenrollment_days(model_EligibilityType): - model_EligibilityType.supports_expiration = True - model_EligibilityType.expiration_days = 14 - model_EligibilityType.expiration_reenrollment_days = 0 - model_EligibilityType.save() - - return model_EligibilityType - - -@pytest.fixture -def model_EligibilityType_supports_expiration(model_EligibilityType): - model_EligibilityType.supports_expiration = True - model_EligibilityType.expiration_days = 365 - model_EligibilityType.expiration_reenrollment_days = 14 - model_EligibilityType.save() - - return model_EligibilityType - - def test_SecretNameField_init(): field = SecretNameField() From d79f06c186a8491f2576e3f5a8588fff3ea66740 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 22 Mar 2024 18:39:40 +0000 Subject: [PATCH 8/9] refactor(models): add property to TransitAgency to get active verifiers update usage in verifier selection form, context processors, and tests --- benefits/core/context_processors.py | 4 +--- benefits/core/models.py | 5 +++++ benefits/eligibility/forms.py | 2 +- tests/pytest/core/test_models.py | 17 +++++++++++++++++ tests/pytest/core/test_views.py | 2 +- tests/pytest/eligibility/test_views.py | 14 ++++++++++---- 6 files changed, 35 insertions(+), 9 deletions(-) diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 3a9ec296a..012a9a645 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -15,9 +15,7 @@ def unique_values(original_list): def _agency_context(agency): return { "eligibility_index_url": agency.eligibility_index_url, - "help_templates": unique_values( - [v.help_template for v in agency.eligibility_verifiers.filter(active=True) if v.help_template is not None] - ), + "help_templates": unique_values([v.help_template for v in agency.active_verifiers if v.help_template is not None]), "info_url": agency.info_url, "long_name": agency.long_name, "phone": agency.phone, diff --git a/benefits/core/models.py b/benefits/core/models.py index b68df626d..58e49205c 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -323,6 +323,11 @@ def public_key_data(self): """This Agency's public key as a string.""" return self.public_key.data + @property + def active_verifiers(self): + """This Agency's eligibility verifiers that are active.""" + return self.eligibility_verifiers.filter(active=True) + @staticmethod def by_id(id): """Get a TransitAgency instance by its ID.""" diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index 3d64c89cf..a088f04ff 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -25,7 +25,7 @@ class EligibilityVerifierSelectionForm(forms.Form): def __init__(self, agency: models.TransitAgency, *args, **kwargs): super().__init__(*args, **kwargs) - verifiers = agency.eligibility_verifiers.filter(active=True) + verifiers = agency.active_verifiers self.classes = "col-lg-8" # second element is not used since we render the whole label using selection_label_template, diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index 4a888a14a..aedfdb571 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -317,6 +317,23 @@ def test_TransitAgency_str(model_TransitAgency): assert str(model_TransitAgency) == model_TransitAgency.long_name +@pytest.mark.django_db +def test_TransitAgency_active_verifiers(model_TransitAgency, model_EligibilityVerifier): + # add another to the list of verifiers by cloning the original + # https://stackoverflow.com/a/48149675/453168 + new_verifier = EligibilityVerifier.objects.get(pk=model_EligibilityVerifier.id) + new_verifier.pk = None + new_verifier.active = False + new_verifier.save() + + model_TransitAgency.eligibility_verifiers.add(new_verifier) + + assert model_TransitAgency.eligibility_verifiers.count() == 2 + assert model_TransitAgency.active_verifiers.count() == 1 + + assert model_TransitAgency.active_verifiers[0] == model_EligibilityVerifier + + @pytest.mark.django_db def test_TransitAgency_get_type_id_matching(model_TransitAgency): eligibility = model_TransitAgency.eligibility_types.first() diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index 1a0aa9220..628110bfb 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -27,7 +27,7 @@ def mocked_active_agency(mocker): # ensure agency.eligibility_verifiers is iterable eligibility_verifiers = mocker.MagicMock() - mock_agency.eligibility_verifiers = eligibility_verifiers + mock_agency.active_verifiers = eligibility_verifiers mock_agency.index_url = "/agency" mocker.patch("benefits.core.session.agency", return_value=mock_agency) diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index 3a8a430e1..ea55ad460 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -85,8 +85,12 @@ def test_index_get_agency_multiple_verifiers( ): # override the mocked session agency with a mock agency that has multiple verifiers mock_agency = mocker.Mock(spec=model_TransitAgency) - mock_agency.eligibility_verifiers.filter.return_value = [model_EligibilityVerifier, model_EligibilityVerifier] - mock_agency.eligibility_verifiers.count.return_value = 2 + + # mock the active_verifiers property on the class - https://stackoverflow.com/a/55642462 + type(mock_agency).active_verifiers = mocker.PropertyMock( + return_value=[model_EligibilityVerifier, model_EligibilityVerifier] + ) + mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" mocked_session_agency.return_value = mock_agency @@ -107,8 +111,10 @@ def test_index_get_agency_single_verifier( ): # override the mocked session agency with a mock agency that has a single verifier mock_agency = mocker.Mock(spec=model_TransitAgency) - mock_agency.eligibility_verifiers.filter.return_value = [model_EligibilityVerifier] - mock_agency.eligibility_verifiers.count.return_value = 1 + + # mock the active_verifiers property on the class - https://stackoverflow.com/a/55642462 + type(mock_agency).active_verifiers = mocker.PropertyMock(return_value=[model_EligibilityVerifier]) + mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" mocked_session_agency.return_value = mock_agency From f3fe500824bd906113038a2ef0c342c1da56f2d8 Mon Sep 17 00:00:00 2001 From: Angela Tran Date: Fri, 22 Mar 2024 18:45:02 +0000 Subject: [PATCH 9/9] test: unit test for helper function expected behavior is to return an ordered list of the unique values --- tests/pytest/core/test_context_processors.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/pytest/core/test_context_processors.py diff --git a/tests/pytest/core/test_context_processors.py b/tests/pytest/core/test_context_processors.py new file mode 100644 index 000000000..9048ed90a --- /dev/null +++ b/tests/pytest/core/test_context_processors.py @@ -0,0 +1,9 @@ +from benefits.core.context_processors import unique_values + + +def test_unique_values(): + original_list = ["a", "b", "c", "a", "a", "zzz", "b", "c", "d", "b"] + + new_list = unique_values(original_list) + + assert new_list == ["a", "b", "c", "zzz", "d"]