From b252418088e8a99d8ae98c644831dedc1841b895 Mon Sep 17 00:00:00 2001 From: igorkorenfeld Date: Fri, 17 Feb 2023 17:06:34 -0500 Subject: [PATCH 1/9] Tighten spacing before input; fix space after h2 on fed page --- .../assets/sass/_theme/_uswds-theme-custom-styles.scss | 2 +- src/registrar/templates/application_org_federal.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss index c61e79337..896856116 100644 --- a/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss +++ b/src/registrar/assets/sass/_theme/_uswds-theme-custom-styles.scss @@ -105,7 +105,7 @@ $letter-space--xs: .0125em; color: color('violet-70v'); //USWDS default } } - +.register-form-step .usa-form-group:first-of-type, .register-form-step .usa-label:first-of-type { margin-top: units(1); } diff --git a/src/registrar/templates/application_org_federal.html b/src/registrar/templates/application_org_federal.html index f51ed7c7a..138a6c334 100644 --- a/src/registrar/templates/application_org_federal.html +++ b/src/registrar/templates/application_org_federal.html @@ -2,7 +2,7 @@ {% load field_helpers %} {% block form_instructions %} -

+

Which federal branch is your organization in?

{% endblock %} @@ -12,4 +12,4 @@ {% with add_class="usa-radio__input--tile" %} {% input_with_errors forms.0.federal_type %} {% endwith %} -{% endblock %} \ No newline at end of file +{% endblock %} From f98cac9d3da8dbed8e4442a593e23dbc772fa153 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Tue, 21 Feb 2023 09:02:36 -0600 Subject: [PATCH 2/9] Justify FRED for registry communication --- .../decisions/0018-registry-integration.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/architecture/decisions/0018-registry-integration.md diff --git a/docs/architecture/decisions/0018-registry-integration.md b/docs/architecture/decisions/0018-registry-integration.md new file mode 100644 index 000000000..5bdb8ea2e --- /dev/null +++ b/docs/architecture/decisions/0018-registry-integration.md @@ -0,0 +1,37 @@ +# 18. Registry Integration + +Date: 2022-02-15 + +## Status + +Accepted + +## Context + +There are relatively few existing open source software projects which implement registry-registrar communications and even fewer of them in Python. + +This creates a twofold problem: first, there are few design patterns which we can consult to determine how to build; second, there are few libraries we can freely use. + +The incoming registry vendor has pointed to [FRED’s epplib](https://gitlab.nic.cz/fred/utils/epplib) as a newly-developed example which may suit most of our needs. This library is able to establish the TCP connection. It also contains a number of helper methods for preparing the XML requests and parsing the XML responses. + +Commands in the EPP protocol are not synchronous, meaning that the response to a command will acknowledge receipt of it, but may not indicate success or failure. + +This creates an additional challenge: we do not desire to have complex background jobs to run polling. The registrar does not anticipate having a volume of daily users to make such an investment worthwhile, nor a supply of system administrators to monitor and troubleshoot such a system. + +Beyond these mechanical requirements, we also need a firm understanding of the rules governing how and when commands can be issued to the registry. + +## Decision + +To use the open source FRED epplib developed by the .cz registry. + +To treat commands given to the registry as asynchronous from a user experience perspective. In other words, “the registry has received your request, please check back later”. + +To develop the Domain model as the interface to epplib. + +## Consequences + +Using the Domain model as an interface will funnel interactions with the registry and consolidate rules in a single location. This will be a significant benefit to future maintainers, but it does stretch the normal metaphor of a Django model as representing a database table. This may introduce some confusion or uncertainty. + +Treating commands as asynchronous will need support from content managers and user interface designers to help registrants and analysts understand the system’s behavior. Without adequate support, users will experience surprise and frustration. + +FRED epplib is in early active development. It may not contain all of the features we’d like. Limitations in what upstream maintainers are able to accept, either due to policy or due to staffing or due to lack of interest, may require CISA to fork the project. This will incur a maintenance burden on CISA. From 2024d1c905c5b10a987294dd450df5e4194942a8 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Tue, 21 Feb 2023 11:57:23 -0700 Subject: [PATCH 3/9] v2 with signed. Referring to PR #414 --- src/registrar/templates/application_form.html | 4 ++++ src/registrar/templates/base.html | 2 ++ src/registrar/templates/includes/form_errors.html | 9 +++++++-- src/registrar/templates/includes/form_messages.html | 10 ++++++++++ src/registrar/views/application.py | 13 +++++++++++++ 5 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 src/registrar/templates/includes/form_messages.html diff --git a/src/registrar/templates/application_form.html b/src/registrar/templates/application_form.html index 9b1240013..628916b83 100644 --- a/src/registrar/templates/application_form.html +++ b/src/registrar/templates/application_form.html @@ -18,6 +18,10 @@ {% endif %} +{% block form_messages %} + {% include "includes/form_messages.html" %} +{% endblock %} + {% block form_errors %} {% comment %} to make sense of this loop, consider that diff --git a/src/registrar/templates/base.html b/src/registrar/templates/base.html index 2479fceea..a864ac48d 100644 --- a/src/registrar/templates/base.html +++ b/src/registrar/templates/base.html @@ -167,9 +167,11 @@ {% if messages %}
    {% for message in messages %} + {% if 'base' in message.extra_tags %} {{ message }} + {% endif %} {% endfor %}
{% endif %} diff --git a/src/registrar/templates/includes/form_errors.html b/src/registrar/templates/includes/form_errors.html index f5e9a8791..a5dd99efc 100644 --- a/src/registrar/templates/includes/form_errors.html +++ b/src/registrar/templates/includes/form_errors.html @@ -1,5 +1,9 @@ +{% comment %} +Commenting the code below to turn off the error because +we are showing the caution dialog instead. But saving in +case we want to revert this. {% if form.errors %} - {% for error in form.non_field_errors %} +{% for error in form.non_field_errors %}
{{ error|escape }} @@ -15,4 +19,5 @@
{% endfor %} {% endfor %} -{% endif %} \ No newline at end of file +{% endif %} +{% endcomment %} \ No newline at end of file diff --git a/src/registrar/templates/includes/form_messages.html b/src/registrar/templates/includes/form_messages.html new file mode 100644 index 000000000..c7b704f67 --- /dev/null +++ b/src/registrar/templates/includes/form_messages.html @@ -0,0 +1,10 @@ +{% if messages %} +{% for message in messages %} +
+
+ {{ message }} +
+
+ +{% endfor %} +{% endif %} \ No newline at end of file diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 35e938bad..81edbf618 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -6,6 +6,8 @@ from django.shortcuts import redirect, render from django.urls import resolve, reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView +from django.contrib import messages +from django.utils.safestring import mark_safe from registrar.forms import application_wizard as forms from registrar.models import DomainApplication @@ -319,6 +321,16 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): self.save(forms) else: # unless there are errors + # no sec because this use of mark_safe does not introduce a cross-site + # scripting vulnerability because there is no untrusted content inside. + # It is only being used to pass a specific HTML entity into a template. + messages.warning( + request, + mark_safe( # nosec + "We could not save all the fields.
The highlighted " + + "fields below could not be saved because they have " + + "missing or invalid data. All other information on this page " + + "has been saved." context = self.get_context_data() context["forms"] = forms return render(request, self.template_name, context) @@ -326,6 +338,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): # if user opted to save their progress, # return them to the page they were already on if button == "save": + messages.success(request, "Your progress has been saved!") return self.goto(self.steps.current) # otherwise, proceed as normal return self.goto_next_step() From 672e8efd8c877b4dc6954bc382238a42520f1596 Mon Sep 17 00:00:00 2001 From: Jon Roberts Date: Tue, 21 Feb 2023 12:14:54 -0700 Subject: [PATCH 4/9] Forgot closed bracket. Ran test and lint again, passed --- src/registrar/views/application.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 81edbf618..c34a8e1c1 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -331,6 +331,8 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): + "fields below could not be saved because they have " + "missing or invalid data. All other information on this page " + "has been saved." + ), + ) context = self.get_context_data() context["forms"] = forms return render(request, self.template_name, context) From ab34629a7be3b9f244179df01928ec6689668b78 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Wed, 22 Feb 2023 10:24:02 -0600 Subject: [PATCH 5/9] Place no other contacts reason box on own page --- src/registrar/config/urls.py | 1 + src/registrar/forms/application_wizard.py | 2 +- src/registrar/models/domain_application.py | 4 ++++ .../application_no_other_contacts.html | 6 ++++++ .../templates/application_other_contacts.html | 4 ---- .../templates/application_review.html | 19 +++++++++++++---- src/registrar/tests/test_views.py | 21 ++++++++++++++++++- src/registrar/views/application.py | 12 ++++++++++- 8 files changed, 58 insertions(+), 11 deletions(-) create mode 100644 src/registrar/templates/application_no_other_contacts.html diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 72c0604a0..a2eeb8f5d 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -35,6 +35,7 @@ for step, view in [ (Step.PURPOSE, views.Purpose), (Step.YOUR_CONTACT, views.YourContact), (Step.OTHER_CONTACTS, views.OtherContacts), + (Step.NO_OTHER_CONTACTS, views.NoOtherContacts), (Step.SECURITY_EMAIL, views.SecurityEmail), (Step.ANYTHING_ELSE, views.AnythingElse), (Step.REQUIREMENTS, views.Requirements), diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 76e10ad8b..6ab5fb8dd 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -662,7 +662,7 @@ OtherContactsFormSet = forms.formset_factory( class NoOtherContactsForm(RegistrarForm): no_other_contacts_rationale = forms.CharField( - required=False, + required=True, # label has to end in a space to get the label_suffix to show label=( "If you can’t provide other contacts for your organization," diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 7cfda1440..de176910c 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -545,6 +545,10 @@ class DomainApplication(TimeStampedModel): DomainApplication.OrganizationChoices.INTERSTATE, ] + def show_no_other_contacts_rationale(self) -> bool: + """Show this step if the other contacts are blank.""" + return not self.other_contacts.exists() + def is_federal(self) -> Union[bool, None]: """Is this application for a federal agency? diff --git a/src/registrar/templates/application_no_other_contacts.html b/src/registrar/templates/application_no_other_contacts.html new file mode 100644 index 000000000..736454831 --- /dev/null +++ b/src/registrar/templates/application_no_other_contacts.html @@ -0,0 +1,6 @@ +{% extends 'application_form.html' %} +{% load static field_helpers %} + +{% block form_fields %} + {% input_with_errors forms.0.no_other_contacts_rationale %} +{% endblock %} diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 7ab7252f0..3f982e260 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -39,8 +39,4 @@ Add another contact - -

