diff --git a/benefits/core/context_processors.py b/benefits/core/context_processors.py index 958ac6d57..012a9a645 100644 --- a/benefits/core/context_processors.py +++ b/benefits/core/context_processors.py @@ -7,10 +7,15 @@ 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.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/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..4e047041d 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-courtesy-card.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-mobility-pass.html" } }, { @@ -270,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" } }, { @@ -334,7 +337,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 +360,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 +383,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..58e49205c 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 @@ -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/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 @@
+ {% 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 %} +
+ ++ {% blocktranslate trimmed %} + No. Your monthly CalFresh allotment will not change. + {% endblocktranslate %} +
+ ++ {% 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 %} +
+ ++ {% 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 %} +
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 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/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_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"] diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index dbf4f44bd..aedfdb571 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() @@ -356,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 74b05fbe1..628110bfb 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.active_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) 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