From 24a18f1562e50d2210a72ce6916aa41d7e7c8e94 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 14:19:25 -0600 Subject: [PATCH 001/171] Remove db_index, add meta index --- src/registrar/models/contact.py | 11 +++++++---- src/registrar/models/domain.py | 7 +++++++ src/registrar/models/domain_information.py | 10 ++++++++-- src/registrar/models/domain_request.py | 11 +++++++++-- src/registrar/models/transition_domain.py | 3 --- src/registrar/models/user.py | 1 - src/registrar/models/verified_by_staff.py | 1 - 7 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 3ebd8bc3e..d401102a8 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -8,6 +8,13 @@ from phonenumber_field.modelfields import PhoneNumberField # type: ignore class Contact(TimeStampedModel): """Contact information follows a similar pattern for each contact.""" + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["user"]), + models.Index(fields=["email"]), + ] + user = models.OneToOneField( "registrar.User", null=True, @@ -19,7 +26,6 @@ class Contact(TimeStampedModel): null=True, blank=True, verbose_name="first name", - db_index=True, ) middle_name = models.CharField( null=True, @@ -29,7 +35,6 @@ class Contact(TimeStampedModel): null=True, blank=True, verbose_name="last name", - db_index=True, ) title = models.CharField( null=True, @@ -39,13 +44,11 @@ class Contact(TimeStampedModel): email = models.EmailField( null=True, blank=True, - db_index=True, max_length=320, ) phone = PhoneNumberField( null=True, blank=True, - db_index=True, ) def _get_all_relations(self): diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 7f53bb234..537735752 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -65,6 +65,13 @@ class Domain(TimeStampedModel, DomainHelper): domain meets the required checks. """ + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["name"]), + ] + + def __init__(self, *args, **kwargs): self._cache = {} super(Domain, self).__init__(*args, **kwargs) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index c724423ce..21fc27a0d 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -22,6 +22,14 @@ class DomainInformation(TimeStampedModel): the domain request once approved, so copying them that way we can make changes after its approved. Most fields here are copied from DomainRequest.""" + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["domain"]), + models.Index(fields=["domain_request"]), + models.Index(fields=["generic_org_type"]), + ] + StateTerritoryChoices = DomainRequest.StateTerritoryChoices # use the short names in Django admin @@ -120,7 +128,6 @@ class DomainInformation(TimeStampedModel): organization_name = models.CharField( null=True, blank=True, - db_index=True, ) address_line1 = models.CharField( null=True, @@ -147,7 +154,6 @@ class DomainInformation(TimeStampedModel): max_length=10, null=True, blank=True, - db_index=True, verbose_name="zip code", ) urbanization = models.CharField( diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 75fbadc3e..19f1a66c1 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -24,6 +24,15 @@ logger = logging.getLogger(__name__) class DomainRequest(TimeStampedModel): """A registrant's domain request for a new domain.""" + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["requested_domain"]), + models.Index(fields=["approved_domain"]), + models.Index(fields=["status"]), + models.Index(fields=["generic_org_type"]), + ] + # https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history # If we note any performace degradation due to this addition, # we can query the auditlogs table in admin.py and add the results to @@ -537,7 +546,6 @@ class DomainRequest(TimeStampedModel): organization_name = models.CharField( null=True, blank=True, - db_index=True, ) address_line1 = models.CharField( @@ -566,7 +574,6 @@ class DomainRequest(TimeStampedModel): null=True, blank=True, verbose_name="zip code", - db_index=True, ) urbanization = models.CharField( null=True, diff --git a/src/registrar/models/transition_domain.py b/src/registrar/models/transition_domain.py index 2dafd6da4..0b0cffcec 100644 --- a/src/registrar/models/transition_domain.py +++ b/src/registrar/models/transition_domain.py @@ -59,7 +59,6 @@ class TransitionDomain(TimeStampedModel): null=True, blank=True, help_text="Organization name", - db_index=True, ) federal_type = models.CharField( max_length=50, @@ -85,7 +84,6 @@ class TransitionDomain(TimeStampedModel): blank=True, help_text="First name / given name", verbose_name="first name", - db_index=True, ) middle_name = models.CharField( null=True, @@ -136,7 +134,6 @@ class TransitionDomain(TimeStampedModel): blank=True, verbose_name="zip code", help_text="Zip code", - db_index=True, ) def __str__(self): diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 5e4c88f63..7e82ee1c1 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -70,7 +70,6 @@ class User(AbstractUser): null=True, blank=True, help_text="Phone", - db_index=True, ) verification_type = models.CharField( diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py index c09dce822..1e3e21057 100644 --- a/src/registrar/models/verified_by_staff.py +++ b/src/registrar/models/verified_by_staff.py @@ -9,7 +9,6 @@ class VerifiedByStaff(TimeStampedModel): email = models.EmailField( null=False, blank=False, - db_index=True, ) requestor = models.ForeignKey( From e265008bc86b9a1f094664494ccb4f46776dbf5e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 6 May 2024 14:25:52 -0600 Subject: [PATCH 002/171] Add migrations --- ...email_alter_contact_first_name_and_more.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py diff --git a/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py new file mode 100644 index 000000000..a35cc6a22 --- /dev/null +++ b/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py @@ -0,0 +1,111 @@ +# Generated by Django 4.2.10 on 2024-05-06 20:21 + +from django.db import migrations, models +import phonenumber_field.modelfields + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0090_waffleflag"), + ] + + operations = [ + migrations.AlterField( + model_name="contact", + name="email", + field=models.EmailField(blank=True, max_length=320, null=True), + ), + migrations.AlterField( + model_name="contact", + name="first_name", + field=models.CharField(blank=True, null=True, verbose_name="first name"), + ), + migrations.AlterField( + model_name="contact", + name="last_name", + field=models.CharField(blank=True, null=True, verbose_name="last name"), + ), + migrations.AlterField( + model_name="contact", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region=None), + ), + migrations.AlterField( + model_name="domaininformation", + name="organization_name", + field=models.CharField(blank=True, null=True), + ), + migrations.AlterField( + model_name="domaininformation", + name="zipcode", + field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="domainrequest", + name="organization_name", + field=models.CharField(blank=True, null=True), + ), + migrations.AlterField( + model_name="domainrequest", + name="zipcode", + field=models.CharField(blank=True, max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="transitiondomain", + name="first_name", + field=models.CharField( + blank=True, help_text="First name / given name", null=True, verbose_name="first name" + ), + ), + migrations.AlterField( + model_name="transitiondomain", + name="organization_name", + field=models.CharField(blank=True, help_text="Organization name", null=True), + ), + migrations.AlterField( + model_name="transitiondomain", + name="zipcode", + field=models.CharField(blank=True, help_text="Zip code", max_length=10, null=True, verbose_name="zip code"), + ), + migrations.AlterField( + model_name="user", + name="phone", + field=phonenumber_field.modelfields.PhoneNumberField( + blank=True, help_text="Phone", max_length=128, null=True, region=None + ), + ), + migrations.AlterField( + model_name="verifiedbystaff", + name="email", + field=models.EmailField(max_length=254), + ), + migrations.AddIndex( + model_name="contact", + index=models.Index(fields=["user"], name="registrar_c_user_id_4059c4_idx"), + ), + migrations.AddIndex( + model_name="contact", + index=models.Index(fields=["email"], name="registrar_c_email_bde2de_idx"), + ), + migrations.AddIndex( + model_name="domain", + index=models.Index(fields=["name"], name="registrar_d_name_5b1956_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["requested_domain"], name="registrar_d_request_6894eb_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["approved_domain"], name="registrar_d_approve_ac4c46_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["status"], name="registrar_d_status_a32b59_idx"), + ), + migrations.AddIndex( + model_name="domainrequest", + index=models.Index(fields=["generic_org_type"], name="registrar_d_generic_4d1d2a_idx"), + ), + ] From b5784a1a88e6fc3ab870d6783e9798bd92ef8b8c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 7 May 2024 10:18:21 -0600 Subject: [PATCH 003/171] Add timers --- src/registrar/admin.py | 15 ++- src/registrar/utility/csv_export.py | 190 ++++++++++++++-------------- 2 files changed, 106 insertions(+), 99 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 76d4a6909..c3ba18bbe 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -32,6 +32,7 @@ from django.contrib.auth.forms import UserChangeForm, UsernameField from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter from django.utils.translation import gettext_lazy as _ +from registrar.models.utility.generic_helper import Timer logger = logging.getLogger(__name__) @@ -2027,12 +2028,14 @@ class DomainAdmin(ListHeaderAdmin): return HttpResponseRedirect(reverse("domain", args=(obj.id,))) def change_view(self, request, object_id): - # If the analyst was recently editing a domain page, - # delete any associated session values - if "analyst_action" in request.session: - del request.session["analyst_action"] - del request.session["analyst_action_location"] - return super().change_view(request, object_id) + logger.info("Timing change_view on domain") + with Timer(): + # If the analyst was recently editing a domain page, + # delete any associated session values + if "analyst_action" in request.session: + del request.session["analyst_action"] + del request.session["analyst_action_location"] + return super().change_view(request, object_id) def has_change_permission(self, request, obj=None): # Fixes a bug wherein users which are only is_staff diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 8787f9e74..83754f574 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -13,7 +13,7 @@ from django.db.models.functions import Concat, Coalesce from registrar.models.public_contact import PublicContact from registrar.models.user_domain_role import UserDomainRole from registrar.utility.enums import DefaultEmail - +from registrar.models.utility.generic_helper import Timer logger = logging.getLogger(__name__) @@ -379,108 +379,112 @@ def write_csv_for_requests( def export_data_type_to_csv(csv_file): """All domains report with extra columns""" + logger.info("Timing export_data_type_to_csv") + with Timer(): + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Status", + "Expiration date", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + # For domain manager we are pass it in as a parameter below in write_body + ] - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Status", - "Expiration date", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Security contact email", - # For domain manager we are pass it in as a parameter below in write_body - ] - - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True - ) + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True + ) def export_data_full_to_csv(csv_file): """All domains report""" - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True - ) + logger.info("Timing def export_data_full_to_csv") + with Timer(): + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True + ) def export_data_federal_to_csv(csv_file): """Federal domains report""" - - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "organization_type__icontains": "federal", - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True - ) + logger.info("Timing def export_data_federal_to_csv") + with Timer(): + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "organization_type__icontains": "federal", + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True + ) def get_default_start_date(): From ca2e2168aa6a8116cc7e1ec818e6978476389ac1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 7 May 2024 14:13:55 -0600 Subject: [PATCH 004/171] Remove timer --- src/registrar/admin.py | 13 +- ...email_alter_contact_first_name_and_more.py | 6 +- src/registrar/models/domain_invitation.py | 6 + src/registrar/models/user.py | 7 + src/registrar/utility/csv_export.py | 185 +++++++++--------- 5 files changed, 114 insertions(+), 103 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index c3ba18bbe..efafbcfc4 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2029,13 +2029,12 @@ class DomainAdmin(ListHeaderAdmin): def change_view(self, request, object_id): logger.info("Timing change_view on domain") - with Timer(): - # If the analyst was recently editing a domain page, - # delete any associated session values - if "analyst_action" in request.session: - del request.session["analyst_action"] - del request.session["analyst_action_location"] - return super().change_view(request, object_id) + # If the analyst was recently editing a domain page, + # delete any associated session values + if "analyst_action" in request.session: + del request.session["analyst_action"] + del request.session["analyst_action_location"] + return super().change_view(request, object_id) def has_change_permission(self, request, obj=None): # Fixes a bug wherein users which are only is_staff diff --git a/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py index a35cc6a22..a3b8fe0e9 100644 --- a/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py +++ b/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-05-06 20:21 +# Generated by Django 4.2.10 on 2024-05-07 20:13 from django.db import migrations, models import phonenumber_field.modelfields @@ -92,6 +92,10 @@ class Migration(migrations.Migration): model_name="domain", index=models.Index(fields=["name"], name="registrar_d_name_5b1956_idx"), ), + migrations.AddIndex( + model_name="domaininvitation", + index=models.Index(fields=["status"], name="registrar_d_status_e84571_idx"), + ), migrations.AddIndex( model_name="domainrequest", index=models.Index(fields=["requested_domain"], name="registrar_d_request_6894eb_idx"), diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 12082142d..39473c0be 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -15,6 +15,12 @@ logger = logging.getLogger(__name__) class DomainInvitation(TimeStampedModel): + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["status"]), + ] + # Constants for status field class DomainInvitationStatus(models.TextChoices): INVITED = "invited", "Invited" diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 7e82ee1c1..e630366a4 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -24,6 +24,13 @@ class User(AbstractUser): but can be customized later. """ + class Meta: + """Contains meta information about this class""" + indexes = [ + models.Index(fields=["username"]), + models.Index(fields=["email"]), + ] + class VerificationTypeChoices(models.TextChoices): """ Users achieve access to our system in a few different ways. diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 83754f574..abd91c0ba 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -380,111 +380,106 @@ def write_csv_for_requests( def export_data_type_to_csv(csv_file): """All domains report with extra columns""" logger.info("Timing export_data_type_to_csv") - with Timer(): - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Status", - "Expiration date", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "AO", - "AO email", - "Security contact email", - # For domain manager we are pass it in as a parameter below in write_body - ] + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Status", + "Expiration date", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "AO", + "AO email", + "Security contact email", + # For domain manager we are pass it in as a parameter below in write_body + ] - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True - ) + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=True, should_write_header=True + ) def export_data_full_to_csv(csv_file): """All domains report""" - logger.info("Timing def export_data_full_to_csv") - with Timer(): - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True - ) + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True + ) def export_data_federal_to_csv(csv_file): """Federal domains report""" - logger.info("Timing def export_data_federal_to_csv") - with Timer(): - writer = csv.writer(csv_file) - # define columns to include in export - columns = [ - "Domain name", - "Domain type", - "Agency", - "Organization name", - "City", - "State", - "Security contact email", - ] - # Coalesce is used to replace federal_type of None with ZZZZZ - sort_fields = [ - "organization_type", - Coalesce("federal_type", Value("ZZZZZ")), - "federal_agency", - "domain__name", - ] - filter_condition = { - "organization_type__icontains": "federal", - "domain__state__in": [ - Domain.State.READY, - Domain.State.DNS_NEEDED, - Domain.State.ON_HOLD, - ], - } - write_csv_for_domains( - writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True - ) + writer = csv.writer(csv_file) + # define columns to include in export + columns = [ + "Domain name", + "Domain type", + "Agency", + "Organization name", + "City", + "State", + "Security contact email", + ] + # Coalesce is used to replace federal_type of None with ZZZZZ + sort_fields = [ + "organization_type", + Coalesce("federal_type", Value("ZZZZZ")), + "federal_agency", + "domain__name", + ] + filter_condition = { + "organization_type__icontains": "federal", + "domain__state__in": [ + Domain.State.READY, + Domain.State.DNS_NEEDED, + Domain.State.ON_HOLD, + ], + } + write_csv_for_domains( + writer, columns, sort_fields, filter_condition, should_get_domain_managers=False, should_write_header=True + ) def get_default_start_date(): From af272c0cd3a4fb81fe760f3f53c965ca10bc54ae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 7 May 2024 14:16:34 -0600 Subject: [PATCH 005/171] Fix migrations after merge --- ..._alter_contact_email_alter_contact_first_name_and_more.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/registrar/migrations/{0091_alter_contact_email_alter_contact_first_name_and_more.py => 0093_alter_contact_email_alter_contact_first_name_and_more.py} (96%) diff --git a/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py similarity index 96% rename from src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py rename to src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py index a3b8fe0e9..ca1127f2e 100644 --- a/src/registrar/migrations/0091_alter_contact_email_alter_contact_first_name_and_more.py +++ b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-05-07 20:13 +# Generated by Django 4.2.10 on 2024-05-07 20:16 from django.db import migrations, models import phonenumber_field.modelfields @@ -7,7 +7,7 @@ import phonenumber_field.modelfields class Migration(migrations.Migration): dependencies = [ - ("registrar", "0090_waffleflag"), + ("registrar", "0092_rename_updated_federal_agency_domaininformation_federal_agency_and_more"), ] operations = [ From 00bcdd16944767d34ee68096f87728de763fb2c6 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 7 May 2024 14:33:15 -0600 Subject: [PATCH 006/171] Remove timer remnants, cleanup --- src/registrar/admin.py | 3 +-- ..._contact_email_alter_contact_first_name_and_more.py | 10 +++++----- src/registrar/models/contact.py | 1 + src/registrar/models/domain.py | 3 ++- src/registrar/models/domain_information.py | 2 +- src/registrar/models/domain_invitation.py | 1 + src/registrar/models/domain_request.py | 2 +- src/registrar/models/user.py | 1 + src/registrar/utility/csv_export.py | 4 ++-- 9 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d8b9ff9f9..55cd358c5 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -32,7 +32,7 @@ from django.contrib.auth.forms import UserChangeForm, UsernameField from django_admin_multiple_choice_list_filter.list_filters import MultipleChoiceListFilter from django.utils.translation import gettext_lazy as _ -from registrar.models.utility.generic_helper import Timer + logger = logging.getLogger(__name__) @@ -2024,7 +2024,6 @@ class DomainAdmin(ListHeaderAdmin): return HttpResponseRedirect(reverse("domain", args=(obj.id,))) def change_view(self, request, object_id): - logger.info("Timing change_view on domain") # If the analyst was recently editing a domain page, # delete any associated session values if "analyst_action" in request.session: diff --git a/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py index ca1127f2e..9696f0c50 100644 --- a/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py +++ b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-05-07 20:16 +# Generated by Django 4.2.10 on 2024-05-07 20:32 from django.db import migrations, models import phonenumber_field.modelfields @@ -92,6 +92,10 @@ class Migration(migrations.Migration): model_name="domain", index=models.Index(fields=["name"], name="registrar_d_name_5b1956_idx"), ), + migrations.AddIndex( + model_name="domain", + index=models.Index(fields=["state"], name="registrar_d_state_84c134_idx"), + ), migrations.AddIndex( model_name="domaininvitation", index=models.Index(fields=["status"], name="registrar_d_status_e84571_idx"), @@ -108,8 +112,4 @@ class Migration(migrations.Migration): model_name="domainrequest", index=models.Index(fields=["status"], name="registrar_d_status_a32b59_idx"), ), - migrations.AddIndex( - model_name="domainrequest", - index=models.Index(fields=["generic_org_type"], name="registrar_d_generic_4d1d2a_idx"), - ), ] diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index d401102a8..5084ea955 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -10,6 +10,7 @@ class Contact(TimeStampedModel): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["user"]), models.Index(fields=["email"]), diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 537735752..fbf2822a8 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -67,11 +67,12 @@ class Domain(TimeStampedModel, DomainHelper): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["name"]), + models.Index(fields=["state"]), ] - def __init__(self, *args, **kwargs): self._cache = {} super(Domain, self).__init__(*args, **kwargs) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index e0b5ad237..81a8c7296 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -24,10 +24,10 @@ class DomainInformation(TimeStampedModel): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["domain"]), models.Index(fields=["domain_request"]), - models.Index(fields=["generic_org_type"]), ] StateTerritoryChoices = DomainRequest.StateTerritoryChoices diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py index 39473c0be..c9cbc8b39 100644 --- a/src/registrar/models/domain_invitation.py +++ b/src/registrar/models/domain_invitation.py @@ -17,6 +17,7 @@ logger = logging.getLogger(__name__) class DomainInvitation(TimeStampedModel): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["status"]), ] diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index a947aba2f..02e30b266 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -26,11 +26,11 @@ class DomainRequest(TimeStampedModel): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["requested_domain"]), models.Index(fields=["approved_domain"]), models.Index(fields=["status"]), - models.Index(fields=["generic_org_type"]), ] # https://django-auditlog.readthedocs.io/en/latest/usage.html#object-history diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index e630366a4..1ff700239 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -26,6 +26,7 @@ class User(AbstractUser): class Meta: """Contains meta information about this class""" + indexes = [ models.Index(fields=["username"]), models.Index(fields=["email"]), diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index abd91c0ba..19d66e598 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -13,7 +13,8 @@ from django.db.models.functions import Concat, Coalesce from registrar.models.public_contact import PublicContact from registrar.models.user_domain_role import UserDomainRole from registrar.utility.enums import DefaultEmail -from registrar.models.utility.generic_helper import Timer + + logger = logging.getLogger(__name__) @@ -379,7 +380,6 @@ def write_csv_for_requests( def export_data_type_to_csv(csv_file): """All domains report with extra columns""" - logger.info("Timing export_data_type_to_csv") writer = csv.writer(csv_file) # define columns to include in export columns = [ From 87aa44cc7788880df863b0ec4685d560224ba098 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 7 May 2024 15:17:40 -0600 Subject: [PATCH 007/171] Cleanup --- src/registrar/admin.py | 1 - ..._email_alter_contact_first_name_and_more.py | 18 +++++++++++++++++- src/registrar/models/domain_information.py | 5 ++--- src/registrar/models/user.py | 13 +++++-------- src/registrar/utility/csv_export.py | 1 - 5 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 55cd358c5..3eea86871 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -33,7 +33,6 @@ from django_admin_multiple_choice_list_filter.list_filters import MultipleChoice from django.utils.translation import gettext_lazy as _ - logger = logging.getLogger(__name__) diff --git a/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py index 9696f0c50..3c45c4ef2 100644 --- a/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py +++ b/src/registrar/migrations/0093_alter_contact_email_alter_contact_first_name_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-05-07 20:32 +# Generated by Django 4.2.10 on 2024-05-07 21:17 from django.db import migrations, models import phonenumber_field.modelfields @@ -96,6 +96,14 @@ class Migration(migrations.Migration): model_name="domain", index=models.Index(fields=["state"], name="registrar_d_state_84c134_idx"), ), + migrations.AddIndex( + model_name="domaininformation", + index=models.Index(fields=["domain"], name="registrar_d_domain__88838a_idx"), + ), + migrations.AddIndex( + model_name="domaininformation", + index=models.Index(fields=["domain_request"], name="registrar_d_domain__d1fba8_idx"), + ), migrations.AddIndex( model_name="domaininvitation", index=models.Index(fields=["status"], name="registrar_d_status_e84571_idx"), @@ -112,4 +120,12 @@ class Migration(migrations.Migration): model_name="domainrequest", index=models.Index(fields=["status"], name="registrar_d_status_a32b59_idx"), ), + migrations.AddIndex( + model_name="user", + index=models.Index(fields=["username"], name="registrar_u_usernam_964b1b_idx"), + ), + migrations.AddIndex( + model_name="user", + index=models.Index(fields=["email"], name="registrar_u_email_c8f2c4_idx"), + ), ] diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 81a8c7296..23c9e4f32 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -30,6 +30,8 @@ class DomainInformation(TimeStampedModel): models.Index(fields=["domain_request"]), ] + verbose_name_plural = "Domain information" + StateTerritoryChoices = DomainRequest.StateTerritoryChoices # use the short names in Django admin @@ -342,6 +344,3 @@ class DomainInformation(TimeStampedModel): def _get_many_to_many_fields(): """Returns a set of each field.name that has the many to many relation""" return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore - - class Meta: - verbose_name_plural = "Domain information" diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 1ff700239..307a7cbcc 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -25,13 +25,16 @@ class User(AbstractUser): """ class Meta: - """Contains meta information about this class""" - indexes = [ models.Index(fields=["username"]), models.Index(fields=["email"]), ] + permissions = [ + ("analyst_access_permission", "Analyst Access Permission"), + ("full_access_permission", "Full Access Permission"), + ] + class VerificationTypeChoices(models.TextChoices): """ Users achieve access to our system in a few different ways. @@ -252,9 +255,3 @@ class User(AbstractUser): """ self.check_domain_invitations_on_login() - - class Meta: - permissions = [ - ("analyst_access_permission", "Analyst Access Permission"), - ("full_access_permission", "Full Access Permission"), - ] diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 19d66e598..9ac83ed86 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -14,7 +14,6 @@ from registrar.models.public_contact import PublicContact from registrar.models.user_domain_role import UserDomainRole from registrar.utility.enums import DefaultEmail - logger = logging.getLogger(__name__) From d268ef54b19e108bbdf8c40080fe4ca6cbe94b22 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 11:42:18 -0600 Subject: [PATCH 008/171] Basic setup stuff --- src/djangooidc/backends.py | 10 ++++-- src/djangooidc/tests/test_backends.py | 10 +++--- src/djangooidc/views.py | 13 ++++++-- .../migrations/0094_user_finished_setup.py | 18 ++++++++++ src/registrar/models/user.py | 7 ++++ src/registrar/views/domain_request.py | 9 ++++- src/registrar/views/utility/mixins.py | 33 +++++++++++++++++++ .../views/utility/permission_views.py | 23 +++++++++++-- 8 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 src/registrar/migrations/0094_user_finished_setup.py diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 41e442f2d..8bdd44698 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -21,10 +21,13 @@ class OpenIdConnectBackend(ModelBackend): """ def authenticate(self, request, **kwargs): + """Returns a tuple of (User, is_new_user)""" logger.debug("kwargs %s" % kwargs) user = None + is_new_user = True + if not kwargs or "sub" not in kwargs.keys(): - return user + return user, is_new_user UserModel = get_user_model() username = self.clean_username(kwargs["sub"]) @@ -48,6 +51,7 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) + is_new_user = created if not created: # If user exists, update existing user @@ -59,10 +63,10 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - return None + return None, is_new_user # run this callback for a each login user.on_each_login() - return user + return user, is_new_user def update_existing_user(self, user, kwargs): """ diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index c15106fa9..7b7b963ea 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -21,7 +21,7 @@ class OpenIdConnectBackendTestCase(TestCase): """Test that authenticate creates a new user if it does not find existing user""" # Ensure that the authenticate method creates a new user - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user.username, "test_user") @@ -39,7 +39,7 @@ class OpenIdConnectBackendTestCase(TestCase): existing_user = User.objects.create_user(username="test_user") # Ensure that the authenticate method updates the existing user - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -68,7 +68,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -89,7 +89,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user = self.backend.authenticate(request=None, **self.kwargs) + user, _ = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -103,5 +103,5 @@ class OpenIdConnectBackendTestCase(TestCase): def test_authenticate_with_unknown_user(self): """Test that authenticate returns None when no kwargs are supplied""" # Ensure that the authenticate method handles the case when the user is not found - user = self.backend.authenticate(request=None, **{}) + user, _ = self.backend.authenticate(request=None, **{}) self.assertIsNone(user) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 815df4ecf..c7a8f1bba 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -85,6 +85,7 @@ def login_callback(request): """Analyze the token returned by the authentication provider (OP).""" global CLIENT try: + request.session["is_new_user"] = False # If the CLIENT is none, attempt to reinitialize before handling the request if _client_is_none(): logger.debug("OIDC client is None, attempting to initialize") @@ -97,9 +98,9 @@ def login_callback(request): # add acr_value to request.session request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) - user = authenticate(request=request, **userinfo) + user, is_new_user = authenticate(request=request, **userinfo) if user: - + should_update_user = False # Fixture users kind of exist in a superposition of verification types, # because while the system "verified" them, if they login, # we don't know how the user themselves was verified through login.gov until @@ -110,9 +111,17 @@ def login_callback(request): # Set the verification type if it doesn't already exist or if its a fixture user if not user.verification_type or is_fixture_user: user.set_user_verification_type() + should_update_user = True + + if is_new_user: + user.finished_setup = False + should_update_user = True + + if should_update_user: user.save() login(request, user) + logger.info("Successfully logged in user %s" % user) # Clear the flag if the exception is not caught diff --git a/src/registrar/migrations/0094_user_finished_setup.py b/src/registrar/migrations/0094_user_finished_setup.py new file mode 100644 index 000000000..660f950c0 --- /dev/null +++ b/src/registrar/migrations/0094_user_finished_setup.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.10 on 2024-05-09 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("registrar", "0093_alter_publiccontact_unique_together"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="finished_setup", + field=models.BooleanField(default=True), + ), + ] diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 5e4c88f63..b3fd95eb3 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -80,6 +80,13 @@ class User(AbstractUser): help_text="The means through which this user was verified", ) + # Tracks if the user finished their profile setup or not. This is so + # we can globally enforce that new users provide additional context before proceeding. + finished_setup = models.BooleanField( + # Default to true so we don't impact existing users. We set this to false downstream. + default=True + ) + def __str__(self): # this info is pulled from Login.gov if self.first_name or self.last_name: diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f93976138..6b0ef7223 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -14,7 +14,7 @@ from registrar.models.contact import Contact from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper -from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView +from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView, ContactPermissionView from .utility import ( DomainRequestPermissionView, @@ -819,3 +819,10 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates + + +class FinishContactProfileSetupView(ContactPermissionView): + """This view forces the user into providing additional details that + we may have missed from Login.gov""" + template_name = "domain_request_your_contact.html" + forms = [forms.YourContactForm] \ No newline at end of file diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index c7083ce48..4fdba113d 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -8,6 +8,7 @@ from registrar.models import ( DomainInvitation, DomainInformation, UserDomainRole, + Contact, ) import logging @@ -324,6 +325,38 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): return True +class ContactPermission(PermissionsLoginMixin): + """Permission mixin for UserDomainRole if user + has access, otherwise 403""" + + def has_permission(self): + """Check if this user has access to this domain request. + + The user is in self.request.user and the domain needs to be looked + up from the domain's primary key in self.kwargs["pk"] + """ + + # Check if the user is authenticated + if not self.request.user.is_authenticated: + return False + + user_pk = self.kwargs["pk"] + + # Check if the user has an associated contact + associated_contacts = Contact.objects.filter(user=user_pk) + associated_contacts_length = len(associated_contacts) + + if associated_contacts_length == 0: + # This means that the user trying to access this page + # is a different user than the contact holder. + return False + elif associated_contacts_length > 1: + # TODO - change this + raise ValueError("User has multiple connected contacts") + else: + return True + + class DomainRequestPermissionWithdraw(PermissionsLoginMixin): """Permission mixin that redirects to withdraw action on domain request if user has access, otherwise 403""" diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index f2752c3b5..c626367fe 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -3,8 +3,7 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView -from registrar.models import Domain, DomainRequest, DomainInvitation -from registrar.models.user_domain_role import UserDomainRole +from registrar.models import Domain, DomainRequest, DomainInvitation, UserDomainRole, Contact from .mixins import ( DomainPermission, @@ -13,6 +12,7 @@ from .mixins import ( DomainInvitationPermission, DomainRequestWizardPermission, UserDeleteDomainRolePermission, + ContactPermission, ) import logging @@ -142,3 +142,22 @@ class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteV # variable name in template context for the model object context_object_name = "userdomainrole" + + +class ContactPermissionView(ContactPermission, DetailView, abc.ABC): + """Abstract base view for domain requests that enforces permissions + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = Contact + # variable name in template context for the model object + context_object_name = "Contact" + + # Abstract property enforces NotImplementedError on an attribute. + @property + @abc.abstractmethod + def template_name(self): + raise NotImplementedError \ No newline at end of file From dda620ee4d648777652890f5b2cb28abc185de05 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:09:23 -0600 Subject: [PATCH 009/171] Add finished setup flag --- src/djangooidc/backends.py | 12 +++++------- src/djangooidc/tests/test_backends.py | 10 +++++----- src/djangooidc/views.py | 3 ++- src/registrar/admin.py | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 8bdd44698..2de6adc3e 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -21,13 +21,11 @@ class OpenIdConnectBackend(ModelBackend): """ def authenticate(self, request, **kwargs): - """Returns a tuple of (User, is_new_user)""" logger.debug("kwargs %s" % kwargs) user = None - is_new_user = True - + request.session["is_new_user"] = True if not kwargs or "sub" not in kwargs.keys(): - return user, is_new_user + return user UserModel = get_user_model() username = self.clean_username(kwargs["sub"]) @@ -51,7 +49,7 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) - is_new_user = created + request.session["is_new_user"] = created if not created: # If user exists, update existing user @@ -63,10 +61,10 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - return None, is_new_user + return None # run this callback for a each login user.on_each_login() - return user, is_new_user + return user def update_existing_user(self, user, kwargs): """ diff --git a/src/djangooidc/tests/test_backends.py b/src/djangooidc/tests/test_backends.py index 7b7b963ea..c15106fa9 100644 --- a/src/djangooidc/tests/test_backends.py +++ b/src/djangooidc/tests/test_backends.py @@ -21,7 +21,7 @@ class OpenIdConnectBackendTestCase(TestCase): """Test that authenticate creates a new user if it does not find existing user""" # Ensure that the authenticate method creates a new user - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user.username, "test_user") @@ -39,7 +39,7 @@ class OpenIdConnectBackendTestCase(TestCase): existing_user = User.objects.create_user(username="test_user") # Ensure that the authenticate method updates the existing user - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -68,7 +68,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -89,7 +89,7 @@ class OpenIdConnectBackendTestCase(TestCase): # Ensure that the authenticate method updates the existing user # and preserves existing first and last names - user, _ = self.backend.authenticate(request=None, **self.kwargs) + user = self.backend.authenticate(request=None, **self.kwargs) self.assertIsNotNone(user) self.assertIsInstance(user, User) self.assertEqual(user, existing_user) # The same user instance should be returned @@ -103,5 +103,5 @@ class OpenIdConnectBackendTestCase(TestCase): def test_authenticate_with_unknown_user(self): """Test that authenticate returns None when no kwargs are supplied""" # Ensure that the authenticate method handles the case when the user is not found - user, _ = self.backend.authenticate(request=None, **{}) + user = self.backend.authenticate(request=None, **{}) self.assertIsNone(user) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index c7a8f1bba..4b111f130 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -98,7 +98,8 @@ def login_callback(request): # add acr_value to request.session request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) - user, is_new_user = authenticate(request=request, **userinfo) + user = authenticate(request=request, **userinfo) + is_new_user = request.session["is_new_user"] if user: should_update_user = False # Fixture users kind of exist in a superposition of verification types, diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 3eea86871..a81e5e414 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -539,7 +539,7 @@ class MyUserAdmin(BaseUserAdmin): fieldsets = ( ( None, - {"fields": ("username", "password", "status", "verification_type")}, + {"fields": ("username", "password", "status", "finished_setup", "verification_type")}, ), ("Personal Info", {"fields": ("first_name", "last_name", "email")}), ( @@ -557,7 +557,7 @@ class MyUserAdmin(BaseUserAdmin): ("Important dates", {"fields": ("last_login", "date_joined")}), ) - readonly_fields = ("verification_type",) + readonly_fields = ("verification_type", "finished_setup") # Hide Username (uuid), Groups and Permissions # Q: Now that we're using Groups and Permissions, From 2f36033eb27f9bd2383f77d33735e8e2bb8663d1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:13:01 -0600 Subject: [PATCH 010/171] User setup stuff --- src/djangooidc/backends.py | 1 + src/djangooidc/views.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 2de6adc3e..3f5c1022e 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -60,6 +60,7 @@ class OpenIdConnectBackend(ModelBackend): else: try: user = UserModel.objects.get_by_natural_key(username) + request.session["is_new_user"] = False except UserModel.DoesNotExist: return None # run this callback for a each login diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 4b111f130..0eaf28f01 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -114,7 +114,9 @@ def login_callback(request): user.set_user_verification_type() should_update_user = True - if is_new_user: + # If we're dealing with a new user and if this field isn't set already, + # Then set this to False. Otherwise, if we set the field manually it'll revert. + if is_new_user and not user.finished_setup: user.finished_setup = False should_update_user = True From 84408fce4888a9d6dec516cf7c2b97598f3351d0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 12:45:21 -0600 Subject: [PATCH 011/171] Add perms checks --- src/djangooidc/views.py | 4 +++- src/registrar/config/urls.py | 7 +++++++ .../templates/finish_contact_setup.html | 7 +++++++ src/registrar/views/domain_request.py | 4 ++-- src/registrar/views/utility/mixins.py | 17 +++++++++++++++-- 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/registrar/templates/finish_contact_setup.html diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 0eaf28f01..c58c3a0aa 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -129,7 +129,9 @@ def login_callback(request): # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - return redirect(request.session.get("next", "/")) + + success_redirect_url = "/" if user.finished_setup else f"/finish-user-setup/{user.id}" + return redirect(request.session.get("next", success_redirect_url)) else: raise o_e.BannedUser() except o_e.StateMismatch as nsd_err: diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 720034150..2c6942ca8 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -100,6 +100,13 @@ urlpatterns = [ name="analytics", ), path("admin/", admin.site.urls), + path( + # We embed the current user ID here, but we have a permission check + # that ensures the user is who they say they are. + "finish-user-setup/", + views.FinishContactProfileSetupView.as_view(), + name="finish-contact-profile-setup", + ), path( "domain-request//edit/", views.DomainRequestWizard.as_view(), diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html new file mode 100644 index 000000000..930eb4a23 --- /dev/null +++ b/src/registrar/templates/finish_contact_setup.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} +{% load static url_helpers %} +{% block title %} Finish setting up your profile {% endblock %} + +{% block content %} +

