From 9747759891b248c65a12178e6f3c643f58a6ca62 Mon Sep 17 00:00:00 2001 From: Luis Alvergue Date: Wed, 25 Sep 2024 17:21:52 +0000 Subject: [PATCH 01/21] refactor(models): change relationship for EnrollmentFlow and TransitAgency This commit changes the implementation of the relationship between EnrollmentFlow and TransitAgency. EnrollmentFlow now has a TransitAgency foreign key field (many flows to one transit agency). This will allow us to have more flexibility with the names we can use in EnrollmentFlow.label (for example, having the name of the transit agency in this field won't be necessary anymore). --- ...transitagency_enrollment_flows_and_more.py | 24 ++ benefits/core/models.py | 290 +++++++++--------- 2 files changed, 171 insertions(+), 143 deletions(-) create mode 100644 benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py diff --git a/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py new file mode 100644 index 0000000000..f2af616bc6 --- /dev/null +++ b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.1 on 2024-09-25 16:47 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0027_enrollmentflow_supported_methods"), + ] + + operations = [ + migrations.RemoveField( + model_name="transitagency", + name="enrollment_flows", + ), + migrations.AddField( + model_name="enrollmentflow", + name="transit_agency", + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), + preserve_default=False, + ), + ] diff --git a/benefits/core/models.py b/benefits/core/models.py index 8eb869331d..29baad8d94 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -105,6 +105,152 @@ def __str__(self) -> str: return self.client_name +class TransitProcessor(models.Model): + """An entity that applies transit agency fare rules to rider transactions.""" + + id = models.AutoField(primary_key=True) + name = models.TextField(help_text="Primary internal display name for this TransitProcessor instance, e.g. in the Admin.") + api_base_url = models.TextField(help_text="The absolute base URL for the TransitProcessor's API, including https://.") + card_tokenize_url = models.TextField( + help_text="The absolute URL for the client-side card tokenization library provided by the TransitProcessor." + ) + card_tokenize_func = models.TextField( + help_text="The function from the card tokenization library to call on the client to initiate the process." + ) + card_tokenize_env = models.TextField(help_text="The environment in which card tokenization is occurring.") + portal_url = models.TextField( + null=True, blank=True, help_text="The absolute base URL for the TransitProcessor's control portal, including https://." + ) + + def __str__(self): + return self.name + + +class TransitAgency(models.Model): + """An agency offering transit service.""" + + id = models.AutoField(primary_key=True) + active = models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users") + slug = models.TextField(help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}") + short_name = models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym.") + long_name = models.TextField( + help_text="The user-facing long name for this agency. Often the short_name acronym, spelled out." + ) + info_url = models.URLField(help_text="URL of a website/page with more information about the agency's discounts") + phone = models.TextField(help_text="Agency customer support phone number") + index_template = models.TextField(help_text="The template used for this agency's landing page") + eligibility_index_template = models.TextField(help_text="The template used for this agency's eligibility landing page") + eligibility_api_id = models.TextField(help_text="The identifier for this agency used in Eligibility API calls.") + eligibility_api_private_key = models.ForeignKey( + PemData, + related_name="+", + on_delete=models.PROTECT, + help_text="Private key used to sign Eligibility API tokens created on behalf of this Agency.", + ) + eligibility_api_public_key = models.ForeignKey( + PemData, + related_name="+", + on_delete=models.PROTECT, + help_text="Public key corresponding to the agency's private key, used by Eligibility Verification servers to encrypt responses.", # noqa: E501 + ) + eligibility_api_jws_signing_alg = models.TextField( + help_text="The JWS-compatible signing algorithm used in Eligibility API calls." + ) + transit_processor = models.ForeignKey(TransitProcessor, on_delete=models.PROTECT) + transit_processor_audience = models.TextField( + help_text="This agency's audience value used to access the TransitProcessor's API.", default="" + ) + transit_processor_client_id = models.TextField( + help_text="This agency's client_id value used to access the TransitProcessor's API.", default="" + ) + transit_processor_client_secret_name = SecretNameField( + help_text="The name of the secret containing this agency's client_secret value used to access the TransitProcessor's API.", # noqa: E501 + default="", + ) + staff_group = models.OneToOneField( + Group, + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + help_text="The group of users associated with this TransitAgency.", + related_name="transit_agency", + ) + sso_domain = models.TextField( + null=True, + blank=True, + default="", + help_text="The email domain of users to automatically add to this agency's staff group upon login.", + ) + customer_service_group = models.OneToOneField( + Group, + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + help_text="The group of users who are allowed to do in-person eligibility verification and enrollment.", + related_name="+", + ) + + def __str__(self): + return self.long_name + + @property + def index_url(self): + """Public-facing URL to the TransitAgency's landing page.""" + return reverse(routes.AGENCY_INDEX, args=[self.slug]) + + @property + def eligibility_index_url(self): + """Public facing URL to the TransitAgency's eligibility page.""" + return reverse(routes.AGENCY_ELIGIBILITY_INDEX, args=[self.slug]) + + @property + def eligibility_api_private_key_data(self): + """This Agency's private key as a string.""" + return self.eligibility_api_private_key.data + + @property + def eligibility_api_public_key_data(self): + """This Agency's public key as a string.""" + return self.eligibility_api_public_key.data + + @property + def transit_processor_client_secret(self): + return get_secret_by_name(self.transit_processor_client_secret_name) + + @property + def enrollment_flows(self): + return self.enrollmentflow_set + + @staticmethod + def by_id(id): + """Get a TransitAgency instance by its ID.""" + logger.debug(f"Get {TransitAgency.__name__} by id: {id}") + return TransitAgency.objects.get(id=id) + + @staticmethod + def by_slug(slug): + """Get a TransitAgency instance by its slug.""" + logger.debug(f"Get {TransitAgency.__name__} by slug: {slug}") + return TransitAgency.objects.filter(slug=slug).first() + + @staticmethod + def all_active(): + """Get all TransitAgency instances marked active.""" + logger.debug(f"Get all active {TransitAgency.__name__}") + return TransitAgency.objects.filter(active=True) + + @staticmethod + def for_user(user: User): + for group in user.groups.all(): + if hasattr(group, "transit_agency"): + return group.transit_agency # this is looking at the TransitAgency's staff_group + + # the loop above returns the first match found. Return None if no match was found. + return None + + class EnrollmentMethods: DIGITAL = "digital" IN_PERSON = "in_person" @@ -235,6 +381,7 @@ class EnrollmentFlow(models.Model): default=[EnrollmentMethods.DIGITAL, EnrollmentMethods.IN_PERSON], help_text="If the flow is supported by digital enrollment, in-person enrollment, or both", ) + transit_agency = models.ForeignKey(TransitAgency, on_delete=models.PROTECT) class Meta: ordering = ["display_order"] @@ -313,149 +460,6 @@ def claims_scheme(self): return self.claims_scheme_override -class TransitProcessor(models.Model): - """An entity that applies transit agency fare rules to rider transactions.""" - - id = models.AutoField(primary_key=True) - name = models.TextField(help_text="Primary internal display name for this TransitProcessor instance, e.g. in the Admin.") - api_base_url = models.TextField(help_text="The absolute base URL for the TransitProcessor's API, including https://.") - card_tokenize_url = models.TextField( - help_text="The absolute URL for the client-side card tokenization library provided by the TransitProcessor." - ) - card_tokenize_func = models.TextField( - help_text="The function from the card tokenization library to call on the client to initiate the process." - ) - card_tokenize_env = models.TextField(help_text="The environment in which card tokenization is occurring.") - portal_url = models.TextField( - null=True, blank=True, help_text="The absolute base URL for the TransitProcessor's control portal, including https://." - ) - - def __str__(self): - return self.name - - -class TransitAgency(models.Model): - """An agency offering transit service.""" - - id = models.AutoField(primary_key=True) - active = models.BooleanField(default=False, help_text="Determines if this Agency is enabled for users") - enrollment_flows = models.ManyToManyField(EnrollmentFlow) - slug = models.TextField(help_text="Used for URL navigation for this agency, e.g. the agency homepage url is /{slug}") - short_name = models.TextField(help_text="The user-facing short name for this agency. Often an uppercase acronym.") - long_name = models.TextField( - help_text="The user-facing long name for this agency. Often the short_name acronym, spelled out." - ) - info_url = models.URLField(help_text="URL of a website/page with more information about the agency's discounts") - phone = models.TextField(help_text="Agency customer support phone number") - index_template = models.TextField(help_text="The template used for this agency's landing page") - eligibility_index_template = models.TextField(help_text="The template used for this agency's eligibility landing page") - eligibility_api_id = models.TextField(help_text="The identifier for this agency used in Eligibility API calls.") - eligibility_api_private_key = models.ForeignKey( - PemData, - related_name="+", - on_delete=models.PROTECT, - help_text="Private key used to sign Eligibility API tokens created on behalf of this Agency.", - ) - eligibility_api_public_key = models.ForeignKey( - PemData, - related_name="+", - on_delete=models.PROTECT, - help_text="Public key corresponding to the agency's private key, used by Eligibility Verification servers to encrypt responses.", # noqa: E501 - ) - eligibility_api_jws_signing_alg = models.TextField( - help_text="The JWS-compatible signing algorithm used in Eligibility API calls." - ) - transit_processor = models.ForeignKey(TransitProcessor, on_delete=models.PROTECT) - transit_processor_audience = models.TextField( - help_text="This agency's audience value used to access the TransitProcessor's API.", default="" - ) - transit_processor_client_id = models.TextField( - help_text="This agency's client_id value used to access the TransitProcessor's API.", default="" - ) - transit_processor_client_secret_name = SecretNameField( - help_text="The name of the secret containing this agency's client_secret value used to access the TransitProcessor's API.", # noqa: E501 - default="", - ) - staff_group = models.OneToOneField( - Group, - on_delete=models.PROTECT, - null=True, - blank=True, - default=None, - help_text="The group of users associated with this TransitAgency.", - related_name="transit_agency", - ) - sso_domain = models.TextField( - null=True, - blank=True, - default="", - help_text="The email domain of users to automatically add to this agency's staff group upon login.", - ) - customer_service_group = models.OneToOneField( - Group, - on_delete=models.PROTECT, - null=True, - blank=True, - default=None, - help_text="The group of users who are allowed to do in-person eligibility verification and enrollment.", - related_name="+", - ) - - def __str__(self): - return self.long_name - - @property - def index_url(self): - """Public-facing URL to the TransitAgency's landing page.""" - return reverse(routes.AGENCY_INDEX, args=[self.slug]) - - @property - def eligibility_index_url(self): - """Public facing URL to the TransitAgency's eligibility page.""" - return reverse(routes.AGENCY_ELIGIBILITY_INDEX, args=[self.slug]) - - @property - def eligibility_api_private_key_data(self): - """This Agency's private key as a string.""" - return self.eligibility_api_private_key.data - - @property - def eligibility_api_public_key_data(self): - """This Agency's public key as a string.""" - return self.eligibility_api_public_key.data - - @property - def transit_processor_client_secret(self): - return get_secret_by_name(self.transit_processor_client_secret_name) - - @staticmethod - def by_id(id): - """Get a TransitAgency instance by its ID.""" - logger.debug(f"Get {TransitAgency.__name__} by id: {id}") - return TransitAgency.objects.get(id=id) - - @staticmethod - def by_slug(slug): - """Get a TransitAgency instance by its slug.""" - logger.debug(f"Get {TransitAgency.__name__} by slug: {slug}") - return TransitAgency.objects.filter(slug=slug).first() - - @staticmethod - def all_active(): - """Get all TransitAgency instances marked active.""" - logger.debug(f"Get all active {TransitAgency.__name__}") - return TransitAgency.objects.filter(active=True) - - @staticmethod - def for_user(user: User): - for group in user.groups.all(): - if hasattr(group, "transit_agency"): - return group.transit_agency # this is looking at the TransitAgency's staff_group - - # the loop above returns the first match found. Return None if no match was found. - return None - - class EnrollmentEvent(models.Model): """A record of a successful enrollment.""" From 89e3ecb7fd164960598aa98800b709f2b8e7a39d Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 22:00:00 +0000 Subject: [PATCH 02/21] chore(fixtures): remove enrollment_methods field from transitagency fixture --- benefits/core/migrations/local_fixtures.json | 1 - 1 file changed, 1 deletion(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 9d8a4896d7..1039d71604 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -191,7 +191,6 @@ "pk": 1, "fields": { "active": true, - "enrollment_flows": [1, 2, 3, 4, 5], "slug": "cst", "short_name": "CST (local)", "long_name": "California State Transit (local)", From 1e7bbebb1ee2a7846fb94d3b4068f05fee5128b5 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:00:58 +0000 Subject: [PATCH 03/21] test(fix): update enrollmentflow, agency changes on fixtures --- tests/pytest/conftest.py | 10 ++++------ tests/pytest/core/test_models.py | 14 +++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 6be63571ef..9690ffc139 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -75,13 +75,14 @@ def model_ClaimsProvider_no_sign_out(model_ClaimsProvider): @pytest.fixture -def model_EnrollmentFlow(): +def model_EnrollmentFlow(model_TransitAgency): flow = EnrollmentFlow.objects.create( system_name="Test Flow", selection_label_template="eligibility/includes/selection-label.html", label="Test flow label", group_id="group123", enrollment_success_template="enrollment/success.html", + transit_agency_id=model_TransitAgency.id, ) return flow @@ -112,6 +113,7 @@ def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow, model_Claims @pytest.fixture def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow_with_scope_and_claim): model_EnrollmentFlow_with_scope_and_claim.claims_scheme_override = "scheme" + model_EnrollmentFlow_with_scope_and_claim.transit_agency_id = 1 model_EnrollmentFlow_with_scope_and_claim.save() return model_EnrollmentFlow_with_scope_and_claim @@ -171,7 +173,7 @@ def model_TransitProcessor(): @pytest.fixture -def model_TransitAgency(model_PemData, model_EnrollmentFlow, model_TransitProcessor): +def model_TransitAgency(model_PemData, model_TransitProcessor): agency = TransitAgency.objects.create( slug="test", short_name="TEST", @@ -191,10 +193,6 @@ def model_TransitAgency(model_PemData, model_EnrollmentFlow, model_TransitProces eligibility_index_template="eligibility/index.html", ) - # add many-to-many relationships after creation, need ID on both sides - agency.enrollment_flows.add(model_EnrollmentFlow) - agency.save() - return agency diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index fa68fa1abe..d52fb07ac8 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -120,7 +120,7 @@ def test_EnrollmentFlow_str(model_EnrollmentFlow): @pytest.mark.django_db -def test_EnrollmentFlow_supports_expiration_False(model_EnrollmentFlow_does_not_support_expiration): +def test_EnrollmentFlow_supports_expiration_False(model_EnrollmentFlow, model_EnrollmentFlow_does_not_support_expiration): # test will fail if any error is raised model_EnrollmentFlow_does_not_support_expiration.full_clean() @@ -168,8 +168,8 @@ def test_EnrollmentFlow_supports_expiration(model_EnrollmentFlow_supports_expira @pytest.mark.django_db -def test_EnrollmentFlow_enrollment_index_template(): - new_flow = EnrollmentFlow.objects.create() +def test_EnrollmentFlow_enrollment_index_template(model_TransitAgency): + new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) assert new_flow.enrollment_index_template == "enrollment/index.html" @@ -180,15 +180,15 @@ def test_EnrollmentFlow_enrollment_index_template(): @pytest.mark.django_db -def test_EnrollmentFlow_enrollment_success_template(): - new_flow = EnrollmentFlow.objects.create() +def test_EnrollmentFlow_enrollment_success_template(model_TransitAgency): + new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) assert new_flow.enrollment_success_template == "enrollment/success.html" @pytest.mark.django_db -def test_EnrollmentFlow_supported_enrollment_methods(): - new_flow = EnrollmentFlow.objects.create() +def test_EnrollmentFlow_supported_enrollment_methods(model_TransitAgency): + new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) assert new_flow.supported_enrollment_methods == ["digital", "in_person"] From a612c47d3ba818ad8beb5e5722ee55ee0e6cf41c Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:01:18 +0000 Subject: [PATCH 04/21] fix(fixtures): add transit_agency_id to enrollmentflow --- benefits/core/migrations/local_fixtures.json | 69 +++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 1039d71604..87cf3aef76 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -48,6 +48,30 @@ "scheme": "dev-cal-itp_benefits" } }, + { + "model": "core.transitagency", + "pk": 1, + "fields": { + "active": true, + "slug": "cst", + "short_name": "CST (local)", + "long_name": "California State Transit (local)", + "info_url": "https://www.agency-website.com", + "phone": "1-800-555-5555", + "index_template": "core/index--cst.html", + "eligibility_index_template": "eligibility/index--cst.html", + "eligibility_api_id": "cst", + "eligibility_api_private_key": 2, + "eligibility_api_public_key": 3, + "eligibility_api_jws_signing_alg": "RS256", + "transit_processor": 1, + "transit_processor_audience": "", + "transit_processor_client_id": "", + "transit_processor_client_secret_name": "cst-transit-processor-client-secret", + "staff_group": 2, + "customer_service_group": 2 + } + }, { "model": "core.enrollmentflow", "pk": 1, @@ -62,7 +86,8 @@ "eligibility_start_template": "eligibility/start--senior.html", "claims_scope": "verify:senior", "claims_claim": "senior", - "supported_enrollment_methods": ["digital", "in_person"] + "supported_enrollment_methods": ["digital", "in_person"], + "transit_agency_id": 1 } }, { @@ -79,7 +104,8 @@ "eligibility_start_template": "eligibility/start--veteran.html", "claims_scope": "verify:veteran", "claims_claim": "veteran", - "supported_enrollment_methods": ["digital", "in_person"] + "supported_enrollment_methods": ["digital", "in_person"], + "transit_agency_id": 1 } }, { @@ -104,7 +130,8 @@ "eligibility_form_class": "benefits.eligibility.forms.CSTAgencyCard", "eligibility_unverified_template": "eligibility/unverified--cst-agency-card.html", "help_template": "core/includes/help--cst-agency-card.html", - "supported_enrollment_methods": ["digital", "in_person"] + "supported_enrollment_methods": ["digital", "in_person"], + "transit_agency_id": 1 } }, { @@ -126,7 +153,8 @@ "help_template": "core/includes/help--calfresh.html", "claims_scope": "verify:calfresh", "claims_claim": "calfresh", - "supported_enrollment_methods": ["digital", "in_person"] + "supported_enrollment_methods": ["digital", "in_person"], + "transit_agency_id": 1 } }, { @@ -144,7 +172,8 @@ "help_template": "core/includes/help--medicare.html", "claims_scope": "verify:medicare", "claims_claim": "medicare", - "supported_enrollment_methods": ["digital", "in_person"] + "supported_enrollment_methods": ["digital", "in_person"], + "transit_agency_id": 1 } }, { @@ -154,7 +183,8 @@ "system_name": "in_person_only", "label": "(CST) In-person Only", "group_id": "group123", - "supported_enrollment_methods": ["in_person"] + "supported_enrollment_methods": ["in_person"], + "transit_agency_id": 1 } }, { @@ -171,7 +201,8 @@ "eligibility_start_template": "eligibility/start--senior.html", "claims_scope": "verify:senior", "claims_claim": "senior", - "supported_enrollment_methods": ["digital"] + "supported_enrollment_methods": ["digital"], + "transit_agency_id": 1 } }, { @@ -186,30 +217,6 @@ "portal_url": "https://www.transit-processor-portal.com" } }, - { - "model": "core.transitagency", - "pk": 1, - "fields": { - "active": true, - "slug": "cst", - "short_name": "CST (local)", - "long_name": "California State Transit (local)", - "info_url": "https://www.agency-website.com", - "phone": "1-800-555-5555", - "index_template": "core/index--cst.html", - "eligibility_index_template": "eligibility/index--cst.html", - "eligibility_api_id": "cst", - "eligibility_api_private_key": 2, - "eligibility_api_public_key": 3, - "eligibility_api_jws_signing_alg": "RS256", - "transit_processor": 1, - "transit_processor_audience": "", - "transit_processor_client_id": "", - "transit_processor_client_secret_name": "cst-transit-processor-client-secret", - "staff_group": 2, - "customer_service_group": 2 - } - }, { "model": "auth.Group", "pk": 2, From 3003f8cd6729afa5d8e90d54bb75c47905a051b2 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:14:31 +0000 Subject: [PATCH 05/21] chore(fixtures): remove (CST) from flow label --- benefits/core/migrations/local_fixtures.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 87cf3aef76..22960052ed 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -77,7 +77,7 @@ "pk": 1, "fields": { "system_name": "senior", - "label": "(CST) Senior Discount", + "label": "Senior Discount", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 2, @@ -95,7 +95,7 @@ "pk": 2, "fields": { "system_name": "veteran", - "label": "(CST) Veteran Discount", + "label": "Veteran Discount", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 4, @@ -113,7 +113,7 @@ "pk": 3, "fields": { "system_name": "agency_card", - "label": "(CST) Agency Card Discount", + "label": "Agency Card Discount", "group_id": "group123", "enrollment_index_template": "enrollment/index--agency-card.html", "enrollment_success_template": "enrollment/success--cst-agency-card.html", @@ -139,7 +139,7 @@ "pk": 4, "fields": { "system_name": "calfresh", - "label": "(CST) CalFresh", + "label": "CalFresh", "group_id": "group123", "supports_expiration": "True", "expiration_days": 5, @@ -162,7 +162,7 @@ "pk": 5, "fields": { "system_name": "medicare", - "label": "(CST) Medicare Discount", + "label": "Medicare Discount", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 1, @@ -181,7 +181,7 @@ "pk": 6, "fields": { "system_name": "in_person_only", - "label": "(CST) In-person Only", + "label": "In-person Only", "group_id": "group123", "supported_enrollment_methods": ["in_person"], "transit_agency_id": 1 @@ -192,7 +192,7 @@ "pk": 7, "fields": { "system_name": "digital_only", - "label": "(CST) Digital Only", + "label": "Digital Only", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 5, From 6a219e89ff683a4e415c5f98e88a36da67ef0663 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:20:31 +0000 Subject: [PATCH 06/21] chore(fixtures): give enrollmentflow label the human-readable name --- benefits/core/migrations/local_fixtures.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 22960052ed..25060497f9 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -77,7 +77,7 @@ "pk": 1, "fields": { "system_name": "senior", - "label": "Senior Discount", + "label": "Older Adult", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 2, @@ -95,7 +95,7 @@ "pk": 2, "fields": { "system_name": "veteran", - "label": "Veteran Discount", + "label": "US Veteran", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 4, @@ -113,7 +113,7 @@ "pk": 3, "fields": { "system_name": "agency_card", - "label": "Agency Card Discount", + "label": "Agency Card", "group_id": "group123", "enrollment_index_template": "enrollment/index--agency-card.html", "enrollment_success_template": "enrollment/success--cst-agency-card.html", @@ -139,7 +139,7 @@ "pk": 4, "fields": { "system_name": "calfresh", - "label": "CalFresh", + "label": "CalFresh Cardholder", "group_id": "group123", "supports_expiration": "True", "expiration_days": 5, @@ -162,7 +162,7 @@ "pk": 5, "fields": { "system_name": "medicare", - "label": "Medicare Discount", + "label": "Medicare Cardholder", "group_id": "group123", "enrollment_success_template": "enrollment/success--cst.html", "display_order": 1, @@ -181,7 +181,7 @@ "pk": 6, "fields": { "system_name": "in_person_only", - "label": "In-person Only", + "label": "In-Person Only", "group_id": "group123", "supported_enrollment_methods": ["in_person"], "transit_agency_id": 1 From 87a3e8e230e806bc42e9ef7d2d19c1faa921ac71 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:29:17 +0000 Subject: [PATCH 07/21] test: update agency/flows relationship in tests --- tests/pytest/core/test_views.py | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index e3590e5b33..bea3cdaa45 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -108,10 +108,6 @@ def test_agency_public_key(client, model_TransitAgency): def test_agency_card_with_eligibility_api_flow( client, model_TransitAgency, model_EnrollmentFlow_with_eligibility_api, mocked_session_update, mocked_session_reset ): - model_TransitAgency.enrollment_flows.clear() - model_TransitAgency.enrollment_flows.add(model_EnrollmentFlow_with_eligibility_api) - model_TransitAgency.save() - url = reverse(routes.AGENCY_CARD, args=[model_TransitAgency.slug]) response = client.get(url) @@ -134,16 +130,9 @@ def test_agency_card_with_multiple_eligibility_api_flows( new_flow.label = "New flow" new_flow.system_name = "new" new_flow.pk = None + new_flow.transit_agency_id = model_TransitAgency.id new_flow.save() - # note the order these are added to the TransitAgency doesn't matter (and thus, their sorting within that agency) - # it is the order they were created in the database that is used for the query - # we always expect new_flow to be the one to match - model_TransitAgency.enrollment_flows.clear() - model_TransitAgency.enrollment_flows.add(model_EnrollmentFlow_with_eligibility_api) - model_TransitAgency.enrollment_flows.add(new_flow) - model_TransitAgency.save() - url = reverse(routes.AGENCY_CARD, args=[model_TransitAgency.slug]) response = client.get(url) @@ -156,9 +145,8 @@ def test_agency_card_with_multiple_eligibility_api_flows( def test_agency_card_without_eligibility_api_flow( client, model_TransitAgency, model_EnrollmentFlow_with_scope_and_claim, mocked_session_update, mocked_session_reset ): - model_TransitAgency.enrollment_flows.clear() - model_TransitAgency.enrollment_flows.add(model_EnrollmentFlow_with_scope_and_claim) - model_TransitAgency.save() + model_EnrollmentFlow_with_scope_and_claim.transit_agency_id = model_TransitAgency.id + model_EnrollmentFlow_with_scope_and_claim.save() url = reverse(routes.AGENCY_CARD, args=[model_TransitAgency.slug]) response = client.get(url) From 3679734c3b6680ae3fcd643506422e5a465164cd Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Wed, 25 Sep 2024 23:45:44 +0000 Subject: [PATCH 08/21] fix(fixtures): remove digital, inperson only flows for now - breaking tests --- benefits/core/migrations/local_fixtures.json | 29 -------------------- 1 file changed, 29 deletions(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 25060497f9..05c1a57453 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -176,35 +176,6 @@ "transit_agency_id": 1 } }, - { - "model": "core.enrollmentflow", - "pk": 6, - "fields": { - "system_name": "in_person_only", - "label": "In-Person Only", - "group_id": "group123", - "supported_enrollment_methods": ["in_person"], - "transit_agency_id": 1 - } - }, - { - "model": "core.enrollmentflow", - "pk": 7, - "fields": { - "system_name": "digital_only", - "label": "Digital Only", - "group_id": "group123", - "enrollment_success_template": "enrollment/success--cst.html", - "display_order": 5, - "claims_provider": 1, - "selection_label_template": "eligibility/includes/selection-label--senior.html", - "eligibility_start_template": "eligibility/start--senior.html", - "claims_scope": "verify:senior", - "claims_claim": "senior", - "supported_enrollment_methods": ["digital"], - "transit_agency_id": 1 - } - }, { "model": "core.transitprocessor", "pk": 1, From e85b9fdac19e122f3aed8d8008737dbb8d70fcc4 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Thu, 26 Sep 2024 00:56:35 +0000 Subject: [PATCH 09/21] feat(admin): display agency, supported methods on enrollmentflow list --- benefits/core/admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/benefits/core/admin.py b/benefits/core/admin.py index f9bf8adf2b..80c51aea16 100644 --- a/benefits/core/admin.py +++ b/benefits/core/admin.py @@ -44,6 +44,8 @@ def get_readonly_fields(self, request, obj=None): @admin.register(models.EnrollmentFlow) class SortableEnrollmentFlowAdmin(SortableAdminMixin, admin.ModelAdmin): # pragma: no cover + list_display = ("label", "transit_agency", "supported_enrollment_methods") + def get_exclude(self, request, obj=None): if not request.user.is_superuser: return [ From ca98527f59eff5f3aed85244184aaa844cebf96c Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Thu, 26 Sep 2024 16:23:52 +0000 Subject: [PATCH 10/21] test(views): fix last test w/ mocked flow+session --- tests/pytest/in_person/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index 75d4df171e..eb72ee903b 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -50,7 +50,7 @@ def test_eligibility_logged_in(admin_client): @pytest.mark.django_db -@pytest.mark.usefixtures("mocked_session_agency") +@pytest.mark.usefixtures("mocked_session_agency", "mocked_session_flow") def test_confirm_post_valid_form_eligibility_verified(admin_client): path = reverse(routes.IN_PERSON_ELIGIBILITY) From c9b7ec17ffccca8c04add950e67b15cb28e83eb8 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Thu, 26 Sep 2024 18:05:36 +0000 Subject: [PATCH 11/21] feat(migration): add data migration so existing flows have an agency --- ..._transitagency_enrollment_flows_and_more.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py index f2af616bc6..14b6ee3213 100644 --- a/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py +++ b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py @@ -4,6 +4,15 @@ from django.db import migrations, models +def migrate_data(apps, schema_editor): + TransitAgency = apps.get_model("core", "TransitAgency") + + for agency in TransitAgency.objects.all(): + for flow in agency.enrollment_flows.all(): + flow.transit_agency = agency + flow.save() + + class Migration(migrations.Migration): dependencies = [ @@ -11,14 +20,15 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name="transitagency", - name="enrollment_flows", - ), migrations.AddField( model_name="enrollmentflow", name="transit_agency", field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), preserve_default=False, ), + migrations.RunPython(migrate_data), + migrations.RemoveField( + model_name="transitagency", + name="enrollment_flows", + ), ] From 3f4390c26ae642614775b5b4f65ef1d8a68fdcfa Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 05:58:56 +0000 Subject: [PATCH 12/21] refactor(migration): rename migration to a more descriptive title --- ...ansitagency_enrollmentflow_relationship.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py diff --git a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py new file mode 100644 index 0000000000..14b6ee3213 --- /dev/null +++ b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py @@ -0,0 +1,34 @@ +# Generated by Django 5.1.1 on 2024-09-25 16:47 + +import django.db.models.deletion +from django.db import migrations, models + + +def migrate_data(apps, schema_editor): + TransitAgency = apps.get_model("core", "TransitAgency") + + for agency in TransitAgency.objects.all(): + for flow in agency.enrollment_flows.all(): + flow.transit_agency = agency + flow.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0027_enrollmentflow_supported_methods"), + ] + + operations = [ + migrations.AddField( + model_name="enrollmentflow", + name="transit_agency", + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), + preserve_default=False, + ), + migrations.RunPython(migrate_data), + migrations.RemoveField( + model_name="transitagency", + name="enrollment_flows", + ), + ] From 9296ce555be99a5743b8418519b08200898c8d3b Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 06:01:47 +0000 Subject: [PATCH 13/21] fix(fixtures): set transitagency directly with object, not id --- benefits/core/migrations/local_fixtures.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/benefits/core/migrations/local_fixtures.json b/benefits/core/migrations/local_fixtures.json index 05c1a57453..30f88e95bd 100644 --- a/benefits/core/migrations/local_fixtures.json +++ b/benefits/core/migrations/local_fixtures.json @@ -87,7 +87,7 @@ "claims_scope": "verify:senior", "claims_claim": "senior", "supported_enrollment_methods": ["digital", "in_person"], - "transit_agency_id": 1 + "transit_agency": 1 } }, { @@ -105,7 +105,7 @@ "claims_scope": "verify:veteran", "claims_claim": "veteran", "supported_enrollment_methods": ["digital", "in_person"], - "transit_agency_id": 1 + "transit_agency": 1 } }, { @@ -131,7 +131,7 @@ "eligibility_unverified_template": "eligibility/unverified--cst-agency-card.html", "help_template": "core/includes/help--cst-agency-card.html", "supported_enrollment_methods": ["digital", "in_person"], - "transit_agency_id": 1 + "transit_agency": 1 } }, { @@ -154,7 +154,7 @@ "claims_scope": "verify:calfresh", "claims_claim": "calfresh", "supported_enrollment_methods": ["digital", "in_person"], - "transit_agency_id": 1 + "transit_agency": 1 } }, { @@ -173,7 +173,7 @@ "claims_scope": "verify:medicare", "claims_claim": "medicare", "supported_enrollment_methods": ["digital", "in_person"], - "transit_agency_id": 1 + "transit_agency": 1 } }, { From 015e612a09b1d3e88ea0f5735afd641c42b07a75 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 06:06:40 +0000 Subject: [PATCH 14/21] fix(tests): assign transit_agency directly, not by id --- tests/pytest/conftest.py | 12 +++++++----- tests/pytest/core/test_models.py | 6 +++--- tests/pytest/core/test_views.py | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/pytest/conftest.py b/tests/pytest/conftest.py index 9690ffc139..780b23fd89 100644 --- a/tests/pytest/conftest.py +++ b/tests/pytest/conftest.py @@ -5,14 +5,16 @@ from django.utils import timezone import pytest -from pytest_socket import disable_socket + +# from pytest_socket import disable_socket from benefits.core import session from benefits.core.models import ClaimsProvider, EnrollmentFlow, TransitProcessor, PemData, TransitAgency def pytest_runtest_setup(): - disable_socket() + # disable_socket() + pass @pytest.fixture @@ -82,7 +84,7 @@ def model_EnrollmentFlow(model_TransitAgency): label="Test flow label", group_id="group123", enrollment_success_template="enrollment/success.html", - transit_agency_id=model_TransitAgency.id, + transit_agency=model_TransitAgency, ) return flow @@ -111,9 +113,9 @@ def model_EnrollmentFlow_with_scope_and_claim(model_EnrollmentFlow, model_Claims @pytest.fixture -def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow_with_scope_and_claim): +def model_EnrollmentFlow_with_claims_scheme(model_EnrollmentFlow_with_scope_and_claim, model_TransitAgency): model_EnrollmentFlow_with_scope_and_claim.claims_scheme_override = "scheme" - model_EnrollmentFlow_with_scope_and_claim.transit_agency_id = 1 + model_EnrollmentFlow_with_scope_and_claim.transit_agency = model_TransitAgency model_EnrollmentFlow_with_scope_and_claim.save() return model_EnrollmentFlow_with_scope_and_claim diff --git a/tests/pytest/core/test_models.py b/tests/pytest/core/test_models.py index d52fb07ac8..50aa34d2af 100644 --- a/tests/pytest/core/test_models.py +++ b/tests/pytest/core/test_models.py @@ -169,7 +169,7 @@ def test_EnrollmentFlow_supports_expiration(model_EnrollmentFlow_supports_expira @pytest.mark.django_db def test_EnrollmentFlow_enrollment_index_template(model_TransitAgency): - new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) + new_flow = EnrollmentFlow.objects.create(transit_agency=model_TransitAgency) assert new_flow.enrollment_index_template == "enrollment/index.html" @@ -181,14 +181,14 @@ def test_EnrollmentFlow_enrollment_index_template(model_TransitAgency): @pytest.mark.django_db def test_EnrollmentFlow_enrollment_success_template(model_TransitAgency): - new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) + new_flow = EnrollmentFlow.objects.create(transit_agency=model_TransitAgency) assert new_flow.enrollment_success_template == "enrollment/success.html" @pytest.mark.django_db def test_EnrollmentFlow_supported_enrollment_methods(model_TransitAgency): - new_flow = EnrollmentFlow.objects.create(transit_agency_id=model_TransitAgency.id) + new_flow = EnrollmentFlow.objects.create(transit_agency=model_TransitAgency) assert new_flow.supported_enrollment_methods == ["digital", "in_person"] diff --git a/tests/pytest/core/test_views.py b/tests/pytest/core/test_views.py index bea3cdaa45..57dfdf66e4 100644 --- a/tests/pytest/core/test_views.py +++ b/tests/pytest/core/test_views.py @@ -130,7 +130,7 @@ def test_agency_card_with_multiple_eligibility_api_flows( new_flow.label = "New flow" new_flow.system_name = "new" new_flow.pk = None - new_flow.transit_agency_id = model_TransitAgency.id + new_flow.transit_agency = model_TransitAgency new_flow.save() url = reverse(routes.AGENCY_CARD, args=[model_TransitAgency.slug]) @@ -145,7 +145,7 @@ def test_agency_card_with_multiple_eligibility_api_flows( def test_agency_card_without_eligibility_api_flow( client, model_TransitAgency, model_EnrollmentFlow_with_scope_and_claim, mocked_session_update, mocked_session_reset ): - model_EnrollmentFlow_with_scope_and_claim.transit_agency_id = model_TransitAgency.id + model_EnrollmentFlow_with_scope_and_claim.transit_agency = model_TransitAgency model_EnrollmentFlow_with_scope_and_claim.save() url = reverse(routes.AGENCY_CARD, args=[model_TransitAgency.slug]) From 55438d8b375c4039a7b9e83e836f2dcc0293a374 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 06:06:51 +0000 Subject: [PATCH 15/21] fix: remove old migration --- ...transitagency_enrollment_flows_and_more.py | 34 ------------------- 1 file changed, 34 deletions(-) delete mode 100644 benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py diff --git a/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py deleted file mode 100644 index 14b6ee3213..0000000000 --- a/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.1.1 on 2024-09-25 16:47 - -import django.db.models.deletion -from django.db import migrations, models - - -def migrate_data(apps, schema_editor): - TransitAgency = apps.get_model("core", "TransitAgency") - - for agency in TransitAgency.objects.all(): - for flow in agency.enrollment_flows.all(): - flow.transit_agency = agency - flow.save() - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0027_enrollmentflow_supported_methods"), - ] - - operations = [ - migrations.AddField( - model_name="enrollmentflow", - name="transit_agency", - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), - preserve_default=False, - ), - migrations.RunPython(migrate_data), - migrations.RemoveField( - model_name="transitagency", - name="enrollment_flows", - ), - ] From f72819f8e83797653f5ba37f1940635f2562477e Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 06:10:25 +0000 Subject: [PATCH 16/21] refactor(db): re-word helptext for enrollmentflow label --- .../0029_alter_enrollmentflow_label.py | 18 ++++++++++++++++++ benefits/core/models.py | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 benefits/core/migrations/0029_alter_enrollmentflow_label.py diff --git a/benefits/core/migrations/0029_alter_enrollmentflow_label.py b/benefits/core/migrations/0029_alter_enrollmentflow_label.py new file mode 100644 index 0000000000..c31b802884 --- /dev/null +++ b/benefits/core/migrations/0029_alter_enrollmentflow_label.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1 on 2024-09-30 06:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0028_refactor_transitagency_enrollmentflow_relationship"), + ] + + operations = [ + migrations.AlterField( + model_name="enrollmentflow", + name="label", + field=models.TextField(help_text="A human readable label, used as the display text in Admin.", null=True), + ), + ] diff --git a/benefits/core/models.py b/benefits/core/models.py index 29baad8d94..c2cf07a07f 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -350,7 +350,7 @@ class EnrollmentFlow(models.Model): ) label = models.TextField( null=True, - help_text="A human readable label, not shown to end-users. Used as the display text in Admin.", + help_text="A human readable label, used as the display text in Admin.", ) group_id = models.TextField(null=True, help_text="Reference to the TransitProcessor group for user enrollment") supports_expiration = models.BooleanField( From b07757aba6847f21ed58702e93e5d0571c0cb642 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 06:28:17 +0000 Subject: [PATCH 17/21] feat(db): allow enrollmentflow.transit_agency to be null --- .../0028_refactor_transitagency_enrollmentflow_relationship.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py index 14b6ee3213..ea8049e783 100644 --- a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py +++ b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py @@ -23,7 +23,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name="enrollmentflow", name="transit_agency", - field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), preserve_default=False, ), migrations.RunPython(migrate_data), From b2876ea2b7b9633261f0376d4f8c8a2d330f3cf2 Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 22:46:43 +0000 Subject: [PATCH 18/21] fix(migration): allow enrollmentflow's transit agency to be null, allow blanks for admin --- ...0028_refactor_transitagency_enrollmentflow_relationship.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py index ea8049e783..680b18baf5 100644 --- a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py +++ b/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py @@ -23,7 +23,9 @@ class Migration(migrations.Migration): migrations.AddField( model_name="enrollmentflow", name="transit_agency", - field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to="core.transitagency"), + field=models.ForeignKey( + blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency" + ), preserve_default=False, ), migrations.RunPython(migrate_data), From d1e22257529f0aaf34bf06e141717e2331e89bbf Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Mon, 30 Sep 2024 23:27:12 +0000 Subject: [PATCH 19/21] refactor(db): combine migration into 1 and add migrate_data() --- ...transitagency_enrollment_flows_and_more.py} | 9 ++++++--- .../0029_alter_enrollmentflow_label.py | 18 ------------------ benefits/core/models.py | 2 +- 3 files changed, 7 insertions(+), 22 deletions(-) rename benefits/core/migrations/{0028_refactor_transitagency_enrollmentflow_relationship.py => 0028_remove_transitagency_enrollment_flows_and_more.py} (77%) delete mode 100644 benefits/core/migrations/0029_alter_enrollmentflow_label.py diff --git a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py similarity index 77% rename from benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py rename to benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py index 680b18baf5..5f1f61a1ff 100644 --- a/benefits/core/migrations/0028_refactor_transitagency_enrollmentflow_relationship.py +++ b/benefits/core/migrations/0028_remove_transitagency_enrollment_flows_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.1 on 2024-09-25 16:47 +# Generated by Django 5.1 on 2024-09-30 23:25 import django.db.models.deletion from django.db import migrations, models @@ -6,7 +6,6 @@ def migrate_data(apps, schema_editor): TransitAgency = apps.get_model("core", "TransitAgency") - for agency in TransitAgency.objects.all(): for flow in agency.enrollment_flows.all(): flow.transit_agency = agency @@ -26,11 +25,15 @@ class Migration(migrations.Migration): field=models.ForeignKey( blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to="core.transitagency" ), - preserve_default=False, ), migrations.RunPython(migrate_data), migrations.RemoveField( model_name="transitagency", name="enrollment_flows", ), + migrations.AlterField( + model_name="enrollmentflow", + name="label", + field=models.TextField(help_text="A human readable label, used as the display text in Admin.", null=True), + ), ] diff --git a/benefits/core/migrations/0029_alter_enrollmentflow_label.py b/benefits/core/migrations/0029_alter_enrollmentflow_label.py deleted file mode 100644 index c31b802884..0000000000 --- a/benefits/core/migrations/0029_alter_enrollmentflow_label.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1 on 2024-09-30 06:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("core", "0028_refactor_transitagency_enrollmentflow_relationship"), - ] - - operations = [ - migrations.AlterField( - model_name="enrollmentflow", - name="label", - field=models.TextField(help_text="A human readable label, used as the display text in Admin.", null=True), - ), - ] diff --git a/benefits/core/models.py b/benefits/core/models.py index c2cf07a07f..7b29a55f08 100644 --- a/benefits/core/models.py +++ b/benefits/core/models.py @@ -381,7 +381,7 @@ class EnrollmentFlow(models.Model): default=[EnrollmentMethods.DIGITAL, EnrollmentMethods.IN_PERSON], help_text="If the flow is supported by digital enrollment, in-person enrollment, or both", ) - transit_agency = models.ForeignKey(TransitAgency, on_delete=models.PROTECT) + transit_agency = models.ForeignKey(TransitAgency, on_delete=models.PROTECT, null=True, blank=True) class Meta: ordering = ["display_order"] From a32e5cb4392c15dd8b3c93ad7b28fbe232892b0f Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Fri, 27 Sep 2024 06:58:42 +0000 Subject: [PATCH 20/21] feat(eligibility): filter flows in form by supported_enrollment_method --- benefits/eligibility/forms.py | 2 +- benefits/in_person/forms.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/benefits/eligibility/forms.py b/benefits/eligibility/forms.py index cd9233d5dc..ae16efa3a9 100644 --- a/benefits/eligibility/forms.py +++ b/benefits/eligibility/forms.py @@ -26,7 +26,7 @@ class EnrollmentFlowSelectionForm(forms.Form): def __init__(self, agency: models.TransitAgency, *args, **kwargs): super().__init__(*args, **kwargs) - flows = agency.enrollment_flows.all() + flows = agency.enrollment_flows.filter(supported_enrollment_methods__contains=models.EnrollmentMethods.DIGITAL) self.classes = "col-lg-8" # second element is not used since we render the whole label using selection_label_template, diff --git a/benefits/in_person/forms.py b/benefits/in_person/forms.py index 752ffd3bf7..e2b09b31b4 100644 --- a/benefits/in_person/forms.py +++ b/benefits/in_person/forms.py @@ -24,7 +24,7 @@ class InPersonEligibilityForm(forms.Form): def __init__(self, agency: models.TransitAgency, *args, **kwargs): super().__init__(*args, **kwargs) - flows = agency.enrollment_flows.all() + flows = agency.enrollment_flows.filter(supported_enrollment_methods__contains=models.EnrollmentMethods.IN_PERSON) self.classes = "checkbox-parent" flow_field = self.fields["flow"] From 00e1ea31163dee7211d836dc82afde7e3fb03d1f Mon Sep 17 00:00:00 2001 From: Machiko Yasuda Date: Fri, 27 Sep 2024 07:16:28 +0000 Subject: [PATCH 21/21] test: add specs for flows filtered by supported method --- tests/pytest/eligibility/test_views.py | 36 ++++++++++++++++++++++++++ tests/pytest/in_person/test_views.py | 30 +++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/tests/pytest/eligibility/test_views.py b/tests/pytest/eligibility/test_views.py index 8aab99a453..78af10934c 100644 --- a/tests/pytest/eligibility/test_views.py +++ b/tests/pytest/eligibility/test_views.py @@ -2,6 +2,7 @@ import pytest +from benefits.core import models from benefits.routes import routes from benefits.core.middleware import TEMPLATE_USER_ERROR import benefits.core.session @@ -71,6 +72,39 @@ def model_EnrollmentFlow_with_form_class(mocker, model_EnrollmentFlow): return model_EnrollmentFlow +@pytest.mark.django_db +def test_eligibility_logged_in_filtering_flows(mocker, model_TransitAgency, client): + model_TransitAgency.pk = None + model_TransitAgency.save() + digital = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, + supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL], + label="Digital", + selection_label_template="eligibility/includes/selection-label.html", + ) + in_person = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, + supported_enrollment_methods=[models.EnrollmentMethods.IN_PERSON], + label="In-Person", + selection_label_template="eligibility/includes/selection-label.html", + ) + both = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, + supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL, models.EnrollmentMethods.IN_PERSON], + label="Both", + selection_label_template="eligibility/includes/selection-label.html", + ) + model_TransitAgency.save() + model_TransitAgency.enrollment_flows.add(digital, in_person, both) + mocker.patch("benefits.core.session.agency", autospec=True, return_value=model_TransitAgency) + + path = reverse(routes.ELIGIBILITY_INDEX) + response = client.get(path) + + assert model_TransitAgency.enrollment_flows.count() == 3 + assert len(response.context_data["form"].fields["flow"].choices) == 2 + + @pytest.mark.django_db def test_index_get_agency_multiple_flows(mocker, model_TransitAgency, model_EnrollmentFlow, mocked_session_agency, client): # override the mocked session agency with a mock agency that has multiple flows @@ -80,6 +114,7 @@ def test_index_get_agency_multiple_flows(mocker, model_TransitAgency, model_Enro mock_manager = mocker.Mock() mock_manager.all.return_value = [model_EnrollmentFlow, model_EnrollmentFlow] type(mock_agency).enrollment_flows = mocker.PropertyMock(return_value=mock_manager) + type(mock_agency).enrollment_flows.filter.return_value = [model_EnrollmentFlow, model_EnrollmentFlow] mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" @@ -103,6 +138,7 @@ def test_index_get_agency_single_flow(mocker, model_TransitAgency, model_Enrollm mock_manager = mocker.Mock() mock_manager.all.return_value = [model_EnrollmentFlow] type(mock_agency).enrollment_flows = mocker.PropertyMock(return_value=mock_manager) + type(mock_agency).enrollment_flows.filter.return_value = [model_EnrollmentFlow] mock_agency.index_url = "/agency" mock_agency.eligibility_index_template = "eligibility/index.html" diff --git a/tests/pytest/in_person/test_views.py b/tests/pytest/in_person/test_views.py index eb72ee903b..00e951df52 100644 --- a/tests/pytest/in_person/test_views.py +++ b/tests/pytest/in_person/test_views.py @@ -37,6 +37,36 @@ def test_view_not_logged_in(client, viewname): assert response.url == "/admin/login/?next=" + path +@pytest.mark.django_db +def test_eligibility_logged_in_filtering_flows(mocker, model_TransitAgency, admin_client): + model_TransitAgency.pk = None + model_TransitAgency.save() + + digital = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL], label="Digital" + ) + in_person = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, + supported_enrollment_methods=[models.EnrollmentMethods.IN_PERSON], + label="In-Person", + ) + both = models.EnrollmentFlow.objects.create( + transit_agency=model_TransitAgency, + supported_enrollment_methods=[models.EnrollmentMethods.DIGITAL, models.EnrollmentMethods.IN_PERSON], + label="Both", + ) + model_TransitAgency.enrollment_flows.add(digital, in_person, both) + mocker.patch("benefits.core.session.agency", autospec=True, return_value=model_TransitAgency) + + path = reverse(routes.IN_PERSON_ELIGIBILITY) + response = admin_client.get(path) + + assert model_TransitAgency.enrollment_flows.count() == 3 + assert len(response.context_data["form"].fields["flow"].choices) == 2 + assert response.context_data["form"].fields["flow"].choices[0][1] == "In-Person" + assert response.context_data["form"].fields["flow"].choices[1][1] == "Both" + + # admin_client is a fixture from pytest # https://pytest-django.readthedocs.io/en/latest/helpers.html#admin-client-django-test-client-logged-in-as-admin @pytest.mark.django_db