No contacts

- - {% input_with_errors forms.1.no_other_contacts_rationale %} {% endblock %} diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html index 7fed7f924..0808b2c39 100644 --- a/src/registrar/templates/application_review.html +++ b/src/registrar/templates/application_review.html @@ -36,7 +36,9 @@ {% endif %} {% if step == Step.AUTHORIZING_OFFICIAL %} {% if application.authorizing_official %} - {% include "includes/contact.html" with contact=application.authorizing_official %} +
+ {% include "includes/contact.html" with contact=application.authorizing_official %} +
{% else %} Incomplete {% endif %} @@ -51,8 +53,10 @@ {% endif %} {% if step == Step.DOTGOV_DOMAIN %} -
    +
    • {{ application.requested_domain.name|default:"Incomplete" }}
    • +
    +
      {% for site in application.alternative_domains.all %}
    • {{ site.website }}
    • {% endfor %} @@ -63,18 +67,25 @@ {% endif %} {% if step == Step.YOUR_CONTACT %} {% if application.submitter %} - {% include "includes/contact.html" with contact=application.submitter %} +
      + {% include "includes/contact.html" with contact=application.submitter %} +
      {% else %} Incomplete {% endif %} {% endif %} {% if step == Step.OTHER_CONTACTS %} {% for other in application.other_contacts.all %} +
      {% include "includes/contact.html" with contact=other %} +
      {% empty %} None {% endfor %} {% endif %} + {% if step == Step.NO_OTHER_CONTACTS %} + {{ application.no_other_contacts_rationale|default:"Incomplete" }} + {% endif %} {% if step == Step.SECURITY_EMAIL %} {{ application.security_email|default:"None" }} {% endif %} @@ -82,7 +93,7 @@ {{ application.anything_else|default:"No" }} {% endif %} {% if step == Step.REQUIREMENTS %} - {{ application.is_policy_acknowledged|yesno:"Agree,Do not agree,Do not agree" }} + {{ application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." }} {% endif %}
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index a168f83cb..9dc36fd06 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -124,7 +124,8 @@ class DomainApplicationTests(TestWithUser, WebTest): this test work. """ num_pages_tested = 0 - SKIPPED_PAGES = 3 # elections, type_of_work, tribal_government + # elections, type_of_work, tribal_government, no_other_contacts + SKIPPED_PAGES = 4 num_pages = len(self.TITLES) - SKIPPED_PAGES type_page = self.app.get(reverse("application:")).follow() @@ -724,6 +725,24 @@ class DomainApplicationTests(TestWithUser, WebTest): self.assertContains(contact_page, self.TITLES[Step.TYPE_OF_WORK]) + def test_application_no_other_contacts(self): + """Applicants with no other contacts have to give a reason.""" + contacts_page = self.app.get(reverse("application:other_contacts")) + # django-webtest does not handle cookie-based sessions well because it keeps + # resetting the session key on each new request, thus destroying the concept + # of a "session". We are going to do it manually, saving the session ID here + # and then setting the cookie on each request. + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + result = contacts_page.form.submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + no_contacts_page = result.follow() + expected_url_slug = str(Step.NO_OTHER_CONTACTS) + actual_url_slug = no_contacts_page.request.path.split("/")[-2] + self.assertEqual(expected_url_slug, actual_url_slug) + def test_application_type_of_work_interstate(self): """Special districts have to answer an additional question.""" type_page = self.app.get(reverse("application:")).follow() diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 35e938bad..8f475b4a1 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -35,6 +35,7 @@ class Step(StrEnum): PURPOSE = "purpose" YOUR_CONTACT = "your_contact" OTHER_CONTACTS = "other_contacts" + NO_OTHER_CONTACTS = "no_other_contacts" SECURITY_EMAIL = "security_email" ANYTHING_ELSE = "anything_else" REQUIREMENTS = "requirements" @@ -80,6 +81,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): Step.PURPOSE: _("Purpose of your domain"), Step.YOUR_CONTACT: _("Your contact information"), Step.OTHER_CONTACTS: _("Other contacts for your organization"), + Step.NO_OTHER_CONTACTS: _("No other contacts?"), Step.SECURITY_EMAIL: _("Security email for public use"), Step.ANYTHING_ELSE: _("Anything else we should know?"), Step.REQUIREMENTS: _( @@ -99,6 +101,9 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): "show_organization_election", False ), Step.TYPE_OF_WORK: lambda w: w.from_model("show_type_of_work", False), + Step.NO_OTHER_CONTACTS: lambda w: w.from_model( + "show_no_other_contacts_rationale", False + ), } def __init__(self): @@ -410,7 +415,12 @@ class YourContact(ApplicationWizard): class OtherContacts(ApplicationWizard): template_name = "application_other_contacts.html" - forms = [forms.OtherContactsFormSet, forms.NoOtherContactsForm] + forms = [forms.OtherContactsFormSet] + + +class NoOtherContacts(ApplicationWizard): + template_name = "application_no_other_contacts.html" + forms = [forms.NoOtherContactsForm] class SecurityEmail(ApplicationWizard): From b601b58c706ba25ee6f19519374806111c3cca91 Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Thu, 23 Feb 2023 09:34:39 -0500 Subject: [PATCH 6/9] Updated "no other contact" labels --- src/registrar/views/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 8f475b4a1..f75b62970 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -80,8 +80,8 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): Step.DOTGOV_DOMAIN: _(".gov domain"), Step.PURPOSE: _("Purpose of your domain"), Step.YOUR_CONTACT: _("Your contact information"), - Step.OTHER_CONTACTS: _("Other contacts for your organization"), - Step.NO_OTHER_CONTACTS: _("No other contacts?"), + Step.OTHER_CONTACTS: _("Other employees from your organization"), + Step.NO_OTHER_CONTACTS: _("No other employees from your organization?"), Step.SECURITY_EMAIL: _("Security email for public use"), Step.ANYTHING_ELSE: _("Anything else we should know?"), Step.REQUIREMENTS: _( From f6deb85e39f03f2b0b1687baf47ace3d417eec0a Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Thu, 23 Feb 2023 13:06:03 -0500 Subject: [PATCH 7/9] Update application_wizard.py --- src/registrar/forms/application_wizard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 6ab5fb8dd..245b362f2 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -665,8 +665,7 @@ class NoOtherContactsForm(RegistrarForm): required=True, # label has to end in a space to get the label_suffix to show label=( - "If you can’t provide other contacts for your organization," - " please explain why." + "Please explain why there are no other employees from your organization that we can contact." ), widget=forms.Textarea(), ) From 36b8fe99efb1651852794461cc1aee2c97dad316 Mon Sep 17 00:00:00 2001 From: Seamus Johnston Date: Mon, 27 Feb 2023 13:18:22 -0600 Subject: [PATCH 8/9] Fix line length --- src/registrar/forms/application_wizard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index 245b362f2..e3cad59c8 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -665,7 +665,8 @@ class NoOtherContactsForm(RegistrarForm): required=True, # label has to end in a space to get the label_suffix to show label=( - "Please explain why there are no other employees from your organization that we can contact." + "Please explain why there are no other employees from your organization" + " that we can contact." ), widget=forms.Textarea(), ) From b65778005a649c6ef1ef91f7c38ccbc1789a56ed Mon Sep 17 00:00:00 2001 From: Michelle Rago <60157596+michelle-rago@users.noreply.github.com> Date: Mon, 27 Feb 2023 14:56:59 -0500 Subject: [PATCH 9/9] Mr/other contacts intro (#436) * Update application_other_contacts.html * Update application.py * Update application_other_contacts.html --- src/registrar/templates/application_other_contacts.html | 4 ++-- src/registrar/views/application.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/templates/application_other_contacts.html b/src/registrar/templates/application_other_contacts.html index 7ab7252f0..784e1b791 100644 --- a/src/registrar/templates/application_other_contacts.html +++ b/src/registrar/templates/application_other_contacts.html @@ -2,7 +2,7 @@ {% load static field_helpers %} {% block form_instructions %} -

We’d like to contact other employees with administrative or technical responsibilities in your organization. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility and understand the purpose of the .gov domain. These contacts should be in addition to you and your authorizing official. They should be employees of your organization.

+

We’d like to contact other employees in your organization about your domain request. For example, they could be involved in managing your organization or its technical infrastructure. This information will help us assess your eligibility for a .gov domain. These contacts should be in addition to you and your authorizing official. They should be employees of your organization.

We’ll email these contacts to let them know that you made this request.

{% endblock %} @@ -14,7 +14,7 @@ {% for form in forms.0.forms %}
-

Administrative or technical contact {{ forloop.counter }}

+

Organization contact {{ forloop.counter }}

{% input_with_errors form.first_name %} diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index c34a8e1c1..821a7063d 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -81,7 +81,7 @@ class ApplicationWizard(LoginRequiredMixin, TemplateView): Step.DOTGOV_DOMAIN: _(".gov domain"), Step.PURPOSE: _("Purpose of your domain"), Step.YOUR_CONTACT: _("Your contact information"), - Step.OTHER_CONTACTS: _("Other contacts for your organization"), + Step.OTHER_CONTACTS: _("Other employees from your organization"), Step.SECURITY_EMAIL: _("Security email for public use"), Step.ANYTHING_ELSE: _("Anything else we should know?"), Step.REQUIREMENTS: _(