TEST

+{% endblock content %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 6b0ef7223..ee97dddf9 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -824,5 +824,5 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): class FinishContactProfileSetupView(ContactPermissionView): """This view forces the user into providing additional details that we may have missed from Login.gov""" - template_name = "domain_request_your_contact.html" - forms = [forms.YourContactForm] \ No newline at end of file + template_name = "finish_contact_setup.html" + forms = [forms.YourContactForm] diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 4fdba113d..45c7c7860 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -9,6 +9,7 @@ from registrar.models import ( DomainInformation, UserDomainRole, Contact, + User, ) import logging @@ -340,10 +341,22 @@ class ContactPermission(PermissionsLoginMixin): if not self.request.user.is_authenticated: return False - user_pk = self.kwargs["pk"] + + given_user_pk = self.kwargs["pk"] + + # Grab the user in the DB to do a full object comparision, not just on ids + current_user = self.request.user + + # Check for the ids existence since we're dealing with requests + requested_user_exists = User.objects.filter(pk=given_user_pk).exists() + + # Compare the PK that was passed in to the user currently logged in + if current_user.pk != given_user_pk and requested_user_exists: + # Don't allow users to modify other users profiles + return False # Check if the user has an associated contact - associated_contacts = Contact.objects.filter(user=user_pk) + associated_contacts = Contact.objects.filter(user=current_user) associated_contacts_length = len(associated_contacts) if associated_contacts_length == 0: From 4c92011279a03dd2e6b9b5aa5fbbed4da0297a75 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 13:04:54 -0600 Subject: [PATCH 012/171] Add some middleware --- src/djangooidc/views.py | 3 +- src/registrar/config/settings.py | 3 +- src/registrar/no_cache_middleware.py | 17 ---------- src/registrar/registrar_middleware.py | 46 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 20 deletions(-) delete mode 100644 src/registrar/no_cache_middleware.py create mode 100644 src/registrar/registrar_middleware.py diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index c58c3a0aa..3716ebf19 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -130,8 +130,7 @@ def login_callback(request): # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - success_redirect_url = "/" if user.finished_setup else f"/finish-user-setup/{user.id}" - return redirect(request.session.get("next", success_redirect_url)) + return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() except o_e.StateMismatch as nsd_err: diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index bbf06b825..d0849e222 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -160,7 +160,7 @@ MIDDLEWARE = [ # django-cors-headers: listen to cors responses "corsheaders.middleware.CorsMiddleware", # custom middleware to stop caching from CloudFront - "registrar.no_cache_middleware.NoCacheMiddleware", + "registrar.registrar_middleware.NoCacheMiddleware", # serve static assets in production "whitenoise.middleware.WhiteNoiseMiddleware", # provide security enhancements to the request/response cycle @@ -186,6 +186,7 @@ MIDDLEWARE = [ "auditlog.middleware.AuditlogMiddleware", # Used for waffle feature flags "waffle.middleware.WaffleMiddleware", + "registrar.registrar_middleware.CheckUserProfileMiddleware", ] # application object used by Django’s built-in servers (e.g. `runserver`) diff --git a/src/registrar/no_cache_middleware.py b/src/registrar/no_cache_middleware.py deleted file mode 100644 index 5edfca20e..000000000 --- a/src/registrar/no_cache_middleware.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Middleware to add Cache-control: no-cache to every response. - -Used to force Cloudfront caching to leave us alone while we develop -better caching responses. -""" - - -class NoCacheMiddleware: - """Middleware to add a single header to every response.""" - - def __init__(self, get_response): - self.get_response = get_response - - def __call__(self, request): - response = self.get_response(request) - response["Cache-Control"] = "no-cache" - return response diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py new file mode 100644 index 000000000..0054f9158 --- /dev/null +++ b/src/registrar/registrar_middleware.py @@ -0,0 +1,46 @@ +""" +Contains middleware used in settings.py +""" + +from django.urls import reverse +from django.http import HttpResponseRedirect + +class CheckUserProfileMiddleware: + """ + Checks if the current user has finished_setup = False. + If they do, redirect them to the setup page regardless of where they are in + the application. + """ + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + """Code that gets executed on each request before the view is called""" + response = self.get_response(request) + return response + + def process_view(self, request, view_func, view_args, view_kwargs): + # Check if the user is authenticated and if the setup is not finished + if request.user.is_authenticated and not request.user.finished_setup: + # Redirect to the setup page + return HttpResponseRedirect(reverse('finish-contact-profile-setup')) + + # Continue processing the view + return None + + +class NoCacheMiddleware: + """ + Middleware to add Cache-control: no-cache to every response. + + Used to force Cloudfront caching to leave us alone while we develop + better caching responses. + """ + + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + response["Cache-Control"] = "no-cache" + return response \ No newline at end of file From 8b41e70840bd6de3def2fd0c91cd62c1f8a0e315 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 13:43:32 -0600 Subject: [PATCH 013/171] Fine tuning --- src/djangooidc/backends.py | 8 +++++--- src/djangooidc/views.py | 6 ++---- src/registrar/registrar_middleware.py | 18 ++++++++++++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 3f5c1022e..96b7a902a 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -23,7 +23,7 @@ class OpenIdConnectBackend(ModelBackend): def authenticate(self, request, **kwargs): logger.debug("kwargs %s" % kwargs) user = None - request.session["is_new_user"] = True + if not kwargs or "sub" not in kwargs.keys(): return user @@ -49,7 +49,9 @@ class OpenIdConnectBackend(ModelBackend): } user, created = UserModel.objects.get_or_create(**args) - request.session["is_new_user"] = created + + if created: + request.session["is_new_user"] = True if not created: # If user exists, update existing user @@ -60,8 +62,8 @@ class OpenIdConnectBackend(ModelBackend): else: try: user = UserModel.objects.get_by_natural_key(username) - request.session["is_new_user"] = False except UserModel.DoesNotExist: + request.session["is_new_user"] = True return None # run this callback for a each login user.on_each_login() diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 3716ebf19..7b5c58527 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -99,7 +99,7 @@ def login_callback(request): request.session["acr_value"] = CLIENT.get_step_up_acr_value() return CLIENT.create_authn_request(request.session) user = authenticate(request=request, **userinfo) - is_new_user = request.session["is_new_user"] + is_new_user = request.session.get("is_new_user", False) if user: should_update_user = False # Fixture users kind of exist in a superposition of verification types, @@ -114,9 +114,7 @@ def login_callback(request): user.set_user_verification_type() should_update_user = True - # If we're dealing with a new user and if this field isn't set already, - # Then set this to False. Otherwise, if we set the field manually it'll revert. - if is_new_user and not user.finished_setup: + if is_new_user: user.finished_setup = False should_update_user = True diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 0054f9158..064757d80 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -20,10 +20,20 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): - # Check if the user is authenticated and if the setup is not finished - if request.user.is_authenticated and not request.user.finished_setup: - # Redirect to the setup page - return HttpResponseRedirect(reverse('finish-contact-profile-setup')) + # Check if setup is not finished + finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup + if request.user.is_authenticated and not finished_setup: + setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.pk}) + logout_page = reverse("logout") + excluded_pages = [ + setup_page, + logout_page, + ] + + # Don't redirect on excluded pages (such as the setup page itself) + if not any(request.path.startswith(page) for page in excluded_pages): + # Redirect to the setup page + return HttpResponseRedirect(setup_page) # Continue processing the view return None From 75499337e056bc1e2cf4998e2d45ee195328bfa8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 9 May 2024 15:56:18 -0600 Subject: [PATCH 014/171] Initial architecture --- src/registrar/config/urls.py | 2 +- src/registrar/fixtures_users.py | 10 +- src/registrar/forms/contact.py | 42 +++++++ src/registrar/forms/domain.py | 2 +- src/registrar/forms/domain_request_wizard.py | 37 +----- .../forms/utility/wizard_form_helper.py | 7 +- src/registrar/models/contact.py | 2 - .../templates/finish_contact_setup.html | 69 ++++++++++- src/registrar/views/__init__.py | 3 + src/registrar/views/contact.py | 107 ++++++++++++++++++ src/registrar/views/domain_request.py | 8 +- .../views/utility/permission_views.py | 10 +- 12 files changed, 238 insertions(+), 61 deletions(-) create mode 100644 src/registrar/forms/contact.py create mode 100644 src/registrar/views/contact.py diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 2c6942ca8..596e5c3d2 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -104,7 +104,7 @@ urlpatterns = [ # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. "finish-user-setup/", - views.FinishContactProfileSetupView.as_view(), + views.ContactProfileSetupView.as_view(), name="finish-contact-profile-setup", ), path( diff --git a/src/registrar/fixtures_users.py b/src/registrar/fixtures_users.py index c31acacfd..d87438bc9 100644 --- a/src/registrar/fixtures_users.py +++ b/src/registrar/fixtures_users.py @@ -126,11 +126,11 @@ class UserFixture: "last_name": "Osos-Analyst", "email": "kosos@truss.works", }, - { - "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", - "first_name": "Zander-Analyst", - "last_name": "Adkinson-Analyst", - }, + # { + # "username": "2cc0cde8-8313-4a50-99d8-5882e71443e8", + # "first_name": "Zander-Analyst", + # "last_name": "Adkinson-Analyst", + # }, { "username": "57ab5847-7789-49fe-a2f9-21d38076d699", "first_name": "Paul-Analyst", diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py new file mode 100644 index 000000000..ae8d28dc8 --- /dev/null +++ b/src/registrar/forms/contact.py @@ -0,0 +1,42 @@ +from django import forms +from phonenumber_field.modelfields import PhoneNumberField # type: ignore +from django.core.validators import MaxLengthValidator + + +class ContactForm(forms.Form): + """Form for adding or editing a contact""" + + first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter your first name / given name."}, + ) + middle_name = forms.CharField( + required=False, + label="Middle name (optional)", + ) + last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter your last name / family name."}, + ) + title = forms.CharField( + label="Title or role in your organization", + error_messages={ + "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") + }, + ) + email = forms.EmailField( + label="Email", + max_length=None, + error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + ) + phone = PhoneNumberField( + label="Phone", + error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, + ) + diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index da1462bdb..9dfd9773a 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -202,7 +202,7 @@ NameserverFormset = formset_factory( validate_max=True, ) - +# TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 9d16a30de..32c59620d 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -16,6 +16,7 @@ from registrar.forms.utility.wizard_form_helper import ( from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType +from registrar.forms import ContactForm logger = logging.getLogger(__name__) @@ -385,7 +386,7 @@ class PurposeForm(RegistrarForm): ) -class YourContactForm(RegistrarForm): +class YourContactForm(RegistrarForm, ContactForm): JOIN = "submitter" def to_database(self, obj): @@ -408,40 +409,6 @@ class YourContactForm(RegistrarForm): contact = getattr(obj, "submitter", None) return super().from_database(contact) - first_name = forms.CharField( - label="First name / given name", - error_messages={"required": "Enter your first name / given name."}, - ) - middle_name = forms.CharField( - required=False, - label="Middle name (optional)", - ) - last_name = forms.CharField( - label="Last name / family name", - error_messages={"required": "Enter your last name / family name."}, - ) - title = forms.CharField( - label="Title or role in your organization", - error_messages={ - "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") - }, - ) - email = forms.EmailField( - label="Email", - max_length=None, - error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, - validators=[ - MaxLengthValidator( - 320, - message="Response must be less than 320 characters.", - ) - ], - ) - phone = PhoneNumberField( - label="Phone", - error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, - ) - class OtherContactsYesNoForm(BaseYesNoForm): """The yes/no field for the OtherContacts form.""" diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2ae50f908..350605c1a 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -21,7 +21,12 @@ class RegistrarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object - self.domain_request = kwargs.pop("domain_request", None) + if "domain_request" in kwargs: + self.domain_request = kwargs.pop("domain_request", None) + + if "contact" in kwargs: + self.contact = kwargs.pop("contact", None) + super(RegistrarForm, self).__init__(*args, **kwargs) def to_database(self, obj: DomainRequest | Contact): diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 3ebd8bc3e..3fdfccb64 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -1,10 +1,8 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel - from phonenumber_field.modelfields import PhoneNumberField # type: ignore - class Contact(TimeStampedModel): """Contact information follows a similar pattern for each contact.""" diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 930eb4a23..c6f02f64b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -1,7 +1,72 @@ {% extends "base.html" %} -{% load static url_helpers %} +{% load static form_helpers url_helpers field_helpers %} {% block title %} Finish setting up your profile {% endblock %} {% block content %} -

TEST

+
+
+
+
+ {% include "includes/form_messages.html" %} + {% comment %} + Repurposed from domain_request_form.html + {% endcomment %} + {% for outer in forms %} + {% if outer|isformset %} + {% for inner in outer.forms %} + {% include "includes/form_errors.html" with form=inner %} + {% endfor %} + {% else %} + {% include "includes/form_errors.html" with form=outer %} + {% endif %} + {% endfor %} + +

Finish setting up your profile

+ +

+ We require that you maintain accurate contact information. + The details you provide will only be used to support the administration of .gov and won’t be made public. +

+ +

What contact information should we use to reach you?

+

+ Review the details below and update any required information. + Note that editing this information won’t affect your Login.gov account information. +

+ {# TODO: maybe remove this? #} +

Required information is marked with an asterisk (*).

+
+ {% csrf_token %} +
+ + Your contact information + + + {% input_with_errors forms.0.first_name %} + + {% input_with_errors forms.0.middle_name %} + + {% input_with_errors forms.0.last_name %} + + {% input_with_errors forms.0.title %} + + {% input_with_errors forms.0.email %} + + {% with add_class="usa-input--medium" %} + {% input_with_errors forms.0.phone %} + {% endwith %} + +
+
+ +
+ {% block form_fields %}{% endblock %} +
+
+
+
+ {% endblock content %} + diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index bd15196d4..692cfd4de 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -14,5 +14,8 @@ from .domain import ( DomainInvitationDeleteView, DomainDeleteUserView, ) +from .contact import ( + ContactProfileSetupView, +) from .health import * from .index import * diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py new file mode 100644 index 000000000..9f0e4e393 --- /dev/null +++ b/src/registrar/views/contact.py @@ -0,0 +1,107 @@ +from registrar.forms.contact import ContactForm +from registrar.views.utility.permission_views import ContactPermissionView +from django.views.generic.edit import FormMixin + + +# TODO we can and probably should generalize this at this rate. +class BaseContactView(ContactPermissionView): + def get(self, request, *args, **kwargs): + self._set_contact(request) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + # TODO - this deserves a small refactor + def _set_contact(self, request): + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ + self.session = request.session + + contact_pk = "contact:" + str(self.kwargs.get("pk")) + cached_contact = self.session.get(contact_pk) + + if cached_contact: + self.object = cached_contact + else: + self.object = self.get_object() + self._update_session_with_contact() + + def _update_session_with_contact(self): + """ + Set contact pk in the session cache + """ + domain_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object + + +class ContactFormBaseView(BaseContactView, FormMixin): + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + # Set the current contact object in cache + self._set_contact(request) + + # Get the current form and validate it + form = self.get_form() + return self.check_form(form) + + # TODO rename? + def check_form(self, form): + return self.form_valid(form) if form.is_valid() else self.form_invalid(form) + + def form_valid(self, form): + # updates session cache with contact + self._update_session_with_contact() + + # superclass has the redirect + return super().form_valid(form) + + def form_invalid(self, form): + # updates session cache with contact + self._update_session_with_contact() + + # superclass has the redirect + return super().form_invalid(form) + + +class ContactProfileSetupView(ContactPermissionView): + """This view forces the user into providing additional details that + we may have missed from Login.gov""" + template_name = "finish_contact_setup.html" + form_class = ContactForm + + def get(self, request, *args, **kwargs): + self._get_contact(request) + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def _get_contact(self, request): + """ + get domain from session cache or from db and set + to self.object + set session to self for downstream functions to + update session cache + """ + self.session = request.session + + contact_pk = "contact:" + str(self.kwargs.get("pk")) + cached_contact = self.session.get(contact_pk) + + if cached_contact: + self.object = cached_contact + else: + self.object = self.get_object() + self._set_session_contact_pk() + + def _set_session_contact_pk(self): + """ + Set contact pk in the session cache + """ + domain_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[domain_pk] = self.object + diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index ee97dddf9..b07f0d53f 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -14,7 +14,7 @@ from registrar.models.contact import Contact from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper -from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView, ContactPermissionView +from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView from .utility import ( DomainRequestPermissionView, @@ -820,9 +820,3 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates - -class FinishContactProfileSetupView(ContactPermissionView): - """This view forces the user into providing additional details that - we may have missed from Login.gov""" - template_name = "finish_contact_setup.html" - forms = [forms.YourContactForm] diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index c626367fe..5587d2d56 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -153,11 +153,7 @@ class ContactPermissionView(ContactPermission, DetailView, abc.ABC): # DetailView property for what model this is viewing model = Contact - # variable name in template context for the model object - context_object_name = "Contact" + object: Contact - # Abstract property enforces NotImplementedError on an attribute. - @property - @abc.abstractmethod - def template_name(self): - raise NotImplementedError \ No newline at end of file + # variable name in template context for the model object + context_object_name = "contact" From dd9df90fb4f9c377532bd0c9b614812b2fdbfd5b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 08:33:10 -0600 Subject: [PATCH 015/171] Infra --- src/registrar/forms/contact.py | 6 ++- src/registrar/forms/domain_request_wizard.py | 37 ++++++++++++++++++- .../forms/utility/wizard_form_helper.py | 3 -- .../templates/finish_contact_setup.html | 16 ++++---- src/registrar/views/contact.py | 6 +++ 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index ae8d28dc8..1ddc1a2a0 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -1,11 +1,15 @@ from django import forms -from phonenumber_field.modelfields import PhoneNumberField # type: ignore +from phonenumber_field.formfields import PhoneNumberField # type: ignore from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): """Form for adding or editing a contact""" + def __init__(self, *args, **kwargs): + kwargs.setdefault("label_suffix", "") + super(ContactForm, self).__init__(*args, **kwargs) + first_name = forms.CharField( label="First name / given name", error_messages={"required": "Enter your first name / given name."}, diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 32c59620d..9d16a30de 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -16,7 +16,6 @@ from registrar.forms.utility.wizard_form_helper import ( from registrar.models import Contact, DomainRequest, DraftDomain, Domain, FederalAgency from registrar.templatetags.url_helpers import public_site_url from registrar.utility.enums import ValidationReturnType -from registrar.forms import ContactForm logger = logging.getLogger(__name__) @@ -386,7 +385,7 @@ class PurposeForm(RegistrarForm): ) -class YourContactForm(RegistrarForm, ContactForm): +class YourContactForm(RegistrarForm): JOIN = "submitter" def to_database(self, obj): @@ -409,6 +408,40 @@ class YourContactForm(RegistrarForm, ContactForm): contact = getattr(obj, "submitter", None) return super().from_database(contact) + first_name = forms.CharField( + label="First name / given name", + error_messages={"required": "Enter your first name / given name."}, + ) + middle_name = forms.CharField( + required=False, + label="Middle name (optional)", + ) + last_name = forms.CharField( + label="Last name / family name", + error_messages={"required": "Enter your last name / family name."}, + ) + title = forms.CharField( + label="Title or role in your organization", + error_messages={ + "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") + }, + ) + email = forms.EmailField( + label="Email", + max_length=None, + error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, + validators=[ + MaxLengthValidator( + 320, + message="Response must be less than 320 characters.", + ) + ], + ) + phone = PhoneNumberField( + label="Phone", + error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, + ) + class OtherContactsYesNoForm(BaseYesNoForm): """The yes/no field for the OtherContacts form.""" diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 350605c1a..9b8a7c4d8 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -23,9 +23,6 @@ class RegistrarForm(forms.Form): # save a reference to a domain request object if "domain_request" in kwargs: self.domain_request = kwargs.pop("domain_request", None) - - if "contact" in kwargs: - self.contact = kwargs.pop("contact", None) super(RegistrarForm, self).__init__(*args, **kwargs) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index c6f02f64b..1849889c8 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -41,20 +41,22 @@ Your contact information - - {% input_with_errors forms.0.first_name %} + {{form.first_name}} + {% comment %} + {% input_with_errors form.first_name %} - {% input_with_errors forms.0.middle_name %} + {% input_with_errors form.middle_name %} - {% input_with_errors forms.0.last_name %} + {% input_with_errors form.last_name %} - {% input_with_errors forms.0.title %} + {% input_with_errors form.title %} - {% input_with_errors forms.0.email %} + {% input_with_errors form.email %} {% with add_class="usa-input--medium" %} - {% input_with_errors forms.0.phone %} + {% input_with_errors form.phone %} {% endwith %} + {% endcomment %}
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 9f0e4e393..110ee254f 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -75,6 +75,12 @@ class ContactProfileSetupView(ContactPermissionView): template_name = "finish_contact_setup.html" form_class = ContactForm + def get_form_kwargs(self, *args, **kwargs): + """Add domain_info.organization_name instance to make a bound form.""" + form_kwargs = super().get_form_kwargs(*args, **kwargs) + form_kwargs["instance"] = self.object + return form_kwargs + def get(self, request, *args, **kwargs): self._get_contact(request) context = self.get_context_data(object=self.object) From a55f3391682b1e79d663b23d2a6dcb34e337cf91 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 09:54:38 -0600 Subject: [PATCH 016/171] Hook form to db --- src/registrar/forms/contact.py | 21 ++++++- src/registrar/registrar_middleware.py | 2 +- .../templates/finish_contact_setup.html | 4 +- src/registrar/views/contact.py | 61 +++++++------------ 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 1ddc1a2a0..d699087c9 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -6,9 +6,24 @@ from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): """Form for adding or editing a contact""" - def __init__(self, *args, **kwargs): - kwargs.setdefault("label_suffix", "") - super(ContactForm, self).__init__(*args, **kwargs) + def to_database(self, obj): + """ + Adds this form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + """ + if not self.is_valid(): + return + for name, value in self.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + @classmethod + def from_database(cls, obj): + """Returns a dict of form field values gotten from `obj`.""" + if obj is None: + return {} + return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore first_name = forms.CharField( label="First name / given name", diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 064757d80..783951279 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -23,7 +23,7 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.pk}) + setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 1849889c8..1d04c6a8f 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -42,7 +42,7 @@ Your contact information {{form.first_name}} - {% comment %} + {% input_with_errors form.first_name %} {% input_with_errors form.middle_name %} @@ -56,7 +56,7 @@ {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} - {% endcomment %} +
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 110ee254f..c640c9150 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,10 +1,13 @@ +from django.urls import reverse from registrar.forms.contact import ContactForm +from registrar.models.contact import Contact from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): + def get(self, request, *args, **kwargs): self._set_contact(request) context = self.get_context_data(object=self.object) @@ -38,6 +41,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): + def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -54,13 +58,6 @@ class ContactFormBaseView(BaseContactView, FormMixin): def check_form(self, form): return self.form_valid(form) if form.is_valid() else self.form_invalid(form) - def form_valid(self, form): - # updates session cache with contact - self._update_session_with_contact() - - # superclass has the redirect - return super().form_valid(form) - def form_invalid(self, form): # updates session cache with contact self._update_session_with_contact() @@ -69,45 +66,33 @@ class ContactFormBaseView(BaseContactView, FormMixin): return super().form_invalid(form) -class ContactProfileSetupView(ContactPermissionView): +class ContactProfileSetupView(ContactFormBaseView): """This view forces the user into providing additional details that we may have missed from Login.gov""" template_name = "finish_contact_setup.html" form_class = ContactForm + model = Contact - def get_form_kwargs(self, *args, **kwargs): - """Add domain_info.organization_name instance to make a bound form.""" - form_kwargs = super().get_form_kwargs(*args, **kwargs) - form_kwargs["instance"] = self.object - return form_kwargs + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + # TODO - some logic should exist that navigates them to the domain request page if + # they clicked it on get.gov - def get(self, request, *args, **kwargs): - self._get_contact(request) - context = self.get_context_data(object=self.object) - return self.render_to_response(context) + # The user has finished their setup - def _get_contact(self, request): - """ - get domain from session cache or from db and set - to self.object - set session to self for downstream functions to - update session cache - """ - self.session = request.session - contact_pk = "contact:" + str(self.kwargs.get("pk")) - cached_contact = self.session.get(contact_pk) + # Add a notification that the update was successful + return reverse("home") - if cached_contact: - self.object = cached_contact - else: - self.object = self.get_object() - self._set_session_contact_pk() + def form_valid(self, form): + self.request.user.finished_setup = True + self.request.user.save() - def _set_session_contact_pk(self): - """ - Set contact pk in the session cache - """ - domain_pk = "contact:" + str(self.kwargs.get("pk")) - self.session[domain_pk] = self.object + form.to_database(self.object) + self._update_session_with_contact() + return super().form_valid(form) + + def get_initial(self): + """The initial value for the form (which is a formset here).""" + return self.form_class.from_database(self.object) From ebf13280444cfdd61da45835b1c9ee48fd925722 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 11:26:42 -0600 Subject: [PATCH 017/171] Template logic --- src/registrar/assets/sass/_theme/_base.scss | 17 ++++++++ src/registrar/forms/contact.py | 10 +---- .../templates/finish_contact_setup.html | 40 ++++++++++++------- .../templates/includes/input_with_errors.html | 36 +++++++++++++++-- src/registrar/templatetags/field_helpers.py | 9 +++++ 5 files changed, 85 insertions(+), 27 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 212df992f..fc132ebaa 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -140,3 +140,20 @@ abbr[title] { .cursor-pointer { cursor: pointer; } + +.input-with-edit-button { + svg.usa-icon { + width: 1.5em !important; + height: 1.5em !important; + // TODO CHANGE + color: green; + position: absolute; + } + &.input-with-edit-button__error { + // TODO CHANGE + svg.usa-icon { + color: red; + } + } + +} diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index d699087c9..e2a47f56c 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -44,15 +44,9 @@ class ContactForm(forms.Form): }, ) email = forms.EmailField( - label="Email", + label="Organization email", max_length=None, - error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")}, - validators=[ - MaxLengthValidator( - 320, - message="Response must be less than 320 characters.", - ) - ], + required=False, ) phone = PhoneNumberField( label="Phone", diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 1d04c6a8f..e825dc90b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -41,23 +41,33 @@ Your contact information - {{form.first_name}} - - {% input_with_errors form.first_name %} - - {% input_with_errors form.middle_name %} - - {% input_with_errors form.last_name %} - - {% input_with_errors form.title %} - - {% input_with_errors form.email %} - - {% with add_class="usa-input--medium" %} - {% input_with_errors form.phone %} + + {% with show_edit_button=True %} + {% input_with_errors form.first_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.middle_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.last_name %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.email %} + {% endwith %} + + {% with show_edit_button=True %} + {% input_with_errors form.title %} + {% endwith %} + + {% with show_edit_button=True %} + {% with add_class="usa-input--medium" %} + {% input_with_errors form.phone %} + {% endwith %} {% endwith %} -
+
+
+ {% else %} + {% include "django/forms/label.html" %} + {% endif %} {% endif %} {% if sublabel_text %} @@ -58,9 +69,26 @@ error messages, if necessary. {% if append_gov %}
{% endif %} - {# this is the input field, itself #} - {% include widget.template_name %} + {% if show_edit_button %} +
+ +
+ {{ field.value }} +
+
+ {# this is the input field, itself #} + {% include widget.template_name %} + {% else %} + {# this is the input field, itself #} + {% include widget.template_name %} + {% endif %} {% if append_gov %} .gov
diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index 811897908..a7aa9d663 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -26,6 +26,7 @@ def input_with_errors(context, field=None): # noqa: C901 add_group_class: append to input element's surrounding tag's `class` attribute attr_* - adds or replaces any single html attribute for the input add_error_attr_* - like `attr_*` but only if field.errors is not empty + show_edit_button: shows a simple edit button, and adds display-none to the input field. Example usage: ``` @@ -91,6 +92,14 @@ def input_with_errors(context, field=None): # noqa: C901 elif key == "add_group_class": group_classes.append(value) + elif key == "show_edit_button": + # Hide the primary input field. + # Used such that we can toggle it with JS + if "display-none" not in classes and isinstance(value, bool) and value: + classes.append("display-none") + # Set this as a context value so we know what we're going to display + context["show_edit_button"] = value + attrs["id"] = field.auto_id # do some work for various edge cases From 3a47a7a61c71a01ebc656789ce106b7aafef4ba0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 10 May 2024 13:08:57 -0600 Subject: [PATCH 018/171] Refinement --- src/registrar/assets/js/get-gov.js | 32 +++++++++++++++++++ src/registrar/assets/sass/_theme/_base.scss | 12 ++++++- src/registrar/assets/sass/_theme/_forms.scss | 17 ++++++++++ src/registrar/forms/contact.py | 5 +++ src/registrar/models/contact.py | 12 +++++++ .../templates/finish_contact_setup.html | 18 +++++++---- .../templates/includes/input_with_errors.html | 11 +++++-- src/registrar/views/contact.py | 10 +++--- 8 files changed, 101 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e7260ee21..6aaca61a9 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -834,3 +834,35 @@ function hideDeletedForms() { (function cisaRepresentativesFormListener() { HookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null) })(); + + + +/** + * An IIFE that hooks up the edit buttons on the finish-user-setup page + */ +(function finishUserSetupListener() { + function showInputFieldHideReadonlyField(inputField, readonlyField, editButton) { + readonlyField.classList.add('display-none'); + inputField.classList.remove('display-none'); + editButton.classList.add('display-none'); + } + + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + let fieldIdParts = button.id.split("__") + + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldId = fieldIdParts[0] + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true + + inputField = document.querySelector(`#id_${fieldId}`) + readonlyField = document.querySelector(`#${fieldId}__edit-button-readonly`) + showInputFieldHideReadonlyField(inputField, readonlyField, button) + + // Unlock after it completes + button.disabled = false + }); + } + }); +})(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index fc132ebaa..7677c8ffe 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -141,6 +141,7 @@ abbr[title] { cursor: pointer; } +// todo this class should ideally be renamed .input-with-edit-button { svg.usa-icon { width: 1.5em !important; @@ -154,6 +155,15 @@ abbr[title] { svg.usa-icon { color: red; } + div.readonly-field { + color: red + } + } +} + +.input-with-edit-button--button { + svg { + width: 1.25em !important; + height: 1.25em !important; } - } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 058a9f6c8..2766f596c 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -26,6 +26,23 @@ } } +.usa-form--edit-button-form { + margin-top: 0.5em; + // todo update + border-top: 2px black solid; + label.usa-label { + font-weight: bold; + } +} + +.usa-form--edit-button-form:first-of-type { + border-top: None +} + +.usa-form--edit-button-form > .usa-form-group:first-of-type { + margin-top: unset; +} + .usa-form-group--unstyled-error { margin-left: 0; padding-left: 0; diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index e2a47f56c..9d7d6a641 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -25,6 +25,11 @@ class ContactForm(forms.Form): return {} return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore + + full_name = forms.CharField( + label="Full name", + error_messages={"required": "Enter your full name"}, + ) first_name = forms.CharField( label="First name / given name", error_messages={"required": "Enter your first name / given name."}, diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 3fdfccb64..69d28df52 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -126,3 +126,15 @@ class Contact(TimeStampedModel): return str(self.pk) else: return "" + + @property + def full_name(self, separator=" "): + """ + Returns the full name (first_name, middle_name, last_name) of this contact. + Seperator (which defaults to a blank space) determines the seperator for each of those fields. + For instance, with seperator=", " - this function would return this: + "First, Middle, Last" + """ + # Filter out empty strings to avoid extra spaces or separators + parts = [self.first_name or "", self.middle_name or "", self.last_name or ""] + return separator.join(parts) \ No newline at end of file diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index e825dc90b..4892a64b7 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -42,27 +42,33 @@ Your contact information - {% with show_edit_button=True %} + {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} + {# Also todo: consolidate all of the scattered classes into this usa form one #} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% input_with_errors form.full_name %} + {% endwith %} + + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} {% input_with_errors form.first_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} {% input_with_errors form.middle_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none"%} {% input_with_errors form.last_name %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% input_with_errors form.email %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True %} + {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 9303fb61d..d1dc38bc3 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -32,8 +32,13 @@ error messages, if necessary.
{% include "django/forms/label.html" %}
-
- +
+
{% else %} @@ -71,7 +76,7 @@ error messages, if necessary. {% endif %} {% if show_edit_button %} -
+

Finish setting up your profile

- We require that you maintain accurate contact information. + {% public_site_url 'help/account-management/#get-help-with-login.gov' %} + We require + that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

@@ -44,31 +46,32 @@ {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} {# Also todo: consolidate all of the scattered classes into this usa form one #} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} {% input_with_errors form.first_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} {% input_with_errors form.middle_name %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form display-none"%} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none"%} {% input_with_errors form.last_name %} {% endwith %} - - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + + {# TODO: I shouldnt need to do add_class here #} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True group_classes="usa-form--edit-button-form" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index d1dc38bc3..43e90dca6 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -28,19 +28,7 @@ error messages, if necessary. {% if not field.widget_type == "checkbox" %} {% if show_edit_button %} -
-
- {% include "django/forms/label.html" %} -
-
- -
-
+ {% include "includes/label_with_edit_button.html" %} {% else %} {% include "django/forms/label.html" %} {% endif %} @@ -75,25 +63,13 @@ error messages, if necessary.
{% endif %} - {% if show_edit_button %} -
- -
- {{ field.value }} -
-
- {# this is the input field, itself #} - {% include widget.template_name %} - {% else %} - {# this is the input field, itself #} - {% include widget.template_name %} + {% if show_readonly %} + {% include "includes/readonly_input.html" %} {% endif %} + + {# this is the input field, itself #} + {% include widget.template_name %} + {% if append_gov %} .gov
diff --git a/src/registrar/templates/includes/label_with_edit_button.html b/src/registrar/templates/includes/label_with_edit_button.html new file mode 100644 index 000000000..bbff26400 --- /dev/null +++ b/src/registrar/templates/includes/label_with_edit_button.html @@ -0,0 +1,15 @@ + +{% load static field_helpers url_helpers %} +
+
+ {% include "django/forms/label.html" %} +
+
+ +
+
\ No newline at end of file diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html new file mode 100644 index 000000000..820d4b66c --- /dev/null +++ b/src/registrar/templates/includes/readonly_input.html @@ -0,0 +1,14 @@ +{% load static field_helpers url_helpers %} + +
+ +
+ {{ field.value }} +
+
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 3c269587b..f70f59a26 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -94,3 +94,11 @@ class ContactProfileSetupView(ContactFormBaseView): """The initial value for the form (which is a formset here).""" db_object = self.form_class.from_database(self.object) return db_object + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["email_sublabel_text"] = ( + "We recommend using your work email for your .gov account. " + "If the wrong email is displayed below, you’ll need to update your Login.gov account " + "and log back in. Get help with your Login.gov account.") + return context From 7ec39e7d13343b8e63383903307c5c8c814ae689 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 10:32:32 -0600 Subject: [PATCH 021/171] Match colors, and js functionality --- src/registrar/assets/js/get-gov.js | 75 +++++++++++++------ src/registrar/assets/sass/_theme/_base.scss | 9 +-- src/registrar/assets/sass/_theme/_forms.scss | 3 +- .../templates/finish_contact_setup.html | 2 +- .../templates/includes/input_with_errors.html | 2 +- .../includes/label_with_edit_button.html | 2 +- src/registrar/views/utility/mixins.py | 17 +++-- 7 files changed, 72 insertions(+), 38 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 18ab1ebce..99fd9b3a1 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -841,6 +841,8 @@ function hideDeletedForms() { * An IIFE that hooks up the edit buttons on the finish-user-setup page */ (function finishUserSetupListener() { + + // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { let inputId = getInputFieldId(fieldName) let inputField = document.querySelector(inputId) @@ -885,35 +887,60 @@ function hideDeletedForms() { } } }); - - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.remove(); - } } } - document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { - let fieldIdParts = button.id.split("__") + function handleEditButtonClick(fieldName, button){ + button.addEventListener('click', function() { + // Lock the edit button while this operation occurs + button.disabled = true - if (fieldIdParts && fieldIdParts.length > 0){ - let fieldName = fieldIdParts[0] - button.addEventListener('click', function() { - // Lock the edit button while this operation occurs - button.disabled = true + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + showInputFieldHideReadonlyField(fieldName, button); + } - if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } + button.classList.add('display-none'); + + // Unlock after it completes + button.disabled = false + }); + } + + function setupListener(){ + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + // Get the "{field_name}" and "edit-button" + let fieldIdParts = button.id.split("__") + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldName = fieldIdParts[0] + + // When the edit button is clicked, show the input field under it + handleEditButtonClick(fieldName, button); + } + }); + } + + function showInputOnErrorFields(){ + document.addEventListener('DOMContentLoaded', function() { + document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { + let fieldIdParts = button.id.split("__") + if (fieldIdParts && fieldIdParts.length > 0){ + let fieldName = fieldIdParts[0] - button.classList.add('display-none'); + let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); + if (errorMessage) { + button.click() + } + } + }); + }); + } - // Unlock after it completes - button.disabled = false - }); - } - }); + // Hookup all edit buttons to the `handleEditButtonClick` function + setupListener(); + + // Show the input fields if an error exists + showInputOnErrorFields(); })(); \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index bfdd99fc2..beb63cdd1 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -1,4 +1,5 @@ @use "uswds-core" as *; +@use "cisa_colors" as *; /* Styles for making visible to screen reader / AT users only. */ .sr-only { @@ -146,17 +147,15 @@ abbr[title] { svg.usa-icon { width: 1.5em !important; height: 1.5em !important; - // TODO CHANGE - color: green; + color: #{$dhs-green}; position: absolute; } &.input-with-edit-button__error { - // TODO CHANGE svg.usa-icon { - color: red; + color: #{$dhs-red}; } div.readonly-field { - color: red; + color: #{$dhs-red}; } } } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index bf1f81113..a4194273d 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -1,4 +1,5 @@ @use "uswds-core" as *; +@use "cisa_colors" as *; .usa-form .usa-button { margin-top: units(3); @@ -28,7 +29,7 @@ .usa-form-readonly { // todo update - border-top: 2px black solid; + border-top: 2px #{$dhs-dark-gray-15} solid; .bold-usa-label label.usa-label{ font-weight: bold; diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 39429d4eb..b9d1ca6f6 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -7,7 +7,7 @@
- {% include "includes/form_messages.html" %} + {% include "includes/form_errors.html" with form=form %} {% comment %} Repurposed from domain_request_form.html {% endcomment %} diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html index 43e90dca6..9348713b3 100644 --- a/src/registrar/templates/includes/input_with_errors.html +++ b/src/registrar/templates/includes/input_with_errors.html @@ -28,7 +28,7 @@ error messages, if necessary. {% if not field.widget_type == "checkbox" %} {% if show_edit_button %} - {% include "includes/label_with_edit_button.html" %} + {% include "includes/label_with_edit_button.html" with bold_label=True %} {% else %} {% include "django/forms/label.html" %} {% endif %} diff --git a/src/registrar/templates/includes/label_with_edit_button.html b/src/registrar/templates/includes/label_with_edit_button.html index bbff26400..ab0a04ee4 100644 --- a/src/registrar/templates/includes/label_with_edit_button.html +++ b/src/registrar/templates/includes/label_with_edit_button.html @@ -1,6 +1,6 @@ {% load static field_helpers url_helpers %} -
+
{% include "django/forms/label.html" %}
diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 45c7c7860..49d172971 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -342,18 +342,25 @@ class ContactPermission(PermissionsLoginMixin): return False - given_user_pk = self.kwargs["pk"] + given_contact_pk = self.kwargs["pk"] # Grab the user in the DB to do a full object comparision, not just on ids current_user = self.request.user - # Check for the ids existence since we're dealing with requests - requested_user_exists = User.objects.filter(pk=given_user_pk).exists() - # Compare the PK that was passed in to the user currently logged in - if current_user.pk != given_user_pk and requested_user_exists: + if current_user.contact.pk != given_contact_pk: # Don't allow users to modify other users profiles return False + + # Check if the object at the id we're searching on actually exists + requested_user_exists = User.objects.filter(pk=current_user.pk).exists() + requested_contact_exists = Contact.objects.filter( + user=current_user.pk, + pk=given_contact_pk + ).exists() + + if not requested_user_exists or not requested_contact_exists: + return False # Check if the user has an associated contact associated_contacts = Contact.objects.filter(user=current_user) From 4592b0c9e67ea6ba90bdf5071f7bf6d5e839afef Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 12:52:35 -0600 Subject: [PATCH 022/171] Redirect logic --- src/registrar/assets/js/get-gov.js | 17 +++-- src/registrar/forms/contact.py | 40 +++++------- src/registrar/models/contact.py | 19 +++--- .../models/utility/generic_helper.py | 20 ++++++ src/registrar/registrar_middleware.py | 4 +- .../templates/finish_contact_setup.html | 13 ++-- src/registrar/views/contact.py | 62 ++++++++++++++----- 7 files changed, 112 insertions(+), 63 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 99fd9b3a1..73afd8131 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -895,13 +895,7 @@ function hideDeletedForms() { // Lock the edit button while this operation occurs button.disabled = true - if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); - }else { - showInputFieldHideReadonlyField(fieldName, button); - } - + showInputFieldHideReadonlyField(fieldName, button); button.classList.add('display-none'); // Unlock after it completes @@ -928,10 +922,15 @@ function hideDeletedForms() { let fieldIdParts = button.id.split("__") if (fieldIdParts && fieldIdParts.length > 0){ let fieldName = fieldIdParts[0] - + let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { - button.click() + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + button.click() + } } } }); diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 51c296375..83b49f548 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -1,6 +1,5 @@ from django import forms from phonenumber_field.formfields import PhoneNumberField # type: ignore -from django.core.validators import MaxLengthValidator class ContactForm(forms.Form): @@ -10,29 +9,25 @@ class ContactForm(forms.Form): cleaned_data = super().clean() # Remove the full name property if "full_name" in cleaned_data: - del cleaned_data["full_name"] + full_name: str = cleaned_data["full_name"] + if full_name: + name_fields = full_name.split(" ") + + + cleaned_data["first_name"] = name_fields[0] + if len(name_fields) == 2: + cleaned_data["last_name"] = " ".join(name_fields[1:]) + elif len(name_fields) > 2: + cleaned_data["middle_name"] = name_fields[1] + cleaned_data["last_name"] = " ".join(name_fields[2:]) + else: + cleaned_data["middle_name"] = None + cleaned_data["last_name"] = None + + # Delete the full name element as we don't need it anymore + del cleaned_data["full_name"] return cleaned_data - def to_database(self, obj): - """ - Adds this form's cleaned data to `obj` and saves `obj`. - - Does nothing if form is not valid. - """ - if not self.is_valid(): - return - for name, value in self.cleaned_data.items(): - setattr(obj, name, value) - obj.save() - - @classmethod - def from_database(cls, obj): - """Returns a dict of form field values gotten from `obj`.""" - if obj is None: - return {} - return {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore - - full_name = forms.CharField( label="Full name", error_messages={"required": "Enter your full name"}, @@ -57,7 +52,6 @@ class ContactForm(forms.Form): ) email = forms.EmailField( label="Organization email", - max_length=None, required=False, ) phone = PhoneNumberField( diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 69d28df52..119b78fa6 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -92,6 +92,13 @@ class Contact(TimeStampedModel): names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] return " ".join(names) if names else "Unknown" + @property + def full_name(self): + """ + Returns the full name (first_name, middle_name, last_name) of this contact. + """ + return self.get_formatted_name() + def has_contact_info(self): return bool(self.title or self.email or self.phone) @@ -126,15 +133,3 @@ class Contact(TimeStampedModel): return str(self.pk) else: return "" - - @property - def full_name(self, separator=" "): - """ - Returns the full name (first_name, middle_name, last_name) of this contact. - Seperator (which defaults to a blank space) determines the seperator for each of those fields. - For instance, with seperator=", " - this function would return this: - "First, Middle, Last" - """ - # Filter out empty strings to avoid extra spaces or separators - parts = [self.first_name or "", self.middle_name or "", self.last_name or ""] - return separator.join(parts) \ No newline at end of file diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 209c0303f..8f504ad9e 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -261,3 +261,23 @@ class CreateOrUpdateOrganizationTypeHelper: return False else: return True + + +def to_database(form, obj): + """ + Adds the form's cleaned data to `obj` and saves `obj`. + + Does nothing if form is not valid. + """ + if not form.is_valid(): + return None + for name, value in form.cleaned_data.items(): + setattr(obj, name, value) + obj.save() + + +def from_database(form_class, obj): + """Returns a dict of form field values gotten from `obj`.""" + if obj is None: + return {} + return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore \ No newline at end of file diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 783951279..2c420de96 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -23,7 +23,8 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={'pk': request.user.contact.pk}) + # redirect_to_domain_request = request.GET.get('domain_request', "") != "" + setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -32,6 +33,7 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): + # Check if 'request' query parameter is not 'True' # Redirect to the setup page return HttpResponseRedirect(setup_page) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index b9d1ca6f6..add20a5a9 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -24,7 +24,6 @@

Finish setting up your profile

- {% public_site_url 'help/account-management/#get-help-with-login.gov' %} We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public. @@ -79,9 +78,15 @@

- + {% if confirm_changes %} + + {% else %} + + {% endif %}
{% block form_fields %}{% endblock %}
diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index f70f59a26..745101ade 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,9 +1,12 @@ +from django.http import HttpResponseRedirect from django.urls import reverse from registrar.forms.contact import ContactForm from registrar.models.contact import Contact +from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin - +from registrar.models.utility.generic_helper import to_database, from_database +from django.utils.safestring import mark_safe # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): @@ -30,14 +33,15 @@ class BaseContactView(ContactPermissionView): self.object = cached_contact else: self.object = self.get_object() + self._update_session_with_contact() def _update_session_with_contact(self): """ Set contact pk in the session cache """ - domain_pk = "contact:" + str(self.kwargs.get("pk")) - self.session[domain_pk] = self.object + contact_pk = "contact:" + str(self.kwargs.get("pk")) + self.session[contact_pk] = self.object class ContactFormBaseView(BaseContactView, FormMixin): @@ -50,13 +54,9 @@ class ContactFormBaseView(BaseContactView, FormMixin): # Set the current contact object in cache self._set_contact(request) - # Get the current form and validate it form = self.get_form() - return self.check_form(form) - - # TODO rename? - def check_form(self, form): + # Get the current form and validate it return self.form_valid(form) if form.is_valid() else self.form_invalid(form) def form_invalid(self, form): @@ -74,31 +74,65 @@ class ContactProfileSetupView(ContactFormBaseView): form_class = ContactForm model = Contact + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + # Set the current contact object in cache + self._set_contact(request) + + form = self.get_form() + + # Get the current form and validate it + if form.is_valid(): + if "redirect_to_home" not in self.session or not self.session["redirect_to_home"]: + self.session["redirect_to_home"] = "contact_setup_submit_button" in request.POST + return self.form_valid(form) + else: + return self.form_invalid(form) + def get_success_url(self): """Redirect to the nameservers page for the domain.""" + # TODO - some logic should exist that navigates them to the domain request page if # they clicked it on get.gov # Add a notification that the update was successful - return reverse("home") + if "redirect_to_home" in self.session and self.session["redirect_to_home"]: + return reverse("home") + else: + # Redirect to the same page with a query parameter to confirm changes + self.session["redirect_to_home"] = True + return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) def form_valid(self, form): self.request.user.finished_setup = True self.request.user.save() - form.to_database(self.object) + to_database(form=form, obj=self.object) self._update_session_with_contact() return super().form_valid(form) def get_initial(self): """The initial value for the form (which is a formset here).""" - db_object = self.form_class.from_database(self.object) + db_object = from_database(form_class=self.form_class, obj=self.object) return db_object def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["email_sublabel_text"] = ( + context["email_sublabel_text"] = self._email_sublabel_text() + + if "redirect_to_home" in self.session and self.session["redirect_to_home"]: + context['confirm_changes'] = True + + return context + + def _email_sublabel_text(self): + """Returns the lengthy sublabel for the email field""" + help_url = public_site_url('help/account-management/#get-help-with-login.gov') + return mark_safe( "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " - "and log back in. Get help with your Login.gov account.") - return context + f'and log back in. Get help with your Login.gov account.' + ) From f1f8a6275331deefee5d7831132ad271ededdf66 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 13 May 2024 14:34:09 -0600 Subject: [PATCH 023/171] Finish redirect logic Still needs some cleanup --- src/registrar/registrar_middleware.py | 11 ++- .../templates/finish_contact_setup.html | 10 ++- src/registrar/views/contact.py | 87 +++++++++++++++---- 3 files changed, 83 insertions(+), 25 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 2c420de96..f357f1050 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -24,7 +24,10 @@ class CheckUserProfileMiddleware: finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: # redirect_to_domain_request = request.GET.get('domain_request', "") != "" - setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) + setup_page = reverse( + "finish-contact-profile-setup", + kwargs={"pk": request.user.contact.pk} + ) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -33,7 +36,11 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): - # Check if 'request' query parameter is not 'True' + # Preserve the original query parameters + query_params = request.GET.urlencode() + if query_params: + setup_page += f"?{query_params}" + # Redirect to the setup page return HttpResponseRedirect(setup_page) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index add20a5a9..58eee0b17 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -78,14 +78,16 @@
+ {% if confirm_changes %} - - {% else %} + {% else %} + + {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 745101ade..2c98730a7 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,3 +1,5 @@ +from enum import Enum +from urllib.parse import urlencode from django.http import HttpResponseRedirect from django.urls import reverse from registrar.forms.contact import ContactForm @@ -8,12 +10,16 @@ from django.views.generic.edit import FormMixin from registrar.models.utility.generic_helper import to_database, from_database from django.utils.safestring import mark_safe +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_protect + # TODO we can and probably should generalize this at this rate. class BaseContactView(ContactPermissionView): def get(self, request, *args, **kwargs): self._set_contact(request) context = self.get_context_data(object=self.object) + return self.render_to_response(context) # TODO - this deserves a small refactor @@ -74,11 +80,56 @@ class ContactProfileSetupView(ContactFormBaseView): form_class = ContactForm model = Contact + redirect_type = None + class RedirectType: + HOME = "home" + BACK_TO_SELF = "back_to_self" + DOMAIN_REQUEST = "domain_request" + + @method_decorator(csrf_protect) + def dispatch(self, request, *args, **kwargs): + # Default redirect type + default_redirect = self.RedirectType.BACK_TO_SELF + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", default_redirect) + + # Store the redirect type in the session + self.redirect_type = redirect_type + + return super().dispatch(request, *args, **kwargs) + + def get_redirect_url(self): + match self.redirect_type: + case self.RedirectType.HOME: + return reverse("home") + case self.RedirectType.BACK_TO_SELF: + return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) + case self.RedirectType.DOMAIN_REQUEST: + # TODO + return reverse("home") + case _: + return reverse("home") + + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + redirect_url = self.get_redirect_url() + return redirect_url + def post(self, request, *args, **kwargs): """Form submission posts to this view. This post method harmonizes using BaseContactView and FormMixin """ + # Default redirect type + default_redirect = self.RedirectType.BACK_TO_SELF + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", default_redirect) + + # Store the redirect type in the session + self.redirect_type = redirect_type + # Set the current contact object in cache self._set_contact(request) @@ -86,28 +137,25 @@ class ContactProfileSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): - if "redirect_to_home" not in self.session or not self.session["redirect_to_home"]: - self.session["redirect_to_home"] = "contact_setup_submit_button" in request.POST + if 'contact_setup_save_button' in request.POST: + # Logic for when the 'Save' button is clicked + self.redirect_type = self.RedirectType.BACK_TO_SELF + self.session["should_redirect_to_home"] = "redirect_to_home" in request.POST + elif 'contact_setup_submit_button' in request.POST: + # Logic for when the 'Save and continue' button is clicked + if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: + self.redirect_type = self.RedirectType.HOME + else: + self.redirect_type = self.RedirectType.DOMAIN_REQUEST return self.form_valid(form) else: return self.form_invalid(form) - def get_success_url(self): - """Redirect to the nameservers page for the domain.""" - - # TODO - some logic should exist that navigates them to the domain request page if - # they clicked it on get.gov - # Add a notification that the update was successful - if "redirect_to_home" in self.session and self.session["redirect_to_home"]: - return reverse("home") - else: - # Redirect to the same page with a query parameter to confirm changes - self.session["redirect_to_home"] = True - return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - def form_valid(self, form): - self.request.user.finished_setup = True - self.request.user.save() + + if self.redirect_type == self.RedirectType.HOME: + self.request.user.finished_setup = True + self.request.user.save() to_database(form=form, obj=self.object) self._update_session_with_contact() @@ -120,11 +168,12 @@ class ContactProfileSetupView(ContactFormBaseView): return db_object def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "redirect_to_home" in self.session and self.session["redirect_to_home"]: - context['confirm_changes'] = True + if "should_redirect_to_home" in self.session: + context["confirm_changes"] = True return context From aaa9047478f369376bbdf92b3e665a2789701c9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 08:07:44 -0600 Subject: [PATCH 024/171] Add summary box to page --- src/registrar/assets/sass/_theme/_forms.scss | 3 ++ .../templates/domain_request_intro.html | 28 ++++++++++++++++++- .../templates/finish_contact_setup.html | 2 +- src/registrar/views/contact.py | 19 ++++--------- 4 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index a4194273d..d291cf333 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -15,6 +15,9 @@ .usa-form--extra-large { max-width: none; + .usa-summary-box { + max-width: 600px; + } } .usa-form--text-width { diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 72abe6a27..28ee3a2ef 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -13,8 +13,34 @@

We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.

Time to complete the form

If you have all the information you need, - completing your domain request might take around 15 minutes.

+ completing your domain request might take around 15 minutes.

+

How we’ll reach you

+

+ While reviewing your domain request, we may need to reach out with questions. + We’ll also email you when we complete our review. + If the contact information below is not correct, visit your profile to make updates. +

+ +
+
+

+ Your contact information +

+
+
    +
  • Full name: {{ user.contact.full_name }}
  • +
  • Organization email: {{ user.contact.email }}
  • +
  • Title or role in your organization: {{ user.contact.title }}
  • +
  • Phone: {{ user.contact.phone }}
  • +
+
+
+
{% block form_buttons %}
diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 58eee0b17..4c9a6595b 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -87,7 +87,7 @@ - + {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 2c98730a7..b7396f734 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -94,7 +94,6 @@ class ContactProfileSetupView(ContactFormBaseView): # Update redirect type based on the query parameter if present redirect_type = request.GET.get("redirect", default_redirect) - # Store the redirect type in the session self.redirect_type = redirect_type return super().dispatch(request, *args, **kwargs) @@ -106,8 +105,7 @@ class ContactProfileSetupView(ContactFormBaseView): case self.RedirectType.BACK_TO_SELF: return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) case self.RedirectType.DOMAIN_REQUEST: - # TODO - return reverse("home") + return reverse("domain-request:") case _: return reverse("home") @@ -116,19 +114,12 @@ class ContactProfileSetupView(ContactFormBaseView): redirect_url = self.get_redirect_url() return redirect_url + # TODO - delete session information def post(self, request, *args, **kwargs): """Form submission posts to this view. This post method harmonizes using BaseContactView and FormMixin """ - # Default redirect type - default_redirect = self.RedirectType.BACK_TO_SELF - - # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", default_redirect) - - # Store the redirect type in the session - self.redirect_type = redirect_type # Set the current contact object in cache self._set_contact(request) @@ -140,7 +131,7 @@ class ContactProfileSetupView(ContactFormBaseView): if 'contact_setup_save_button' in request.POST: # Logic for when the 'Save' button is clicked self.redirect_type = self.RedirectType.BACK_TO_SELF - self.session["should_redirect_to_home"] = "redirect_to_home" in request.POST + self.session["should_redirect"] = "redirect_to_confirmation_page" in request.POST elif 'contact_setup_submit_button' in request.POST: # Logic for when the 'Save and continue' button is clicked if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: @@ -153,7 +144,7 @@ class ContactProfileSetupView(ContactFormBaseView): def form_valid(self, form): - if self.redirect_type == self.RedirectType.HOME: + if self.redirect_type != self.RedirectType.BACK_TO_SELF: self.request.user.finished_setup = True self.request.user.save() @@ -172,7 +163,7 @@ class ContactProfileSetupView(ContactFormBaseView): context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "should_redirect_to_home" in self.session: + if "should_redirect" in self.session: context["confirm_changes"] = True return context From b741540bdfd18ec495b33a6975daf87a3eb99775 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 10:03:10 -0600 Subject: [PATCH 025/171] Polishing touches --- src/registrar/assets/js/get-gov.js | 53 ++++++++++++------- src/registrar/assets/sass/_theme/_forms.scss | 11 ++++ src/registrar/forms/contact.py | 22 ++------ .../templates/finish_contact_setup.html | 10 ++-- .../templates/includes/readonly_input.html | 4 +- .../templates/includes/required_fields.html | 2 +- 6 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 73afd8131..98a08fc2c 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -842,6 +842,14 @@ function hideDeletedForms() { */ (function finishUserSetupListener() { + function getInputFieldId(fieldName){ + return `#id_${fieldName}` + } + + function getReadonlyFieldId(fieldName){ + return `#${fieldName}__edit-button-readonly` + } + // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { let inputId = getInputFieldId(fieldName) @@ -854,29 +862,19 @@ function hideDeletedForms() { inputField.classList.toggle('display-none'); // Toggle the bold style on the grid row - let formGroup = button.closest(".usa-form-group") - if (formGroup){ - gridRow = button.querySelector(".grid-row") - if (gridRow){ - gridRow.toggle("bold-usa-label") - } + let gridRow = button.closest(".grid-col-2").closest(".grid-row") + if (gridRow){ + gridRow.classList.toggle("bold-usa-label") } } - function getInputFieldId(fieldName){ - return `#id_${fieldName}` - } - - function getReadonlyFieldId(fieldName){ - return `#${fieldName}__edit-button-readonly` - } - function handleFullNameField(fieldName, nameFields) { // Remove the display-none class from the nearest parent div let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); if (inputField) { + // Show each name field nameFields.forEach(function(fieldName) { let nameId = getInputFieldId(fieldName) let nameField = document.querySelector(nameId); @@ -887,7 +885,14 @@ function hideDeletedForms() { } } }); + + // Remove the "full_name" field + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.remove(); + } } + } function handleEditButtonClick(fieldName, button){ @@ -895,8 +900,14 @@ function hideDeletedForms() { // Lock the edit button while this operation occurs button.disabled = true - showInputFieldHideReadonlyField(fieldName, button); - button.classList.add('display-none'); + if (fieldName == "full_name"){ + let nameFields = ["first_name", "middle_name", "last_name"] + handleFullNameField(fieldName, nameFields); + }else { + showInputFieldHideReadonlyField(fieldName, button); + } + + button.classList.add("display-none"); // Unlock after it completes button.disabled = false @@ -925,10 +936,16 @@ function hideDeletedForms() { let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { + let nameFields = ["first_name", "middle_name", "last_name"] + // If either the full_name field errors out, + // or if any of its associated fields do - show all name related fields. + // Otherwise, just show the problematic field. if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] handleFullNameField(fieldName, nameFields); - }else { + }else if (nameFields.includes(fieldName)){ + handleFullNameField("full_name", nameFields); + } + else { button.click() } } diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index d291cf333..b198bbc18 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -38,6 +38,10 @@ font-weight: bold; } + &.bold-usa-label label.usa-label{ + font-weight: bold; + } + } .usa-form-readonly:first-of-type { @@ -62,6 +66,13 @@ legend.float-left-tablet + button.float-right-tablet { } } +@media (min-width: 35em) { + .usa-form--largest { + max-width: 35rem; + } +} + + // Custom style for disabled inputs @media (prefers-color-scheme: light) { .usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled { diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index 83b49f548..fa0bf1fce 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -9,23 +9,10 @@ class ContactForm(forms.Form): cleaned_data = super().clean() # Remove the full name property if "full_name" in cleaned_data: - full_name: str = cleaned_data["full_name"] - if full_name: - name_fields = full_name.split(" ") - - - cleaned_data["first_name"] = name_fields[0] - if len(name_fields) == 2: - cleaned_data["last_name"] = " ".join(name_fields[1:]) - elif len(name_fields) > 2: - cleaned_data["middle_name"] = name_fields[1] - cleaned_data["last_name"] = " ".join(name_fields[2:]) - else: - cleaned_data["middle_name"] = None - cleaned_data["last_name"] = None - - # Delete the full name element as we don't need it anymore - del cleaned_data["full_name"] + # Delete the full name element as its purely decorative. + # We include it as a normal Charfield for all the advantages + # and utility that it brings, but we're playing pretend. + del cleaned_data["full_name"] return cleaned_data full_name = forms.CharField( @@ -53,6 +40,7 @@ class ContactForm(forms.Form): email = forms.EmailField( label="Organization email", required=False, + max_length=None, ) phone = PhoneNumberField( label="Phone", diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 4c9a6595b..e856927bb 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -34,9 +34,9 @@ Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.

- {# TODO: maybe remove this? #} -

Required information is marked with an asterisk (*).

- + {# We use a var called 'remove_margin_top' rather than 'add_margin_top' because most fields use this #} + {% include "includes/required_fields.html" with remove_margin_top=True %} + {% csrf_token %}
@@ -61,8 +61,8 @@ {% input_with_errors form.last_name %} {% endwith %} - {# TODO: I shouldnt need to do add_class here #} - {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2" sublabel_text=email_sublabel_text %} + {# This field doesn't have the readonly button but it has common design elements from it #} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2 bold-usa-label" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} {% endwith %} diff --git a/src/registrar/templates/includes/readonly_input.html b/src/registrar/templates/includes/readonly_input.html index 820d4b66c..59a55090c 100644 --- a/src/registrar/templates/includes/readonly_input.html +++ b/src/registrar/templates/includes/readonly_input.html @@ -1,6 +1,6 @@ {% load static field_helpers url_helpers %} -
+
-
+
{{ field.value }}
diff --git a/src/registrar/templates/includes/required_fields.html b/src/registrar/templates/includes/required_fields.html index 0087b048a..be0395979 100644 --- a/src/registrar/templates/includes/required_fields.html +++ b/src/registrar/templates/includes/required_fields.html @@ -1,3 +1,3 @@ -

+

Required fields are marked with an asterisk (*).

From 1fd58d6d843b916dd7551b6c636b887867c10342 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 11:49:55 -0600 Subject: [PATCH 026/171] Redirect logic --- .../templates/finish_contact_setup.html | 1 - src/registrar/views/contact.py | 129 ++++++++++++++---- 2 files changed, 100 insertions(+), 30 deletions(-) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index e856927bb..68e860267 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -87,7 +87,6 @@ - {% endif %}
{% block form_fields %}{% endblock %} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index b7396f734..0fbd92435 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,7 +1,6 @@ from enum import Enum -from urllib.parse import urlencode -from django.http import HttpResponseRedirect -from django.urls import reverse +from urllib.parse import urlencode, urlunparse, urlparse, quote +from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url @@ -13,19 +12,23 @@ from django.utils.safestring import mark_safe from django.utils.decorators import method_decorator from django.views.decorators.csrf import csrf_protect -# TODO we can and probably should generalize this at this rate. +import logging + +logger = logging.getLogger(__name__) + + class BaseContactView(ContactPermissionView): def get(self, request, *args, **kwargs): + """Sets the current contact in cache, defines the current object as self.object + then returns render_to_response""" self._set_contact(request) context = self.get_context_data(object=self.object) - return self.render_to_response(context) - # TODO - this deserves a small refactor def _set_contact(self, request): """ - get domain from session cache or from db and set + get contact from session cache or from db and set to self.object set session to self for downstream functions to update session cache @@ -81,34 +84,99 @@ class ContactProfileSetupView(ContactFormBaseView): model = Contact redirect_type = None + + # TODO - make this an enum class RedirectType: + """ + Contains constants for each type of redirection. + Not an enum as we just need to track string values, + but we don't care about enforcing it. + + - HOME: We want to redirect to reverse("home") + - BACK_TO_SELF: We want to redirect back to reverse("finish-contact-profile-setup") + - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" + - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent + redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE + """ HOME = "home" BACK_TO_SELF = "back_to_self" - DOMAIN_REQUEST = "domain_request" + COMPLETE_SETUP = "complete_setup" + TO_SPECIFIC_PAGE = "domain_request" + # TODO - refactor @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): + # Default redirect type default_redirect = self.RedirectType.BACK_TO_SELF # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", default_redirect) + redirect_type = request.GET.get("redirect", None) - self.redirect_type = redirect_type + is_default = False + # We set this here rather than in .get so we don't override + # existing data if no queryparam is present. + if redirect_type is None: + is_default = True + redirect_type = default_redirect + + # Set the default if nothing exists already + if self.redirect_type is None: + self.redirect_type = redirect_type + + if not is_default: + default_redirects = [ + self.RedirectType.HOME, + self.RedirectType.COMPLETE_SETUP, + self.RedirectType.BACK_TO_SELF, + self.RedirectType.TO_SPECIFIC_PAGE + ] + if redirect_type not in default_redirects: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + request.session["profile_setup_redirect_viewname"] = redirect_type + else: + self.redirect_type = redirect_type return super().dispatch(request, *args, **kwargs) def get_redirect_url(self): + base_url = "" + query_params = {} match self.redirect_type: case self.RedirectType.HOME: - return reverse("home") - case self.RedirectType.BACK_TO_SELF: - return reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - case self.RedirectType.DOMAIN_REQUEST: - return reverse("domain-request:") + base_url = reverse("home") + case self.RedirectType.BACK_TO_SELF | self.RedirectType.COMPLETE_SETUP: + base_url = reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) + case self.RedirectType.TO_SPECIFIC_PAGE: + + # We only allow this session value to use viewnames, + # because otherwise this allows anyone to enter any value in here. + # This restricts what can be redirected to. + try: + desired_view = self.session["profile_setup_redirect_viewname"] + base_url = reverse(desired_view) + except NoReverseMatch as err: + logger.error(err) + logger.error( + "ContactProfileSetupView -> get_redirect_url -> Could not find specified page." + ) + base_url = reverse("home") case _: - return reverse("home") - + base_url = reverse("home") + + # Quote cleans up the value so that it can be used in a url + query_params["redirect"] = quote(self.redirect_type) + + # Parse the base URL + url_parts = list(urlparse(base_url)) + + # Update the query part of the URL + url_parts[4] = urlencode(query_params) + + # Construct the full URL with query parameters + full_url = urlunparse(url_parts) + return full_url + def get_success_url(self): """Redirect to the nameservers page for the domain.""" redirect_url = self.get_redirect_url() @@ -128,23 +196,26 @@ class ContactProfileSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): - if 'contact_setup_save_button' in request.POST: + if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked - self.redirect_type = self.RedirectType.BACK_TO_SELF - self.session["should_redirect"] = "redirect_to_confirmation_page" in request.POST - elif 'contact_setup_submit_button' in request.POST: - # Logic for when the 'Save and continue' button is clicked - if self.redirect_type != self.RedirectType.DOMAIN_REQUEST: - self.redirect_type = self.RedirectType.HOME + self.redirect_type = self.RedirectType.COMPLETE_SETUP + elif "contact_setup_submit_button" in request.POST: + if "profile_setup_redirect_viewname" in self.session: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE else: - self.redirect_type = self.RedirectType.DOMAIN_REQUEST + self.redirect_type = self.RedirectType.HOME + return self.form_valid(form) else: return self.form_invalid(form) def form_valid(self, form): - - if self.redirect_type != self.RedirectType.BACK_TO_SELF: + + completed_states = [ + self.RedirectType.TO_SPECIFIC_PAGE, + self.RedirectType.HOME + ] + if self.redirect_type in completed_states: self.request.user.finished_setup = True self.request.user.save() @@ -163,14 +234,14 @@ class ContactProfileSetupView(ContactFormBaseView): context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() - if "should_redirect" in self.session: + if self.redirect_type == self.RedirectType.COMPLETE_SETUP: context["confirm_changes"] = True return context def _email_sublabel_text(self): """Returns the lengthy sublabel for the email field""" - help_url = public_site_url('help/account-management/#get-help-with-login.gov') + help_url = public_site_url("help/account-management/#get-help-with-login.gov") return mark_safe( "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " From c352869f6be066c3b3718227a047b073c958ae0e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 12:40:29 -0600 Subject: [PATCH 027/171] Redirect logic from home --- src/registrar/registrar_middleware.py | 30 ++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index f357f1050..a9b2f7a23 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -1,8 +1,8 @@ """ Contains middleware used in settings.py """ - -from django.urls import reverse +from urllib.parse import urlparse, urlunparse, parse_qs, urlencode +from django.urls import reverse, resolve from django.http import HttpResponseRedirect class CheckUserProfileMiddleware: @@ -20,6 +20,8 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): + + # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: @@ -33,13 +35,31 @@ class CheckUserProfileMiddleware: setup_page, logout_page, ] + custom_redirect = None + + # In some cases, we don't want to redirect to home. + # This handles that. + if request.path == "/request/": + # This can be generalized if need be, but for now lets keep this easy to read. + custom_redirect = "domain-request:" # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): - # Preserve the original query parameters - query_params = request.GET.urlencode() + # Preserve the original query parameters, and coerce them into a dict + query_params = parse_qs(request.META['QUERY_STRING']) + + if custom_redirect is not None: + # Set the redirect value to our redirect location + query_params["redirect"] = custom_redirect + if query_params: - setup_page += f"?{query_params}" + # Split the URL into parts + setup_page_parts = list(urlparse(setup_page)) + # Modify the query param bit + setup_page_parts[4] = urlencode(query_params) + # Reassemble the URL + setup_page = urlunparse(setup_page_parts) + # Redirect to the setup page return HttpResponseRedirect(setup_page) From 059a99aca1bd62812c6d549b9caab607c17e75fb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:26:48 -0600 Subject: [PATCH 028/171] Add waffle flags --- src/registrar/registrar_middleware.py | 10 ++++++++-- src/registrar/views/contact.py | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index a9b2f7a23..427775f34 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -2,8 +2,9 @@ Contains middleware used in settings.py """ from urllib.parse import urlparse, urlunparse, parse_qs, urlencode -from django.urls import reverse, resolve +from django.urls import reverse from django.http import HttpResponseRedirect +from waffle.decorators import flag_is_active class CheckUserProfileMiddleware: """ @@ -20,12 +21,17 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): + + # Check that the user is "opted-in" to the profile feature flag + has_profile_feature_flag = flag_is_active(request, "profile_feature") + # If they aren't, skip this entirely + if not has_profile_feature_flag: + return None # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - # redirect_to_domain_request = request.GET.get('domain_request', "") != "" setup_page = reverse( "finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk} diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 0fbd92435..f2f5e29a9 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -1,4 +1,4 @@ -from enum import Enum +from waffle.decorators import waffle_flag from urllib.parse import urlencode, urlunparse, urlparse, quote from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm @@ -18,7 +18,8 @@ logger = logging.getLogger(__name__) class BaseContactView(ContactPermissionView): - + """Provides a base view for the contact object. On get, the contact + is saved in the session and on self.object.""" def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -54,7 +55,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): - + """Adds a FormMixin to BaseContactView, and handles post""" def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -104,6 +105,7 @@ class ContactProfileSetupView(ContactFormBaseView): TO_SPECIFIC_PAGE = "domain_request" # TODO - refactor + @waffle_flag('profile_feature') @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): @@ -140,6 +142,16 @@ class ContactProfileSetupView(ContactFormBaseView): return super().dispatch(request, *args, **kwargs) def get_redirect_url(self): + """ + Returns a URL string based on the current value of self.redirect_type. + + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as + HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. + + Returns: + str: The full URL with the appropriate query parameters. + """ base_url = "" query_params = {} match self.redirect_type: From 54c532f3b241df5f64b7f5857ee8314ef3e1fdf2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:33:07 -0600 Subject: [PATCH 029/171] Add comment on dispatch --- src/registrar/views/contact.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index f2f5e29a9..d8ea5a041 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -108,7 +108,16 @@ class ContactProfileSetupView(ContactFormBaseView): @waffle_flag('profile_feature') @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): - + """ + Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. + + This method sets the redirect type based on the 'redirect' query parameter, + defaulting to BACK_TO_SELF if not provided. + It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + + Returns: + HttpResponse: The response generated by the parent class's dispatch method. + """ # Default redirect type default_redirect = self.RedirectType.BACK_TO_SELF From f55d0a655a7e4093d8ffbe27b87fa096fa6d2693 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 13:49:43 -0600 Subject: [PATCH 030/171] Cleanup --- src/djangooidc/backends.py | 5 ++- src/registrar/forms/contact.py | 1 - src/registrar/forms/domain.py | 1 + src/registrar/forms/domain_request_wizard.py | 2 +- src/registrar/models/contact.py | 1 + .../models/utility/generic_helper.py | 2 +- src/registrar/registrar_middleware.py | 17 ++++---- src/registrar/views/contact.py | 43 +++++++++---------- src/registrar/views/domain_request.py | 1 - src/registrar/views/utility/mixins.py | 8 +--- 10 files changed, 38 insertions(+), 43 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 96b7a902a..8e4eb023d 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -50,7 +50,7 @@ class OpenIdConnectBackend(ModelBackend): user, created = UserModel.objects.get_or_create(**args) - if created: + if created and request is not None: request.session["is_new_user"] = True if not created: @@ -63,7 +63,8 @@ class OpenIdConnectBackend(ModelBackend): try: user = UserModel.objects.get_by_natural_key(username) except UserModel.DoesNotExist: - request.session["is_new_user"] = True + if request is not None: + request.session["is_new_user"] = True return None # run this callback for a each login user.on_each_login() diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/contact.py index fa0bf1fce..9a752bab4 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/contact.py @@ -46,4 +46,3 @@ class ContactForm(forms.Form): label="Phone", error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, ) - diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index 9dfd9773a..db247ad21 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -202,6 +202,7 @@ NameserverFormset = formset_factory( validate_max=True, ) + # TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index 9d16a30de..dd29df523 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -150,7 +150,7 @@ class OrganizationContactForm(RegistrarForm): """Require something to be selected when this is a federal agency.""" federal_agency = self.cleaned_data.get("federal_agency", None) # need the domain request object to know if this is federal - if self.domain_request is None: + if hasattr(self, "domain_request") and self.domain_request is None: # hmm, no saved domain request object?, default require the agency if not federal_agency: # no answer was selected diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 119b78fa6..6e196783b 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -3,6 +3,7 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel from phonenumber_field.modelfields import PhoneNumberField # type: ignore + class Contact(TimeStampedModel): """Contact information follows a similar pattern for each contact.""" diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 8f504ad9e..6ee598c13 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -280,4 +280,4 @@ def from_database(form_class, obj): """Returns a dict of form field values gotten from `obj`.""" if obj is None: return {} - return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore \ No newline at end of file + return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 427775f34..8dca06019 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -1,17 +1,20 @@ """ Contains middleware used in settings.py """ + from urllib.parse import urlparse, urlunparse, parse_qs, urlencode from django.urls import reverse from django.http import HttpResponseRedirect from waffle.decorators import flag_is_active + class CheckUserProfileMiddleware: """ - Checks if the current user has finished_setup = False. + Checks if the current user has finished_setup = False. If they do, redirect them to the setup page regardless of where they are in the application. """ + def __init__(self, get_response): self.get_response = get_response @@ -21,7 +24,7 @@ class CheckUserProfileMiddleware: return response def process_view(self, request, view_func, view_args, view_kwargs): - + # Check that the user is "opted-in" to the profile feature flag has_profile_feature_flag = flag_is_active(request, "profile_feature") @@ -32,10 +35,7 @@ class CheckUserProfileMiddleware: # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse( - "finish-contact-profile-setup", - kwargs={"pk": request.user.contact.pk} - ) + setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) logout_page = reverse("logout") excluded_pages = [ setup_page, @@ -52,7 +52,7 @@ class CheckUserProfileMiddleware: # Don't redirect on excluded pages (such as the setup page itself) if not any(request.path.startswith(page) for page in excluded_pages): # Preserve the original query parameters, and coerce them into a dict - query_params = parse_qs(request.META['QUERY_STRING']) + query_params = parse_qs(request.META["QUERY_STRING"]) if custom_redirect is not None: # Set the redirect value to our redirect location @@ -65,7 +65,6 @@ class CheckUserProfileMiddleware: setup_page_parts[4] = urlencode(query_params) # Reassemble the URL setup_page = urlunparse(setup_page_parts) - # Redirect to the setup page return HttpResponseRedirect(setup_page) @@ -88,4 +87,4 @@ class NoCacheMiddleware: def __call__(self, request): response = self.get_response(request) response["Cache-Control"] = "no-cache" - return response \ No newline at end of file + return response diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index d8ea5a041..322630b07 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -20,6 +20,7 @@ logger = logging.getLogger(__name__) class BaseContactView(ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" + def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -56,6 +57,7 @@ class BaseContactView(ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): """Adds a FormMixin to BaseContactView, and handles post""" + def post(self, request, *args, **kwargs): """Form submission posts to this view. @@ -78,8 +80,9 @@ class ContactFormBaseView(BaseContactView, FormMixin): class ContactProfileSetupView(ContactFormBaseView): - """This view forces the user into providing additional details that + """This view forces the user into providing additional details that we may have missed from Login.gov""" + template_name = "finish_contact_setup.html" form_class = ContactForm model = Contact @@ -99,22 +102,23 @@ class ContactProfileSetupView(ContactFormBaseView): - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE """ + HOME = "home" BACK_TO_SELF = "back_to_self" COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" # TODO - refactor - @waffle_flag('profile_feature') + @waffle_flag("profile_feature") @method_decorator(csrf_protect) def dispatch(self, request, *args, **kwargs): """ Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - - This method sets the redirect type based on the 'redirect' query parameter, + + This method sets the redirect type based on the 'redirect' query parameter, defaulting to BACK_TO_SELF if not provided. It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. - + Returns: HttpResponse: The response generated by the parent class's dispatch method. """ @@ -140,7 +144,7 @@ class ContactProfileSetupView(ContactFormBaseView): self.RedirectType.HOME, self.RedirectType.COMPLETE_SETUP, self.RedirectType.BACK_TO_SELF, - self.RedirectType.TO_SPECIFIC_PAGE + self.RedirectType.TO_SPECIFIC_PAGE, ] if redirect_type not in default_redirects: self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE @@ -154,8 +158,8 @@ class ContactProfileSetupView(ContactFormBaseView): """ Returns a URL string based on the current value of self.redirect_type. - Depending on self.redirect_type, constructs a base URL and appends a - 'redirect' query parameter. Handles different redirection types such as + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. Returns: @@ -178,13 +182,11 @@ class ContactProfileSetupView(ContactFormBaseView): base_url = reverse(desired_view) except NoReverseMatch as err: logger.error(err) - logger.error( - "ContactProfileSetupView -> get_redirect_url -> Could not find specified page." - ) + logger.error("ContactProfileSetupView -> get_redirect_url -> Could not find specified page.") base_url = reverse("home") case _: base_url = reverse("home") - + # Quote cleans up the value so that it can be used in a url query_params["redirect"] = quote(self.redirect_type) @@ -231,27 +233,24 @@ class ContactProfileSetupView(ContactFormBaseView): return self.form_invalid(form) def form_valid(self, form): - - completed_states = [ - self.RedirectType.TO_SPECIFIC_PAGE, - self.RedirectType.HOME - ] + + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] if self.redirect_type in completed_states: self.request.user.finished_setup = True self.request.user.save() - + to_database(form=form, obj=self.object) self._update_session_with_contact() return super().form_valid(form) - + def get_initial(self): """The initial value for the form (which is a formset here).""" db_object = from_database(form_class=self.form_class, obj=self.object) return db_object - + def get_context_data(self, **kwargs): - + context = super().get_context_data(**kwargs) context["email_sublabel_text"] = self._email_sublabel_text() @@ -259,7 +258,7 @@ class ContactProfileSetupView(ContactFormBaseView): context["confirm_changes"] = True return context - + def _email_sublabel_text(self): """Returns the lengthy sublabel for the email field""" help_url = public_site_url("help/account-management/#get-help-with-login.gov") diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index b07f0d53f..f93976138 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -819,4 +819,3 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView): duplicates = [item for item, count in object_dict.items() if count > 1] return duplicates - diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 49d172971..3e5e10816 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -341,7 +341,6 @@ class ContactPermission(PermissionsLoginMixin): if not self.request.user.is_authenticated: return False - given_contact_pk = self.kwargs["pk"] # Grab the user in the DB to do a full object comparision, not just on ids @@ -351,13 +350,10 @@ class ContactPermission(PermissionsLoginMixin): if current_user.contact.pk != given_contact_pk: # Don't allow users to modify other users profiles return False - + # Check if the object at the id we're searching on actually exists requested_user_exists = User.objects.filter(pk=current_user.pk).exists() - requested_contact_exists = Contact.objects.filter( - user=current_user.pk, - pk=given_contact_pk - ).exists() + requested_contact_exists = Contact.objects.filter(user=current_user.pk, pk=given_contact_pk).exists() if not requested_user_exists or not requested_contact_exists: return False From 56e21f4c8ef33421fbec68ad3effe57e9ba02239 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 14:30:48 -0600 Subject: [PATCH 031/171] Remove old code --- src/registrar/forms/domain_request_wizard.py | 2 +- src/registrar/forms/utility/wizard_form_helper.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py index dd29df523..9d16a30de 100644 --- a/src/registrar/forms/domain_request_wizard.py +++ b/src/registrar/forms/domain_request_wizard.py @@ -150,7 +150,7 @@ class OrganizationContactForm(RegistrarForm): """Require something to be selected when this is a federal agency.""" federal_agency = self.cleaned_data.get("federal_agency", None) # need the domain request object to know if this is federal - if hasattr(self, "domain_request") and self.domain_request is None: + if self.domain_request is None: # hmm, no saved domain request object?, default require the agency if not federal_agency: # no answer was selected diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 9b8a7c4d8..2dd1a2b42 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -21,8 +21,7 @@ class RegistrarForm(forms.Form): def __init__(self, *args, **kwargs): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object - if "domain_request" in kwargs: - self.domain_request = kwargs.pop("domain_request", None) + self.domain_request = kwargs.pop("domain_request", None) super(RegistrarForm, self).__init__(*args, **kwargs) From 21f5c43b34c28fbba80a3895ae4e058e186bb284 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 15:12:39 -0600 Subject: [PATCH 032/171] Add success message --- src/djangooidc/views.py | 46 ++++++++++++++++++++-------------- src/registrar/views/contact.py | 9 ++++++- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index 7b5c58527..bf5fe379e 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -101,25 +101,9 @@ def login_callback(request): user = authenticate(request=request, **userinfo) is_new_user = request.session.get("is_new_user", False) if user: - should_update_user = False - # Fixture users kind of exist in a superposition of verification types, - # because while the system "verified" them, if they login, - # we don't know how the user themselves was verified through login.gov until - # they actually try logging in. This edge-case only matters in non-production environments. - fixture_user = User.VerificationTypeChoices.FIXTURE_USER - is_fixture_user = user.verification_type and user.verification_type == fixture_user - - # Set the verification type if it doesn't already exist or if its a fixture user - if not user.verification_type or is_fixture_user: - user.set_user_verification_type() - should_update_user = True - - if is_new_user: - user.finished_setup = False - should_update_user = True - - if should_update_user: - user.save() + # Set login metadata about this user + # (verification_type for instance) + _set_authenticated_user_metadata(user, is_new_user) login(request, user) @@ -149,6 +133,30 @@ def login_callback(request): return error_page(request, err) +def _set_authenticated_user_metadata(user, is_new_user): + """Does checks on the recieved authenticated user from login_callback, + and updates fields accordingly. U""" + should_update_user = False + # Fixture users kind of exist in a superposition of verification types, + # because while the system "verified" them, if they login, + # we don't know how the user themselves was verified through login.gov until + # they actually try logging in. This edge-case only matters in non-production environments. + fixture_user = User.VerificationTypeChoices.FIXTURE_USER + is_fixture_user = user.verification_type and user.verification_type == fixture_user + + # Set the verification type if it doesn't already exist or if its a fixture user + if not user.verification_type or is_fixture_user: + user.set_user_verification_type() + should_update_user = True + + if is_new_user: + user.finished_setup = False + should_update_user = True + + if should_update_user: + user.save() + + def _requires_step_up_auth(userinfo): """if User.needs_identity_verification and step_up_acr_value not in ial returned from callback, return True""" diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index 322630b07..a0b99c4a0 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -2,6 +2,7 @@ from waffle.decorators import waffle_flag from urllib.parse import urlencode, urlunparse, urlparse, quote from django.urls import NoReverseMatch, reverse from registrar.forms.contact import ContactForm +from django.contrib.messages.views import SuccessMessageMixin from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView @@ -17,10 +18,13 @@ import logging logger = logging.getLogger(__name__) -class BaseContactView(ContactPermissionView): +class BaseContactView(SuccessMessageMixin, ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" + def get_success_message(self): + return "Contact updated successfully." + def get(self, request, *args, **kwargs): """Sets the current contact in cache, defines the current object as self.object then returns render_to_response""" @@ -108,6 +112,9 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" + def get_success_message(self): + return "Your profile has been successfully updated." + # TODO - refactor @waffle_flag("profile_feature") @method_decorator(csrf_protect) From f06620cef468d07bf200cef063c5c3ede04ae0bb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 14 May 2024 15:18:10 -0600 Subject: [PATCH 033/171] Add nosec on mark_safe We directly control help_url, so this is not a problem --- src/registrar/views/contact.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index a0b99c4a0..de3525efb 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -273,4 +273,4 @@ class ContactProfileSetupView(ContactFormBaseView): "We recommend using your work email for your .gov account. " "If the wrong email is displayed below, you’ll need to update your Login.gov account " f'and log back in. Get help with your Login.gov account.' - ) + ) # nosec From 6c5e05f89b7924323b6c56bb7021599b086fcbae Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 11:13:03 -0600 Subject: [PATCH 034/171] Tooltips, style refinement --- src/registrar/assets/js/get-gov.js | 51 ++++++++++------- src/registrar/assets/sass/_theme/_base.scss | 10 ++++ .../assets/sass/_theme/_cisa_colors.scss | 1 + .../assets/sass/_theme/_fieldsets.scss | 16 ++++++ .../assets/sass/_theme/_headers.scss | 14 +++++ .../assets/sass/_theme/_tooltips.scss | 7 +++ src/registrar/assets/sass/_theme/styles.scss | 1 + src/registrar/templates/base.html | 10 ++-- .../templates/finish_contact_setup.html | 55 ++++++++++++++----- src/registrar/templates/includes/footer.html | 2 + .../templates/includes/gov_extended_logo.html | 17 ++++++ .../includes/label_with_edit_button.html | 3 +- src/registrar/views/contact.py | 6 +- 13 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 src/registrar/assets/sass/_theme/_headers.scss create mode 100644 src/registrar/templates/includes/gov_extended_logo.html diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 98a08fc2c..d18129456 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -868,31 +868,42 @@ function hideDeletedForms() { } } - function handleFullNameField(fieldName, nameFields) { + function handleFullNameField(fieldName) { // Remove the display-none class from the nearest parent div let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); + let nameFieldset = document.querySelector("#contact-full-name-fieldset"); + if (nameFieldset){ + nameFieldset.classList.remove("display-none"); + } + if (inputField) { - // Show each name field - nameFields.forEach(function(fieldName) { - let nameId = getInputFieldId(fieldName) - let nameField = document.querySelector(nameId); - if (nameField){ - let parentDiv = nameField.closest("div"); - if (parentDiv) { - parentDiv.classList.remove("display-none"); + let readonlyId = getReadonlyFieldId(fieldName) + let readonlyField = document.querySelector(readonlyId) + if (readonlyField) { + // Update the element's xlink:href attribute + let useElement = readonlyField.querySelector("use"); + if (useElement) { + let currentHref = useElement.getAttribute("xlink:href"); + let parts = currentHref.split("#"); + + // Update the icon reference to the info icon + if (parts.length > 1) { + parts[1] = "info"; + useElement.setAttribute("xlink:href", parts.join("#")); + + // Change the color to => $dhs-dark-gray-60 + useElement.closest('svg').style.fill = '#444547'; } } - }); - - // Remove the "full_name" field - inputFieldParentDiv = inputField.closest("div"); - if (inputFieldParentDiv) { - inputFieldParentDiv.remove(); + + let parentDiv = readonlyField.closest("div"); + if (parentDiv) { + parentDiv.classList.toggle("overlapped-full-name-field"); + } } } - } function handleEditButtonClick(fieldName, button){ @@ -901,8 +912,7 @@ function hideDeletedForms() { button.disabled = true if (fieldName == "full_name"){ - let nameFields = ["first_name", "middle_name", "last_name"] - handleFullNameField(fieldName, nameFields); + handleFullNameField(fieldName); }else { showInputFieldHideReadonlyField(fieldName, button); } @@ -941,9 +951,10 @@ function hideDeletedForms() { // or if any of its associated fields do - show all name related fields. // Otherwise, just show the problematic field. if (fieldName == "full_name"){ - handleFullNameField(fieldName, nameFields); + handleFullNameField(fieldName); }else if (nameFields.includes(fieldName)){ - handleFullNameField("full_name", nameFields); + handleFullNameField("full_name"); + button.click() } else { button.click() diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index beb63cdd1..2cdaefb7d 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -170,3 +170,13 @@ abbr[title] { height: 1.25em !important; } } + +// todo - move this to a better location + better name +@media (min-width: 800px){ + .overlapped-full-name-field { + position: absolute; + top: 325px; + background-color: white; + padding-right: 8px; + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_cisa_colors.scss b/src/registrar/assets/sass/_theme/_cisa_colors.scss index 7466a3490..23ecf7989 100644 --- a/src/registrar/assets/sass/_theme/_cisa_colors.scss +++ b/src/registrar/assets/sass/_theme/_cisa_colors.scss @@ -46,6 +46,7 @@ $dhs-gray-10: #fcfdfd; /*--- Dark Gray ---*/ $dhs-dark-gray-90: #040404; +$dhs-dark-gray-85: #1b1b1b; $dhs-dark-gray-80: #19191a; $dhs-dark-gray-70: #2f2f30; $dhs-dark-gray-60: #444547; diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index c60080cb9..5cdd52026 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -8,3 +8,19 @@ fieldset { fieldset:not(:first-child) { margin-top: units(2); } + +fieldset.registrar-fieldset__contact { + border-width: 2px; + border-left: none; + border-right: none; + border-bottom: none; + padding-bottom: 0; +} + +@media (max-width: 800px){ + fieldset.registrar-fieldset__contact { + padding: 0; + margin: 0; + border: none; + } +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_headers.scss b/src/registrar/assets/sass/_theme/_headers.scss new file mode 100644 index 000000000..36868fdd3 --- /dev/null +++ b/src/registrar/assets/sass/_theme/_headers.scss @@ -0,0 +1,14 @@ +@use "uswds-core" as *; +@use "cisa_colors" as *; + +.usa-logo button { + color: #{$dhs-dark-gray-85}; + font-weight: 700; + font-family: family('sans'); + font-size: 1.6rem; + line-height: 1.1; +} + +.usa-logo button.usa-button--unstyled.disabled-button:hover{ + color: #{$dhs-dark-gray-85}; +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/_tooltips.scss b/src/registrar/assets/sass/_theme/_tooltips.scss index 04c6f3cda..203bb4f85 100644 --- a/src/registrar/assets/sass/_theme/_tooltips.scss +++ b/src/registrar/assets/sass/_theme/_tooltips.scss @@ -24,3 +24,10 @@ text-align: center !important; } } + +.usa-tooltip--registrar-logo .usa-tooltip__body { + max-width: 50px !important; + font-weight: 400 !important; + white-space: normal; + text-align: center; +} \ No newline at end of file diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index 64b113a29..e24618a23 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -20,6 +20,7 @@ @forward "tables"; @forward "sidenav"; @forward "register-form"; +@forward "_headers"; /*-------------------------------------------------- --- Admin ---------------------------------*/ diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index c0702e78f..3b541d946 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -138,11 +138,7 @@
{% block logo %} - + {% include "includes/gov_extended_logo.html" with logo_clickable=True %} {% endblock %}
@@ -199,7 +195,9 @@
{% endblock wrapper%} - {% include "includes/footer.html" %} + {% block footer %} + {% include "includes/footer.html" with show_manage_your_domains=True %} + {% endblock footer %}
{% block init_js %}{% endblock %}{# useful for vars and other initializations #} diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_contact_setup.html index 68e860267..301f5837a 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_contact_setup.html @@ -1,16 +1,36 @@ {% extends "base.html" %} {% load static form_helpers url_helpers field_helpers %} -{% block title %} Finish setting up your profile {% endblock %} +{% block title %} Finish setting up your profile | {% endblock %} + +{# Disable the redirect #} +{% block logo %} + {% include "includes/gov_extended_logo.html" with logo_clickable=False %} +{% endblock %} {% block content %}
- {% include "includes/form_errors.html" with form=form %} + {% comment %} - Repurposed from domain_request_form.html + Form success messages. {% endcomment %} + {% if messages %} + {% for message in messages %} +
+
+ {{ message }} +
+
+ {% endfor %} + {% endif %} + + {% comment %} + Repurposed from domain_request_form.html. + Form errors. + {% endcomment %} + {% include "includes/form_errors.html" with form=form %} {% for outer in forms %} {% if outer|isformset %} {% for inner in outer.forms %} @@ -48,19 +68,21 @@ {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} - {% input_with_errors form.first_name %} - {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none" %} - {% input_with_errors form.middle_name %} - {% endwith %} - - {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2 display-none"%} - {% input_with_errors form.last_name %} - {% endwith %} + + {# This field doesn't have the readonly button but it has common design elements from it #} {% with show_readonly=True add_class="display-none" group_classes="usa-form-readonly padding-top-2 bold-usa-label" sublabel_text=email_sublabel_text %} {% input_with_errors form.email %} @@ -97,3 +119,6 @@ {% endblock content %} +{% block footer %} + {% include "includes/footer.html" %} +{% endblock footer %} \ No newline at end of file diff --git a/src/registrar/templates/includes/footer.html b/src/registrar/templates/includes/footer.html index 303c0ef74..5e10955e4 100644 --- a/src/registrar/templates/includes/footer.html +++ b/src/registrar/templates/includes/footer.html @@ -26,10 +26,12 @@ >
\ No newline at end of file diff --git a/src/registrar/views/contact.py b/src/registrar/views/contact.py index de3525efb..257f0db01 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/contact.py @@ -22,7 +22,8 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): """Provides a base view for the contact object. On get, the contact is saved in the session and on self.object.""" - def get_success_message(self): + def get_success_message(self, cleaned_data): + """Content of the returned success message""" return "Contact updated successfully." def get(self, request, *args, **kwargs): @@ -112,7 +113,8 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" - def get_success_message(self): + def get_success_message(self, cleaned_data): + """Content of the returned success message""" return "Your profile has been successfully updated." # TODO - refactor From b2e52b52676ff7d56450d81625cb8bb4998a7c20 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 11:28:56 -0600 Subject: [PATCH 035/171] Pixel adjustment --- src/registrar/assets/sass/_theme/_base.scss | 2 +- src/registrar/assets/sass/_theme/_fieldsets.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 2cdaefb7d..3fe5c6f68 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -175,7 +175,7 @@ abbr[title] { @media (min-width: 800px){ .overlapped-full-name-field { position: absolute; - top: 325px; + top: 337px; background-color: white; padding-right: 8px; } diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index 5cdd52026..da900f128 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -17,6 +17,12 @@ fieldset.registrar-fieldset__contact { padding-bottom: 0; } +@media (min-width: 800px) { + fieldset.registrar-fieldset__contact { + margin-top: 28px; + } +} + @media (max-width: 800px){ fieldset.registrar-fieldset__contact { padding: 0; From 04ced13eb6bc4468e40b22cbf9d8503058749f95 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 12:08:53 -0600 Subject: [PATCH 036/171] Minor tweaks --- src/registrar/assets/js/get-gov.js | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index d18129456..315ff38f4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -890,7 +890,7 @@ function hideDeletedForms() { // Update the icon reference to the info icon if (parts.length > 1) { - parts[1] = "info"; + parts[1] = "info_outline"; useElement.setAttribute("xlink:href", parts.join("#")); // Change the color to => $dhs-dark-gray-60 @@ -938,6 +938,7 @@ function hideDeletedForms() { } function showInputOnErrorFields(){ + let fullNameButtonClicked = false document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) { let fieldIdParts = button.id.split("__") @@ -947,18 +948,26 @@ function hideDeletedForms() { let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); if (errorMessage) { let nameFields = ["first_name", "middle_name", "last_name"] + + button.click() + // If either the full_name field errors out, // or if any of its associated fields do - show all name related fields. // Otherwise, just show the problematic field. - if (fieldName == "full_name"){ - handleFullNameField(fieldName); - }else if (nameFields.includes(fieldName)){ - handleFullNameField("full_name"); - button.click() - } - else { - button.click() + if (nameFields.includes(fieldName) && !fullNameButtonClicked){ + fullNameButton = document.querySelector("#full_name__edit-button") + if (fullNameButton) { + fullNameButton.click() + fullNameButtonClicked = true + } + + let readonlyId = getReadonlyFieldId("full_name"); + let readonlyField = document.querySelector(readonlyId); + if (readonlyField) { + readonlyField.classList.toggle("overlapped-full-name-field"); + } } + } } }); From bb42732c80a2cf0f018480da47a14d0e6d151432 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 15 May 2024 15:24:19 -0600 Subject: [PATCH 037/171] Cleanup Pushing old commit content to save work already done --- src/registrar/config/urls.py | 4 +- src/registrar/forms/domain.py | 1 - .../{contact.py => finish_user_setup.py} | 4 +- .../forms/utility/wizard_form_helper.py | 1 - .../models/utility/generic_helper.py | 27 +- src/registrar/registrar_middleware.py | 68 ++-- src/registrar/views/__init__.py | 4 +- .../{contact.py => finish_user_setup.py} | 323 ++++++++---------- src/registrar/views/utility/mixins.py | 14 +- 9 files changed, 211 insertions(+), 235 deletions(-) rename src/registrar/forms/{contact.py => finish_user_setup.py} (94%) rename src/registrar/views/{contact.py => finish_user_setup.py} (63%) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 596e5c3d2..215e1740e 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -104,8 +104,8 @@ urlpatterns = [ # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. "finish-user-setup/", - views.ContactProfileSetupView.as_view(), - name="finish-contact-profile-setup", + views.FinishUserSetupView.as_view(), + name="finish-user-profile-setup", ), path( "domain-request//edit/", diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index db247ad21..da1462bdb 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -203,7 +203,6 @@ NameserverFormset = formset_factory( ) -# TODO - refactor, wait until daves PR class ContactForm(forms.ModelForm): """Form for updating contacts.""" diff --git a/src/registrar/forms/contact.py b/src/registrar/forms/finish_user_setup.py similarity index 94% rename from src/registrar/forms/contact.py rename to src/registrar/forms/finish_user_setup.py index 9a752bab4..46b75090f 100644 --- a/src/registrar/forms/contact.py +++ b/src/registrar/forms/finish_user_setup.py @@ -2,8 +2,8 @@ from django import forms from phonenumber_field.formfields import PhoneNumberField # type: ignore -class ContactForm(forms.Form): - """Form for adding or editing a contact""" +class FinishUserSetupForm(forms.Form): + """Form for adding or editing user information""" def clean(self): cleaned_data = super().clean() diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py index 2dd1a2b42..2ae50f908 100644 --- a/src/registrar/forms/utility/wizard_form_helper.py +++ b/src/registrar/forms/utility/wizard_form_helper.py @@ -22,7 +22,6 @@ class RegistrarForm(forms.Form): kwargs.setdefault("label_suffix", "") # save a reference to a domain request object self.domain_request = kwargs.pop("domain_request", None) - super(RegistrarForm, self).__init__(*args, **kwargs) def to_database(self, obj: DomainRequest | Contact): diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 861fcea07..5f92b5953 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,7 +2,8 @@ import time import logging - +from typing import Any +from urllib.parse import urlparse, urlunparse, urlencode logger = logging.getLogger(__name__) @@ -286,3 +287,27 @@ def from_database(form_class, obj): if obj is None: return {} return {name: getattr(obj, name) for name in form_class.declared_fields.keys()} # type: ignore + + +def replace_url_queryparams(url_to_modify: str, query_params: dict[Any, list]): + """ + Replaces the query parameters of a given URL. + + Args: + url_to_modify (str): The URL whose query parameters need to be modified. + query_params (dict): Dictionary of query parameters to use. + + Returns: + str: The modified URL with the updated query parameters. + """ + + # Split the URL into parts + url_parts = list(urlparse(url_to_modify)) + + # Modify the query param bit + url_parts[4] = urlencode(query_params) + + # Reassemble the URL + new_url = urlunparse(url_parts) + + return new_url \ No newline at end of file diff --git a/src/registrar/registrar_middleware.py b/src/registrar/registrar_middleware.py index 8dca06019..5707c4d5d 100644 --- a/src/registrar/registrar_middleware.py +++ b/src/registrar/registrar_middleware.py @@ -7,6 +7,8 @@ from django.urls import reverse from django.http import HttpResponseRedirect from waffle.decorators import flag_is_active +from registrar.models.utility.generic_helper import replace_url_queryparams + class CheckUserProfileMiddleware: """ @@ -28,49 +30,47 @@ class CheckUserProfileMiddleware: # Check that the user is "opted-in" to the profile feature flag has_profile_feature_flag = flag_is_active(request, "profile_feature") - # If they aren't, skip this entirely + # If they aren't, skip this check entirely if not has_profile_feature_flag: return None # Check if setup is not finished finished_setup = hasattr(request.user, "finished_setup") and request.user.finished_setup if request.user.is_authenticated and not finished_setup: - setup_page = reverse("finish-contact-profile-setup", kwargs={"pk": request.user.contact.pk}) - logout_page = reverse("logout") - excluded_pages = [ - setup_page, - logout_page, - ] - custom_redirect = None - - # In some cases, we don't want to redirect to home. - # This handles that. - if request.path == "/request/": - # This can be generalized if need be, but for now lets keep this easy to read. - custom_redirect = "domain-request:" - - # Don't redirect on excluded pages (such as the setup page itself) - if not any(request.path.startswith(page) for page in excluded_pages): - # Preserve the original query parameters, and coerce them into a dict - query_params = parse_qs(request.META["QUERY_STRING"]) - - if custom_redirect is not None: - # Set the redirect value to our redirect location - query_params["redirect"] = custom_redirect - - if query_params: - # Split the URL into parts - setup_page_parts = list(urlparse(setup_page)) - # Modify the query param bit - setup_page_parts[4] = urlencode(query_params) - # Reassemble the URL - setup_page = urlunparse(setup_page_parts) - - # Redirect to the setup page - return HttpResponseRedirect(setup_page) + return self._handle_setup_not_finished(request) # Continue processing the view return None + + def _handle_setup_not_finished(self, request): + setup_page = reverse("finish-user-profile-setup", kwargs={"pk": request.user.contact.pk}) + logout_page = reverse("logout") + excluded_pages = [ + setup_page, + logout_page, + ] + + # In some cases, we don't want to redirect to home. This handles that. + # Can easily be generalized if need be, but for now lets keep this easy to read. + custom_redirect = "domain-request:" if request.path == "/request/" else None + + # Don't redirect on excluded pages (such as the setup page itself) + if not any(request.path.startswith(page) for page in excluded_pages): + # Preserve the original query parameters, and coerce them into a dict + query_params = parse_qs(request.META["QUERY_STRING"]) + + if custom_redirect is not None: + # Set the redirect value to our redirect location + query_params["redirect"] = custom_redirect + + if query_params: + setup_page = replace_url_queryparams(setup_page, query_params) + + # Redirect to the setup page + return HttpResponseRedirect(setup_page) + else: + # Process the view as normal + return None class NoCacheMiddleware: diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index 692cfd4de..961d0d94b 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -14,8 +14,8 @@ from .domain import ( DomainInvitationDeleteView, DomainDeleteUserView, ) -from .contact import ( - ContactProfileSetupView, +from .finish_user_setup import ( + FinishUserSetupView, ) from .health import * from .index import * diff --git a/src/registrar/views/contact.py b/src/registrar/views/finish_user_setup.py similarity index 63% rename from src/registrar/views/contact.py rename to src/registrar/views/finish_user_setup.py index 257f0db01..f545b1be9 100644 --- a/src/registrar/views/contact.py +++ b/src/registrar/views/finish_user_setup.py @@ -1,13 +1,14 @@ +from enum import Enum from waffle.decorators import waffle_flag -from urllib.parse import urlencode, urlunparse, urlparse, quote +from urllib.parse import quote from django.urls import NoReverseMatch, reverse -from registrar.forms.contact import ContactForm +from registrar.forms.finish_user_setup import FinishUserSetupForm from django.contrib.messages.views import SuccessMessageMixin from registrar.models.contact import Contact from registrar.templatetags.url_helpers import public_site_url from registrar.views.utility.permission_views import ContactPermissionView from django.views.generic.edit import FormMixin -from registrar.models.utility.generic_helper import to_database, from_database +from registrar.models.utility.generic_helper import replace_url_queryparams, to_database, from_database from django.utils.safestring import mark_safe from django.utils.decorators import method_decorator @@ -19,27 +20,17 @@ logger = logging.getLogger(__name__) class BaseContactView(SuccessMessageMixin, ContactPermissionView): - """Provides a base view for the contact object. On get, the contact - is saved in the session and on self.object.""" def get_success_message(self, cleaned_data): """Content of the returned success message""" return "Contact updated successfully." def get(self, request, *args, **kwargs): - """Sets the current contact in cache, defines the current object as self.object - then returns render_to_response""" - self._set_contact(request) + self._update_object_and_session(request) context = self.get_context_data(object=self.object) return self.render_to_response(context) - def _set_contact(self, request): - """ - get contact from session cache or from db and set - to self.object - set session to self for downstream functions to - update session cache - """ + def _update_object_and_session(self, request): self.session = request.session contact_pk = "contact:" + str(self.kwargs.get("pk")) @@ -50,9 +41,9 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): else: self.object = self.get_object() - self._update_session_with_contact() + self._refresh_session() - def _update_session_with_contact(self): + def _refresh_session(self): """ Set contact pk in the session cache """ @@ -63,46 +54,30 @@ class BaseContactView(SuccessMessageMixin, ContactPermissionView): class ContactFormBaseView(BaseContactView, FormMixin): """Adds a FormMixin to BaseContactView, and handles post""" - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using BaseContactView and FormMixin - """ - # Set the current contact object in cache - self._set_contact(request) - - form = self.get_form() - - # Get the current form and validate it - return self.form_valid(form) if form.is_valid() else self.form_invalid(form) - def form_invalid(self, form): # updates session cache with contact - self._update_session_with_contact() + self._refresh_session() # superclass has the redirect return super().form_invalid(form) -class ContactProfileSetupView(ContactFormBaseView): +class FinishUserSetupView(ContactFormBaseView): """This view forces the user into providing additional details that we may have missed from Login.gov""" template_name = "finish_contact_setup.html" - form_class = ContactForm + form_class = FinishUserSetupForm model = Contact redirect_type = None - # TODO - make this an enum - class RedirectType: + class RedirectType(Enum): """ - Contains constants for each type of redirection. - Not an enum as we just need to track string values, - but we don't care about enforcing it. + Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. - HOME: We want to redirect to reverse("home") - - BACK_TO_SELF: We want to redirect back to reverse("finish-contact-profile-setup") + - BACK_TO_SELF: We want to redirect back to reverse("finish-user-profile-setup") - TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect" - COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE @@ -113,146 +88,6 @@ class ContactProfileSetupView(ContactFormBaseView): COMPLETE_SETUP = "complete_setup" TO_SPECIFIC_PAGE = "domain_request" - def get_success_message(self, cleaned_data): - """Content of the returned success message""" - return "Your profile has been successfully updated." - - # TODO - refactor - @waffle_flag("profile_feature") - @method_decorator(csrf_protect) - def dispatch(self, request, *args, **kwargs): - """ - Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. - - This method sets the redirect type based on the 'redirect' query parameter, - defaulting to BACK_TO_SELF if not provided. - It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. - - Returns: - HttpResponse: The response generated by the parent class's dispatch method. - """ - # Default redirect type - default_redirect = self.RedirectType.BACK_TO_SELF - - # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", None) - - is_default = False - # We set this here rather than in .get so we don't override - # existing data if no queryparam is present. - if redirect_type is None: - is_default = True - redirect_type = default_redirect - - # Set the default if nothing exists already - if self.redirect_type is None: - self.redirect_type = redirect_type - - if not is_default: - default_redirects = [ - self.RedirectType.HOME, - self.RedirectType.COMPLETE_SETUP, - self.RedirectType.BACK_TO_SELF, - self.RedirectType.TO_SPECIFIC_PAGE, - ] - if redirect_type not in default_redirects: - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - request.session["profile_setup_redirect_viewname"] = redirect_type - else: - self.redirect_type = redirect_type - - return super().dispatch(request, *args, **kwargs) - - def get_redirect_url(self): - """ - Returns a URL string based on the current value of self.redirect_type. - - Depending on self.redirect_type, constructs a base URL and appends a - 'redirect' query parameter. Handles different redirection types such as - HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. - - Returns: - str: The full URL with the appropriate query parameters. - """ - base_url = "" - query_params = {} - match self.redirect_type: - case self.RedirectType.HOME: - base_url = reverse("home") - case self.RedirectType.BACK_TO_SELF | self.RedirectType.COMPLETE_SETUP: - base_url = reverse("finish-contact-profile-setup", kwargs={"pk": self.object.pk}) - case self.RedirectType.TO_SPECIFIC_PAGE: - - # We only allow this session value to use viewnames, - # because otherwise this allows anyone to enter any value in here. - # This restricts what can be redirected to. - try: - desired_view = self.session["profile_setup_redirect_viewname"] - base_url = reverse(desired_view) - except NoReverseMatch as err: - logger.error(err) - logger.error("ContactProfileSetupView -> get_redirect_url -> Could not find specified page.") - base_url = reverse("home") - case _: - base_url = reverse("home") - - # Quote cleans up the value so that it can be used in a url - query_params["redirect"] = quote(self.redirect_type) - - # Parse the base URL - url_parts = list(urlparse(base_url)) - - # Update the query part of the URL - url_parts[4] = urlencode(query_params) - - # Construct the full URL with query parameters - full_url = urlunparse(url_parts) - return full_url - - def get_success_url(self): - """Redirect to the nameservers page for the domain.""" - redirect_url = self.get_redirect_url() - return redirect_url - - # TODO - delete session information - def post(self, request, *args, **kwargs): - """Form submission posts to this view. - - This post method harmonizes using BaseContactView and FormMixin - """ - - # Set the current contact object in cache - self._set_contact(request) - - form = self.get_form() - - # Get the current form and validate it - if form.is_valid(): - if "contact_setup_save_button" in request.POST: - # Logic for when the 'Save' button is clicked - self.redirect_type = self.RedirectType.COMPLETE_SETUP - elif "contact_setup_submit_button" in request.POST: - if "profile_setup_redirect_viewname" in self.session: - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - else: - self.redirect_type = self.RedirectType.HOME - - return self.form_valid(form) - else: - return self.form_invalid(form) - - def form_valid(self, form): - - completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] - if self.redirect_type in completed_states: - self.request.user.finished_setup = True - self.request.user.save() - - to_database(form=form, obj=self.object) - self._update_session_with_contact() - - return super().form_valid(form) - def get_initial(self): """The initial value for the form (which is a formset here).""" db_object = from_database(form_class=self.form_class, obj=self.object) @@ -276,3 +111,133 @@ class ContactProfileSetupView(ContactFormBaseView): "If the wrong email is displayed below, you’ll need to update your Login.gov account " f'and log back in. Get help with your Login.gov account.' ) # nosec + + + def get_success_message(self, cleaned_data): + """Content of the returned success message""" + return "Your profile has been successfully updated." + + @waffle_flag("profile_feature") + @method_decorator(csrf_protect) + def dispatch(self, request, *args, **kwargs): + """ + Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag. + + This method sets the redirect type based on the 'redirect' query parameter, + defaulting to BACK_TO_SELF if not provided. + It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE. + + Returns: + HttpResponse: The response generated by the parent class's dispatch method. + """ + + # Update redirect type based on the query parameter if present + redirect_type = request.GET.get("redirect", None) + + # We set this here rather than in .get so we don't override + # existing data if no queryparam is present. + is_default = redirect_type is None + if is_default: + # Set to the default + redirect_type = self.RedirectType.BACK_TO_SELF + self.redirect_type = redirect_type + else: + all_redirect_types = [r.value for r in self.RedirectType] + if redirect_type in all_redirect_types: + self.redirect_type = self.RedirectType(redirect_type) + else: + # If the redirect type is undefined, then we assume that + # we are specifying a particular page to redirect to. + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + + # Store the page that we want to redirect to for later use + request.session["redirect_viewname"] = str(redirect_type) + + return super().dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + """Form submission posts to this view. + + This post method harmonizes using BaseContactView and FormMixin + """ + + # Set the current object in cache + self._update_object_and_session(request) + + form = self.get_form() + + # Get the current form and validate it + if form.is_valid(): + if "contact_setup_save_button" in request.POST: + # Logic for when the 'Save' button is clicked + self.redirect_type = self.RedirectType.COMPLETE_SETUP + elif "contact_setup_submit_button" in request.POST: + if "redirect_viewname" in self.session: + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + else: + self.redirect_type = self.RedirectType.HOME + + return self.form_valid(form) + else: + return self.form_invalid(form) + + def form_valid(self, form): + """Saves the current contact to the database, and if the user is complete + with their setup, then we mark user.finished_setup to True.""" + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] + if self.redirect_type in completed_states: + self.request.user.finished_setup = True + self.request.user.save() + + to_database(form=form, obj=self.object) + self._refresh_session() + + return super().form_valid(form) + + def get_success_url(self): + """Redirect to the nameservers page for the domain.""" + redirect_url = self.get_redirect_url() + return redirect_url + + def get_redirect_url(self): + """ + Returns a URL string based on the current value of self.redirect_type. + + Depending on self.redirect_type, constructs a base URL and appends a + 'redirect' query parameter. Handles different redirection types such as + HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE. + + Returns: + str: The full URL with the appropriate query parameters. + """ + + # These redirect types redirect to the same page + self_redirect = [ + self.RedirectType.BACK_TO_SELF, + self.RedirectType.COMPLETE_SETUP + ] + + # Maps the redirect type to a URL + base_url = "" + try: + if self.redirect_type in self_redirect: + base_url = reverse("finish-user-profile-setup", kwargs={"pk": self.object.pk}) + elif self.redirect_type == self.RedirectType.TO_SPECIFIC_PAGE: + # We only allow this session value to use viewnames, + # because this restricts what can be redirected to. + desired_view = self.session["redirect_viewname"] + base_url = reverse(desired_view) + else: + base_url = reverse("home") + except NoReverseMatch as err: + logger.error(f"get_redirect_url -> Could not find the specified page. Err: {err}") + + query_params = {} + + # Quote cleans up the value so that it can be used in a url + query_params["redirect"] = quote(self.redirect_type.value) + + # Generate the full url from the given query params + full_url = replace_url_queryparams(base_url, query_params) + return full_url + diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 3e5e10816..6f5c08b98 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -358,19 +358,7 @@ class ContactPermission(PermissionsLoginMixin): if not requested_user_exists or not requested_contact_exists: return False - # Check if the user has an associated contact - associated_contacts = Contact.objects.filter(user=current_user) - associated_contacts_length = len(associated_contacts) - - if associated_contacts_length == 0: - # This means that the user trying to access this page - # is a different user than the contact holder. - return False - elif associated_contacts_length > 1: - # TODO - change this - raise ValueError("User has multiple connected contacts") - else: - return True + return True class DomainRequestPermissionWithdraw(PermissionsLoginMixin): From 5ed0a56d79b997ba956f271557ee41a615550f85 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 16 May 2024 09:50:08 -0600 Subject: [PATCH 038/171] Fix migration --- ...{0094_user_finished_setup.py => 0095_user_finished_setup.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/registrar/migrations/{0094_user_finished_setup.py => 0095_user_finished_setup.py} (83%) diff --git a/src/registrar/migrations/0094_user_finished_setup.py b/src/registrar/migrations/0095_user_finished_setup.py similarity index 83% rename from src/registrar/migrations/0094_user_finished_setup.py rename to src/registrar/migrations/0095_user_finished_setup.py index 660f950c0..87e247330 100644 --- a/src/registrar/migrations/0094_user_finished_setup.py +++ b/src/registrar/migrations/0095_user_finished_setup.py @@ -6,7 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0093_alter_publiccontact_unique_together"), + ("registrar", "0094_create_groups_v12"), ] operations = [ From 7f42ecb86786a9b9841aa5a01c7fc4b04b322d71 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Fri, 17 May 2024 13:03:08 -0700 Subject: [PATCH 039/171] Adding biz logic in --- src/registrar/assets/sass/_theme/_forms.scss | 7 +- .../templates/domain_request_form.html | 17 +++ .../templates/domain_request_review.html | 2 +- src/registrar/views/domain_request.py | 101 +++++++++++++++--- 4 files changed, 112 insertions(+), 15 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 058a9f6c8..bdef522eb 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -32,6 +32,11 @@ border-left: none; } +.incomplete { + color: red; + font-weight: bold; +} + legend.float-left-tablet + button.float-right-tablet { margin-top: .5rem; @include at-media('tablet') { @@ -52,4 +57,4 @@ legend.float-left-tablet + button.float-right-tablet { background-color: var(--body-fg); color: var(--close-button-hover-bg); } -} \ No newline at end of file +} diff --git a/src/registrar/templates/domain_request_form.html b/src/registrar/templates/domain_request_form.html index cde12ad80..405876da6 100644 --- a/src/registrar/templates/domain_request_form.html +++ b/src/registrar/templates/domain_request_form.html @@ -84,6 +84,13 @@ value="save_and_return" class="usa-button usa-button--outline" >Save and return to manage your domains + {% else %} + + {% block after_form_content %}{% endblock %} diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 5f359e95f..88a9c128e 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -37,7 +37,7 @@ {% if step == Step.TRIBAL_GOVERNMENT %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete" %} + {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% if domain_request.federally_recognized_tribe %}

Federally-recognized tribe

{% endif %} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index f93976138..c233b01d0 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -374,25 +374,100 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } return [key for key, value in history_dict.items() if value] + + def _form_complete(self): + # So in theory each part of the form individually should be already doing the check, correct? + # In theory, that means we just only need to check for the title pages that are completed which is + # Technically some of these don't even show up at all depending on which "state" its in or chosen + + # If we choose Federal -> check "Federal government branch (has defaults) + # If we choose Interstate -> check "About your organization" + # If we choose State -> check "Election office" (has default) + # If we choose Tribal -> check "Tribal name" and "Election office" + # County -> "Election office" + # City -> "Election office" + # Special district -> "Election office" and "About your organization" + # School district -> clears common + + + if ( + # (self.domain_request.tribe_name is None) + (self.domain_request.generic_org_type is None) + or (self.domain_request.tribe_name is None) + or (self.domain_request.federal_type is None) + or (self.domain_request.is_election_board is None) + or ( + self.domain_request.federal_agency is None + or self.domain_request.organization_name is None + or self.domain_request.address_line1 is None + or self.domain_request.city is None + or self.domain_request.state_territory is None + or self.domain_request.zipcode is None + or self.domain_request.urbanization is None + ) # organization contact + or (self.domain_request.about_your_organization is None) + or (self.domain_request.authorizing_official is None) + or ( + self.domain_request.current_websites.exists() or self.domain_request.requested_domain is None + ) # for current_sites + or (self.domain_request.requested_domain is None) # for dotgov_domain + or (self.domain_request.purpose is None) + or (self.domain_request.submitter is None) # your_contact + or (self.domain_request.other_contacts is None) + or ( + (self.domain_request.anything_else is None and self.domain_request.cisa_representative_email) + or self.domain_request.is_policy_acknowledged is None + ) # additional detail + or (self.domain_request.is_policy_acknowledged is None) # review + ): + # print("!!!!!! self.domain_request.tribe_name is", self.domain_request.tribe_name) + # context = self.get_context_data() + # context["forms"] = self.get_forms() + # context["form_is_not_complete"] = False + + return False + else: + # print("!!!!!! self.domain_request.tribe_name is", self.domain_request.tribe_name) + return True + + # return None + def get_context_data(self): """Define context for access on all wizard pages.""" # Build the submit button that we'll pass to the modal. modal_button = '" # Concatenate the modal header that we'll pass to the modal. - if self.domain_request.requested_domain: - modal_heading = "You are about to submit a domain request for " + str(self.domain_request.requested_domain) - else: - modal_heading = "You are about to submit an incomplete request" - return { - "form_titles": self.TITLES, - "steps": self.steps, - # Add information about which steps should be unlocked - "visited": self.storage.get("step_history", []), - "is_federal": self.domain_request.is_federal(), - "modal_button": modal_button, - "modal_heading": modal_heading, - } + # TODO: Still need to log! + context_stuff = {} + print("!!!!!!!!! before form complete") + print("!!!!!!!!! self.form_complete is", self._form_complete()) + if self._form_complete(): + print("!!!!!!!in form complete section") + context_stuff = { + "form_titles": self.TITLES, + "steps": self.steps, + # Add information about which steps should be unlocked + "visited": self.storage.get("step_history", []), + "is_federal": self.domain_request.is_federal(), + "modal_button": modal_button, + "modal_heading": "You are about to submit a domain request for " + + str(self.domain_request.requested_domain), + } + else: # form is not complete + print("!!!!!!! form is not complete") + context_stuff = { + "form_titles": self.TITLES, + "steps": self.steps, + # Add information about which steps should be unlocked + "visited": self.storage.get("step_history", []), + "is_federal": self.domain_request.is_federal(), + # "modal_button": We'll have to set some kind of go back button + # And fix wording in text for domain_request_form + "modal_heading": "You can’t submit this request", + } + + return context_stuff def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" From 9d576ed1cbbe24f0e225cedcdd9c06a92c054326 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 20 May 2024 10:15:53 -0600 Subject: [PATCH 040/171] Simplify --- src/registrar/views/finish_user_setup.py | 32 +++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/registrar/views/finish_user_setup.py b/src/registrar/views/finish_user_setup.py index f545b1be9..75eddb011 100644 --- a/src/registrar/views/finish_user_setup.py +++ b/src/registrar/views/finish_user_setup.py @@ -132,26 +132,18 @@ class FinishUserSetupView(ContactFormBaseView): """ # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", None) + redirect_type = request.GET.get("redirect", self.RedirectType.BACK_TO_SELF) - # We set this here rather than in .get so we don't override - # existing data if no queryparam is present. - is_default = redirect_type is None - if is_default: - # Set to the default - redirect_type = self.RedirectType.BACK_TO_SELF - self.redirect_type = redirect_type + all_redirect_types = [r.value for r in self.RedirectType] + if redirect_type in all_redirect_types: + self.redirect_type = self.RedirectType(redirect_type) else: - all_redirect_types = [r.value for r in self.RedirectType] - if redirect_type in all_redirect_types: - self.redirect_type = self.RedirectType(redirect_type) - else: - # If the redirect type is undefined, then we assume that - # we are specifying a particular page to redirect to. - self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE + # If the redirect type is undefined, then we assume that + # we are specifying a particular page to redirect to. + self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE - # Store the page that we want to redirect to for later use - request.session["redirect_viewname"] = str(redirect_type) + # Store the page that we want to redirect to for later use + request.session["redirect_viewname"] = str(redirect_type) return super().dispatch(request, *args, **kwargs) @@ -168,6 +160,12 @@ class FinishUserSetupView(ContactFormBaseView): # Get the current form and validate it if form.is_valid(): + + completed_states = [self.RedirectType.TO_SPECIFIC_PAGE, self.RedirectType.HOME] + if self.redirect_type in completed_states: + self.request.user.finished_setup = True + self.request.user.save() + if "contact_setup_save_button" in request.POST: # Logic for when the 'Save' button is clicked self.redirect_type = self.RedirectType.COMPLETE_SETUP From 9a6ccc1c44d136ff12e1a2ff910f705dd3b5f266 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 20 May 2024 11:25:32 -0600 Subject: [PATCH 041/171] Replace form with modelform Pull in content from related PR and refactor around it --- src/registrar/assets/js/get-gov.js | 29 +-- .../assets/sass/_theme/_fieldsets.scss | 23 +- src/registrar/config/urls.py | 4 +- src/registrar/forms/finish_user_setup.py | 48 ---- src/registrar/forms/user_profile.py | 30 ++- src/registrar/models/contact.py | 7 - ...t_setup.html => finish_profile_setup.html} | 18 +- src/registrar/views/__init__.py | 5 +- src/registrar/views/finish_user_setup.py | 241 ------------------ src/registrar/views/user_profile.py | 188 +++++++++++++- 10 files changed, 230 insertions(+), 363 deletions(-) delete mode 100644 src/registrar/forms/finish_user_setup.py rename src/registrar/templates/{finish_contact_setup.html => finish_profile_setup.html} (82%) delete mode 100644 src/registrar/views/finish_user_setup.py diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 315ff38f4..2fd534f04 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -873,35 +873,16 @@ function hideDeletedForms() { let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#contact-full-name-fieldset"); + let nameFieldset = document.querySelector("#profile-name-fieldset"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } if (inputField) { - let readonlyId = getReadonlyFieldId(fieldName) - let readonlyField = document.querySelector(readonlyId) - if (readonlyField) { - // Update the element's xlink:href attribute - let useElement = readonlyField.querySelector("use"); - if (useElement) { - let currentHref = useElement.getAttribute("xlink:href"); - let parts = currentHref.split("#"); - - // Update the icon reference to the info icon - if (parts.length > 1) { - parts[1] = "info_outline"; - useElement.setAttribute("xlink:href", parts.join("#")); - - // Change the color to => $dhs-dark-gray-60 - useElement.closest('svg').style.fill = '#444547'; - } - } - - let parentDiv = readonlyField.closest("div"); - if (parentDiv) { - parentDiv.classList.toggle("overlapped-full-name-field"); - } + // Remove the "full_name" field + inputFieldParentDiv = inputField.closest("div"); + if (inputFieldParentDiv) { + inputFieldParentDiv.remove(); } } } diff --git a/src/registrar/assets/sass/_theme/_fieldsets.scss b/src/registrar/assets/sass/_theme/_fieldsets.scss index da900f128..7ad0a2a82 100644 --- a/src/registrar/assets/sass/_theme/_fieldsets.scss +++ b/src/registrar/assets/sass/_theme/_fieldsets.scss @@ -10,23 +10,8 @@ fieldset:not(:first-child) { } fieldset.registrar-fieldset__contact { - border-width: 2px; - border-left: none; - border-right: none; - border-bottom: none; - padding-bottom: 0; + // This fieldset is for SR purposes only + border: 0; + margin: 0; + padding: 0; } - -@media (min-width: 800px) { - fieldset.registrar-fieldset__contact { - margin-top: 28px; - } -} - -@media (max-width: 800px){ - fieldset.registrar-fieldset__contact { - padding: 0; - margin: 0; - border: none; - } -} \ No newline at end of file diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 5ed9adae0..2d4c33569 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -103,8 +103,8 @@ urlpatterns = [ path( # We embed the current user ID here, but we have a permission check # that ensures the user is who they say they are. - "finish-user-setup/", - views.FinishUserSetupView.as_view(), + "finish-profile-setup/", + views.FinishProfileSetupView.as_view(), name="finish-user-profile-setup", ), path( diff --git a/src/registrar/forms/finish_user_setup.py b/src/registrar/forms/finish_user_setup.py deleted file mode 100644 index 46b75090f..000000000 --- a/src/registrar/forms/finish_user_setup.py +++ /dev/null @@ -1,48 +0,0 @@ -from django import forms -from phonenumber_field.formfields import PhoneNumberField # type: ignore - - -class FinishUserSetupForm(forms.Form): - """Form for adding or editing user information""" - - def clean(self): - cleaned_data = super().clean() - # Remove the full name property - if "full_name" in cleaned_data: - # Delete the full name element as its purely decorative. - # We include it as a normal Charfield for all the advantages - # and utility that it brings, but we're playing pretend. - del cleaned_data["full_name"] - return cleaned_data - - full_name = forms.CharField( - label="Full name", - error_messages={"required": "Enter your full name"}, - ) - first_name = forms.CharField( - label="First name / given name", - error_messages={"required": "Enter your first name / given name."}, - ) - middle_name = forms.CharField( - required=False, - label="Middle name (optional)", - ) - last_name = forms.CharField( - label="Last name / family name", - error_messages={"required": "Enter your last name / family name."}, - ) - title = forms.CharField( - label="Title or role in your organization", - error_messages={ - "required": ("Enter your title or role in your organization (e.g., Chief Information Officer).") - }, - ) - email = forms.EmailField( - label="Organization email", - required=False, - max_length=None, - ) - phone = PhoneNumberField( - label="Phone", - error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."}, - ) diff --git a/src/registrar/forms/user_profile.py b/src/registrar/forms/user_profile.py index 036b03751..abc90d195 100644 --- a/src/registrar/forms/user_profile.py +++ b/src/registrar/forms/user_profile.py @@ -55,6 +55,34 @@ class UserProfileForm(forms.ModelForm): "required": "Enter your email address in the required format, like name@example.com." } self.fields["phone"].error_messages["required"] = "Enter your phone number." - self.domainInfo = None DomainHelper.disable_field(self.fields["email"], disable_required=True) + + +class FinishSetupProfileForm(UserProfileForm): + """Form for updating user profile.""" + + full_name = forms.CharField(required=True, label="Full name") + + def clean(self): + cleaned_data = super().clean() + # Remove the full name property + if "full_name" in cleaned_data: + # Delete the full name element as its purely decorative. + # We include it as a normal Charfield for all the advantages + # and utility that it brings, but we're playing pretend. + del cleaned_data["full_name"] + return cleaned_data + + def __init__(self, *args, **kwargs): + """Override the inerited __init__ method to update the fields.""" + + super().__init__(*args, **kwargs) + + # Set custom form label for email + self.fields["email"].label = "Organization email" + self.fields["title"].label = "Title or role in your organization" + + # Define the "full_name" value + if self.instance and hasattr(self.instance, 'full_name'): + self.fields["full_name"].initial = self.instance.get_formatted_name() diff --git a/src/registrar/models/contact.py b/src/registrar/models/contact.py index 716849b32..0f55ed863 100644 --- a/src/registrar/models/contact.py +++ b/src/registrar/models/contact.py @@ -102,13 +102,6 @@ class Contact(TimeStampedModel): names = [n for n in [self.first_name, self.middle_name, self.last_name] if n] return " ".join(names) if names else "Unknown" - @property - def full_name(self): - """ - Returns the full name (first_name, middle_name, last_name) of this contact. - """ - return self.get_formatted_name() - def has_contact_info(self): return bool(self.title or self.email or self.phone) diff --git a/src/registrar/templates/finish_contact_setup.html b/src/registrar/templates/finish_profile_setup.html similarity index 82% rename from src/registrar/templates/finish_contact_setup.html rename to src/registrar/templates/finish_profile_setup.html index 301f5837a..a8e16724e 100644 --- a/src/registrar/templates/finish_contact_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -62,25 +62,17 @@ Your contact information - - {# TODO: if an error is thrown here or edit clicked, show first last and middle fields #} - {# Also todo: consolidate all of the scattered classes into this usa form one #} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-readonly padding-top-2" %} {% input_with_errors form.full_name %} {% endwith %} -
diff --git a/src/registrar/templates/includes/profile_form.html b/src/registrar/templates/includes/profile_form.html index 378bb66a4..ce337b4d5 100644 --- a/src/registrar/templates/includes/profile_form.html +++ b/src/registrar/templates/includes/profile_form.html @@ -48,4 +48,4 @@
-{% endblock profile_form %} \ No newline at end of file +{% endblock profile_form %} diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 6215d1fdc..045641ef9 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -523,6 +523,10 @@ class HomeTests(TestWithUser): class FinishUserProfileTests(TestWithUser, WebTest): """A series of tests that target the finish setup page for user profile""" + # csrf checks do not work well with WebTest. + # We disable them here. + csrf_checks = False + def setUp(self): super().setUp() self.user.title = None @@ -556,7 +560,8 @@ class FinishUserProfileTests(TestWithUser, WebTest): """Tests that a new user is redirected to the profile setup page when profile_feature is on""" self.app.set_user(self.incomplete_user.username) with override_flag("profile_feature", active=True): - # This will redirect the user to the setup page + # This will redirect the user to the setup page. + # Follow implicity checks if our redirect is working. finish_setup_page = self.app.get(reverse("home")).follow() self._set_session_cookie() @@ -578,10 +583,14 @@ class FinishUserProfileTests(TestWithUser, WebTest): finish_setup_form["phone"] = "(201) 555-0123" finish_setup_form["title"] = "CEO" finish_setup_form["last_name"] = "example" - completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True) + save_page = self._submit_form_webtest(finish_setup_form, follow=True) - self.assertEqual(completed_setup_page.status_code, 200) - # Assert that we're on the home page + self.assertEqual(save_page.status_code, 200) + self.assertContains(save_page, "Your profile has been updated.") + + # Try to navigate back to the home page. + # This is the same as clicking the back button. + completed_setup_page = self.app.get(reverse("home")) self.assertContains(completed_setup_page, "Manage your domain") @less_console_noise_decorator From 32135ef4b917bace506cef26bee80a1fbef0d7b1 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 21 May 2024 15:55:13 -0600 Subject: [PATCH 059/171] Cleanup js --- src/djangooidc/backends.py | 1 - src/djangooidc/views.py | 2 -- src/registrar/assets/js/get-gov.js | 6 +----- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/djangooidc/backends.py b/src/djangooidc/backends.py index 25179f7fd..41e442f2d 100644 --- a/src/djangooidc/backends.py +++ b/src/djangooidc/backends.py @@ -23,7 +23,6 @@ class OpenIdConnectBackend(ModelBackend): def authenticate(self, request, **kwargs): logger.debug("kwargs %s" % kwargs) user = None - if not kwargs or "sub" not in kwargs.keys(): return user diff --git a/src/djangooidc/views.py b/src/djangooidc/views.py index bbf708d1c..815df4ecf 100644 --- a/src/djangooidc/views.py +++ b/src/djangooidc/views.py @@ -113,12 +113,10 @@ def login_callback(request): user.save() login(request, user) - logger.info("Successfully logged in user %s" % user) # Clear the flag if the exception is not caught request.session.pop("redirect_attempted", None) - return redirect(request.session.get("next", "/")) else: raise o_e.BannedUser() diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2e536c973..2958399ec 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -920,11 +920,9 @@ function hideDeletedForms() { function showInputOnErrorFields(){ document.addEventListener('DOMContentLoaded', function() { - let form = document.querySelector("#finish-profile-setup-form"); - console.log(`form: ${form}`) // Get all input elements within the form + let form = document.querySelector("#finish-profile-setup-form"); let inputs = form ? form.querySelectorAll("input") : null; - console.log(`look: ${inputs}`) if (!inputs) { return null; } @@ -933,13 +931,11 @@ function hideDeletedForms() { inputs.forEach(function(input) { let fieldName = input.name; let errorMessage = document.querySelector(`#id_${fieldName}__error-message`); - console.log(`fieldName: ${inputs} vs err message ${errorMessage}`) if (!fieldName || !errorMessage) { return null; } let editButton = document.querySelector(`#${fieldName}__edit-button`); - console.log(`edit button is ${editButton} vs id #${fieldName}__edit-button`) if (editButton){ // Show the input field of the field that errored out editButton.click(); From 373538c78a8976484c1994c7c4e768ea1589e783 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 22 May 2024 10:30:47 -0600 Subject: [PATCH 060/171] Cleanup --- .../templates/finish_profile_setup.html | 4 +- src/registrar/tests/test_views.py | 40 ++++++++++--------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/registrar/templates/finish_profile_setup.html b/src/registrar/templates/finish_profile_setup.html index d1bae6fa3..4b087124d 100644 --- a/src/registrar/templates/finish_profile_setup.html +++ b/src/registrar/templates/finish_profile_setup.html @@ -5,7 +5,7 @@ {# Disable the redirect #} {% block logo %} - {% include "includes/gov_extended_logo.html" with logo_clickable=False %} + {% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %} {% endblock %} {# Add the new form #} @@ -16,5 +16,5 @@ {% endblock content_bottom %} {% block footer %} - {% include "includes/footer.html" %} + {% include "includes/footer.html" with show_manage_your_domains=confirm_changes %} {% endblock footer %} \ No newline at end of file diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 045641ef9..4a73e4c8b 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -55,9 +55,13 @@ class TestWithUser(MockEppLib): first_name = "First" last_name = "Last" email = "info@example.com" + phone = "8003111234" self.user = get_user_model().objects.create( - username=username, first_name=first_name, last_name=last_name, email=email + username=username, first_name=first_name, last_name=last_name, email=email, phone=phone ) + title="test title" + self.user.contact.title = title + self.user.save() username_incomplete = "test_user_incomplete" first_name_2 = "Incomplete" @@ -671,7 +675,7 @@ class UserProfileTests(TestWithUser, WebTest): assume that the same test results hold true for 401 and 403.""" with override_flag("profile_feature", active=True): with self.assertRaises(Exception): - response = self.client.get(reverse("home")) + response = self.client.get(reverse("home"), follow=True) self.assertEqual(response.status_code, 500) self.assertContains(response, "Your profile") @@ -691,49 +695,49 @@ class UserProfileTests(TestWithUser, WebTest): def test_home_page_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of home page when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/") + response = self.client.get("/", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_home_page_main_nav_with_profile_feature_off(self): """test that Your profile is not in main nav of home page when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/") + response = self.client.get("/", follow=True) self.assertNotContains(response, "Your profile") @less_console_noise_decorator def test_new_request_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of new request when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/request/") + response = self.client.get("/request/", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_new_request_main_nav_with_profile_feature_off(self): """test that Your profile is not in main nav of new request when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/request/") + response = self.client.get("/request/", follow=True) self.assertNotContains(response, "Your profile") @less_console_noise_decorator def test_user_profile_main_nav_with_profile_feature_on(self): """test that Your profile is in main nav of user profile when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get("/user-profile") + response = self.client.get("/user-profile", follow=True) self.assertContains(response, "Your profile") @less_console_noise_decorator def test_user_profile_returns_404_when_feature_off(self): """test that Your profile returns 404 when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get("/user-profile") + response = self.client.get("/user-profile", follow=True) self.assertEqual(response.status_code, 404) @less_console_noise_decorator def test_domain_detail_profile_feature_on(self): """test that domain detail view when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(reverse("domain", args=[self.domain.pk])) + response = self.client.get(reverse("domain", args=[self.domain.pk]), follow=True) self.assertContains(response, "Your profile") self.assertNotContains(response, "Your contact information") @@ -741,14 +745,14 @@ class UserProfileTests(TestWithUser, WebTest): def test_domain_your_contact_information_when_profile_feature_off(self): """test that Your contact information is accessible when profile_feature is off""" with override_flag("profile_feature", active=False): - response = self.client.get(f"/domain/{self.domain.id}/your-contact-information") + response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True) self.assertContains(response, "Your contact information") @less_console_noise_decorator def test_domain_your_contact_information_when_profile_feature_on(self): """test that Your contact information is not accessible when profile feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(f"/domain/{self.domain.id}/your-contact-information") + response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True) self.assertEqual(response.status_code, 404) @less_console_noise_decorator @@ -765,9 +769,9 @@ class UserProfileTests(TestWithUser, WebTest): submitter=contact_user, ) with override_flag("profile_feature", active=True): - response = self.client.get(f"/domain-request/{domain_request.id}") + response = self.client.get(f"/domain-request/{domain_request.id}", follow=True) self.assertContains(response, "Your profile") - response = self.client.get(f"/domain-request/{domain_request.id}/withdraw") + response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True) self.assertContains(response, "Your profile") # cleanup domain_request.delete() @@ -787,9 +791,9 @@ class UserProfileTests(TestWithUser, WebTest): submitter=contact_user, ) with override_flag("profile_feature", active=False): - response = self.client.get(f"/domain-request/{domain_request.id}") + response = self.client.get(f"/domain-request/{domain_request.id}", follow=True) self.assertNotContains(response, "Your profile") - response = self.client.get(f"/domain-request/{domain_request.id}/withdraw") + response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True) self.assertNotContains(response, "Your profile") # cleanup domain_request.delete() @@ -800,16 +804,14 @@ class UserProfileTests(TestWithUser, WebTest): """test user profile form submission""" self.app.set_user(self.user.username) with override_flag("profile_feature", active=True): - profile_page = self.app.get(reverse("user-profile")) + profile_page = self.app.get(reverse("user-profile")).follow() session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) profile_form = profile_page.form profile_page = profile_form.submit() self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # assert that first result contains errors - self.assertContains(profile_page, "Enter your title") - self.assertContains(profile_page, "Enter your phone number") + profile_form = profile_page.form profile_form["title"] = "sample title" profile_form["phone"] = "(201) 555-1212" From 03afa329c5544b39dde9a41de187c7b625af3382 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 22 May 2024 11:45:48 -0600 Subject: [PATCH 061/171] Cleanup --- src/registrar/tests/test_views.py | 2 +- src/registrar/views/domain_request.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 4a73e4c8b..595544526 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -59,7 +59,7 @@ class TestWithUser(MockEppLib): self.user = get_user_model().objects.create( username=username, first_name=first_name, last_name=last_name, email=email, phone=phone ) - title="test title" + title = "test title" self.user.contact.title = title self.user.save() diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index a90eaf271..67e01e5be 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -15,6 +15,7 @@ from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView +from waffle.decorators import flag_is_active, waffle_flag from .utility import ( DomainRequestPermissionView, @@ -400,7 +401,15 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] + excluded_steps = [ + Step.YOUR_CONTACT + ] + should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: + + if should_exclude and step in excluded_steps: + continue + condition = self.WIZARD_CONDITIONS.get(step, True) if callable(condition): condition = condition(self) @@ -540,6 +549,10 @@ class YourContact(DomainRequestWizard): template_name = "domain_request_your_contact.html" forms = [forms.YourContactForm] + @waffle_flag("!profile_feature") # type: ignore + def dispatch(self, request, *args, **kwargs): # type: ignore + return super().dispatch(request, *args, **kwargs) + class OtherContacts(DomainRequestWizard): template_name = "domain_request_other_contacts.html" From 27906bc3100547b96d565ae63c0a6727cea6583a Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 22 May 2024 13:36:20 -0700 Subject: [PATCH 062/171] Update from views to models to testing in backend --- src/registrar/models/domain_request.py | 104 +++++ src/registrar/tests/test_models.py | 195 +++++++++ src/registrar/tests/test_views_request.py | 511 ++++++++++++---------- src/registrar/views/domain_request.py | 175 ++++---- 4 files changed, 660 insertions(+), 325 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 2501cdc87..4a1d71964 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -938,3 +938,107 @@ class DomainRequest(TimeStampedModel): for field in opts.many_to_many: data[field.name] = field.value_from_object(self) return data + + def _is_federal_complete(self): + # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None + return not (self.federal_type is None or self.federal_agency is None) + + def _is_interstate_complete(self): + # Interstate -> "About your organization" page can't be empty + return self.about_your_organization is not None + + def _is_state_or_territory_complete(self): + # State -> ""Election office" page can't be empty + return self.is_election_board is not None + + def _is_tribal_complete(self): + # Tribal -> "Tribal name" and "Election office" page can't be empty + return self.tribe_name is not None and self.is_election_board is not None + + def _is_county_complete(self): + # County -> "Election office" page can't be empty + return self.is_election_board is not None + + def _is_city_complete(self): + # City -> "Election office" page can't be empty + return self.is_election_board is not None + + def _is_special_district_complete(self): + # Special District -> "Election office" and "About your organization" page can't be empty + return ( + self.is_election_board is not None + and self.about_your_organization is not None + ) + + def _is_organization_name_and_address_complete(self): + return not ( + self.organization_name is None + or self.address_line1 is None + or self.city is None + or self.state_territory is None + or self.zipcode is None + ) + + def _is_authorizing_official_complete(self): + return self.authorizing_official is not None + + def _is_requested_domain_complete(self): + return self.requested_domain is not None + + def _is_purpose_complete(self): + return self.purpose is not None + + def _is_submitter_complete(self): + return self.submitter is not None + + def _is_other_contacts_complete(self): + return self.other_contacts is not None + + def _is_additional_details_complete(self): + return not ( + self.has_cisa_representative is None + or self.has_anything_else_text is None + # RARE EDGE CASE: You click yes on having a cisa rep, but you dont type in email (should block in form) + or ( + self.has_cisa_representative is True + and self.cisa_representative_email is None + ) + or self.is_policy_acknowledged is None + ) + + def _is_general_form_complete(self): + return ( + self._is_organization_name_and_address_complete() + and self._is_authorizing_official_complete() + and self._is_requested_domain_complete() + and self._is_purpose_complete() + and self._is_submitter_complete() + and self._is_other_contacts_complete() + and self._is_additional_details_complete() + ) + + def _form_complete(self): + if self.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL: + is_complete = self._is_federal_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.INTERSTATE: + is_complete = self._is_interstate_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.STATE_OR_TERRITORY: + is_complete = self._is_state_or_territory_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL: + is_complete = self._is_tribal_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.COUNTY: + is_complete = self._is_county_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.CITY: + is_complete = self._is_city_complete() + elif self.generic_org_type == DomainRequest.OrganizationChoices.SPECIAL_DISTRICT: + is_complete = self._is_special_district_complete() + else: + # NOTE: This shouldn't happen, this is only if somehow they didn't choose an org type + is_complete = False + + if not is_complete or not self._is_general_form_complete(): + print("!!!! We are in the False if statement - form is not complete") + return False + + print("!!!! We are in the True if statement - form is complete") + return True \ No newline at end of file diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index fa074c3c6..849f92176 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1,6 +1,7 @@ from django.test import TestCase from django.db.utils import IntegrityError from unittest.mock import patch +from django.contrib.auth import get_user_model from registrar.models import ( Contact, @@ -1602,3 +1603,197 @@ class TestDomainInformationCustomSave(TestCase): ) self.assertEqual(domain_information_election.is_election_board, True) self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY) + +class TestDomainRequestIncomplete(TestCase): + def setUp(self): + super().setUp() + username = "test_user" + first_name = "First" + last_name = "Last" + email = "info@example.com" + self.user = get_user_model().objects.create( + username=username, first_name=first_name, last_name=last_name, email=email + ) + ao, _ = Contact.objects.get_or_create( + first_name="Meowy", + last_name="Meoward", + title="Chief Cat", + email="meoward@chiefcat.com", + phone="(206) 206 2060", + ) + draft_domain, _ = DraftDomain.objects.get_or_create(name="MeowardMeowardMeoward.gov") + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(555) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(555) 555 5557", + ) + # domain, _ = Domain.objects.get_or_create(name="MeowardMeowardMeoward.gov") + alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov") + current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com") + self.domain_request = DomainRequest.objects.create( + generic_org_type=DomainRequest.OrganizationChoices.FEDERAL, + federal_type="executive", + federal_agency=FederalAgency.objects.get(agency="AMTRAK"), + about_your_organization="Some description", + is_election_board=True, + tribe_name="Some tribe name", + organization_name="Some organization", + address_line1="address 1", + state_territory="CA", + zipcode="94044", + authorizing_official=ao, + requested_domain=draft_domain, + purpose="Some purpose", + submitter=you, + has_cisa_representative=False, + has_anything_else_text="Some text", + is_policy_acknowledged=True, + creator=self.user, + ) + + self.domain_request.other_contacts.add(other) + self.domain_request.current_websites.add(current) + self.domain_request.alternative_domains.add(alt) + + def test_is_federal_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.FEDERAL + self.assertTrue(self.domain_request._is_federal_complete()) + self.domain_request.federal_type = None + self.assertFalse(self.domain_request._is_federal_complete()) + + def test_is_interstate_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE + self.assertTrue(self.domain_request._is_interstate_complete()) + self.domain_request.about_your_organization = None + self.assertFalse(self.domain_request._is_interstate_complete()) + + def test_is_state_or_territory_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY + self.assertTrue(self.domain_request._is_state_or_territory_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._is_state_or_territory_complete()) + + def test_is_tribal_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL + self.assertTrue(self.domain_request._is_tribal_complete()) + self.domain_request.tribe_name = None + self.assertFalse(self.domain_request._is_tribal_complete()) + + def test_is_county_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY + self.assertTrue(self.domain_request._is_county_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._is_county_complete()) + + def test_is_city_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY + self.assertTrue(self.domain_request._is_city_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._is_city_complete()) + + def test_is_special_district_complete(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT + self.assertTrue(self.domain_request._is_special_district_complete()) + self.domain_request.about_your_organization = None + self.assertFalse(self.domain_request._is_special_district_complete()) + + def test_is_organization_name_and_address_complete(self): + self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) + self.domain_request.organization_name = None + self.assertFalse(self.domain_request._is_organization_name_and_address_complete()) + + def test_is_authorizing_official_complete(self): + self.assertTrue(self.domain_request._is_authorizing_official_complete()) + self.domain_request.authorizing_official = None + self.assertFalse(self.domain_request._is_authorizing_official_complete()) + + def test_is_requested_domain_complete(self): + self.assertTrue(self.domain_request._is_requested_domain_complete()) + self.domain_request.requested_domain = None + self.assertFalse(self.domain_request._is_requested_domain_complete()) + + def test_is_purpose_complete(self): + self.assertTrue(self.domain_request._is_purpose_complete()) + self.domain_request.purpose = None + self.assertFalse(self.domain_request._is_purpose_complete()) + + def test_is_submitter_complete(self): + self.assertTrue(self.domain_request._is_submitter_complete()) + self.domain_request.submitter = None + self.assertFalse(self.domain_request._is_submitter_complete()) + + def test_is_other_contacts_complete(self): + self.assertTrue(self.domain_request._is_other_contacts_complete()) + none_other_contacts, _ = Contact.objects.get_or_create( + first_name=None, + last_name=None, + title=None, + email=None, + phone=None, + ) + self.domain_request.other_contacts.add(none_other_contacts) + self.assertFalse(self.domain_request._is_other_contacts_complete()) + + def test_is_additional_details_complete(self): + self.assertTrue(self.domain_request._is_additional_details_complete()) + self.domain_request.has_cisa_representative = None + self.assertFalse(self.domain_request._is_additional_details_complete()) + self.domain_request.has_cisa_representative = True + self.domain_request.cisa_representative_email = None + self.assertFalse(self.domain_request._is_additional_details_complete()) + + def test_is_general_form_complete(self): + self.assertTrue(self.domain_request._is_general_form_complete()) + self.domain_request.organization_name = None + self.assertFalse(self.domain_request._is_general_form_complete()) + + def test_form_complete_for_federal(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.FEDERAL + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.federal_type = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_interstate(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.about_your_organization = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_state_or_territory(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_tribal(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.tribe_name = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_county(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_city(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.is_election_board = None + self.assertFalse(self.domain_request._form_complete()) + + def test_form_complete_for_special_district(self): + self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.about_your_organization = None + self.assertFalse(self.domain_request._form_complete()) \ No newline at end of file diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 515aff55b..83c9589bf 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -511,271 +511,306 @@ class DomainRequestTests(TestWithUser, WebTest): type_result = type_form.submit() # should see results in db domain_request = DomainRequest.objects.get() # there's only one + session = self.client.session + print("$$$$$$$$$$$$$$$$$$$ 514!!!!!!", session.values()) + self.assertEqual(domain_request.generic_org_type, "tribal") # the post request should return a redirect to the next form in # the domain request page self.assertEqual(type_result.status_code, 302) self.assertEqual(type_result["Location"], "/request/tribal_government/") - num_pages_tested += 1 + # num_pages_tested += 1 # -- TRIBAL PAGE -- - # We want to skip the tribal page right?? but how do we not fill it out............. type_form = type_page.forms[0] - type_form["generic_org_type-generic_org_type"] = DomainRequest.OrganizationChoices.TRIBAL self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # the tribal government page comes immediately afterwards - self.assertIn("/tribal_government", type_result.headers["Location"]) - # follow first redirect + + # session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]" + url = reverse("domain-request:review") self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - tribal_government_page = type_result.follow() + session = self.client.session + # print("$$$$$$$$$$$$$$$$$$$ BEFORE", session.request) + print("$$$$$$$$$$$$$$$$$$$ BEFORE", session.values()) - # and the step is on the sidebar list. - self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) - - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page + review_page = self.client.get(url, follow=True) + session = self.client.session + # print("$$$$$$$$$$$$$$$$$$$ AFTER", session.request) + print("$$$$$$$$$$$$$$$$$$$ AFTER", session.values()) self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = type_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - # federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") - # org_contact_form["organization_contact-federal_agency"] = federal_agency.id - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-address_line2"] = "address 2" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" + # Org type is filled in + self.assertContains(review_page, "Tribal") + self.assertContains(review_page, "Incomplete", count=9) - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.organization_name, "Testorg") - self.assertEqual(domain_request.address_line1, "address 1") - self.assertEqual(domain_request.address_line2, "address 2") - self.assertEqual(domain_request.city, "NYC") - self.assertEqual(domain_request.state_territory, "NY") - self.assertEqual(domain_request.zipcode, "10002") - self.assertEqual(domain_request.urbanization, "URB Royal Oaks") - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(org_contact_result.status_code, 302) - self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") - num_pages_tested += 1 + # In theory we just need to check that tribal is incomplete + # I don't need to re-look at any of these underneath + # self.assertContains(review_page, "Executive") + # self.assertContains(review_page, "You can’t submit this request") - # ---- AUTHORIZING OFFICIAL PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - ao_form = ao_page.forms[0] - ao_form["authorizing_official-first_name"] = "Testy ATO" - ao_form["authorizing_official-last_name"] = "Tester ATO" - ao_form["authorizing_official-title"] = "Chief Tester" - ao_form["authorizing_official-email"] = "testy@town.com" + # # final submission results in a redirect to the "finished" URL + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # with less_console_noise(): + # review_result = review_form.submit() + # print("!!!!!!!!!!!!!!!!!!! review_result", review_result) - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_result = ao_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO") - self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO") - self.assertEqual(domain_request.authorizing_official.title, "Chief Tester") - self.assertEqual(domain_request.authorizing_official.email, "testy@town.com") - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(ao_result.status_code, 302) - self.assertEqual(ao_result["Location"], "/request/current_sites/") - num_pages_tested += 1 + # print("!!!!!!!!!!!!!!!!!!! review_result.status_code", review_result.status_code) + # print("!!!!!!!!!!!!!!!!!!! review_results location", review_result["Location"]) - # ---- CURRENT SITES PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_page = ao_result.follow() - current_sites_form = current_sites_page.forms[0] - current_sites_form["current_sites-0-website"] = "www.city.com" + # self.assertEqual(review_result.status_code, 302) + # self.assertEqual(review_result["Location"], "/request/finished/") - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual( - domain_request.current_websites.filter(website="http://www.city.com").count(), - 1, - ) - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(current_sites_result.status_code, 302) - self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") - num_pages_tested += 1 + # type_result = type_form.submit() + # # the tribal government page comes immediately afterwards + # self.assertIn("/tribal_government", type_result.headers["Location"]) + # # follow first redirect + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # tribal_government_page = type_result.follow() - # ---- DOTGOV DOMAIN PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = current_sites_result.follow() - dotgov_form = dotgov_page.forms[0] - dotgov_form["dotgov_domain-requested_domain"] = "city" - dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" + # # and the step is on the sidebar list. + # self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_result = dotgov_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.requested_domain.name, "city.gov") - self.assertEqual(domain_request.alternative_domains.filter(website="city1.gov").count(), 1) - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(dotgov_result.status_code, 302) - self.assertEqual(dotgov_result["Location"], "/request/purpose/") - num_pages_tested += 1 - # ---- PURPOSE PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - purpose_page = dotgov_result.follow() - purpose_form = purpose_page.forms[0] - purpose_form["purpose-purpose"] = "For all kinds of things." + # # ---- ORG CONTACT PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # org_contact_page = type_result.follow() + # org_contact_form = org_contact_page.forms[0] + # # federal agency so we have to fill in federal_agency + # # federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration") + # # org_contact_form["organization_contact-federal_agency"] = federal_agency.id + # org_contact_form["organization_contact-organization_name"] = "Testorg" + # org_contact_form["organization_contact-address_line1"] = "address 1" + # org_contact_form["organization_contact-address_line2"] = "address 2" + # org_contact_form["organization_contact-city"] = "NYC" + # org_contact_form["organization_contact-state_territory"] = "NY" + # org_contact_form["organization_contact-zipcode"] = "10002" + # org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks" - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - purpose_result = purpose_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.purpose, "For all kinds of things.") - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(purpose_result.status_code, 302) - self.assertEqual(purpose_result["Location"], "/request/your_contact/") - num_pages_tested += 1 + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # org_contact_result = org_contact_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.organization_name, "Testorg") + # self.assertEqual(domain_request.address_line1, "address 1") + # self.assertEqual(domain_request.address_line2, "address 2") + # self.assertEqual(domain_request.city, "NYC") + # self.assertEqual(domain_request.state_territory, "NY") + # self.assertEqual(domain_request.zipcode, "10002") + # self.assertEqual(domain_request.urbanization, "URB Royal Oaks") + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(org_contact_result.status_code, 302) + # self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") + # num_pages_tested += 1 - # ---- YOUR CONTACT INFO PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - your_contact_page = purpose_result.follow() - your_contact_form = your_contact_page.forms[0] + # # ---- AUTHORIZING OFFICIAL PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # ao_page = org_contact_result.follow() + # ao_form = ao_page.forms[0] + # ao_form["authorizing_official-first_name"] = "Testy ATO" + # ao_form["authorizing_official-last_name"] = "Tester ATO" + # ao_form["authorizing_official-title"] = "Chief Tester" + # ao_form["authorizing_official-email"] = "testy@town.com" - your_contact_form["your_contact-first_name"] = "Testy you" - your_contact_form["your_contact-last_name"] = "Tester you" - your_contact_form["your_contact-title"] = "Admin Tester" - your_contact_form["your_contact-email"] = "testy-admin@town.com" - your_contact_form["your_contact-phone"] = "(201) 555 5556" + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # ao_result = ao_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO") + # self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO") + # self.assertEqual(domain_request.authorizing_official.title, "Chief Tester") + # self.assertEqual(domain_request.authorizing_official.email, "testy@town.com") + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(ao_result.status_code, 302) + # self.assertEqual(ao_result["Location"], "/request/current_sites/") + # num_pages_tested += 1 - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - your_contact_result = your_contact_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.submitter.first_name, "Testy you") - self.assertEqual(domain_request.submitter.last_name, "Tester you") - self.assertEqual(domain_request.submitter.title, "Admin Tester") - self.assertEqual(domain_request.submitter.email, "testy-admin@town.com") - self.assertEqual(domain_request.submitter.phone, "(201) 555 5556") - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(your_contact_result.status_code, 302) - self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") - num_pages_tested += 1 + # # ---- CURRENT SITES PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # current_sites_page = ao_result.follow() + # current_sites_form = current_sites_page.forms[0] + # current_sites_form["current_sites-0-website"] = "www.city.com" - # ---- OTHER CONTACTS PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - other_contacts_page = your_contact_result.follow() + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # current_sites_result = current_sites_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual( + # domain_request.current_websites.filter(website="http://www.city.com").count(), + # 1, + # ) + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(current_sites_result.status_code, 302) + # self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") + # num_pages_tested += 1 - # This page has 3 forms in 1. - # Let's set the yes/no radios to enable the other contacts fieldsets - other_contacts_form = other_contacts_page.forms[0] + # # ---- DOTGOV DOMAIN PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # dotgov_page = current_sites_result.follow() + # dotgov_form = dotgov_page.forms[0] + # dotgov_form["dotgov_domain-requested_domain"] = "city" + # dotgov_form["dotgov_domain-0-alternative_domain"] = "city1" - other_contacts_form["other_contacts-has_other_contacts"] = "True" + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # dotgov_result = dotgov_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.requested_domain.name, "city.gov") + # self.assertEqual(domain_request.alternative_domains.filter(website="city1.gov").count(), 1) + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(dotgov_result.status_code, 302) + # self.assertEqual(dotgov_result["Location"], "/request/purpose/") + # num_pages_tested += 1 - other_contacts_form["other_contacts-0-first_name"] = "Testy2" - other_contacts_form["other_contacts-0-last_name"] = "Tester2" - other_contacts_form["other_contacts-0-title"] = "Another Tester" - other_contacts_form["other_contacts-0-email"] = "testy2@town.com" - other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" + # # ---- PURPOSE PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # purpose_page = dotgov_result.follow() + # purpose_form = purpose_page.forms[0] + # purpose_form["purpose-purpose"] = "For all kinds of things." - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - other_contacts_result = other_contacts_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual( - domain_request.other_contacts.filter( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ).count(), - 1, - ) - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(other_contacts_result.status_code, 302) - self.assertEqual(other_contacts_result["Location"], "/request/additional_details/") - num_pages_tested += 1 + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # purpose_result = purpose_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.purpose, "For all kinds of things.") + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(purpose_result.status_code, 302) + # self.assertEqual(purpose_result["Location"], "/request/your_contact/") + # num_pages_tested += 1 - # ---- ADDITIONAL DETAILS PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - additional_details_page = other_contacts_result.follow() - additional_details_form = additional_details_page.forms[0] + # # ---- YOUR CONTACT INFO PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # your_contact_page = purpose_result.follow() + # your_contact_form = your_contact_page.forms[0] - # load inputs with test data + # your_contact_form["your_contact-first_name"] = "Testy you" + # your_contact_form["your_contact-last_name"] = "Tester you" + # your_contact_form["your_contact-title"] = "Admin Tester" + # your_contact_form["your_contact-email"] = "testy-admin@town.com" + # your_contact_form["your_contact-phone"] = "(201) 555 5556" - additional_details_form["additional_details-has_cisa_representative"] = "True" - additional_details_form["additional_details-has_anything_else_text"] = "True" - additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com" - additional_details_form["additional_details-anything_else"] = "Nothing else." + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # your_contact_result = your_contact_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.submitter.first_name, "Testy you") + # self.assertEqual(domain_request.submitter.last_name, "Tester you") + # self.assertEqual(domain_request.submitter.title, "Admin Tester") + # self.assertEqual(domain_request.submitter.email, "testy-admin@town.com") + # self.assertEqual(domain_request.submitter.phone, "(201) 555 5556") + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(your_contact_result.status_code, 302) + # self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") + # num_pages_tested += 1 - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - additional_details_result = additional_details_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com") - self.assertEqual(domain_request.anything_else, "Nothing else.") - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(additional_details_result.status_code, 302) - self.assertEqual(additional_details_result["Location"], "/request/requirements/") - num_pages_tested += 1 + # # ---- OTHER CONTACTS PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # other_contacts_page = your_contact_result.follow() - # ---- REQUIREMENTS PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - requirements_page = additional_details_result.follow() - requirements_form = requirements_page.forms[0] + # # This page has 3 forms in 1. + # # Let's set the yes/no radios to enable the other contacts fieldsets + # other_contacts_form = other_contacts_page.forms[0] - requirements_form["requirements-is_policy_acknowledged"] = True + # other_contacts_form["other_contacts-has_other_contacts"] = "True" - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - requirements_result = requirements_form.submit() - # validate that data from this step are being saved - domain_request = DomainRequest.objects.get() # there's only one - self.assertEqual(domain_request.is_policy_acknowledged, True) - # the post request should return a redirect to the next form in - # the domain request page - self.assertEqual(requirements_result.status_code, 302) - self.assertEqual(requirements_result["Location"], "/request/review/") - num_pages_tested += 1 + # other_contacts_form["other_contacts-0-first_name"] = "Testy2" + # other_contacts_form["other_contacts-0-last_name"] = "Tester2" + # other_contacts_form["other_contacts-0-title"] = "Another Tester" + # other_contacts_form["other_contacts-0-email"] = "testy2@town.com" + # other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557" + + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # other_contacts_result = other_contacts_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual( + # domain_request.other_contacts.filter( + # first_name="Testy2", + # last_name="Tester2", + # title="Another Tester", + # email="testy2@town.com", + # phone="(201) 555 5557", + # ).count(), + # 1, + # ) + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(other_contacts_result.status_code, 302) + # self.assertEqual(other_contacts_result["Location"], "/request/additional_details/") + # num_pages_tested += 1 + + # # ---- ADDITIONAL DETAILS PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # additional_details_page = other_contacts_result.follow() + # additional_details_form = additional_details_page.forms[0] + + # # load inputs with test data + + # additional_details_form["additional_details-has_cisa_representative"] = "True" + # additional_details_form["additional_details-has_anything_else_text"] = "True" + # additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com" + # additional_details_form["additional_details-anything_else"] = "Nothing else." + + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # additional_details_result = additional_details_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com") + # self.assertEqual(domain_request.anything_else, "Nothing else.") + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(additional_details_result.status_code, 302) + # self.assertEqual(additional_details_result["Location"], "/request/requirements/") + # num_pages_tested += 1 + + # # ---- REQUIREMENTS PAGE ---- + # # Follow the redirect to the next form page + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # requirements_page = additional_details_result.follow() + # requirements_form = requirements_page.forms[0] + + # requirements_form["requirements-is_policy_acknowledged"] = True + + # # test next button + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # requirements_result = requirements_form.submit() + # # validate that data from this step are being saved + # domain_request = DomainRequest.objects.get() # there's only one + # self.assertEqual(domain_request.is_policy_acknowledged, True) + # # the post request should return a redirect to the next form in + # # the domain request page + # self.assertEqual(requirements_result.status_code, 302) + # self.assertEqual(requirements_result["Location"], "/request/review/") + # num_pages_tested += 1 # ---- REVIEW AND FINSIHED PAGES ---- # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - review_page = requirements_result.follow() - review_form = review_page.forms[0] - print("$$$$$$$$$$$$$$$$$$$$$$", review_page) + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # review_page = requirements_result.follow() + # review_form = review_page.forms[0] + # print("$$$$$$$$$$$$$$$$$$$$$$", review_page) - # Review page contains all the previously entered data - # Let's make sure the long org name is displayed - self.assertContains(review_page, "Incomplete") + # # Review page contains all the previously entered data + # # Let's make sure the long org name is displayed + # self.assertContains(review_page, "Incomplete") # In theory we just need to check that tribal is incomplete # I don't need to re-look at any of these underneath # self.assertContains(review_page, "Executive") @@ -813,22 +848,22 @@ class DomainRequestTests(TestWithUser, WebTest): # And the existence of the modal's data parked and ready for the js init. # The next assert also tests for the passed requested domain context from # the view > domain_request_form > modal - self.assertContains(review_page, "You can’t submit this request") + # self.assertContains(review_page, "You can’t submit this request") - # final submission results in a redirect to the "finished" URL - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): - review_result = review_form.submit() - print("!!!!!!!!!!!!!!!!!!! review_result", review_result) + # # final submission results in a redirect to the "finished" URL + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # with less_console_noise(): + # review_result = review_form.submit() + # print("!!!!!!!!!!!!!!!!!!! review_result", review_result) - print("!!!!!!!!!!!!!!!!!!! review_result.status_code", review_result.status_code) - print("!!!!!!!!!!!!!!!!!!! review_results location", review_result["Location"]) + # print("!!!!!!!!!!!!!!!!!!! review_result.status_code", review_result.status_code) + # print("!!!!!!!!!!!!!!!!!!! review_results location", review_result["Location"]) - self.assertEqual(review_result.status_code, 302) - self.assertEqual(review_result["Location"], "/request/finished/") + # self.assertEqual(review_result.status_code, 302) + # self.assertEqual(review_result["Location"], "/request/finished/") - # self.assertEqual(review_result["Location"], "/tribal_government/") - num_pages_tested += 1 + # # self.assertEqual(review_result["Location"], "/tribal_government/") + # num_pages_tested += 1 # following this redirect is a GET request, so include the cookie # here too. @@ -838,7 +873,7 @@ class DomainRequestTests(TestWithUser, WebTest): # self.assertContains(final_result, "Thanks for your domain request!") # check that any new pages are added to this test - self.assertEqual(num_pages, num_pages_tested) + # self.assertEqual(num_pages, num_pages_tested) # This is the start of a test to check an existing domain_request, it currently # does not work and results in errors as noted in: diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 8659fcd70..7e864987d 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -374,108 +374,109 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } return [key for key, value in history_dict.items() if value] - def _is_federal_complete(self): - # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None - return not (self.domain_request.federal_type is None or self.domain_request.federal_agency is None) + # def _is_federal_complete(self): + # # Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None + # return not (self.domain_request.federal_type is None or self.domain_request.federal_agency is None) - def _is_interstate_complete(self): - # Interstate -> "About your organization" page can't be empty - return self.domain_request.about_your_organization is not None + # def _is_interstate_complete(self): + # # Interstate -> "About your organization" page can't be empty + # return self.domain_request.about_your_organization is not None - def _is_state_or_territory_complete(self): - # State -> ""Election office" page can't be empty - return self.domain_request.is_election_board is not None + # def _is_state_or_territory_complete(self): + # # State -> ""Election office" page can't be empty + # return self.domain_request.is_election_board is not None - def _is_tribal_complete(self): - # Tribal -> "Tribal name" and "Election office" page can't be empty - return self.domain_request.tribe_name is not None and self.domain_request.is_election_board is not None + # def _is_tribal_complete(self): + # # Tribal -> "Tribal name" and "Election office" page can't be empty + # return self.domain_request.tribe_name is not None and self.domain_request.is_election_board is not None - def _is_county_complete(self): - # County -> "Election office" page can't be empty - return self.domain_request.is_election_board is not None + # def _is_county_complete(self): + # # County -> "Election office" page can't be empty + # return self.domain_request.is_election_board is not None - def _is_city_complete(self): - # City -> "Election office" page can't be empty - return self.domain_request.is_election_board is not None + # def _is_city_complete(self): + # # City -> "Election office" page can't be empty + # return self.domain_request.is_election_board is not None - def _is_special_district_complete(self): - # Special District -> "Election office" and "About your organization" page can't be empty - return ( - self.domain_request.is_election_board is not None - and self.domain_request.about_your_organization is not None - ) + # def _is_special_district_complete(self): + # # Special District -> "Election office" and "About your organization" page can't be empty + # return ( + # self.domain_request.is_election_board is not None + # and self.domain_request.about_your_organization is not None + # ) - def _is_organization_name_and_address_complete(self): - return not ( - self.domain_request.organization_name is None - or self.domain_request.address_line1 is None - or self.domain_request.city is None - or self.domain_request.state_territory is None - or self.domain_request.zipcode is None - ) + # def _is_organization_name_and_address_complete(self): + # return not ( + # self.domain_request.organization_name is None + # or self.domain_request.address_line1 is None + # or self.domain_request.city is None + # or self.domain_request.state_territory is None + # or self.domain_request.zipcode is None + # ) - def _is_authorizing_official_complete(self): - return self.domain_request.authorizing_official is not None + # def _is_authorizing_official_complete(self): + # return self.domain_request.authorizing_official is not None - def _is_requested_domain_complete(self): - return self.domain_request.requested_domain is not None + # def _is_requested_domain_complete(self): + # return self.domain_request.requested_domain is not None - def _is_purpose_complete(self): - return self.domain_request.purpose is not None + # def _is_purpose_complete(self): + # return self.domain_request.purpose is not None - def _is_submitter_complete(self): - return self.domain_request.submitter is not None + # def _is_submitter_complete(self): + # return self.domain_request.submitter is not None - def _is_other_contacts_complete(self): - return self.domain_request.other_contacts is not None + # def _is_other_contacts_complete(self): + # return self.domain_request.other_contacts is not None - def _is_additional_details_complete(self): - return not ( - self.domain_request.has_cisa_representative is None - or self.domain_request.has_anything_else_text is None - # RARE EDGE CASE: You click yes on having a cisa rep, but you dont type in email (should block in form) - or ( - self.domain_request.has_cisa_representative is True - and self.domain_request.cisa_representative_email is None - ) - or self.domain_request.is_policy_acknowledged is None - ) + # def _is_additional_details_complete(self): + # return not ( + # self.domain_request.has_cisa_representative is None + # or self.domain_request.has_anything_else_text is None + # # RARE EDGE CASE: You click yes on having a cisa rep, but you dont type in email (should block in form) + # or ( + # self.domain_request.has_cisa_representative is True + # and self.domain_request.cisa_representative_email is None + # ) + # or self.domain_request.is_policy_acknowledged is None + # ) - def _is_general_form_complete(self): - return ( - self._is_organization_name_and_address_complete() - and self._is_authorizing_official_complete() - and self._is_requested_domain_complete() - and self._is_purpose_complete() - and self._is_submitter_complete() - and self._is_other_contacts_complete() - and self._is_additional_details_complete() - ) + # def _is_general_form_complete(self): + # return ( + # self._is_organization_name_and_address_complete() + # and self._is_authorizing_official_complete() + # and self._is_requested_domain_complete() + # and self._is_purpose_complete() + # and self._is_submitter_complete() + # and self._is_other_contacts_complete() + # and self._is_additional_details_complete() + # ) - def _form_complete(self): - if self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL: - is_complete = self._is_federal_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.INTERSTATE: - is_complete = self._is_interstate_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.STATE_OR_TERRITORY: - is_complete = self._is_state_or_territory_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL: - is_complete = self._is_tribal_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.COUNTY: - is_complete = self._is_county_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.CITY: - is_complete = self._is_city_complete() - elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.SPECIAL_DISTRICT: - is_complete = self._is_special_district_complete() - else: - is_complete = False + # def _form_complete(self): + # if self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.FEDERAL: + # is_complete = self._is_federal_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.INTERSTATE: + # is_complete = self._is_interstate_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.STATE_OR_TERRITORY: + # is_complete = self._is_state_or_territory_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.TRIBAL: + # is_complete = self._is_tribal_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.COUNTY: + # is_complete = self._is_county_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.CITY: + # is_complete = self._is_city_complete() + # elif self.domain_request.generic_org_type == DomainRequest.OrganizationChoices.SPECIAL_DISTRICT: + # is_complete = self._is_special_district_complete() + # else: + # # NOTE: This shouldn't happen, this is only if somehow they didn't choose an org type + # is_complete = False - if not is_complete or not self._is_general_form_complete(): - print("!!!! We are in the False if statement - form is not complete") - return False + # if not is_complete or not self._is_general_form_complete(): + # print("!!!! We are in the False if statement - form is not complete") + # return False - print("!!!! We are in the True if statement - form is complete") - return True + # print("!!!! We are in the True if statement - form is complete") + # return True def get_context_data(self): """Define context for access on all wizard pages.""" @@ -483,7 +484,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): # Concatenate the modal header that we'll pass to the modal. context_stuff = {} - if self._form_complete(): + if DomainRequest._form_complete(self.domain_request): print("!!!!!!!in form complete section") modal_button = '" context_stuff = { @@ -773,7 +774,7 @@ class Review(DomainRequestWizard): forms = [] # type: ignore def get_context_data(self): - if self._form_complete() is False: + if DomainRequest._form_complete(self.domain_request) is False: logger.warning("User arrived at review page with an incomplete form.") context = super().get_context_data() context["Step"] = Step.__members__ From 1fd86875c04fadc1f48944cfdf3791f1ef887586 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 23 May 2024 09:33:34 -0600 Subject: [PATCH 063/171] Linting --- src/registrar/tests/test_views.py | 4 ++-- src/registrar/views/domain_request.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 595544526..5275b8314 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -61,7 +61,7 @@ class TestWithUser(MockEppLib): ) title = "test title" self.user.contact.title = title - self.user.save() + self.user.contact.save() username_incomplete = "test_user_incomplete" first_name_2 = "Incomplete" @@ -737,7 +737,7 @@ class UserProfileTests(TestWithUser, WebTest): def test_domain_detail_profile_feature_on(self): """test that domain detail view when profile_feature is on""" with override_flag("profile_feature", active=True): - response = self.client.get(reverse("domain", args=[self.domain.pk]), follow=True) + response = self.client.get(reverse("domain", args=[self.domain.pk])) self.assertContains(response, "Your profile") self.assertNotContains(response, "Your contact information") diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 67e01e5be..1d14e4b57 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -23,7 +23,6 @@ from .utility import ( DomainRequestWizardPermissionView, ) -from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -401,9 +400,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] - excluded_steps = [ - Step.YOUR_CONTACT - ] + excluded_steps = [Step.YOUR_CONTACT] should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: From 7684fff0afd67226de29c5d00f215399ead072b5 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 23 May 2024 10:24:30 -0600 Subject: [PATCH 064/171] Remove unused follow --- src/registrar/tests/test_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5275b8314..738a0afe2 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -804,7 +804,7 @@ class UserProfileTests(TestWithUser, WebTest): """test user profile form submission""" self.app.set_user(self.user.username) with override_flag("profile_feature", active=True): - profile_page = self.app.get(reverse("user-profile")).follow() + profile_page = self.app.get(reverse("user-profile")) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) profile_form = profile_page.form From 95c2a46a2c8d1d22df2003568e88172eaf2b548d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Thu, 23 May 2024 15:27:51 -0400 Subject: [PATCH 065/171] color class revision, solidify modal cancel button hide logic --- src/registrar/assets/sass/_theme/_forms.scss | 5 ---- .../templates/domain_request_form.html | 2 +- .../templates/domain_request_review.html | 24 +++++++++---------- src/registrar/templates/includes/modal.html | 2 +- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 490bf7986..9aeb54372 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -32,11 +32,6 @@ border-left: none; } -.incomplete { - color: #950E24; - font-weight: bold; -} - legend.float-left-tablet + button.float-right-tablet { margin-top: .5rem; @include at-media('tablet') { diff --git a/src/registrar/templates/domain_request_form.html b/src/registrar/templates/domain_request_form.html index b176866cc..11150df96 100644 --- a/src/registrar/templates/domain_request_form.html +++ b/src/registrar/templates/domain_request_form.html @@ -105,7 +105,7 @@ aria-describedby="Are you sure you want to submit a domain request?" data-force-action > - {% include 'includes/modal.html' with review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %} + {% include 'includes/modal.html' with resets_domain_request=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
{% block after_form_content %}{% endblock %} diff --git a/src/registrar/templates/domain_request_review.html b/src/registrar/templates/domain_request_review.html index 2960e40c7..1f21683a5 100644 --- a/src/registrar/templates/domain_request_review.html +++ b/src/registrar/templates/domain_request_review.html @@ -25,11 +25,11 @@ {% if step == Step.ORGANIZATION_TYPE %} {% namespaced_url 'domain-request' step as domain_request_url %} {% if domain_request.generic_org_type is not None %} - {% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% else %} - {% with title=form_titles|get_item:step value="Incomplete"|safe %} + {% with title=form_titles|get_item:step value="Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -37,7 +37,7 @@ {% if step == Step.TRIBAL_GOVERNMENT %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% if domain_request.federally_recognized_tribe %}

Federally-recognized tribe

{% endif %} @@ -47,7 +47,7 @@ {% if step == Step.ORGANIZATION_FEDERAL %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -66,7 +66,7 @@ {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %} {% endwith %} {% else %} - {% with title=form_titles|get_item:step value="Incomplete"|safe %} + {% with title=form_titles|get_item:step value="Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -74,7 +74,7 @@ {% if step == Step.ABOUT_YOUR_ORGANIZATION %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -86,7 +86,7 @@ {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %} {% endwith %} {% else %} - {% with title=form_titles|get_item:step value="Incomplete"|safe %} + {% with title=form_titles|get_item:step value="Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -107,7 +107,7 @@ {% if step == Step.DOTGOV_DOMAIN %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe%} + {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe%} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} @@ -123,7 +123,7 @@ {% if step == Step.PURPOSE %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -135,7 +135,7 @@ {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %} {% endwith %} {% else %} - {% with title=form_titles|get_item:step value="Incomplete"|safe %} + {% with title=form_titles|get_item:step value="Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -148,7 +148,7 @@ {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %} {% endwith %} {% else %} - {% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %} {% endwith %} {% endif %} @@ -157,7 +157,7 @@ {% if step == Step.ADDITIONAL_DETAILS %} {% namespaced_url 'domain-request' step as domain_request_url %} - {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe %} + {% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete"|safe %} {% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %} {% endwith %} diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index 24b581516..3c82aac5b 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -39,7 +39,7 @@ Cancel - {% elif review_form_is_complete %} + {% elif not resets_domain_request or review_form_is_complete %}
{% block after_form_content %}{% endblock %} diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index 3c82aac5b..cb99fa321 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -39,7 +39,7 @@ Cancel - {% elif not resets_domain_request or review_form_is_complete %} + {% elif not is_domain_request_form or review_form_is_complete %} + {% endblock profile_form %} From fa1fcba49aaec2f0620d0de09e22e92abdc30649 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 11:50:44 -0700 Subject: [PATCH 089/171] Add fixed iselection and the whole additional details unit tests --- src/registrar/models/domain_request.py | 12 +- src/registrar/tests/test_models.py | 275 ++++++++++++++++--------- 2 files changed, 181 insertions(+), 106 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index e3bd1c8f9..ed98433fa 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -970,10 +970,10 @@ class DomainRequest(TimeStampedModel): def _is_organization_name_and_address_complete(self): return not ( self.organization_name is None - or self.address_line1 is None - or self.city is None - or self.state_territory is None - or self.zipcode is None + and self.address_line1 is None + and self.city is None + and self.state_territory is None + and self.zipcode is None ) def _is_authorizing_official_complete(self): @@ -1004,6 +1004,10 @@ class DomainRequest(TimeStampedModel): return False def _is_additional_details_complete(self): + # has_cisa_representative is True and the cisa_representative_email is not empty and is not an empty string + # OR has_cisa_representative is No + # AND + # the anything else boolean is True and there is text and it's not an empty string of text OR the boolean is No return ( ( self.has_cisa_representative is True diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 0ed038be3..825f86be6 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1655,7 +1655,8 @@ class TestDomainRequestIncomplete(TestCase): requested_domain=draft_domain, purpose="Some purpose", submitter=you, - has_cisa_representative=False, + has_cisa_representative=True, + cisa_representative_email="somerep@cisa.com", has_anything_else_text=True, anything_else="Anything else", is_policy_acknowledged=True, @@ -1695,7 +1696,8 @@ class TestDomainRequestIncomplete(TestCase): # self.domain_request.is_election_board.clear() self.domain_request.is_election_board = None self.domain_request.save() - self.assertFalse(self.domain_request._is_state_or_territory_complete()) + # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election + self.assertTrue(self.domain_request._is_state_or_territory_complete()) def test_is_tribal_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL @@ -1706,18 +1708,18 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.tribe_name = None self.domain_request.is_election_board = None self.domain_request.save() + # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertFalse(self.domain_request._is_tribal_complete()) def test_is_county_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY - self.domain_request.about_your_organization = "Something something about your organization" self.domain_request.is_election_board = False self.domain_request.save() self.assertTrue(self.domain_request._is_county_complete()) - self.domain_request.about_your_organization = None self.domain_request.is_election_board = None self.domain_request.save() - self.assertFalse(self.domain_request._is_county_complete()) + # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election + self.assertTrue(self.domain_request._is_county_complete()) def test_is_city_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY @@ -1726,9 +1728,8 @@ class TestDomainRequestIncomplete(TestCase): self.assertTrue(self.domain_request._is_city_complete()) self.domain_request.is_election_board = None self.domain_request.save() - self.domain_request.refresh_from_db() - print(f"self.domain_request.is_election_board {self.domain_request.is_election_board }") - self.assertFalse(self.domain_request._is_city_complete()) + # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election + self.assertTrue(self.domain_request._is_city_complete()) def test_is_special_district_complete(self): self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT @@ -1739,15 +1740,15 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.about_your_organization = None self.domain_request.is_election_board = None self.domain_request.save() + # is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election self.assertFalse(self.domain_request._is_special_district_complete()) - # TODO: Fix def test_is_organization_name_and_address_complete(self): self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) self.domain_request.organization_name = None self.domain_request.address_line1 = None self.domain_request.save() - self.assertFalse(self.domain_request._is_organization_name_and_address_complete()) + self.assertTrue(self.domain_request._is_organization_name_and_address_complete()) def test_is_authorizing_official_complete(self): self.assertTrue(self.domain_request._is_authorizing_official_complete()) @@ -1785,94 +1786,164 @@ class TestDomainRequestIncomplete(TestCase): self.assertFalse(self.domain_request._is_other_contacts_complete()) def test_is_additional_details_complete(self): - - # CISA Rep - No, Anything Else Radio - Anything Else Text - Filled - self.assertTrue(self.domain_request._is_additional_details_complete()) - - # CISA Rep - No, Anything Else Radio - No - self.domain_request.has_anything_else_text = False - self.assertTrue(self.domain_request._is_additional_details_complete()) - - # CISA Rep - Yes, CISA Rep Email - Yes (And has above Anything Else Radio - No) - self.domain_request.has_cisa_representative = True - self.domain_request.cisa_representative_email = "some@cisarepemail.com" - self.domain_request.save() - self.assertTrue(self.domain_request._is_additional_details_complete()) - - # # Check immediately after saving - print("After setting to None and saving:") - print(f"has_cisa_representative (before refresh): {self.domain_request.has_cisa_representative}") - print(f"cisa_representative_email (before refresh): {self.domain_request.cisa_representative_email}") - print(f"has_anything_else_text (before refresh): {self.domain_request.has_anything_else_text}") - print(f"anything_else (before refresh): {self.domain_request.anything_else}") - - # CISA Rep - Yes, CISA Rep Email - Yes, Anything Else Radio - Yes, Anything Else Text - No - self.domain_request.has_anything_else_text = True - self.domain_request.anything_else = "" - self.domain_request.save() - - # Refresh from the database - self.domain_request.refresh_from_db() - - print("After setting to None and saving:") - print(f"has_cisa_representative (after refresh): {self.domain_request.has_cisa_representative}") - print(f"cisa_representative_email (after refresh): {self.domain_request.cisa_representative_email}") - print(f"has_anything_else_text (after refresh): {self.domain_request.has_anything_else_text}") - print(f"anything_else (after refresh): {self.domain_request.anything_else}") - # has_cisa_representative (after refresh): True - # cisa_representative_email (after refresh): some@cisarepemail.com - # has_anything_else_text (after refresh): False - # anything_else (after refresh): None - - # # This ensures that if we have prefilled data, the form is prepopulated - # if self.anything_else is not None: - # self.has_anything_else_text = self.anything_else != "" - - # # This check is required to ensure that the form doesn't start out checked. - # if self.has_anything_else_text is not None: - # self.has_anything_else_text = self.anything_else != "" and self.anything_else is not None - - self.assertFalse(self.domain_request._is_additional_details_complete()) - - # CISA Rep - None, CISA Rep Email - None, Anything Else Radio - No - self.domain_request.cisa_representative_email = None - self.domain_request.has_cisa_representative = None - self.domain_request.save() - self.assertFalse(self.domain_request._is_additional_details_complete()) - - # Refresh from the database - self.domain_request.refresh_from_db() - - # # Check immediately after saving - print("After setting to None and saving:") - print(f"has_cisa_representative (before refresh): {self.domain_request.has_cisa_representative}") - print(f"cisa_representative_email (before refresh): {self.domain_request.cisa_representative_email}") - print(f"has_anything_else_text (before refresh): {self.domain_request.has_anything_else_text}") - print(f"anything_else (before refresh): {self.domain_request.anything_else}") - - # self.domain_request.has_cisa_representative = True - # self.domain_request.cisa_representative_email = "some@cisarepemail.com" - # # If you choose yes on radio button but dont have text it should error - # self.domain_request.has_anything_else_text = True - # self.domain_request.anything_else = None - # self.domain_request.save() - - # self.domain_request.anything_else = "Some text here" - # self.domain_request.save() - # self.assertFalse(self.domain_request._is_additional_details_complete()) - - # # Check immediately after saving - # print("After setting to None and saving:") - # print(f'has_cisa_representative (before refresh): {self.domain_request.has_cisa_representative}') - - # Refresh from the database - # self.domain_request.refresh_from_db() - - # # Check after refreshing from the database - # print("After refreshing from DB:") - # print(f'has_cisa_representative (after refresh): {self.domain_request.has_cisa_representative}') - - # Expect False because has_cisa_representative is None (which we now explicitly handle) + test_cases = [ + # CISA Rep - Yes + # Email - Yes + # Anything Else Radio - Yes + # Anything Else Text - Yes + { + "has_cisa_representative": True, + "cisa_representative_email": "some@cisarepemail.com", + "has_anything_else_text": True, + "anything_else": "Some text", + "expected": True, + }, + # CISA Rep - Yes + # Email - Yes + # Anything Else Radio - Yes + # Anything Else Text - None + { + "has_cisa_representative": True, + "cisa_representative_email": "some@cisarepemail.com", + "has_anything_else_text": True, + "anything_else": None, + "expected": True, + }, + # CISA Rep - Yes + # Email - Yes + # Anything Else Radio - No + # Anything Else Text - No + { + "has_cisa_representative": True, + "cisa_representative_email": "some@cisarepemail.com", + "has_anything_else_text": False, + "anything_else": None, + "expected": True, + }, + # CISA Rep - Yes + # Email - Yes + # Anything Else Radio - None + # Anything Else Text - None + { + "has_cisa_representative": True, + "cisa_representative_email": "some@cisarepemail.com", + "has_anything_else_text": None, + "anything_else": None, + "expected": False, + }, + # CISA Rep - Yes + # Email - None + # Anything Else Radio - None + # Anything Else Text - None + { + "has_cisa_representative": True, + "cisa_representative_email": None, + "has_anything_else_text": None, + "anything_else": None, + "expected": False, + }, + # CISA Rep - Yes + # Email - None + # Anything Else Radio - No + # Anything Else Text - No + # sync_yes_no will override has_cisa_representative to be False if cisa_representative_email is None + # therefore, our expected will be True + { + "has_cisa_representative": True, + # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields + "cisa_representative_email": None, + "has_anything_else_text": False, + "anything_else": None, + "expected": True, + }, + # CISA Rep - Yes + # Email - None + # Anything Else Radio - Yes + # Anything Else Text - None + { + "has_cisa_representative": True, + # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields + "cisa_representative_email": None, + "has_anything_else_text": True, + "anything_else": None, + "expected": True, + }, + # CISA Rep - Yes + # Email - None + # Anything Else Radio - Yes + # Anything Else Text - Yes + { + "has_cisa_representative": True, + # Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields + "cisa_representative_email": None, + "has_anything_else_text": True, + "anything_else": "Some text", + "expected": True, + }, + # CISA Rep - No + # Anything Else Radio - Yes + # Anything Else Text - Yes + { + "has_cisa_representative": False, + "cisa_representative_email": None, + "has_anything_else_text": True, + "anything_else": "Some text", + "expected": True, + }, + # CISA Rep - No + # Anything Else Radio - Yes + # Anything Else Text - None + { + "has_cisa_representative": False, + "cisa_representative_email": None, + "has_anything_else_text": True, + "anything_else": None, + "expected": True, + }, + # CISA Rep - No + # Anything Else Radio - None + # Anything Else Text - None + { + "has_cisa_representative": False, + "cisa_representative_email": None, + "has_anything_else_text": None, + "anything_else": None, + # Above is both None, so it does NOT get overwritten + "expected": False, + }, + # CISA Rep - No + # Anything Else Radio - No + # Anything Else Text - No + { + "has_cisa_representative": False, + "cisa_representative_email": None, + "has_anything_else_text": False, + "anything_else": None, + "expected": True, + }, + # CISA Rep - None + # Anything Else Radio - None + { + "has_cisa_representative": None, + "cisa_representative_email": None, + "has_anything_else_text": None, + "anything_else": None, + "expected": False, + }, + ] + for case in test_cases: + with self.subTest(case=case): + self.domain_request.has_cisa_representative = case["has_cisa_representative"] + self.domain_request.cisa_representative_email = case["cisa_representative_email"] + self.domain_request.has_anything_else_text = case["has_anything_else_text"] + self.domain_request.anything_else = case["anything_else"] + self.domain_request.save() + self.domain_request.refresh_from_db() + self.assertEqual( + self.domain_request._is_additional_details_complete(), + case["expected"], + msg=f"Failed for case: {case}", + ) def test_is_policy_acknowledgement_complete(self): self.assertTrue(self.domain_request._is_policy_acknowledgement_complete()) @@ -1881,8 +1952,8 @@ class TestDomainRequestIncomplete(TestCase): self.domain_request.is_policy_acknowledged = None self.assertFalse(self.domain_request._is_policy_acknowledgement_complete()) - def test_is_general_form_complete(self): - self.assertTrue(self.domain_request._is_general_form_complete()) - self.domain_request.organization_name = None + def test_form_complete(self): + self.assertTrue(self.domain_request._form_complete()) + self.domain_request.generic_org_type = None self.domain_request.save() - self.assertFalse(self.domain_request._is_general_form_complete()) + self.assertFalse(self.domain_request._form_complete()) From a32a228e6ccb8edf9f7a809cd65a26100c284b02 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 13:06:06 -0600 Subject: [PATCH 090/171] Code cleanup --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/assets/sass/_theme/_base.scss | 13 +++++++ src/registrar/assets/sass/_theme/_forms.scss | 9 ++--- .../assets/sass/_theme/_headers.scss | 14 -------- src/registrar/assets/sass/_theme/styles.scss | 1 - .../templates/domain_request_intro.html | 1 + .../includes/finish_profile_form.html | 24 +++++++------ .../templates/includes/gov_extended_logo.html | 2 +- src/registrar/templates/profile.html | 3 +- src/registrar/templatetags/field_helpers.py | 8 +++-- src/registrar/views/domain_request.py | 12 +------ src/registrar/views/user_profile.py | 36 +++++++++++-------- src/registrar/views/utility/mixins.py | 1 - .../views/utility/permission_views.py | 4 ++- 14 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 src/registrar/assets/sass/_theme/_headers.scss diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 5db6cea54..a48d9b46b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -873,7 +873,7 @@ function hideDeletedForms() { let fieldId = getInputFieldId(fieldName) let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#profile-name-fieldset"); + let nameFieldset = document.querySelector("#profile-name-group"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss index 5e7fa0c6c..e88d75f4e 100644 --- a/src/registrar/assets/sass/_theme/_base.scss +++ b/src/registrar/assets/sass/_theme/_base.scss @@ -198,3 +198,16 @@ abbr[title] { height: 1.25em !important; } } + +// Define some styles for the .gov header/logo +.usa-logo button { + color: #{$dhs-dark-gray-85}; + font-weight: 700; + font-family: family('sans'); + font-size: 1.6rem; + line-height: 1.1; +} + +.usa-logo button.usa-button--unstyled.disabled-button:hover{ + color: #{$dhs-dark-gray-85}; +} diff --git a/src/registrar/assets/sass/_theme/_forms.scss b/src/registrar/assets/sass/_theme/_forms.scss index 46d41059a..b5229fae1 100644 --- a/src/registrar/assets/sass/_theme/_forms.scss +++ b/src/registrar/assets/sass/_theme/_forms.scss @@ -15,9 +15,6 @@ .usa-form--extra-large { max-width: none; - .usa-summary-box { - max-width: 600px; - } } .usa-form--text-width { @@ -30,7 +27,7 @@ } } -.usa-form-readonly { +.usa-form-editable { border-top: 2px #{$dhs-dark-gray-15} solid; .bold-usa-label label.usa-label{ @@ -41,14 +38,14 @@ font-weight: bold; } - &.usa-form-readonly--no-border { + &.usa-form-editable--no-border { border-top: None; margin-top: 0px !important; } } -.usa-form-readonly > .usa-form-group:first-of-type { +.usa-form-editable > .usa-form-group:first-of-type { margin-top: unset; } diff --git a/src/registrar/assets/sass/_theme/_headers.scss b/src/registrar/assets/sass/_theme/_headers.scss deleted file mode 100644 index 2e3992e5a..000000000 --- a/src/registrar/assets/sass/_theme/_headers.scss +++ /dev/null @@ -1,14 +0,0 @@ -@use "uswds-core" as *; -@use "cisa_colors" as *; - -.usa-logo button { - color: #{$dhs-dark-gray-85}; - font-weight: 700; - font-family: family('sans'); - font-size: 1.6rem; - line-height: 1.1; -} - -.usa-logo button.usa-button--unstyled.disabled-button:hover{ - color: #{$dhs-dark-gray-85}; -} diff --git a/src/registrar/assets/sass/_theme/styles.scss b/src/registrar/assets/sass/_theme/styles.scss index e24618a23..64b113a29 100644 --- a/src/registrar/assets/sass/_theme/styles.scss +++ b/src/registrar/assets/sass/_theme/styles.scss @@ -20,7 +20,6 @@ @forward "tables"; @forward "sidenav"; @forward "register-form"; -@forward "_headers"; /*-------------------------------------------------- --- Admin ---------------------------------*/ diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index 259657ad4..c0b566799 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -15,6 +15,7 @@

If you have all the information you need, completing your domain request might take around 15 minutes.

+ {% block form_buttons %}
{% endif %} -
\ No newline at end of file +
diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index eeab1f945..12441da66 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -36,5 +36,4 @@ Edit your User Profile | {% include "includes/profile_form.html" with form=form %}
-{% endblock %} - +{% endblock content_bottom %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index a7aa9d663..be78099db 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -95,10 +95,12 @@ def input_with_errors(context, field=None): # noqa: C901 elif key == "show_edit_button": # Hide the primary input field. # Used such that we can toggle it with JS - if "display-none" not in classes and isinstance(value, bool) and value: + if "display-none" not in classes: classes.append("display-none") - # Set this as a context value so we know what we're going to display - context["show_edit_button"] = value + + # Tag that this form contains the edit button. + if "usa-form-editable" not in group_classes: + group_classes.append("usa-form-editable") attrs["id"] = field.auto_id diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 1d14e4b57..a90eaf271 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -15,7 +15,6 @@ from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper from registrar.views.utility.permission_views import DomainRequestPermissionDeleteView -from waffle.decorators import flag_is_active, waffle_flag from .utility import ( DomainRequestPermissionView, @@ -23,6 +22,7 @@ from .utility import ( DomainRequestWizardPermissionView, ) +from waffle.decorators import flag_is_active logger = logging.getLogger(__name__) @@ -400,13 +400,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_step_list(self) -> list: """Dynamically generated list of steps in the form wizard.""" step_list = [] - excluded_steps = [Step.YOUR_CONTACT] - should_exclude = flag_is_active(self.request, "profile_feature") for step in Step: - - if should_exclude and step in excluded_steps: - continue - condition = self.WIZARD_CONDITIONS.get(step, True) if callable(condition): condition = condition(self) @@ -546,10 +540,6 @@ class YourContact(DomainRequestWizard): template_name = "domain_request_your_contact.html" forms = [forms.YourContactForm] - @waffle_flag("!profile_feature") # type: ignore - def dispatch(self, request, *args, **kwargs): # type: ignore - return super().dispatch(request, *args, **kwargs) - class OtherContacts(DomainRequestWizard): template_name = "domain_request_other_contacts.html" diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index b12812849..a4756f482 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -96,10 +96,6 @@ class FinishProfileSetupView(UserProfileView): """This view forces the user into providing additional details that we may have missed from Login.gov""" - template_name = "finish_profile_setup.html" - form_class = FinishSetupProfileForm - model = Contact - class RedirectType(Enum): """ Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`. @@ -116,8 +112,17 @@ class FinishProfileSetupView(UserProfileView): BACK_TO_SELF = "back_to_self" COMPLETE_SETUP = "complete_setup" - redirect_type = None - all_redirect_types = [r.value for r in RedirectType] + @classmethod + def get_all_redirect_types(cls) -> list[str]: + """Returns the value of every redirect type defined in this enum.""" + return [r.value for r in cls] + + template_name = "finish_profile_setup.html" + form_class = FinishSetupProfileForm + model = Contact + + all_redirect_types = RedirectType.get_all_redirect_types() + redirect_type: RedirectType def get_context_data(self, **kwargs): @@ -151,16 +156,18 @@ class FinishProfileSetupView(UserProfileView): """ # Update redirect type based on the query parameter if present - redirect_type = request.GET.get("redirect", self.RedirectType.BACK_TO_SELF.value) - if redirect_type in self.all_redirect_types: - self.redirect_type = self.RedirectType(redirect_type) + default_redirect_value = self.RedirectType.BACK_TO_SELF.value + redirect_value = request.GET.get("redirect", default_redirect_value) + + if redirect_value in self.all_redirect_types: + # If the redirect value is a preexisting value in our enum, set it to that. + self.redirect_type = self.RedirectType(redirect_value) else: - # If the redirect type is undefined, then we assume that - # we are specifying a particular page to redirect to. + # If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to. self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE # Store the page that we want to redirect to for later use - request.session["redirect_viewname"] = str(redirect_type) + request.session["redirect_viewname"] = str(redirect_value) return super().dispatch(request, *args, **kwargs) @@ -183,8 +190,7 @@ class FinishProfileSetupView(UserProfileView): def get_success_url(self): """Redirect to the nameservers page for the domain.""" - redirect_url = self.get_redirect_url() - return redirect_url + return self.get_redirect_url() def get_redirect_url(self): """ @@ -220,7 +226,7 @@ class FinishProfileSetupView(UserProfileView): query_params = {} # Quote cleans up the value so that it can be used in a url - if self.redirect_type: + if self.redirect_type and self.redirect_type.value: query_params["redirect"] = quote(self.redirect_type.value) # Generate the full url from the given query params diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 0b8a7605a..926ee4a8c 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -296,7 +296,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin): domain_pk = self.kwargs["pk"] user_pk = self.kwargs["user_pk"] - # Check if the user is authenticated if not self.request.user.is_authenticated: return False diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index eb40621b5..d35647af2 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -3,7 +3,9 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView -from registrar.models import Domain, DomainRequest, DomainInvitation, UserDomainRole, Contact +from registrar.models import Domain, DomainRequest, DomainInvitation +from registrar.models.contact import Contact +from registrar.models.user_domain_role import UserDomainRole from .mixins import ( DomainPermission, From e34c72426476ea59ff6e1ef6ea2d28928ae89730 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 13:18:41 -0600 Subject: [PATCH 091/171] Minor refactor --- src/registrar/assets/js/get-gov.js | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index a48d9b46b..baa3f4bbd 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -842,21 +842,14 @@ function hideDeletedForms() { */ (function finishUserSetupListener() { - function getInputFieldId(fieldName){ - return `#id_${fieldName}` - } - - function getReadonlyFieldId(fieldName){ - return `#${fieldName}__edit-button-readonly` + function getInputField(fieldName){ + return document.querySelector(`#id_${fieldName}`) } // Shows the hidden input field and hides the readonly one function showInputFieldHideReadonlyField(fieldName, button) { - let inputId = getInputFieldId(fieldName) - let inputField = document.querySelector(inputId) - - let readonlyId = getReadonlyFieldId(fieldName) - let readonlyField = document.querySelector(readonlyId) + let inputField = getInputField(fieldName) + let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`) readonlyField.classList.toggle('display-none'); inputField.classList.toggle('display-none'); @@ -868,18 +861,16 @@ function hideDeletedForms() { } } - function handleFullNameField(fieldName) { + function handleFullNameField(fieldName = "full_name") { // Remove the display-none class from the nearest parent div - let fieldId = getInputFieldId(fieldName) - let inputField = document.querySelector(fieldId); - let nameFieldset = document.querySelector("#profile-name-group"); if (nameFieldset){ nameFieldset.classList.remove("display-none"); } + // Hide the "full_name" field + let inputField = getInputField(fieldName); if (inputField) { - // Hide the "full_name" field inputFieldParentDiv = inputField.closest("div"); if (inputFieldParentDiv) { inputFieldParentDiv.classList.add("display-none"); @@ -893,11 +884,12 @@ function hideDeletedForms() { button.disabled = true if (fieldName == "full_name"){ - handleFullNameField(fieldName); + handleFullNameField(); }else { showInputFieldHideReadonlyField(fieldName, button); } + // Hide the button itself button.classList.add("display-none"); // Unlock after it completes From 9f2a9594f6403346c667b1e6b5e0308ad239eab6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 12:39:57 -0700 Subject: [PATCH 092/171] Add in other contacts rationale check and clean up tests --- src/registrar/models/domain_request.py | 8 ++++---- src/registrar/tests/test_models.py | 17 +++++++++++++++- src/registrar/tests/test_views_request.py | 24 ++--------------------- src/registrar/views/domain_request.py | 3 --- 4 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index ed98433fa..869a6bcca 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -989,7 +989,7 @@ class DomainRequest(TimeStampedModel): return self.submitter is not None def _is_other_contacts_complete(self): - # If the object even exists and double check for + # If the object even exists and double check each part is filled out if ( self.has_other_contacts() and self.other_contacts.filter( @@ -999,6 +999,8 @@ class DomainRequest(TimeStampedModel): email__isnull=False, phone__isnull=False, ).exists() + # Radio button is No + has rationale + or (self.has_other_contacts() is False and self.no_other_contacts_rationale is not None) ): return True return False @@ -1030,7 +1032,7 @@ class DomainRequest(TimeStampedModel): and self._is_requested_domain_complete() and self._is_purpose_complete() and self._is_submitter_complete() - and self._is_other_contacts_complete() + and self._is_other_contacts_complete() # -- ISSUE HERE and self._is_additional_details_complete() and self._is_policy_acknowledgement_complete() ) @@ -1055,8 +1057,6 @@ class DomainRequest(TimeStampedModel): is_complete = False if not is_complete or not self._is_general_form_complete(): - print("!!!! We are in the False if statement - form is not complete") return False - print("!!!! We are in the True if statement - form is complete") return True diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index 825f86be6..ee784af87 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1655,6 +1655,7 @@ class TestDomainRequestIncomplete(TestCase): requested_domain=draft_domain, purpose="Some purpose", submitter=you, + no_other_contacts_rationale=None, has_cisa_representative=True, cisa_representative_email="somerep@cisa.com", has_anything_else_text=True, @@ -1781,10 +1782,24 @@ class TestDomainRequestIncomplete(TestCase): contact.save() self.assertFalse(self.domain_request._is_other_contacts_complete()) - def test_is_other_contacts_complete(self): + def test_is_other_contacts_complete_all_none(self): self.domain_request.other_contacts.clear() self.assertFalse(self.domain_request._is_other_contacts_complete()) + def test_is_other_contacts_False_and_has_rationale(self): + # Click radio button "No" for no other contacts and give rationale + self.domain_request.other_contacts.clear() + self.domain_request.other_contacts.exists = False + self.domain_request.no_other_contacts_rationale = "Some rationale" + self.assertTrue(self.domain_request._is_other_contacts_complete()) + + def test_is_other_contacts_False_and_NO_rationale(self): + # Click radio button "No" for no other contacts and DONT give rationale + self.domain_request.other_contacts.clear() + self.domain_request.other_contacts.exists = False + self.domain_request.no_other_contacts_rationale = None + self.assertFalse(self.domain_request._is_other_contacts_complete()) + def test_is_additional_details_complete(self): test_cases = [ # CISA Rep - Yes diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index d1ad07ebb..8816840da 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -473,9 +473,7 @@ class DomainRequestTests(TestWithUser, WebTest): @boto3_mocking.patching def test_domain_request_form_submission_incomplete(self): num_pages_tested = 0 - # elections, type_of_work, tribal_government - SKIPPED_PAGES = 3 - # num_pages = len(self.TITLES) - SKIPPED_PAGES + # skipping elections, type_of_work, tribal_government intro_page = self.app.get(reverse("domain-request:")) # django-webtest does not handle cookie-based sessions well because it keeps @@ -772,7 +770,7 @@ class DomainRequestTests(TestWithUser, WebTest): # Follow the redirect to the next form page self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) review_page = requirements_result.follow() - review_form = review_page.forms[0] + # review_form = review_page.forms[0] # Review page contains all the previously entered data # Let's make sure the long org name is displayed @@ -789,24 +787,6 @@ class DomainRequestTests(TestWithUser, WebTest): self.assertNotContains(review_page, "You are about to submit a domain request for city.gov") self.assertContains(review_page, "You can’t submit this request because it’s incomplete.") - # DO WE NEED TO BLOCK SUBMISSIONS WITH INCOMPLETE FORMS ON THE BACKEND @Alysia? - - # final submission results in a redirect to the "finished" URL - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # with less_console_noise(): - # review_result = review_form.submit() - - # self.assertEqual(review_result.status_code, 302) - # self.assertEqual(review_result["Location"], "/request/finished/") - # num_pages_tested += 1 - - # # following this redirect is a GET request, so include the cookie - # # here too. - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # with less_console_noise(): - # final_result = review_result.follow() - # self.assertContains(final_result, "Thanks for your domain request!") - # This is the start of a test to check an existing domain_request, it currently # does not work and results in errors as noted in: # https://github.com/cisagov/getgov/pull/728 diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 7e8f9e1d4..d13c88599 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -381,12 +381,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): context_stuff = {} if DomainRequest._form_complete(self.domain_request): - print("!!!!!!!in form complete section") modal_button = '" context_stuff = { "form_titles": self.TITLES, "steps": self.steps, - # Add information about which steps should be unlocked "visited": self.storage.get("step_history", []), "is_federal": self.domain_request.is_federal(), "modal_button": modal_button, @@ -397,7 +395,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "review_form_is_complete": True, } else: # form is not complete - print("!!!!!!! form is not complete") modal_button = '" context_stuff = { "form_titles": self.TITLES, From 54c5052b2bf001fbc5cadd2f8c99cd5cedfa888d Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 12:46:41 -0700 Subject: [PATCH 093/171] Fix merge conflict --- src/registrar/views/domain_request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index d13c88599..6659b3fab 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -22,6 +22,8 @@ from .utility import ( DomainRequestWizardPermissionView, ) +from waffle.decorators import flag_is_active + logger = logging.getLogger(__name__) @@ -376,8 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): def get_context_data(self): """Define context for access on all wizard pages.""" - # Build the submit button that we'll pass to the modal. - # Concatenate the modal header that we'll pass to the modal. context_stuff = {} if DomainRequest._form_complete(self.domain_request): @@ -393,6 +393,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\ You’ll only be able to withdraw your request.", "review_form_is_complete": True, + "has_profile_feature_flag": flag_is_active(self.request, "profile_feature"), } else: # form is not complete modal_button = '" @@ -406,6 +407,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "modal_description": "You can’t submit this request because it’s incomplete.\ Click return to request and complete the sections that are missing information.", "review_form_is_complete": False, + "has_profile_feature_flag": flag_is_active(self.request, "profile_feature"), } return context_stuff From 6668a292a3558a25ddc8cc173a788cc2bf2f8633 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 12:50:38 -0700 Subject: [PATCH 094/171] Remove duplicate waffle header --- src/registrar/views/domain_request.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 5e1dc1844..a098f778a 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -24,8 +24,6 @@ from .utility import ( from waffle.decorators import flag_is_active -from waffle.decorators import flag_is_active - logger = logging.getLogger(__name__) From b6e931e5a9cd780ed868b49377f3ccc76a70a5db Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 13:02:34 -0700 Subject: [PATCH 095/171] Remove comment --- src/registrar/models/domain_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain_request.py b/src/registrar/models/domain_request.py index 869a6bcca..57a1c53d7 100644 --- a/src/registrar/models/domain_request.py +++ b/src/registrar/models/domain_request.py @@ -1032,7 +1032,7 @@ class DomainRequest(TimeStampedModel): and self._is_requested_domain_complete() and self._is_purpose_complete() and self._is_submitter_complete() - and self._is_other_contacts_complete() # -- ISSUE HERE + and self._is_other_contacts_complete() and self._is_additional_details_complete() and self._is_policy_acknowledgement_complete() ) From 1c6a565ec3508b4a262dddef9478b9d2f13656bd Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 13:08:33 -0700 Subject: [PATCH 096/171] Fix test in views with new wording --- src/registrar/tests/test_views_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_views_request.py b/src/registrar/tests/test_views_request.py index 8816840da..f40c72114 100644 --- a/src/registrar/tests/test_views_request.py +++ b/src/registrar/tests/test_views_request.py @@ -2705,7 +2705,7 @@ class DomainRequestTests(TestWithUser, WebTest): review_page = self.app.get(reverse("domain-request:review")) self.assertContains(review_page, "toggle-submit-domain-request") - self.assertContains(review_page, "You are about to submit an incomplete request") + self.assertContains(review_page, "You can’t submit this request because it’s incomplete.") class DomainRequestTestDifferentStatuses(TestWithUser, WebTest): From 05868966c56681db29484416a1f546602b9e62b9 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 13:14:29 -0700 Subject: [PATCH 097/171] pally fix maybe --- src/registrar/templates/includes/modal.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index cb99fa321..c745fc946 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -41,7 +41,7 @@ {% elif not is_domain_request_form or review_form_is_complete %}
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %} - {% with show_readonly=True add_class="display-none" group_classes="usa-form-editable padding-top-2 bold-usa-label" %} + {% with show_readonly=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %} {% with link_href=login_help_url %} {% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %} {% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %} @@ -62,11 +62,11 @@ {% endwith %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="padding-top-2" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% input_with_errors form.title %} {% endwith %} - {% with show_edit_button=True show_readonly=True group_classes="padding-top-2" %} + {% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %} {% with add_class="usa-input--medium" %} {% input_with_errors form.phone %} {% endwith %} diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py index be78099db..b72f77e21 100644 --- a/src/registrar/templatetags/field_helpers.py +++ b/src/registrar/templatetags/field_helpers.py @@ -97,10 +97,6 @@ def input_with_errors(context, field=None): # noqa: C901 # Used such that we can toggle it with JS if "display-none" not in classes: classes.append("display-none") - - # Tag that this form contains the edit button. - if "usa-form-editable" not in group_classes: - group_classes.append("usa-form-editable") attrs["id"] = field.auto_id diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index bfe4fd142..4c0ba08ee 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -628,9 +628,12 @@ class FinishUserProfileTests(TestWithUser, WebTest): completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True) self.assertEqual(completed_setup_page.status_code, 200) + # Assert that we're on the domain request page - self.assertContains(completed_setup_page, "How we’ll reach you") - self.assertContains(completed_setup_page, "Your contact information") + self.assertNotContains(completed_setup_page, "Finish setting up your profile") + self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?") + + self.assertContains(completed_setup_page, "You’re about to start your .gov domain request") @less_console_noise_decorator def test_new_user_with_profile_feature_off(self): @@ -645,8 +648,11 @@ class FinishUserProfileTests(TestWithUser, WebTest): when profile_feature is off but not the setup page""" with override_flag("profile_feature", active=False): response = self.client.get("/request/") - self.assertContains(response, "How we’ll reach you") - self.assertContains(response, "Your contact information") + + self.assertNotContains(response, "Finish setting up your profile") + self.assertNotContains(response, "What contact information should we use to reach you?") + + self.assertContains(response, "You’re about to start your .gov domain request") class UserProfileTests(TestWithUser, WebTest): @@ -806,11 +812,6 @@ class UserProfileTests(TestWithUser, WebTest): profile_page = self.app.get(reverse("user-profile")) session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - profile_form = profile_page.form - profile_page = profile_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - profile_form = profile_page.form profile_form["title"] = "sample title" profile_form["phone"] = "(201) 555-1212" From 0a079c0a8633864256baaecc843dd3d2b26e804f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 28 May 2024 14:18:06 -0600 Subject: [PATCH 099/171] Lint --- .../management/commands/copy_names_from_contacts_to_users.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/management/commands/copy_names_from_contacts_to_users.py b/src/registrar/management/commands/copy_names_from_contacts_to_users.py index 602a3f0b7..384029400 100644 --- a/src/registrar/management/commands/copy_names_from_contacts_to_users.py +++ b/src/registrar/management/commands/copy_names_from_contacts_to_users.py @@ -111,7 +111,6 @@ class Command(BaseCommand): {TerminalColors.ENDC}""", # noqa ) - # Get the fields that exist on both User and Contact. Excludes id. common_fields = DomainHelper.get_common_fields(User, Contact) if "email" in common_fields: From 0b0f8a4e1af5355041a8baef4242be09d862ec41 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 15:55:59 -0700 Subject: [PATCH 100/171] Maybe Pa11y fix: --- src/registrar/templates/includes/modal.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index c745fc946..04363c2c2 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -39,13 +39,15 @@ Cancel - {% elif not is_domain_request_form or review_form_is_complete %} + {% elif is_domain_request_form or review_form_is_complete %} + + {% else %} {% endif %} From a7e0ddc44358410af6571e971ba17db3498ac7f8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 16:08:09 -0700 Subject: [PATCH 101/171] This should fix pa11y --- src/registrar/views/domain_request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index a098f778a..7fa8d3384 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -396,7 +396,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "has_profile_feature_flag": flag_is_active(self.request, "profile_feature"), } else: # form is not complete - modal_button = '" + modal_button = '" context_stuff = { "form_titles": self.TITLES, "steps": self.steps, From a1bf9085a7bb81694158b2f090244b397f5dde10 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 16:17:30 -0700 Subject: [PATCH 102/171] Change modal logic back --- src/registrar/templates/includes/modal.html | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index 04363c2c2..cb99fa321 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -39,15 +39,13 @@ Cancel - {% elif is_domain_request_form or review_form_is_complete %} - - {% else %} + {% elif not is_domain_request_form or review_form_is_complete %} {% endif %} From 3f276f1bdf6e643390e85c00f7f6fe95367b83b3 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 28 May 2024 16:32:04 -0700 Subject: [PATCH 103/171] Remove extraneous comment --- src/registrar/tests/test_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ee784af87..7b4b9fccd 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -1637,7 +1637,6 @@ class TestDomainRequestIncomplete(TestCase): email="testy2@town.com", phone="(555) 555 5557", ) - # domain, _ = Domain.objects.get_or_create(name="MeowardMeowardMeoward.gov") alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov") current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com") self.domain_request = DomainRequest.objects.create( From 29288aca6274266b48608d9da23efdf5901922b9 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Tue, 28 May 2024 20:38:50 -0600 Subject: [PATCH 104/171] Updated ALL table rows outside header row to use --hairline color --- src/registrar/assets/sass/_theme/_admin.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index c716ad49c..5f2b95b34 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -151,6 +151,10 @@ html[data-theme="dark"] { } } +.usa-table td { + border-bottom: 1px solid var(--hairline-color); +} + #branding h1 a:link, #branding h1 a:visited { color: var(--primary-fg); } From 7221e2c0d0d37ce86918d7432bbe56ac219398d0 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 29 May 2024 10:45:37 -0600 Subject: [PATCH 105/171] updated CSS for table to target only tables in change forms. Updated styling for dark mode. --- src/registrar/assets/sass/_theme/_admin.scss | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 5f2b95b34..562ffdd11 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -151,9 +151,6 @@ html[data-theme="dark"] { } } -.usa-table td { - border-bottom: 1px solid var(--hairline-color); -} #branding h1 a:link, #branding h1 a:visited { color: var(--primary-fg); @@ -186,6 +183,18 @@ div#content > h2 { } } +.change-form { + .usa-table--striped tbody tr:nth-child(odd) td, + .usa-table--striped tbody tr:nth-child(odd) th, + .usa-table td, + .usa-table th { + background-color: transparent; + } + .usa-table th { + background-color: transparent; + } +} + #nav-sidebar { padding-top: 20px; } From a5a0c136df3df99fb4e2f51613bb5f2f3e833bc3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 29 May 2024 14:11:39 -0600 Subject: [PATCH 106/171] Add link --- .../models/utility/generic_helper.py | 28 +++++++++++++++++ .../templates/domain_request_intro.html | 12 ++++---- src/registrar/templates/profile.html | 9 ++++-- src/registrar/views/user_profile.py | 30 +++++++++++++++++-- 4 files changed, 68 insertions(+), 11 deletions(-) diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 0befd6627..3a6e04d4e 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,6 +2,8 @@ import time import logging +from typing import Any +from urllib.parse import urlparse, urlunparse, urlencode logger = logging.getLogger(__name__) @@ -266,3 +268,29 @@ class CreateOrUpdateOrganizationTypeHelper: return False else: return True + + +def replace_url_queryparams(url_to_modify: str, query_params: dict[Any, list]): + """ + Replaces the query parameters of a given URL. + Because this replaces them, this can be used to either add, delete, or modify. + Args: + url_to_modify (str): The URL whose query parameters need to be modified. + query_params (dict): Dictionary of query parameters to use. + Returns: + str: The modified URL with the updated query parameters. + """ + + # Ensure each key in query_params maps to a single value, not a list + query_params = {k: v[0] if isinstance(v, list) else v for k, v in query_params.items()} + + # Split the URL into parts + url_parts = list(urlparse(url_to_modify)) + + # Modify the query param bit + url_parts[4] = urlencode(query_params) + + # Reassemble the URL + new_url = urlunparse(url_parts) + + return new_url \ No newline at end of file diff --git a/src/registrar/templates/domain_request_intro.html b/src/registrar/templates/domain_request_intro.html index d6d3b3b7f..285777a80 100644 --- a/src/registrar/templates/domain_request_intro.html +++ b/src/registrar/templates/domain_request_intro.html @@ -13,12 +13,12 @@

We’ll use the information you provide to verify your organization’s eligibility for a .gov domain. We’ll also verify that the domain you request meets our guidelines.

Time to complete the form

If you have all the information you need, - completing your domain request might take around 15 minutes.

- {% if has_profile_feature_flag %} -

How we’ll reach you

-

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.

- {% include "includes/profile_information.html" with user=user%} - {% endif %} + completing your domain request might take around 15 minutes.

+ {% if has_profile_feature_flag %} +

How we’ll reach you

+

While reviewing your domain request, we may need to reach out with questions. We’ll also email you when we complete our review If the contact information below is not correct, visit your profile to make updates.

+ {% include "includes/profile_information.html" with user=user%} + {% endif %} {% block form_buttons %} diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 13179e4ac..368bc7367 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -10,14 +10,19 @@ Edit your User Profile | {% block content %}
- + - + {% if not return_to_request %}

Back to manage your domains

+ {% else %} +

+ Go back to your domain request +

+ {% endif %}
{# messages block is under the back breadcrumb link #} {% if messages %} diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index b386e6a62..9662f71e4 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -3,6 +3,7 @@ """ import logging +from urllib.parse import parse_qs, unquote, urlencode from django.contrib import messages from django.views.generic.edit import FormMixin @@ -11,6 +12,7 @@ from django.urls import reverse from registrar.models import ( Contact, ) +from registrar.models.utility.generic_helper import replace_url_queryparams from registrar.views.utility.permission_views import UserProfilePermissionView from waffle.decorators import flag_is_active, waffle_flag @@ -30,13 +32,26 @@ class UserProfileView(UserProfilePermissionView, FormMixin): def get(self, request, *args, **kwargs): """Handle get requests by getting user's contact object and setting object and form to context before rendering.""" - self.object = self.get_object() + self._refresh_session_and_object(request) form = self.form_class(instance=self.object) context = self.get_context_data(object=self.object, form=form) + + return_to_request = request.GET.get("return_to_request") + if return_to_request: + context["return_to_request"] = True + return self.render_to_response(context) + def _refresh_session_and_object(self, request): + """Sets the current session to self.session and the current object to self.object""" + self.session = request.session + self.object = self.get_object() + @waffle_flag("profile_feature") # type: ignore def dispatch(self, request, *args, **kwargs): # type: ignore + # Store the original queryparams to persist them + query_params = request.META["QUERY_STRING"] + request.session["query_params"] = query_params return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): @@ -48,11 +63,20 @@ class UserProfileView(UserProfilePermissionView, FormMixin): def get_success_url(self): """Redirect to the user's profile page.""" - return reverse("user-profile") + + query_params = {} + if "query_params" in self.session: + params = unquote(self.session["query_params"]) + query_params = parse_qs(params) + + # Preserve queryparams and add them back to the url + base_url = reverse("user-profile") + new_redirect = replace_url_queryparams(base_url, query_params) if query_params else base_url + return new_redirect def post(self, request, *args, **kwargs): """Handle post requests (form submissions)""" - self.object = self.get_object() + self._refresh_session_and_object(request) form = self.form_class(request.POST, instance=self.object) if form.is_valid(): From 6b9cb3a2289121e39bcd46747625d1617dc36990 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 29 May 2024 15:49:35 -0700 Subject: [PATCH 107/171] See if fix for pa11y works --- src/registrar/views/domain_request.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 7f8f7dec0..a86fa500d 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -401,13 +401,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): "user": self.request.user, } else: # form is not complete - # modal_button = '" - # modal_button = '' - # modal_button = ( - # '' - # ) - modal_button = '" + modal_button = ( + '' + ) context_stuff = { "form_titles": self.TITLES, "steps": self.steps, From a34f55d44fe2edfd6ab7b249a17a3726fc2152c3 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 08:31:48 -0700 Subject: [PATCH 108/171] Test 3 of Pa11y fix --- src/registrar/views/domain_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index a86fa500d..8d780e9dc 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -402,8 +402,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } else: # form is not complete modal_button = ( - '' + "" + '' ) context_stuff = { "form_titles": self.TITLES, From 061920f538a833bdeb74f4e8bc990fd8fac16ad6 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 08:49:48 -0700 Subject: [PATCH 109/171] Add ignore rule for pa11y --- src/.pa11yci | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 56ea40416..e20ebb179 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -21,5 +21,8 @@ "http://localhost:8080/request/requirements/", "http://localhost:8080/request/finished/", "http://localhost:8080/user-profile/" - ] -} + ], + "ignore": [ + "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" + ], +} \ No newline at end of file From e94412e7a557e6245aacbfd1cceda87d1af47513 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 30 May 2024 09:52:50 -0600 Subject: [PATCH 110/171] Add unit test --- .../models/utility/generic_helper.py | 12 ++++--- src/registrar/templates/profile.html | 31 ++++++++++--------- src/registrar/tests/test_views.py | 10 ++++++ src/registrar/views/user_profile.py | 2 +- 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/src/registrar/models/utility/generic_helper.py b/src/registrar/models/utility/generic_helper.py index 3a6e04d4e..ca6ce6c31 100644 --- a/src/registrar/models/utility/generic_helper.py +++ b/src/registrar/models/utility/generic_helper.py @@ -2,7 +2,6 @@ import time import logging -from typing import Any from urllib.parse import urlparse, urlunparse, urlencode @@ -270,19 +269,24 @@ class CreateOrUpdateOrganizationTypeHelper: return True -def replace_url_queryparams(url_to_modify: str, query_params: dict[Any, list]): +def replace_url_queryparams(url_to_modify: str, query_params, convert_list_to_csv=False): """ Replaces the query parameters of a given URL. Because this replaces them, this can be used to either add, delete, or modify. Args: url_to_modify (str): The URL whose query parameters need to be modified. query_params (dict): Dictionary of query parameters to use. + convert_list_to_csv (bool): If the queryparam contains a list of items, + convert it to a csv representation instead. Returns: str: The modified URL with the updated query parameters. """ # Ensure each key in query_params maps to a single value, not a list - query_params = {k: v[0] if isinstance(v, list) else v for k, v in query_params.items()} + if convert_list_to_csv: + for key, value in query_params.items(): + if isinstance(value, list): + query_params[key] = ",".join(value) # Split the URL into parts url_parts = list(urlparse(url_to_modify)) @@ -293,4 +297,4 @@ def replace_url_queryparams(url_to_modify: str, query_params: dict[Any, list]): # Reassemble the URL new_url = urlunparse(url_parts) - return new_url \ No newline at end of file + return new_url diff --git a/src/registrar/templates/profile.html b/src/registrar/templates/profile.html index 368bc7367..e3cf3e2ae 100644 --- a/src/registrar/templates/profile.html +++ b/src/registrar/templates/profile.html @@ -10,21 +10,7 @@ Edit your User Profile | {% block content %}
- - - {% if not return_to_request %} -

- Back to manage your domains -

- {% else %} -

- Go back to your domain request -

- {% endif %} -
- {# messages block is under the back breadcrumb link #} + {# messages block #} {% if messages %} {% for message in messages %}
@@ -35,6 +21,21 @@ Edit your User Profile | {% endfor %} {% endif %} {% include "includes/form_errors.html" with form=form %} + + + + {% if not return_to_request %} +

+ Back to manage your domains +

+ {% else %} +

+ Go back to your domain request +

+ {% endif %} +

Your profile

We require that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.

diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8c8a0fda0..76407e97e 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -597,6 +597,16 @@ class UserProfileTests(TestWithUser, WebTest): response = self.client.get("/user-profile") self.assertEqual(response.status_code, 404) + @less_console_noise_decorator + def test_user_profile_back_button_when_coming_from_domain_request(self): + """tests user profile when profile_feature is on, + and when they are redirected from the domain request page""" + with override_flag("profile_feature", active=True): + response = self.client.get("/user-profile?return_to_request=True") + self.assertContains(response, "Your profile") + self.assertContains(response, "Go back to your domain request") + self.assertNotContains(response, "Back to manage your domains") + @less_console_noise_decorator def test_domain_detail_profile_feature_on(self): """test that domain detail view when profile_feature is on""" diff --git a/src/registrar/views/user_profile.py b/src/registrar/views/user_profile.py index 9662f71e4..31cb7697b 100644 --- a/src/registrar/views/user_profile.py +++ b/src/registrar/views/user_profile.py @@ -71,7 +71,7 @@ class UserProfileView(UserProfilePermissionView, FormMixin): # Preserve queryparams and add them back to the url base_url = reverse("user-profile") - new_redirect = replace_url_queryparams(base_url, query_params) if query_params else base_url + new_redirect = replace_url_queryparams(base_url, query_params, convert_list_to_csv=True) return new_redirect def post(self, request, *args, **kwargs): From 493998bbd99096e163bb37d9844cb45ee773b82c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 08:54:54 -0700 Subject: [PATCH 111/171] Fix parameterization --- src/.pa11yci | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index e20ebb179..448817778 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -3,7 +3,8 @@ "concurrency": 1, "timeout": 30000 }, - "urls": [ + "urls": { + [ "http://localhost:8080/", "http://localhost:8080/health/", "http://localhost:8080/request/", @@ -21,8 +22,9 @@ "http://localhost:8080/request/requirements/", "http://localhost:8080/request/finished/", "http://localhost:8080/user-profile/" - ], - "ignore": [ + ], + "ignore": [ "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" - ], + ], + } } \ No newline at end of file From d6b6d98d96b9a879f20c58842b67e9ed5454283b Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 08:58:34 -0700 Subject: [PATCH 112/171] Fix parameterization pt 2 --- src/.pa11yci | 49 +++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 448817778..6dac13a5c 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -3,28 +3,29 @@ "concurrency": 1, "timeout": 30000 }, - "urls": { - [ - "http://localhost:8080/", - "http://localhost:8080/health/", - "http://localhost:8080/request/", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/authorizing_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/user-profile/" - ], - "ignore": [ - "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" - ], - } + "urls": [ + { + ["http://localhost:8080/", + "http://localhost:8080/health/", + "http://localhost:8080/request/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/authorizing_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/user-profile/" + ], + "ignore": [ + "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" + ], + } + ] } \ No newline at end of file From bc08e8e556cb142c15b3b9b971d596d38a83a6bd Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 09:53:18 -0700 Subject: [PATCH 113/171] Formatting fix --- src/.pa11yci | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 6dac13a5c..897682628 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -5,27 +5,28 @@ }, "urls": [ { - ["http://localhost:8080/", - "http://localhost:8080/health/", - "http://localhost:8080/request/", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/authorizing_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/user-profile/" + "urls": [ + "http://localhost:8080/", + "http://localhost:8080/health/", + "http://localhost:8080/request/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/authorizing_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/user-profile/" ], "ignore": [ "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" - ], + ] } ] } \ No newline at end of file From 05f19bd6cc4e9f5474b5b28ca6da3401be21417e Mon Sep 17 00:00:00 2001 From: CocoByte Date: Thu, 30 May 2024 12:51:58 -0600 Subject: [PATCH 114/171] Fixed CSS to what it was suppoed to be... --- src/registrar/assets/sass/_theme/_admin.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 562ffdd11..0c4d1da3f 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -190,8 +190,8 @@ div#content > h2 { .usa-table th { background-color: transparent; } - .usa-table th { - background-color: transparent; + .usa-table td { + border-bottom: 1px solid var(--hairline-color); } } From 009d8948bc6335462cbdac4b9dab360eba496f26 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 12:00:16 -0700 Subject: [PATCH 115/171] Revert pa11y changes and add role --- src/.pa11yci | 43 +++++++++++---------------- src/registrar/views/domain_request.py | 3 +- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 897682628..56ea40416 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -4,29 +4,22 @@ "timeout": 30000 }, "urls": [ - { - "urls": [ - "http://localhost:8080/", - "http://localhost:8080/health/", - "http://localhost:8080/request/", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/authorizing_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/user-profile/" - ], - "ignore": [ - "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" - ] - } + "http://localhost:8080/", + "http://localhost:8080/health/", + "http://localhost:8080/request/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/authorizing_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/user-profile/" ] -} \ No newline at end of file +} diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index 8d780e9dc..c0c63a47b 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -402,8 +402,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } else: # form is not complete modal_button = ( - "" - '' + '' ) context_stuff = { "form_titles": self.TITLES, From ae4fb7b5e5a1eff9567aec283b02c8c96e0a953c Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 12:25:38 -0700 Subject: [PATCH 116/171] Fix ignore in pallyci --- src/.pa11yci | 47 ++++++++++++++++----------- src/registrar/views/domain_request.py | 3 +- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 56ea40416..056922545 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -3,23 +3,32 @@ "concurrency": 1, "timeout": 30000 }, - "urls": [ - "http://localhost:8080/", - "http://localhost:8080/health/", - "http://localhost:8080/request/", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/authorizing_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/user-profile/" - ] + "urls": { + [ + "http://localhost:8080/", + "http://localhost:8080/health/", + "http://localhost:8080/request/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/authorizing_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/user-profile/" + { + "url": "http://localhost:8080/request/authorizing_official/", + "viewport": { "width": 320, "height": 480 }, + "ignore": [ + "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" + ], + } + ] + } } diff --git a/src/registrar/views/domain_request.py b/src/registrar/views/domain_request.py index c0c63a47b..8d780e9dc 100644 --- a/src/registrar/views/domain_request.py +++ b/src/registrar/views/domain_request.py @@ -402,7 +402,8 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView): } else: # form is not complete modal_button = ( - '' + "" + '' ) context_stuff = { "form_titles": self.TITLES, From 3d44bdf5e92350ae421f62f2746409422e629846 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 12:39:02 -0700 Subject: [PATCH 117/171] Try adding in ignore --- src/.pa11yci | 51 +++++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 056922545..49841d346 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -3,32 +3,27 @@ "concurrency": 1, "timeout": 30000 }, - "urls": { - [ - "http://localhost:8080/", - "http://localhost:8080/health/", - "http://localhost:8080/request/", - "http://localhost:8080/request/organization/", - "http://localhost:8080/request/org_federal/", - "http://localhost:8080/request/org_election/", - "http://localhost:8080/request/org_contact/", - "http://localhost:8080/request/authorizing_official/", - "http://localhost:8080/request/current_sites/", - "http://localhost:8080/request/dotgov_domain/", - "http://localhost:8080/request/purpose/", - "http://localhost:8080/request/your_contact/", - "http://localhost:8080/request/other_contacts/", - "http://localhost:8080/request/anything_else/", - "http://localhost:8080/request/requirements/", - "http://localhost:8080/request/finished/", - "http://localhost:8080/user-profile/" - { - "url": "http://localhost:8080/request/authorizing_official/", - "viewport": { "width": 320, "height": 480 }, - "ignore": [ - "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" - ], - } - ] - } + "urls": [ + "http://localhost:8080/", + "http://localhost:8080/health/", + "http://localhost:8080/request/", + "http://localhost:8080/request/organization/", + "http://localhost:8080/request/org_federal/", + "http://localhost:8080/request/org_election/", + "http://localhost:8080/request/org_contact/", + "http://localhost:8080/request/authorizing_official/", + "http://localhost:8080/request/current_sites/", + "http://localhost:8080/request/dotgov_domain/", + "http://localhost:8080/request/purpose/", + "http://localhost:8080/request/your_contact/", + "http://localhost:8080/request/other_contacts/", + "http://localhost:8080/request/anything_else/", + "http://localhost:8080/request/requirements/", + "http://localhost:8080/request/finished/", + "http://localhost:8080/user-profile/" + ], + "ignore": [ + "notice", + "warnings" + ] } From 5faf480e3e249a4ebc74b876b4256c3a0030d2b7 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 12:41:37 -0700 Subject: [PATCH 118/171] Add in specific requirement to ignore --- src/.pa11yci | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index 49841d346..ecca42a34 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -23,7 +23,6 @@ "http://localhost:8080/user-profile/" ], "ignore": [ - "notice", - "warnings" + "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" ] } From 707e5a21eacf19d606d9ff86acffeaeb5f0909e2 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 30 May 2024 12:59:40 -0700 Subject: [PATCH 119/171] Removing post form for certain situations --- src/.pa11yci | 3 --- src/registrar/templates/includes/modal.html | 14 ++++++++++---- src/registrar/views/domain_request.py | 7 +++---- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/.pa11yci b/src/.pa11yci index ecca42a34..56ea40416 100644 --- a/src/.pa11yci +++ b/src/.pa11yci @@ -21,8 +21,5 @@ "http://localhost:8080/request/requirements/", "http://localhost:8080/request/finished/", "http://localhost:8080/user-profile/" - ], - "ignore": [ - "WCAG2AA.Principle2.Guideline2_2.2_2_1.H91" ] } diff --git a/src/registrar/templates/includes/modal.html b/src/registrar/templates/includes/modal.html index cb99fa321..b0b316a2f 100644 --- a/src/registrar/templates/includes/modal.html +++ b/src/registrar/templates/includes/modal.html @@ -18,12 +18,18 @@