From 32c138032bf20cad5f51bf458d07bbd743394ae2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:11:42 -0700 Subject: [PATCH 001/120] Rough mock --- src/registrar/assets/sass/_theme/_tables.scss | 4 ++++ src/registrar/templates/domain_users.html | 23 +++++++++++++++++++ src/registrar/views/domain.py | 15 ++++++++++++ 3 files changed, 42 insertions(+) diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 6dcc6f3bc..892427f82 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -25,6 +25,10 @@ color: color('primary-darker'); padding-bottom: units(2px); } + td.shift-action-button { + padding-right: 0; + transform: translateX(10px); + } } .dotgov-table { diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 0eecd35b3..147c0bb8e 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -31,6 +31,7 @@ Email Role + Action @@ -40,6 +41,28 @@ {{ permission.user.email }} {{ permission.role|title }} + + Remove + {# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #} +
+
+ {% include 'includes/modal.html' with modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} +
+
+ {% endfor %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 59b206993..aaad016a7 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -625,6 +625,21 @@ class DomainUsersView(DomainBaseView): template_name = "domain_users.html" + def get_context_data(self, **kwargs): + """The initial value for the form (which is a formset here).""" + context = super().get_context_data(**kwargs) + + # Create HTML for the modal button + modal_button = ( + '' + ) + + context["modal_button"] = modal_button + + return context + class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. From 64687ea7862f230b90263d98ef0d34602b0fb98b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 10:27:51 -0700 Subject: [PATCH 002/120] Some view logic --- src/registrar/config/urls.py | 5 +++ src/registrar/templates/domain_users.html | 6 ++-- src/registrar/views/__init__.py | 1 + src/registrar/views/domain.py | 13 ++++++++ src/registrar/views/utility/mixins.py | 30 ++++++++++++++++++ .../views/utility/permission_views.py | 31 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 607bf5f61..416cb7c36 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -138,6 +138,11 @@ urlpatterns = [ views.DomainInvitationDeleteView.as_view(http_method_names=["post"]), name="invitation-delete", ), + path( + "domain//users//delete", + views.DomainDeleteUserView.as_view(http_method_names=["post"]), + name="domain-user-delete", + ), ] # we normally would guard these with `if settings.DEBUG` but tests run with diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 147c0bb8e..22ef88533 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -58,8 +58,10 @@ aria-describedby="Your DNSSEC records will be deleted from the registry." data-force-action > -
- {% include 'includes/modal.html' with modal_heading="Warning: You are about to remove all DS records on your domain" modal_description="To fully disable DNSSEC: In addition to removing your DS records here you’ll also need to delete the DS records at your DNS host. To avoid causing your domain to appear offline you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %} + + {% with heading="Are you sure you want to remove <"|add:permission.user.email|add:">?" %} + {% include 'includes/modal.html' with modal_heading=heading modal_description="<"|add:permission.user.email|add:"> will no longer be able to manage the domain "|add:domain.name|add:"." modal_button=modal_button|safe %} + {% endwith %}
diff --git a/src/registrar/views/__init__.py b/src/registrar/views/__init__.py index c1400d7c0..8785c9076 100644 --- a/src/registrar/views/__init__.py +++ b/src/registrar/views/__init__.py @@ -12,6 +12,7 @@ from .domain import ( DomainUsersView, DomainAddUserView, DomainInvitationDeleteView, + DomainDeleteUserView, ) from .health import * from .index import * diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index aaad016a7..8884f9436 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -33,6 +33,7 @@ from registrar.utility.errors import ( SecurityEmailErrorCodes, ) from registrar.models.utility.contact_error import ContactError +from registrar.views.utility.permission_views import UserDomainRolePermissionView from ..forms import ( ContactForm, @@ -753,3 +754,15 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe def get_success_message(self, cleaned_data): return f"Successfully canceled invitation for {self.object.email}." + + +class DomainDeleteUserView(UserDomainRolePermissionView, SuccessMessageMixin): + """Inside of a domain's user management, a form for deleting users. + """ + object: UserDomainRole # workaround for type mismatch in DeleteView + + def get_success_url(self): + return reverse("domain-users", kwargs={"pk": self.object.domain.id}) + + def get_success_message(self, cleaned_data): + return f"Successfully removed manager for {self.object.email}." diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 0cf5970df..af8f66279 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -286,6 +286,36 @@ class DomainApplicationPermission(PermissionsLoginMixin): return True +class UserDomainRolePermission(PermissionsLoginMixin): + + """Permission mixin for UserDomainRole if user + has access, otherwise 403""" + + def has_permission(self): + """Check if this user has access to this domain application. + + 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"] + """ + domain_pk = self.kwargs["pk"] + user_pk = self.kwargs["user_pk"] + print(f"here is the user: {self.request.user} and kwargs: {domain_pk}") + if not self.request.user.is_authenticated: + return False + print("User was authenticated!") + x = UserDomainRole.objects.filter( + id=user_pk + ).get() + print(x) + # TODO - exclude the creator from this + if not UserDomainRole.objects.filter( + domain__id=domain_pk, domain__permissions__user=self.request.user + ).exists(): + return False + + return True + + class DomainApplicationPermissionWithdraw(PermissionsLoginMixin): """Permission mixin that redirects to withdraw action on domain application diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 1798ec79d..5c5ebc494 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -4,6 +4,7 @@ import abc # abstract base class from django.views.generic import DetailView, DeleteView, TemplateView from registrar.models import Domain, DomainApplication, DomainInvitation +from registrar.models.user_domain_role import UserDomainRole from .mixins import ( DomainPermission, @@ -11,6 +12,7 @@ from .mixins import ( DomainApplicationPermissionWithdraw, DomainInvitationPermission, ApplicationWizardPermission, + UserDomainRolePermission, ) import logging @@ -122,3 +124,32 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie model = DomainInvitation object: DomainInvitation # workaround for type mismatch in DeleteView + + +class UserDomainRolePermissionView(UserDomainRolePermission, DetailView, abc.ABC): + + """Abstract base view for UserDomainRole that enforces permissions. + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = UserDomainRole + # variable name in template context for the model object + context_object_name = "userdomainrole" + + +class UserDomainRolePermissionDeleteView(UserDomainRolePermissionView, DeleteView, abc.ABC): + + """Abstract base view for domain application withdraw function + + This abstract view cannot be instantiated. Actual views must specify + `template_name`. + """ + + # DetailView property for what model this is viewing + model = UserDomainRole + # variable name in template context for the model object + context_object_name = "userdomainrole" + From 1710c902da22c7168cef3b1ee861984ed4f17a70 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:39:47 -0700 Subject: [PATCH 003/120] Add delete --- src/registrar/views/domain.py | 10 ++++++++-- src/registrar/views/utility/mixins.py | 10 +++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 8884f9436..80cc50a1d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -33,7 +33,7 @@ from registrar.utility.errors import ( SecurityEmailErrorCodes, ) from registrar.models.utility.contact_error import ContactError -from registrar.views.utility.permission_views import UserDomainRolePermissionView +from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView, UserDomainRolePermissionView from ..forms import ( ContactForm, @@ -756,11 +756,17 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe return f"Successfully canceled invitation for {self.object.email}." -class DomainDeleteUserView(UserDomainRolePermissionView, SuccessMessageMixin): +class DomainDeleteUserView(UserDomainRolePermissionDeleteView, SuccessMessageMixin): """Inside of a domain's user management, a form for deleting users. """ object: UserDomainRole # workaround for type mismatch in DeleteView + def get_object(self, queryset=None): + """Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id""" + domain_id = self.kwargs.get('pk') + user_id = self.kwargs.get('user_pk') + return UserDomainRole.objects.get(domain=domain_id, user=user_id) + def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.domain.id}) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index af8f66279..ca5dceeb8 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -299,17 +299,13 @@ class UserDomainRolePermission(PermissionsLoginMixin): """ domain_pk = self.kwargs["pk"] user_pk = self.kwargs["user_pk"] - print(f"here is the user: {self.request.user} and kwargs: {domain_pk}") + if not self.request.user.is_authenticated: return False - print("User was authenticated!") - x = UserDomainRole.objects.filter( - id=user_pk - ).get() - print(x) + # TODO - exclude the creator from this if not UserDomainRole.objects.filter( - domain__id=domain_pk, domain__permissions__user=self.request.user + user=user_pk, domain=domain_pk ).exists(): return False From b0d05dd5df386143fc1a798f8c1e7941660ba74b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 12:52:49 -0700 Subject: [PATCH 004/120] Update mixins.py --- src/registrar/views/utility/mixins.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index ca5dceeb8..bfa9d7330 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -300,13 +300,26 @@ class UserDomainRolePermission(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 - # TODO - exclude the creator from this - if not UserDomainRole.objects.filter( - user=user_pk, domain=domain_pk - ).exists(): + # Check if the UserDomainRole object exists, then check + # if the user requesting the delete has permissions to do so + has_delete_permission = UserDomainRole.objects.filter( + user=user_pk, + domain=domain_pk, + domain__permissions__user=self.request.user, + ).exists() + if not has_delete_permission: + return False + + # Check if more than one manager exists on the domain. + # If only one exists, prevent this from happening + has_multiple_managers = len(UserDomainRole.objects.filter( + domain=domain_pk + )) > 1 + if not has_multiple_managers: return False return True From 232a3e2d06820b0f72c54f98411c98353cac1ba4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 13:09:04 -0700 Subject: [PATCH 005/120] Add success message --- src/registrar/views/domain.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 80cc50a1d..da81ba6a3 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -756,19 +756,26 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe return f"Successfully canceled invitation for {self.object.email}." -class DomainDeleteUserView(UserDomainRolePermissionDeleteView, SuccessMessageMixin): +class DomainDeleteUserView(UserDomainRolePermissionDeleteView): """Inside of a domain's user management, a form for deleting users. """ object: UserDomainRole # workaround for type mismatch in DeleteView def get_object(self, queryset=None): """Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id""" - domain_id = self.kwargs.get('pk') - user_id = self.kwargs.get('user_pk') + domain_id = self.kwargs.get("pk") + user_id = self.kwargs.get("user_pk") return UserDomainRole.objects.get(domain=domain_id, user=user_id) def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.domain.id}) def get_success_message(self, cleaned_data): - return f"Successfully removed manager for {self.object.email}." + return f"Successfully removed manager for {self.object.user.email}." + + def form_valid(self, form): + """Delete the specified user on this domain.""" + super().form_valid(form) + messages.success(self.request, f"Successfully removed manager for {self.object.user.email}.") + + return redirect(self.get_success_url()) \ No newline at end of file From 9d11653386670edfe9b4660e70aeb9ba29741ff7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 13:14:44 -0700 Subject: [PATCH 006/120] Update domain_users.html --- src/registrar/templates/domain_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 22ef88533..20079f64f 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -55,7 +55,7 @@ class="usa-modal" id="toggle-user-alert-{{ forloop.counter }}" aria-labelledby="Are you sure you want to continue?" - aria-describedby="Your DNSSEC records will be deleted from the registry." + aria-describedby="User will be removed" data-force-action >
From 01a6de2392307a9a580e5150a629ee0fc0649fd4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 4 Jan 2024 13:53:59 -0700 Subject: [PATCH 007/120] Clean up html --- src/registrar/templates/domain_users.html | 43 ++++++++++++----------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 20079f64f..b6798ac16 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -43,27 +43,28 @@ {{ permission.role|title }} Remove - {# Use data-force-action to take esc out of the equation and pass cancel_button_resets_ds_form to effectuate a reset in the view #} -
- - {% with heading="Are you sure you want to remove <"|add:permission.user.email|add:">?" %} - {% include 'includes/modal.html' with modal_heading=heading modal_description="<"|add:permission.user.email|add:"> will no longer be able to manage the domain "|add:domain.name|add:"." modal_button=modal_button|safe %} - {% endwith %} - -
+ id="button-toggle-user-alert-{{ forloop.counter }}" + href="#toggle-user-alert-{{ forloop.counter }}" + class="usa-button--unstyled" + aria-controls="toggle-user-alert-{{ forloop.counter }}" + data-open-modal + > + Remove + + +
+
+ {% with heading="Are you sure you want to remove <"|add:permission.user.email|add:">?" %} + {% include 'includes/modal.html' with modal_heading=heading modal_description="<"|add:permission.user.email|add:"> will no longer be able to manage the domain "|add:domain.name|add:"." modal_button=modal_button|safe %} + {% endwith %} +
+
{% endfor %} From 4c2718654585b1a3734bfd593f8133cff48737bf Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:59:24 -0700 Subject: [PATCH 008/120] Add conditional logic --- src/registrar/templates/domain_users.html | 54 ++++++++++++----------- src/registrar/views/domain.py | 9 ++++ 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index b6798ac16..5375b45e8 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -31,7 +31,9 @@ Email Role - Action + {% if can_delete_users %} + Action + {% endif %} @@ -41,31 +43,33 @@ {{ permission.user.email }} {{ permission.role|title }} - - - Remove - + {% if can_delete_users %} + + + Remove + -
-
- {% with heading="Are you sure you want to remove <"|add:permission.user.email|add:">?" %} - {% include 'includes/modal.html' with modal_heading=heading modal_description="<"|add:permission.user.email|add:"> will no longer be able to manage the domain "|add:domain.name|add:"." modal_button=modal_button|safe %} - {% endwith %} -
-
- +
+
+ {% with heading="Are you sure you want to remove <"|add:permission.user.email|add:">?" %} + {% include 'includes/modal.html' with modal_heading=heading modal_description="<"|add:permission.user.email|add:"> will no longer be able to manage the domain "|add:domain.name|add:"." modal_button=modal_button|safe %} + {% endwith %} +
+
+ + {% endif %} {% endfor %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index da81ba6a3..c8aa7083f 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -630,6 +630,15 @@ class DomainUsersView(DomainBaseView): """The initial value for the form (which is a formset here).""" context = super().get_context_data(**kwargs) + domain_pk = None + can_delete_users = False + if self.kwargs is not None and "pk" in self.kwargs: + domain_pk = self.kwargs["pk"] + # Prevent the end user from deleting themselves as a manager if they are the + # only manager that exists on a domain. + can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1 + context["can_delete_users"] = can_delete_users + # Create HTML for the modal button modal_button = ( '' - ) + return context + def _add_modal_buttons_to_context(self, context): + """Adds modal buttons (and their HTML) to the context""" + # Create HTML for the modal button + modal_button = self._create_modal_button_html( + button_name="delete_domain_manager", + button_text_content="Yes, remove domain manager", + classes=["usa-button", "usa-button--secondary"] + ) context["modal_button"] = modal_button + # Create HTML for the modal button when deleting yourself + modal_button_self= self._create_modal_button_html( + button_name="delete_domain_manager_self", + button_text_content="Yes, remove myself", + classes=["usa-button", "usa-button--secondary"] + ) + context["modal_button_self"] = modal_button_self + return context + def _create_modal_button_html(self, button_name: str, button_text_content: str, classes: List[str] | str): + """Template for modal submit buttons""" + + if isinstance(classes, list): + class_list = " ".join(classes) + elif isinstance(classes, str): + class_list = classes + + html_class = f'class="{class_list}"' if class_list else None + + modal_button = ( + '' + ) + return modal_button + class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. @@ -779,12 +822,30 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): def get_success_url(self): return reverse("domain-users", kwargs={"pk": self.object.domain.id}) - def get_success_message(self, cleaned_data): - return f"Successfully removed manager for {self.object.user.email}." + def get_success_message(self, delete_self = False): + if delete_self: + message = f"You are no longer managing the domain {self.object.domain}." + else: + message = f"Removed {self.object.user.email} as a manager for this domain." + return message def form_valid(self, form): """Delete the specified user on this domain.""" super().form_valid(form) - messages.success(self.request, f"Successfully removed manager for {self.object.user.email}.") - return redirect(self.get_success_url()) \ No newline at end of file + # Is the user deleting themselves? If so, display a different message + delete_self = self.request.user.email == self.object.user.email + + # Add a success message + messages.success(self.request, self.get_success_message(delete_self)) + + return redirect(self.get_success_url()) + + def post(self, request, *args, **kwargs): + response = super().post(request, *args, **kwargs) + + # If the user is deleting themselves, redirect to home + if self.request.user.email == self.object.user.email: + return redirect(reverse("home")) + + return response \ No newline at end of file From 40e91ead1f40939b7f28b636945a08a71506b55e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:56:56 -0700 Subject: [PATCH 014/120] Add logic for self deletion --- .../assets/sass/_theme/_buttons.scss | 9 +- src/registrar/templates/domain_users.html | 97 +++++++++++-------- src/registrar/views/domain.py | 10 +- 3 files changed, 69 insertions(+), 47 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 02089ec6d..c890b7a55 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -26,18 +26,21 @@ a.usa-button { text-decoration: none; } -a.usa-button.disabled-link { +a.usa-button.disabled-link, +a.usa-button--unstyled.disabled-link { background-color: #ccc !important; color: #454545 !important } -a.usa-button.disabled-link:hover { +a.usa-button.disabled-link:hover, +a.usa-button--unstyled.disabled-link { background-color: #ccc !important; cursor: not-allowed !important; color: #454545 !important } -a.usa-button.disabled-link:focus { +a.usa-button.disabled-link:focus, +a.usa-button--unstyled.disabled-link { background-color: #ccc !important; cursor: not-allowed !important; outline: none !important; diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 4a0d25625..216c21942 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -16,10 +16,8 @@
  • There is no limit to the number of domain managers you can add.
  • After adding a domain manager, an email invitation will be sent to that user with instructions on how to set up an account.
  • -
  • To remove a domain manager, contact us for - assistance.
  • All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.
  • +
  • Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.
  • {% if domain.permissions %} @@ -31,9 +29,7 @@ Email Role - {% if can_delete_users %} - Action - {% endif %} + Action @@ -43,49 +39,66 @@ {{ permission.user.email }} {{ permission.role|title }} + {% if can_delete_users %} - - + Remove + + {# Display a custom message if the user is trying to delete themselves #} + {% if permission.user.email == current_user_email %} +
    - Remove - - {# Display a custom message if the user is trying to delete themselves #} - {% if permission.user.email == current_user_email %} -
    + {% with domain_name=domain.name|force_escape %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager for "|add:domain_name|add:"?"|safe modal_description="You will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} + {% endwith %} + +
    + {% else %} +
    -
    - {% with domain_name=domain.name|force_escape %} - {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager for "|add:domain_name|add:"?"|safe modal_description="You will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} - {% endwith %} -
    -
    - {% else %} -
    -
    - {% with email=permission.user.email|force_escape domain_name=domain.name|force_escape %} - {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove <"|add:email|add:">?"|safe modal_description="<"|add:email|add:"> will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} - {% endwith %} -
    -
    - {% endif %} - + > +
    + {% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove <"|add:email|add:">?"|safe modal_description="<"|add:email|add:"> will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} + {% endwith %} +
    +
    + {% endif %} + {% else %} + + Remove + {% endif %} + + {% comment %} + usa-tooltip disabled-link" + data-position="right" + title="Coming in 2024" + aria-disabled="true" + data-tooltip="true" + {% endcomment %} {% endfor %} diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index b55d490ce..0f894a985 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -820,13 +820,18 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): return UserDomainRole.objects.get(domain=domain_id, user=user_id) def get_success_url(self): + """Refreshes the page after a delete is successful""" return reverse("domain-users", kwargs={"pk": self.object.domain.id}) def get_success_message(self, delete_self = False): + """Returns confirmation content for the deletion event """ + email_or_name = self.object.user.email + if email_or_name is None: + email_or_name = self.object.user if delete_self: message = f"You are no longer managing the domain {self.object.domain}." else: - message = f"Removed {self.object.user.email} as a manager for this domain." + message = f"Removed {email_or_name} as a manager for this domain." return message def form_valid(self, form): @@ -842,10 +847,11 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): return redirect(self.get_success_url()) def post(self, request, *args, **kwargs): + """Custom post implementation to redirect to home in the event that the user deletes themselves""" response = super().post(request, *args, **kwargs) # If the user is deleting themselves, redirect to home - if self.request.user.email == self.object.user.email: + if self.request.user == self.object.user: return redirect(reverse("home")) return response \ No newline at end of file From 12d5905ef975597b493fa856c8880df081774e9e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 16 Jan 2024 11:56:49 -0700 Subject: [PATCH 015/120] Add success message for self delete --- src/registrar/templates/dashboard_base.html | 26 ++++++++++----------- src/registrar/templates/home.html | 3 +++ src/registrar/tests/test_url_auth.py | 1 + src/registrar/views/domain.py | 13 ++++++++--- 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/src/registrar/templates/dashboard_base.html b/src/registrar/templates/dashboard_base.html index 27b5ea717..46b04570b 100644 --- a/src/registrar/templates/dashboard_base.html +++ b/src/registrar/templates/dashboard_base.html @@ -3,22 +3,22 @@ {% block wrapper %}
    - {% block messages %} - {% if messages %} -
      - {% for message in messages %} - - {{ message }} - - {% endfor %} -
    - {% endif %} - {% endblock %} - {% block section_nav %}{% endblock %} {% block hero %}{% endblock %} - {% block content %}{% endblock %} + {% block content %} + {% block messages %} + {% if messages %} +
      + {% for message in messages %} + + {{ message }} + + {% endfor %} +
    + {% endif %} + {% endblock %} + {% endblock %}
    {% block complementary %}{% endblock %}
    diff --git a/src/registrar/templates/home.html b/src/registrar/templates/home.html index 15835920b..d25cd3de8 100644 --- a/src/registrar/templates/home.html +++ b/src/registrar/templates/home.html @@ -10,6 +10,9 @@ {# the entire logged in page goes here #}
    + {% block messages %} + {% include "includes/form_messages.html" %} + {% endblock %}

    Manage your domains

    diff --git a/src/registrar/tests/test_url_auth.py b/src/registrar/tests/test_url_auth.py index 34f80ac44..3e0514a85 100644 --- a/src/registrar/tests/test_url_auth.py +++ b/src/registrar/tests/test_url_auth.py @@ -23,6 +23,7 @@ SAMPLE_KWARGS = { "content_type_id": "2", "object_id": "3", "domain": "whitehouse.gov", + "user_pk": "1", } # Our test suite will ignore some namespaces. diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 0f894a985..e00c90b19 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -825,13 +825,19 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): def get_success_message(self, delete_self = False): """Returns confirmation content for the deletion event """ + + # Grab the text representation of the user we want to delete email_or_name = self.object.user.email - if email_or_name is None: + if email_or_name is None or email_or_name.strip() == "": email_or_name = self.object.user + + # If the user is deleting themselves, return a special message. + # If not, return something more generic. if delete_self: message = f"You are no longer managing the domain {self.object.domain}." else: message = f"Removed {email_or_name} as a manager for this domain." + return message def form_valid(self, form): @@ -839,7 +845,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): super().form_valid(form) # Is the user deleting themselves? If so, display a different message - delete_self = self.request.user.email == self.object.user.email + delete_self = self.request.user == self.object.user # Add a success message messages.success(self.request, self.get_success_message(delete_self)) @@ -851,7 +857,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): response = super().post(request, *args, **kwargs) # If the user is deleting themselves, redirect to home - if self.request.user == self.object.user: + delete_self = self.request.user == self.object.user + if delete_self: return redirect(reverse("home")) return response \ No newline at end of file From f16382e946f62e8a9b0813d4a26f75d20b808ed9 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:41:19 -0700 Subject: [PATCH 016/120] Linting --- src/registrar/views/application.py | 12 +++++++- src/registrar/views/domain.py | 29 ++++++++----------- src/registrar/views/utility/mixins.py | 6 ++-- .../views/utility/permission_views.py | 1 - 4 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 23c8cf55e..cfb16336b 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -10,6 +10,7 @@ from django.contrib import messages from registrar.forms import application_wizard as forms from registrar.models import DomainApplication +from registrar.models.user import User from registrar.utility import StrEnum from registrar.views.utility import StepsHelper @@ -131,11 +132,19 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView): if self._application: return self._application + # For linter. The else block should never be hit, but if it does, + # there may be a UI consideration. That will need to be handled in another ticket. + creator = None + if self.request.user is not None and isinstance(self.request.user, User): + creator = self.request.user + else: + raise ValueError("Invalid value for User") + if self.has_pk(): id = self.storage["application_id"] try: self._application = DomainApplication.objects.get( - creator=self.request.user, # type: ignore + creator=creator, pk=id, ) return self._application @@ -476,6 +485,7 @@ class DotgovDomain(ApplicationWizard): self.application.save() return response + class Purpose(ApplicationWizard): template_name = "application_purpose.html" forms = [forms.PurposeForm] diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index e00c90b19..d4a7b8066 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -34,7 +34,7 @@ from registrar.utility.errors import ( SecurityEmailErrorCodes, ) from registrar.models.utility.contact_error import ContactError -from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView, UserDomainRolePermissionView +from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView from ..forms import ( ContactForm, @@ -643,7 +643,6 @@ class DomainUsersView(DomainBaseView): return context def _add_booleans_to_context(self, context): - # Determine if the current user can delete managers domain_pk = None can_delete_users = False @@ -660,17 +659,17 @@ class DomainUsersView(DomainBaseView): """Adds modal buttons (and their HTML) to the context""" # Create HTML for the modal button modal_button = self._create_modal_button_html( - button_name="delete_domain_manager", + button_name="delete_domain_manager", button_text_content="Yes, remove domain manager", - classes=["usa-button", "usa-button--secondary"] + classes=["usa-button", "usa-button--secondary"], ) context["modal_button"] = modal_button # Create HTML for the modal button when deleting yourself - modal_button_self= self._create_modal_button_html( + modal_button_self = self._create_modal_button_html( button_name="delete_domain_manager_self", button_text_content="Yes, remove myself", - classes=["usa-button", "usa-button--secondary"] + classes=["usa-button", "usa-button--secondary"], ) context["modal_button_self"] = modal_button_self @@ -686,11 +685,7 @@ class DomainUsersView(DomainBaseView): html_class = f'class="{class_list}"' if class_list else None - modal_button = ( - '' - ) + modal_button = '' return modal_button @@ -809,8 +804,8 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe class DomainDeleteUserView(UserDomainRolePermissionDeleteView): - """Inside of a domain's user management, a form for deleting users. - """ + """Inside of a domain's user management, a form for deleting users.""" + object: UserDomainRole # workaround for type mismatch in DeleteView def get_object(self, queryset=None): @@ -823,8 +818,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): """Refreshes the page after a delete is successful""" return reverse("domain-users", kwargs={"pk": self.object.domain.id}) - def get_success_message(self, delete_self = False): - """Returns confirmation content for the deletion event """ + def get_success_message(self, delete_self=False): + """Returns confirmation content for the deletion event""" # Grab the text representation of the user we want to delete email_or_name = self.object.user.email @@ -860,5 +855,5 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): delete_self = self.request.user == self.object.user if delete_self: return redirect(reverse("home")) - - return response \ No newline at end of file + + return response diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index bfa9d7330..980c0dad5 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -307,7 +307,7 @@ class UserDomainRolePermission(PermissionsLoginMixin): # Check if the UserDomainRole object exists, then check # if the user requesting the delete has permissions to do so has_delete_permission = UserDomainRole.objects.filter( - user=user_pk, + user=user_pk, domain=domain_pk, domain__permissions__user=self.request.user, ).exists() @@ -316,9 +316,7 @@ class UserDomainRolePermission(PermissionsLoginMixin): # Check if more than one manager exists on the domain. # If only one exists, prevent this from happening - has_multiple_managers = len(UserDomainRole.objects.filter( - domain=domain_pk - )) > 1 + has_multiple_managers = len(UserDomainRole.objects.filter(domain=domain_pk)) > 1 if not has_multiple_managers: return False diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 5c5ebc494..295fbc65c 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -152,4 +152,3 @@ class UserDomainRolePermissionDeleteView(UserDomainRolePermissionView, DeleteVie model = UserDomainRole # variable name in template context for the model object context_object_name = "userdomainrole" - From 353e2d518f158c678733fc7da834a7f2e2a8967e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 17 Jan 2024 13:01:02 -0700 Subject: [PATCH 017/120] Update get-gov.js --- src/registrar/assets/js/get-gov.js | 61 +++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 68e8af69c..23e40858a 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -227,10 +227,69 @@ function handleValidationClick(e) { for(const button of activatesValidation) { button.addEventListener('click', handleValidationClick); } + + + // Add event listener to the "Check availability" button + const checkAvailabilityButton = document.getElementById('check-availability-button'); + if (checkAvailabilityButton) { + const targetId = checkAvailabilityButton.getAttribute('validate-for'); + const checkAvailabilityInput = document.getElementById(targetId); + checkAvailabilityButton.addEventListener('click', + function() { + removeFormErrors(checkAvailabilityInput) + } + ); + } + + // Add event listener to the alternate domains input + const alternateDomainsInputs = document.querySelectorAll('[auto-validate]'); + if (alternateDomainsInputs) { + for (const domainInput of alternateDomainsInputs){ + // Only apply this logic to alternate domains input + if (domainInput.classList.contains('alternate-domain-input')){ + domainInput.addEventListener('input', function() { + removeFormErrors(domainInput); + } + ); + } + } + } })(); /** - * Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data) + * Removes form errors surrounding a form input + */ +function removeFormErrors(input){ + console.log("in the function...") + // Remove error message + let errorMessage = document.getElementById(`${input.id}__error-message`); + if (errorMessage) { + errorMessage.remove(); + console.log("Error message removed") + }else{ + return + } + + // Remove error classes + if (input.classList.contains('usa-input--error')) { + input.classList.remove('usa-input--error'); + } + + let label = document.querySelector(`label[for="${input.id}"]`); + if (label) { + label.classList.remove('usa-label--error'); + + // Remove error classes from parent div + let parentDiv = label.parentElement; + if (parentDiv) { + parentDiv.classList.remove('usa-form-group--error'); + } + } +} + +/** + * Prepare the namerservers and DS data forms delete buttons + * We will call this on the forms init, and also every time we add a form * */ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){ From 1a5c8930edbc591f9854c4795460a3a66d6e1352 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:37:13 -0700 Subject: [PATCH 018/120] Button align --- src/registrar/assets/sass/_theme/_buttons.scss | 16 ++++++++++------ src/registrar/assets/sass/_theme/_tables.scss | 4 ---- src/registrar/templates/domain_users.html | 6 +++--- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index c890b7a55..78c06f0f4 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -26,27 +26,31 @@ a.usa-button { text-decoration: none; } -a.usa-button.disabled-link, -a.usa-button--unstyled.disabled-link { +a.usa-button.disabled-link { background-color: #ccc !important; color: #454545 !important } -a.usa-button.disabled-link:hover, -a.usa-button--unstyled.disabled-link { +a.usa-button.disabled-link:hover{ background-color: #ccc !important; cursor: not-allowed !important; color: #454545 !important } -a.usa-button.disabled-link:focus, -a.usa-button--unstyled.disabled-link { +a.usa-button.disabled-link:focus { background-color: #ccc !important; cursor: not-allowed !important; outline: none !important; color: #454545 !important } +a.usa-button--unstyled.disabled-link, +a.usa-button--unstyled.disabled-link:hover, +a.usa-button--unstyled.disabled-link:focus { + cursor: not-allowed !important; + outline: none !important; +} + a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { color: color('white'); } diff --git a/src/registrar/assets/sass/_theme/_tables.scss b/src/registrar/assets/sass/_theme/_tables.scss index 892427f82..6dcc6f3bc 100644 --- a/src/registrar/assets/sass/_theme/_tables.scss +++ b/src/registrar/assets/sass/_theme/_tables.scss @@ -25,10 +25,6 @@ color: color('primary-darker'); padding-bottom: units(2px); } - td.shift-action-button { - padding-right: 0; - transform: translateX(10px); - } } .dotgov-table { diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 216c21942..e78d88781 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -29,7 +29,7 @@ Email Role - Action + Action @@ -39,7 +39,7 @@ {{ permission.user.email }} {{ permission.role|title }} - + {% if can_delete_users %} {{ invitation.created_at|date }} {{ invitation.status|title }} -

    + {% csrf_token %} From ac81ecd11a06e83a543229f1a5ff24be35eab4e7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:30:40 -0700 Subject: [PATCH 019/120] Remove logger --- src/registrar/assets/js/get-gov.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 23e40858a..ad44f3d44 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -260,7 +260,6 @@ function handleValidationClick(e) { * Removes form errors surrounding a form input */ function removeFormErrors(input){ - console.log("in the function...") // Remove error message let errorMessage = document.getElementById(`${input.id}__error-message`); if (errorMessage) { From 4c5d8b2c55ab24ad3c8e0198630b75605430fcd0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 12:45:39 -0700 Subject: [PATCH 020/120] Update get-gov.js --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index ad44f3d44..ee9b7165d 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -264,7 +264,6 @@ function removeFormErrors(input){ let errorMessage = document.getElementById(`${input.id}__error-message`); if (errorMessage) { errorMessage.remove(); - console.log("Error message removed") }else{ return } @@ -274,6 +273,7 @@ function removeFormErrors(input){ input.classList.remove('usa-input--error'); } + // Get the form label let label = document.querySelector(`label[for="${input.id}"]`); if (label) { label.classList.remove('usa-label--error'); From 0a1da49c33e6b43977ed57e0b58629a49078864c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:14:13 -0700 Subject: [PATCH 021/120] Logic to remove stale alerts --- src/registrar/assets/js/get-gov.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index ee9b7165d..bbf1791ed 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -236,7 +236,7 @@ function handleValidationClick(e) { const checkAvailabilityInput = document.getElementById(targetId); checkAvailabilityButton.addEventListener('click', function() { - removeFormErrors(checkAvailabilityInput) + removeFormErrors(checkAvailabilityInput, true); } ); } @@ -248,7 +248,7 @@ function handleValidationClick(e) { // Only apply this logic to alternate domains input if (domainInput.classList.contains('alternate-domain-input')){ domainInput.addEventListener('input', function() { - removeFormErrors(domainInput); + removeFormErrors(domainInput, true); } ); } @@ -259,7 +259,7 @@ function handleValidationClick(e) { /** * Removes form errors surrounding a form input */ -function removeFormErrors(input){ +function removeFormErrors(input, removeStaleAlerts=false){ // Remove error message let errorMessage = document.getElementById(`${input.id}__error-message`); if (errorMessage) { @@ -284,6 +284,16 @@ function removeFormErrors(input){ parentDiv.classList.remove('usa-form-group--error'); } } + + if (removeStaleAlerts){ + let staleAlerts = document.getElementsByClassName("usa-alert--error") + for (let alert of staleAlerts){ + // Don't remove the error associated with the input + if (alert.id !== `${input.id}--toast`) { + alert.remove() + } + } + } } /** From a9253b6689098dbe29723104feafe3d3dcdfeed7 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:30:23 -0700 Subject: [PATCH 022/120] Update get-gov.js --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index bbf1791ed..e2e116569 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -286,7 +286,7 @@ function removeFormErrors(input, removeStaleAlerts=false){ } if (removeStaleAlerts){ - let staleAlerts = document.getElementsByClassName("usa-alert--error") + let staleAlerts = Array.from(document.getElementsByClassName("usa-alert--error")) for (let alert of staleAlerts){ // Don't remove the error associated with the input if (alert.id !== `${input.id}--toast`) { From 3d7c651c72b8ff04abf85b0dedcb22ae5ca45690 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:31:18 -0700 Subject: [PATCH 023/120] Use querySelectorAll --- src/registrar/assets/js/get-gov.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e2e116569..4a1ce005f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -286,7 +286,7 @@ function removeFormErrors(input, removeStaleAlerts=false){ } if (removeStaleAlerts){ - let staleAlerts = Array.from(document.getElementsByClassName("usa-alert--error")) + let staleAlerts = document.querySelectorAll(".usa-alert--error") for (let alert of staleAlerts){ // Don't remove the error associated with the input if (alert.id !== `${input.id}--toast`) { From 234af2501fe803958a00859a01846fc795074f46 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:01:49 -0700 Subject: [PATCH 024/120] Unit test --- src/registrar/tests/test_views.py | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 8f812b815..0b73f7bef 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1443,6 +1443,76 @@ class TestDomainManagers(TestDomainOverview): response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) self.assertContains(response, "Add a domain manager") + def test_domain_delete_link(self): + """Tests if the user delete link works""" + + # Add additional users + dummy_user_1 = User.objects.create( + username="macncheese", + email="cheese@igorville.com", + ) + dummy_user_2 = User.objects.create( + username="pastapizza", + email="pasta@igorville.com", + ) + + role_1 = UserDomainRole.objects.create( + user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + role_2 = UserDomainRole.objects.create( + user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + + # Make sure we're on the right page + self.assertContains(response, "Domain managers") + + # Make sure the desired user exists + self.assertContains(response, "cheese@igorville.com") + + # Delete dummy_user_1 + response = self.client.post( + reverse( + "domain-user-delete", + kwargs={ + "pk": self.domain.id, + "user_pk": dummy_user_1.id + } + ), + follow=True + ) + + # Grab the displayed messages + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + + # Ensure the error we recieve is in line with what we expect + message = messages[0] + self.assertEqual(message.message, "Removed cheese@igorville.com as a manager for this domain.") + self.assertEqual(message.tags, "success") + + # Check that role_1 deleted in the DB after the post + deleted_user_exists = UserDomainRole.objects.filter(id=role_1.id).exists() + self.assertFalse(deleted_user_exists) + + # Ensure that the current user wasn't deleted + current_user_exists = UserDomainRole.objects.filter(id=self.user.id).exists() + self.assertTrue(current_user_exists) + + # Ensure that the other userdomainrole was not deleted + role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() + self.assertTrue(role_2_exists) + + # Check that the view no longer displays the deleted user + # TODO - why is this not working? + # self.assertNotContains(response, "cheese@igorville.com") + + @skip("TODO") + def test_domain_delete_self_redirects_home(self): + """Tests if deleting yourself redirects to home""" + raise + @boto3_mocking.patching def test_domain_user_add_form(self): """Adding an existing user works.""" From 474e70d1bd30398923d7930c9153f851e20842c8 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:43:42 -0700 Subject: [PATCH 025/120] Linting on test case, stylistic changes --- src/registrar/templates/domain_users.html | 6 +++--- src/registrar/tests/test_views.py | 21 +++++---------------- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index e78d88781..fa94c5f51 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -29,7 +29,7 @@ Email Role - Action + Action @@ -137,8 +137,8 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} -
    - {% csrf_token %} + + {% csrf_token %}
    diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 0b73f7bef..f022ec09c 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1456,31 +1456,20 @@ class TestDomainManagers(TestDomainOverview): email="pasta@igorville.com", ) - role_1 = UserDomainRole.objects.create( - user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - role_2 = UserDomainRole.objects.create( - user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) + role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) # Make sure we're on the right page self.assertContains(response, "Domain managers") - + # Make sure the desired user exists self.assertContains(response, "cheese@igorville.com") # Delete dummy_user_1 response = self.client.post( - reverse( - "domain-user-delete", - kwargs={ - "pk": self.domain.id, - "user_pk": dummy_user_1.id - } - ), - follow=True + reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": dummy_user_1.id}), follow=True ) # Grab the displayed messages @@ -1497,7 +1486,7 @@ class TestDomainManagers(TestDomainOverview): self.assertFalse(deleted_user_exists) # Ensure that the current user wasn't deleted - current_user_exists = UserDomainRole.objects.filter(id=self.user.id).exists() + current_user_exists = UserDomainRole.objects.filter(user=self.user.id).exists() self.assertTrue(current_user_exists) # Ensure that the other userdomainrole was not deleted From b23f361a0843e5ac9d563e02e843420be84b9038 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:53:59 -0700 Subject: [PATCH 026/120] Update permission_views.py --- src/registrar/views/utility/permission_views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 8c414f6ad..762612128 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -158,5 +158,8 @@ class UserDomainRolePermissionDeleteView(UserDomainRolePermissionView, DeleteVie # DetailView property for what model this is viewing model = UserDomainRole + # workaround for type mismatch in DeleteView + object: UserDomainRole + # variable name in template context for the model object context_object_name = "userdomainrole" From ef5617cca2e9ca7690307696d7262219313223b1 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 18 Jan 2024 17:11:12 -0800 Subject: [PATCH 027/120] Add button and wording and update styling --- .../templates/application_dotgov_domain.html | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 1838f33f4..22b4093c2 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -50,7 +50,7 @@ @@ -83,6 +83,17 @@ Add another alternative - +
    + + + +

    If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

    + + {% endblock %} From ae8220587ea9cdf2333cefa11c429827d023a2a5 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:48:53 -0800 Subject: [PATCH 028/120] Change submit button types on domain availability --- src/registrar/templates/application_dotgov_domain.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 22b4093c2..b1b952475 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -77,7 +77,7 @@ {% endwith %} {% endwith %} - From 816cbe23dd6e6b65e8edae4dd129049971fc775d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 08:06:33 -0700 Subject: [PATCH 029/120] Add unit tests --- src/registrar/templates/domain_users.html | 8 ++-- src/registrar/tests/test_views.py | 56 +++++++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index fa94c5f51..e7659e409 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -28,7 +28,7 @@ Email - Role + Role Action @@ -125,8 +125,8 @@ Email Date created - Status - Action + Status + Action @@ -137,7 +137,7 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} -
    + {% csrf_token %}
    diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index bac76b5e2..d692fd3dc 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2543,6 +2543,7 @@ class TestDomainManagers(TestDomainOverview): super().tearDown() self.user.is_staff = False self.user.save() + User.objects.all().delete() def test_domain_managers(self): response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) @@ -2609,13 +2610,62 @@ class TestDomainManagers(TestDomainOverview): self.assertTrue(role_2_exists) # Check that the view no longer displays the deleted user - # TODO - why is this not working? + # why is this not working? Its not in the response when printed? # self.assertNotContains(response, "cheese@igorville.com") - @skip("TODO") def test_domain_delete_self_redirects_home(self): """Tests if deleting yourself redirects to home""" - raise + # Add additional users + dummy_user_1 = User.objects.create( + username="macncheese", + email="cheese@igorville.com", + ) + dummy_user_2 = User.objects.create( + username="pastapizza", + email="pasta@igorville.com", + ) + + role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + + # Make sure we're on the right page + self.assertContains(response, "Domain managers") + + # Make sure the desired user exists + self.assertContains(response, "info@example.com") + + # Make sure more than one UserDomainRole exists on this object + self.assertContains(response, "cheese@igorville.com") + + # Delete the current user + response = self.client.post( + reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True + ) + + # Check if we've been redirected to the home page + self.assertContains(response, "Manage your domains") + + # Grab the displayed messages + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + + # Ensure the error we recieve is in line with what we expect + message = messages[0] + self.assertEqual(message.message, "You are no longer managing the domain igorville.gov") + self.assertEqual(message.tags, "success") + + # Ensure that the current user was deleted + current_user_exists = UserDomainRole.objects.filter(user=self.user.id).exists() + self.assertFalse(current_user_exists) + + # Ensure that the other userdomainroles are not deleted + role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists() + self.assertTrue(role_1_exists) + + role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() + self.assertTrue(role_2_exists) @boto3_mocking.patching def test_domain_user_add_form(self): From b334fba42190af6aa19a17cb5235b3b35eae9e93 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 08:15:30 -0700 Subject: [PATCH 030/120] Fix unit test --- 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 d692fd3dc..3cb9a7792 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2653,7 +2653,7 @@ class TestDomainManagers(TestDomainOverview): # Ensure the error we recieve is in line with what we expect message = messages[0] - self.assertEqual(message.message, "You are no longer managing the domain igorville.gov") + self.assertEqual(message.message, "You are no longer managing the domain igorville.gov.") self.assertEqual(message.tags, "success") # Ensure that the current user was deleted From 4e667ea54640c37fad7934e0d3f467884b428ebd Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:09:37 -0700 Subject: [PATCH 031/120] Minor styling change --- src/registrar/templates/domain_users.html | 2 +- src/registrar/templates/includes/form_messages.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index e7659e409..cfcea717c 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -83,7 +83,7 @@ {% else %}
    - {{ message }} + {{ message }}
    From 421c2bd2ead62d4e7ec42ab95f24faf56c487fe3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:34:43 -0700 Subject: [PATCH 032/120] Remove redundant filter --- src/registrar/models/domain.py | 30 +++++++++++++++++++---------- src/registrar/utility/csv_export.py | 14 +++++--------- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..3d64f8873 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -909,11 +909,22 @@ class Domain(TimeStampedModel, DomainHelper): """Time to renew. Not implemented.""" raise NotImplementedError() - def get_security_email(self): - logger.info("get_security_email-> getting the contact ") - secContact = self.security_contact - if secContact is not None: - return secContact.email + def get_security_email(self, skip_epp_call=False): + logger.info("get_security_email-> getting the contact") + + # If specified, skip the epp call outright. + # Otherwise, proceed as normal. + if skip_epp_call: + logger.info("get_security_email-> skipping epp call") + security = PublicContact.ContactTypeChoices.SECURITY + security_contact = self.generic_contact_getter(security, skip_epp_call) + else: + security_contact = self.security_contact + + # If we get a valid value for security_contact, pull its email + # Otherwise, just return nothing + if security_contact is not None and isinstance(security_contact, PublicContact): + return security_contact.email else: return None @@ -1110,7 +1121,7 @@ class Domain(TimeStampedModel, DomainHelper): ) raise error - def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices) -> PublicContact | None: + def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices, skip_epp_call=False) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for all contact items and thus may result in EPP calls being sent. @@ -1121,7 +1132,6 @@ class Domain(TimeStampedModel, DomainHelper): If you wanted to setup getter logic for Security, you would call: cache_contact_helper(PublicContact.ContactTypeChoices.SECURITY), or cache_contact_helper("security"). - """ # registrant_contact(s) are an edge case. They exist on # the "registrant" property as opposed to contacts. @@ -1131,7 +1141,7 @@ class Domain(TimeStampedModel, DomainHelper): try: # Grab from cache - contacts = self._get_property(desired_property) + contacts = self._get_property(desired_property, skip_epp_call) except KeyError as error: # if contact type is security, attempt to retrieve registry id # for the security contact from domain.security_contact_registry_id @@ -1866,9 +1876,9 @@ class Domain(TimeStampedModel, DomainHelper): """Remove cache data when updates are made.""" self._cache = {} - def _get_property(self, property): + def _get_property(self, property, skip_epp_call=False): """Get some piece of info about a domain.""" - if property not in self._cache: + if property not in self._cache and not skip_epp_call: self._fetch_cache( fetch_hosts=(property == "hosts"), fetch_contacts=(property == "contacts"), diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 3924c03c4..1e5ae148f 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -24,9 +24,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos -def write_row(writer, columns, domain_info): - security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) - +def write_row(writer, columns, domain_info: DomainInformation): # For linter ao = " " if domain_info.authorizing_official: @@ -34,9 +32,9 @@ def write_row(writer, columns, domain_info): last_name = domain_info.authorizing_official.last_name or "" ao = first_name + " " + last_name - security_email = " " - if security_contacts: - security_email = security_contacts[0].email + security_email = domain_info.domain.get_security_email(skip_epp_call=True) + if security_email is None: + security_email = " " invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report @@ -78,9 +76,7 @@ def write_body( """ # Get the domainInfos - domain_infos = get_domain_infos(filter_condition, sort_fields) - - all_domain_infos = list(domain_infos) + all_domain_infos = get_domain_infos(filter_condition, sort_fields) # Write rows to CSV for domain_info in all_domain_infos: From eb5ae025f8a58d217706ae42dd6127f4a8159989 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:21:31 -0700 Subject: [PATCH 033/120] Additional improvements --- src/registrar/utility/csv_export.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 1e5ae148f..10fab2628 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation -from registrar.models.public_contact import PublicContact from django.db.models import Value from django.db.models.functions import Coalesce from django.utils import timezone @@ -24,29 +23,32 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos -def write_row(writer, columns, domain_info: DomainInformation): +def write_row(writer, columns, domain_info: DomainInformation, skip_epp_call=True): # For linter ao = " " if domain_info.authorizing_official: first_name = domain_info.authorizing_official.first_name or "" last_name = domain_info.authorizing_official.last_name or "" - ao = first_name + " " + last_name + ao = f"{first_name} {last_name}" - security_email = domain_info.domain.get_security_email(skip_epp_call=True) + security_email = domain_info.domain.get_security_email(skip_epp_call) if security_email is None: security_email = " " invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report - if security_email is not None and security_email.lower() in invalid_emails: + if security_email.lower() in invalid_emails: security_email = "(blank)" + if domain_info.federal_type: + domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}" + else: + domain_type = domain_info.get_organization_type_display() + # create a dictionary of fields which can be included in output FIELDS = { "Domain name": domain_info.domain.name, - "Domain type": domain_info.get_organization_type_display() + " - " + domain_info.get_federal_type_display() - if domain_info.federal_type - else domain_info.get_organization_type_display(), + "Domain type": domain_type, "Agency": domain_info.federal_agency, "Organization name": domain_info.organization_name, "City": domain_info.city, @@ -61,7 +63,8 @@ def write_row(writer, columns, domain_info: DomainInformation): "Deleted": domain_info.domain.deleted, } - writer.writerow([FIELDS.get(column, "") for column in columns]) + row = [FIELDS.get(column, "") for column in columns] + writer.writerow(row) def write_body( From 63b31a4764041b6475a72e9ff4f7b8ee7638d88d Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 14:27:22 -0700 Subject: [PATCH 034/120] Additional performance improvements --- src/registrar/utility/csv_export.py | 75 ++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 10fab2628..cf2e1484f 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -6,6 +6,11 @@ from registrar.models.domain_information import DomainInformation from django.db.models import Value from django.db.models.functions import Coalesce from django.utils import timezone +from django.core.paginator import Paginator +import time +from django.db.models import F, Value, CharField +from django.db.models.functions import Concat, Coalesce + logger = logging.getLogger(__name__) @@ -20,20 +25,35 @@ def write_header(writer, columns): def get_domain_infos(filter_condition, sort_fields): domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) - return domain_infos + + # Do a mass concat of the first and last name fields for authorizing_official. + # The old operation was computationally heavy for some reason, so if we precompute + # this here, it is vastly more efficient. + domain_infos_cleaned = domain_infos.annotate( + ao=Concat( + Coalesce(F('authorizing_official__first_name'), Value('')), + Value(' '), + Coalesce(F('authorizing_official__last_name'), Value('')), + output_field=CharField(), + ) + ) + return domain_infos_cleaned -def write_row(writer, columns, domain_info: DomainInformation, skip_epp_call=True): - # For linter - ao = " " - if domain_info.authorizing_official: - first_name = domain_info.authorizing_official.first_name or "" - last_name = domain_info.authorizing_official.last_name or "" - ao = f"{first_name} {last_name}" +def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): + """Given a set of columns, generate a new row from cleaned column data""" - security_email = domain_info.domain.get_security_email(skip_epp_call) + domain = domain_info.domain + + start_time = time.time() + # TODO - speed up + security_email = domain.security_contact_registry_id if security_email is None: - security_email = " " + cached_sec_email = domain.get_security_email(skip_epp_call) + security_email = cached_sec_email if cached_sec_email is not None else " " + + end_time = time.time() + print(f"parse security email operation took {end_time - start_time} seconds") invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report @@ -47,25 +67,24 @@ def write_row(writer, columns, domain_info: DomainInformation, skip_epp_call=Tru # create a dictionary of fields which can be included in output FIELDS = { - "Domain name": domain_info.domain.name, + "Domain name": domain.name, "Domain type": domain_type, "Agency": domain_info.federal_agency, "Organization name": domain_info.organization_name, "City": domain_info.city, "State": domain_info.state_territory, - "AO": ao, + "AO": domain_info.ao, "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "Security contact email": security_email, - "Status": domain_info.domain.get_state_display(), - "Expiration date": domain_info.domain.expiration_date, - "Created at": domain_info.domain.created_at, - "First ready": domain_info.domain.first_ready, - "Deleted": domain_info.domain.deleted, + "Status": domain.get_state_display(), + "Expiration date": domain.expiration_date, + "Created at": domain.created_at, + "First ready": domain.first_ready, + "Deleted": domain.deleted, } row = [FIELDS.get(column, "") for column in columns] - writer.writerow(row) - + return row def write_body( writer, @@ -81,9 +100,19 @@ def write_body( # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) - # Write rows to CSV - for domain_info in all_domain_infos: - write_row(writer, columns, domain_info) + # Reduce the memory overhead when performing the write operation + paginator = Paginator(all_domain_infos, 1000) + for page_num in paginator.page_range: + page = paginator.page(page_num) + rows = [] + start_time = time.time() + for domain_info in page.object_list: + row = parse_row(columns, domain_info) + rows.append(row) + + end_time = time.time() + print(f"new parse Operation took {end_time - start_time} seconds") + writer.writerows(rows) def export_data_type_to_csv(csv_file): @@ -150,7 +179,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_header(writer, columns) + write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) From 7bd1356858c292ab53d894d65edca06bd3e22733 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:02:45 -0700 Subject: [PATCH 035/120] Additional optimizations --- src/registrar/utility/csv_export.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index cf2e1484f..5d3b7a46d 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -24,7 +24,9 @@ def write_header(writer, columns): def get_domain_infos(filter_condition, sort_fields): - domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields) + domain_infos = DomainInformation.objects.select_related( + 'domain', 'authorizing_official' + ).filter(**filter_condition).order_by(*sort_fields) # Do a mass concat of the first and last name fields for authorizing_official. # The old operation was computationally heavy for some reason, so if we precompute @@ -46,7 +48,6 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): domain = domain_info.domain start_time = time.time() - # TODO - speed up security_email = domain.security_contact_registry_id if security_email is None: cached_sec_email = domain.get_security_email(skip_epp_call) @@ -82,8 +83,10 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): "First ready": domain.first_ready, "Deleted": domain.deleted, } - + start_time = time.time() row = [FIELDS.get(column, "") for column in columns] + end_time = time.time() + print(f"parse some cols operation took {end_time - start_time} seconds") return row def write_body( @@ -101,6 +104,7 @@ def write_body( all_domain_infos = get_domain_infos(filter_condition, sort_fields) # Reduce the memory overhead when performing the write operation + a1_start_time = time.time() paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: page = paginator.page(page_num) @@ -114,6 +118,8 @@ def write_body( print(f"new parse Operation took {end_time - start_time} seconds") writer.writerows(rows) + a1_end_time = time.time() + print(f"parse all stuff operation took {a1_end_time - a1_start_time} seconds") def export_data_type_to_csv(csv_file): """All domains report with extra columns""" From 3838be3960c004d33ea0c28050e7d05fc8df88d0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:09:50 -0700 Subject: [PATCH 036/120] Linting --- src/registrar/utility/csv_export.py | 33 +++++++++++------------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 5d3b7a46d..3b671c9cc 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -7,7 +7,6 @@ from django.db.models import Value from django.db.models.functions import Coalesce from django.utils import timezone from django.core.paginator import Paginator -import time from django.db.models import F, Value, CharField from django.db.models.functions import Concat, Coalesce @@ -24,18 +23,20 @@ def write_header(writer, columns): def get_domain_infos(filter_condition, sort_fields): - domain_infos = DomainInformation.objects.select_related( - 'domain', 'authorizing_official' - ).filter(**filter_condition).order_by(*sort_fields) + domain_infos = ( + DomainInformation.objects.select_related("domain", "authorizing_official") + .filter(**filter_condition) + .order_by(*sort_fields) + ) # Do a mass concat of the first and last name fields for authorizing_official. # The old operation was computationally heavy for some reason, so if we precompute # this here, it is vastly more efficient. domain_infos_cleaned = domain_infos.annotate( ao=Concat( - Coalesce(F('authorizing_official__first_name'), Value('')), - Value(' '), - Coalesce(F('authorizing_official__last_name'), Value('')), + Coalesce(F("authorizing_official__first_name"), Value("")), + Value(" "), + Coalesce(F("authorizing_official__last_name"), Value("")), output_field=CharField(), ) ) @@ -47,15 +48,11 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): domain = domain_info.domain - start_time = time.time() security_email = domain.security_contact_registry_id if security_email is None: cached_sec_email = domain.get_security_email(skip_epp_call) security_email = cached_sec_email if cached_sec_email is not None else " " - end_time = time.time() - print(f"parse security email operation took {end_time - start_time} seconds") - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report if security_email.lower() in invalid_emails: @@ -83,12 +80,11 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): "First ready": domain.first_ready, "Deleted": domain.deleted, } - start_time = time.time() + row = [FIELDS.get(column, "") for column in columns] - end_time = time.time() - print(f"parse some cols operation took {end_time - start_time} seconds") return row + def write_body( writer, columns, @@ -109,17 +105,12 @@ def write_body( for page_num in paginator.page_range: page = paginator.page(page_num) rows = [] - start_time = time.time() for domain_info in page.object_list: row = parse_row(columns, domain_info) rows.append(row) - - end_time = time.time() - print(f"new parse Operation took {end_time - start_time} seconds") + writer.writerows(rows) - a1_end_time = time.time() - print(f"parse all stuff operation took {a1_end_time - a1_start_time} seconds") def export_data_type_to_csv(csv_file): """All domains report with extra columns""" @@ -185,7 +176,7 @@ def export_data_full_to_csv(csv_file): Domain.State.ON_HOLD, ], } - write_header(writer, columns) + write_header(writer, columns) write_body(writer, columns, sort_fields, filter_condition) From 59486a922d24d7aa69bd000ad58c96b90567fbd4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:19:28 -0700 Subject: [PATCH 037/120] Remove accidental inclusion --- src/registrar/utility/csv_export.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 3b671c9cc..c94f05131 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -100,7 +100,6 @@ def write_body( all_domain_infos = get_domain_infos(filter_condition, sort_fields) # Reduce the memory overhead when performing the write operation - a1_start_time = time.time() paginator = Paginator(all_domain_infos, 1000) for page_num in paginator.page_range: page = paginator.page(page_num) From ec7c2244402e59f311272b62a62557da262f17a2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:30:28 -0700 Subject: [PATCH 038/120] Fix bug --- src/registrar/utility/csv_export.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index c94f05131..a65fc283c 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -48,10 +48,8 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): domain = domain_info.domain - security_email = domain.security_contact_registry_id - if security_email is None: - cached_sec_email = domain.get_security_email(skip_epp_call) - security_email = cached_sec_email if cached_sec_email is not None else " " + cached_sec_email = domain.get_security_email(skip_epp_call) + security_email = cached_sec_email if cached_sec_email is not None else " " invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report From f21287c0c74158199f972dc07c2c37f8ec4e4b60 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 22 Jan 2024 15:54:00 -0700 Subject: [PATCH 039/120] Linting --- src/registrar/models/domain.py | 4 +++- src/registrar/utility/csv_export.py | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 3d64f8873..0cbe8c071 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -1121,7 +1121,9 @@ class Domain(TimeStampedModel, DomainHelper): ) raise error - def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices, skip_epp_call=False) -> PublicContact | None: + def generic_contact_getter( + self, contact_type_choice: PublicContact.ContactTypeChoices, skip_epp_call=False + ) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for all contact items and thus may result in EPP calls being sent. diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index a65fc283c..e513dc3d3 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation -from django.db.models import Value from django.db.models.functions import Coalesce from django.utils import timezone from django.core.paginator import Paginator From ac6d46b9f8c1cde61b77721ba8660b7cb7ad09ad Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Mon, 22 Jan 2024 19:36:02 -0500 Subject: [PATCH 040/120] IMplement add form for alternative domains --- src/registrar/assets/js/get-gov.js | 63 ++++++++++++++++--- src/registrar/forms/application_wizard.py | 2 +- .../templates/application_dotgov_domain.html | 26 ++++---- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 3995e975c..fc6cfbe61 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -48,6 +48,7 @@ function createLiveRegion(id) { /** Announces changes to assistive technology users. */ function announce(id, text) { + console.log('announce: ' + text) let liveRegion = document.getElementById(id + "-live-region"); if (!liveRegion) liveRegion = createLiveRegion(id); liveRegion.innerHTML = text; @@ -86,6 +87,7 @@ function fetchJSON(endpoint, callback, url="/api/v1/") { /** Modifies CSS and HTML when an input is valid/invalid. */ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { + console.log('toggleInputValidity: ' + valid) if (valid) { el.setCustomValidity(""); el.removeAttribute("aria-invalid"); @@ -100,6 +102,7 @@ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { /** Display (or hide) a message beneath an element. */ function inlineToast(el, id, style, msg) { + console.log('inine toast creates alerts') if (!el.id && !id) { console.error("Elements must have an `id` to show an inline toast."); return; @@ -130,8 +133,10 @@ function inlineToast(el, id, style, msg) { } } -function _checkDomainAvailability(el) { +function checkDomainAvailability(el) { + console.log('checkDomainAvailability: ' + el.value) const callback = (response) => { + console.log('inside callback') toggleInputValidity(el, (response && response.available), msg=response.message); announce(el.id, response.message); @@ -142,6 +147,7 @@ function _checkDomainAvailability(el) { // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration inlineToast(el.parentElement, el.id, SUCCESS, response.message); } else if (ignore_blank && response.code == "required"){ + console.log('ignore_blank && response.code == "required"') // Visually remove the error error = "usa-input--error" if (el.classList.contains(error)){ @@ -155,7 +161,7 @@ function _checkDomainAvailability(el) { } /** Call the API to see if the domain is good. */ -const checkDomainAvailability = debounce(_checkDomainAvailability); +// const checkDomainAvailability = debounce(_checkDomainAvailability); /** Hides the toast message and clears the aira live region. */ function clearDomainAvailability(el) { @@ -167,6 +173,7 @@ function clearDomainAvailability(el) { /** Runs all the validators associated with this element. */ function runValidators(el) { + console.log(el.getAttribute("id")) const attribute = el.getAttribute("validate") || ""; if (!attribute.length) return; const validators = attribute.split(" "); @@ -207,12 +214,37 @@ function handleInputValidation(e) { /** On button click, handles running any associated validators. */ function handleValidationClick(e) { + console.log('validating dotgov domain') + const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; - const input = document.getElementById(attribute); + + const input = document.getElementById(attribute); // You might need to define 'attribute' runValidators(input); } + +function handleFormsetValidationClick(e) { + // Check availability for alternative domains + + console.log('validating alternative domains') + + const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); + + // Collect input IDs from the repeatable forms + let inputIds = Array.from(document.querySelectorAll('.repeatable-form input')).map(input => input.id); + + // Run validators for each input + inputIds.forEach(inputId => { + const input = document.getElementById(inputId); + runValidators(input); + }); + + // Set the validate-for attribute on the button with the collected input IDs + // Not needed for functionality but nice for accessibility + alternativeDomainsAvailability.setAttribute('validate-for', inputIds.join(', ')); +} + // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> // Initialization code. @@ -232,9 +264,16 @@ function handleValidationClick(e) { for(const input of needsValidation) { input.addEventListener('input', handleInputValidation); } + const dotgovDomainsAvailability = document.getElementById('check-availability-for-dotgov-domain'); + const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); const activatesValidation = document.querySelectorAll('[validate-for]'); for(const button of activatesValidation) { - button.addEventListener('click', handleValidationClick); + if (alternativeDomainsAvailability) { + alternativeDomainsAvailability.addEventListener('click', handleFormsetValidationClick); + dotgovDomainsAvailability.addEventListener('click', handleValidationClick); + } else { + button.addEventListener('click', handleValidationClick); + } } })(); @@ -453,6 +492,7 @@ function hideDeletedForms() { let isNameserversForm = document.querySelector(".nameservers-form"); let isOtherContactsForm = document.querySelector(".other-contacts-form"); let isDsDataForm = document.querySelector(".ds-data-form"); + let isDotgovDomain = document.querySelector(".dotgov-domain-form"); // The Nameservers formset features 2 required and 11 optionals if (isNameserversForm) { cloneIndex = 2; @@ -465,6 +505,8 @@ function hideDeletedForms() { formLabel = "Organization contact"; container = document.querySelector("#other-employees"); formIdentifier = "other_contacts" + } else if (isDotgovDomain) { + formIdentifier = "dotgov_domain" } let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`); @@ -539,6 +581,7 @@ function hideDeletedForms() { // Reset the values of each input to blank inputs.forEach((input) => { input.classList.remove("usa-input--error"); + input.classList.remove("usa-input--success"); if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") { input.value = ""; // Set the value to an empty string @@ -551,22 +594,25 @@ function hideDeletedForms() { let selects = newForm.querySelectorAll("select"); selects.forEach((select) => { select.classList.remove("usa-input--error"); + select.classList.remove("usa-input--success"); select.selectedIndex = 0; // Set the value to an empty string }); let labels = newForm.querySelectorAll("label"); labels.forEach((label) => { label.classList.remove("usa-label--error"); + label.classList.remove("usa-label--success"); }); let usaFormGroups = newForm.querySelectorAll(".usa-form-group"); usaFormGroups.forEach((usaFormGroup) => { usaFormGroup.classList.remove("usa-form-group--error"); + usaFormGroup.classList.remove("usa-form-group--success"); }); - // Remove any existing error messages - let usaErrorMessages = newForm.querySelectorAll(".usa-error-message"); - usaErrorMessages.forEach((usaErrorMessage) => { + // Remove any existing error and success messages + let usaMessages = newForm.querySelectorAll(".usa-error-message, .usa-alert"); + usaMessages.forEach((usaErrorMessage) => { let parentDiv = usaErrorMessage.closest('div'); if (parentDiv) { parentDiv.remove(); // Remove the parent div if it exists @@ -577,7 +623,8 @@ function hideDeletedForms() { // Attach click event listener on the delete buttons of the new form let newDeleteButton = newForm.querySelector(".delete-record"); - prepareNewDeleteButton(newDeleteButton, formLabel); + if (newDeleteButton) + prepareNewDeleteButton(newDeleteButton, formLabel); // Disable the add more button if we have 13 forms if (isNameserversForm && formNum == 13) { diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py index ae6188133..284705a9a 100644 --- a/src/registrar/forms/application_wizard.py +++ b/src/registrar/forms/application_wizard.py @@ -420,7 +420,7 @@ class AlternativeDomainForm(RegistrarForm): alternative_domain = forms.CharField( required=False, - label="", + label="Alternative domain", ) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index b1b952475..74c6dce06 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -48,7 +48,7 @@ {% endwith %} {% endwith %}

    If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

    From 8405600cef8a015cb4eec17b139d79e9cc8f3dd2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 23 Jan 2024 11:03:01 -0700 Subject: [PATCH 041/120] Linting --- src/registrar/utility/csv_export.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index e513dc3d3..68dc126db 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -3,7 +3,6 @@ import logging from datetime import datetime from registrar.models.domain import Domain from registrar.models.domain_information import DomainInformation -from django.db.models.functions import Coalesce from django.utils import timezone from django.core.paginator import Paginator from django.db.models import F, Value, CharField @@ -45,7 +44,11 @@ def get_domain_infos(filter_condition, sort_fields): def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): """Given a set of columns, generate a new row from cleaned column data""" - domain = domain_info.domain + # Domain should never be none when parsing this information + if domain_info.domain is None: + raise ValueError("Domain is none") + + domain = domain_info.domain # type: ignore cached_sec_email = domain.get_security_email(skip_epp_call) security_email = cached_sec_email if cached_sec_email is not None else " " @@ -68,7 +71,7 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): "Organization name": domain_info.organization_name, "City": domain_info.city, "State": domain_info.state_territory, - "AO": domain_info.ao, + "AO": domain_info.ao, # type: ignore "AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ", "Security contact email": security_email, "Status": domain.get_state_display(), @@ -102,8 +105,14 @@ def write_body( page = paginator.page(page_num) rows = [] for domain_info in page.object_list: - row = parse_row(columns, domain_info) - rows.append(row) + try: + row = parse_row(columns, domain_info) + rows.append(row) + except ValueError: + # This should not happen. If it does, just skip this row. + # It indicates that DomainInformation.domain is None. + logger.error("csv_export -> Error when parsing row, domain was None") + continue writer.writerows(rows) From 259d713ed61fb83f6c30d2000dcad07546fb91f8 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 23 Jan 2024 10:34:23 -0800 Subject: [PATCH 042/120] Swap to INFO setting --- src/registrar/assets/js/get-gov.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index fc6cfbe61..f38fc3813 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -143,9 +143,9 @@ function checkDomainAvailability(el) { // Determines if we ignore the field if it is just blank ignore_blank = el.classList.contains("blank-ok") if (el.validity.valid) { - el.classList.add('usa-input--success'); + el.classList.add('usa-input--info'); // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration - inlineToast(el.parentElement, el.id, SUCCESS, response.message); + inlineToast(el.parentElement, el.id, INFORMATIVE, response.message); } else if (ignore_blank && response.code == "required"){ console.log('ignore_blank && response.code == "required"') // Visually remove the error From 8417c773683d7c8fe1c22a34981228ce41533d54 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 23 Jan 2024 13:21:52 -0700 Subject: [PATCH 043/120] Code cleanup --- src/registrar/assets/sass/_theme/_buttons.scss | 6 +++--- src/registrar/templates/dashboard_base.html | 6 +++--- src/registrar/templates/domain_users.html | 12 +++--------- src/registrar/tests/test_views.py | 4 ---- src/registrar/views/application.py | 8 -------- src/registrar/views/domain.py | 8 +++----- 6 files changed, 12 insertions(+), 32 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 78c06f0f4..b168551b5 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -31,7 +31,7 @@ a.usa-button.disabled-link { color: #454545 !important } -a.usa-button.disabled-link:hover{ +a.usa-button.disabled-link:hover { background-color: #ccc !important; cursor: not-allowed !important; color: #454545 !important @@ -47,8 +47,8 @@ a.usa-button.disabled-link:focus { a.usa-button--unstyled.disabled-link, a.usa-button--unstyled.disabled-link:hover, a.usa-button--unstyled.disabled-link:focus { - cursor: not-allowed !important; - outline: none !important; + cursor: not-allowed; + outline: none; } a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { diff --git a/src/registrar/templates/dashboard_base.html b/src/registrar/templates/dashboard_base.html index 46b04570b..6dd2ce8fd 100644 --- a/src/registrar/templates/dashboard_base.html +++ b/src/registrar/templates/dashboard_base.html @@ -11,10 +11,10 @@ {% if messages %}
      {% for message in messages %} - - {{ message }} +
    • + {{ message }}
    • - {% endfor %} + {% endfor %}
    {% endif %} {% endblock %} diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index cfcea717c..e22800677 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -92,13 +92,6 @@
    {% endif %} - {% comment %} - usa-tooltip disabled-link" - data-position="right" - title="Coming in 2024" - aria-disabled="true" - data-tooltip="true" - {% endcomment %} {% endfor %} @@ -137,8 +130,9 @@ {{ invitation.created_at|date }} {{ invitation.status|title }} -
    - {% csrf_token %} + + + {% csrf_token %}
    diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 5a32615a4..b128724a0 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2740,10 +2740,6 @@ class TestDomainManagers(TestDomainOverview): role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() self.assertTrue(role_2_exists) - # Check that the view no longer displays the deleted user - # why is this not working? Its not in the response when printed? - # self.assertNotContains(response, "cheese@igorville.com") - def test_domain_delete_self_redirects_home(self): """Tests if deleting yourself redirects to home""" # Add additional users diff --git a/src/registrar/views/application.py b/src/registrar/views/application.py index 4e7180cd7..031b93dee 100644 --- a/src/registrar/views/application.py +++ b/src/registrar/views/application.py @@ -481,14 +481,6 @@ class DotgovDomain(ApplicationWizard): context["federal_type"] = self.application.federal_type return context - def post(self, request, *args, **kwargs): - """Override for the post method to mark the DraftDomain as complete""" - response = super().post(request, *args, **kwargs) - # Set the DraftDomain to "complete" - self.application.requested_domain.is_incomplete = False - self.application.save() - return response - class Purpose(ApplicationWizard): template_name = "application_purpose.html" diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index bbf3d43a0..08ac0424d 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -651,13 +651,14 @@ class DomainUsersView(DomainBaseView): # Determine if the current user can delete managers domain_pk = None can_delete_users = False + if self.kwargs is not None and "pk" in self.kwargs: domain_pk = self.kwargs["pk"] # Prevent the end user from deleting themselves as a manager if they are the # only manager that exists on a domain. can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1 - context["can_delete_users"] = can_delete_users + context["can_delete_users"] = can_delete_users return context def _add_modal_buttons_to_context(self, context): @@ -689,7 +690,6 @@ class DomainUsersView(DomainBaseView): class_list = classes html_class = f'class="{class_list}"' if class_list else None - modal_button = '' return modal_button @@ -831,7 +831,7 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): if email_or_name is None or email_or_name.strip() == "": email_or_name = self.object.user - # If the user is deleting themselves, return a special message. + # If the user is deleting themselves, return a specific message. # If not, return something more generic. if delete_self: message = f"You are no longer managing the domain {self.object.domain}." @@ -842,14 +842,12 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): def form_valid(self, form): """Delete the specified user on this domain.""" - super().form_valid(form) # Is the user deleting themselves? If so, display a different message delete_self = self.request.user == self.object.user # Add a success message messages.success(self.request, self.get_success_message(delete_self)) - return redirect(self.get_success_url()) def post(self, request, *args, **kwargs): From f1c5e9668ddeb666583707c247e7b2c6ed5384be Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:23:54 -0700 Subject: [PATCH 044/120] Add back accidental super deletion --- src/registrar/views/domain.py | 3 +++ src/registrar/views/utility/permission_views.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index 08ac0424d..188671f27 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -843,6 +843,9 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView): def form_valid(self, form): """Delete the specified user on this domain.""" + # Delete the object + super().form_valid(form) + # Is the user deleting themselves? If so, display a different message delete_self = self.request.user == self.object.user diff --git a/src/registrar/views/utility/permission_views.py b/src/registrar/views/utility/permission_views.py index 762612128..a274db0d9 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -150,7 +150,7 @@ class UserDomainRolePermissionView(UserDomainRolePermission, DetailView, abc.ABC class UserDomainRolePermissionDeleteView(UserDomainRolePermissionView, DeleteView, abc.ABC): - """Abstract base view for domain application withdraw function + """Abstract base view for deleting a UserDomainRole. This abstract view cannot be instantiated. Actual views must specify `template_name`. From 33cf59530d1535eee900239d165b9b196f46d2c4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:34:59 -0700 Subject: [PATCH 045/120] Fix unit test Fixed unit test filtering too broadly --- src/registrar/tests/test_views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index b128724a0..9b17357f5 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2690,7 +2690,7 @@ class TestDomainManagers(TestDomainOverview): response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) self.assertContains(response, "Add a domain manager") - def test_domain_delete_link(self): + def test_domain_user_delete_link(self): """Tests if the user delete link works""" # Add additional users @@ -2733,14 +2733,14 @@ class TestDomainManagers(TestDomainOverview): self.assertFalse(deleted_user_exists) # Ensure that the current user wasn't deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id).exists() + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() self.assertTrue(current_user_exists) # Ensure that the other userdomainrole was not deleted role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() self.assertTrue(role_2_exists) - def test_domain_delete_self_redirects_home(self): + def test_domain_user_delete_self_redirects_home(self): """Tests if deleting yourself redirects to home""" # Add additional users dummy_user_1 = User.objects.create( @@ -2784,7 +2784,7 @@ class TestDomainManagers(TestDomainOverview): self.assertEqual(message.tags, "success") # Ensure that the current user was deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id).exists() + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() self.assertFalse(current_user_exists) # Ensure that the other userdomainroles are not deleted From 4710de9b15fe4954b0df584c34c542f791fb2f32 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 23 Jan 2024 15:16:04 -0800 Subject: [PATCH 046/120] Allow multifield validation and clean up --- src/registrar/assets/js/get-gov.js | 19 ++++--------------- .../templates/application_dotgov_domain.html | 5 ++--- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index f38fc3813..14e6e45f6 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -48,7 +48,6 @@ function createLiveRegion(id) { /** Announces changes to assistive technology users. */ function announce(id, text) { - console.log('announce: ' + text) let liveRegion = document.getElementById(id + "-live-region"); if (!liveRegion) liveRegion = createLiveRegion(id); liveRegion.innerHTML = text; @@ -87,7 +86,6 @@ function fetchJSON(endpoint, callback, url="/api/v1/") { /** Modifies CSS and HTML when an input is valid/invalid. */ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { - console.log('toggleInputValidity: ' + valid) if (valid) { el.setCustomValidity(""); el.removeAttribute("aria-invalid"); @@ -102,7 +100,6 @@ function toggleInputValidity(el, valid, msg=DEFAULT_ERROR) { /** Display (or hide) a message beneath an element. */ function inlineToast(el, id, style, msg) { - console.log('inine toast creates alerts') if (!el.id && !id) { console.error("Elements must have an `id` to show an inline toast."); return; @@ -134,9 +131,7 @@ function inlineToast(el, id, style, msg) { } function checkDomainAvailability(el) { - console.log('checkDomainAvailability: ' + el.value) const callback = (response) => { - console.log('inside callback') toggleInputValidity(el, (response && response.available), msg=response.message); announce(el.id, response.message); @@ -147,7 +142,6 @@ function checkDomainAvailability(el) { // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration inlineToast(el.parentElement, el.id, INFORMATIVE, response.message); } else if (ignore_blank && response.code == "required"){ - console.log('ignore_blank && response.code == "required"') // Visually remove the error error = "usa-input--error" if (el.classList.contains(error)){ @@ -173,7 +167,6 @@ function clearDomainAvailability(el) { /** Runs all the validators associated with this element. */ function runValidators(el) { - console.log(el.getAttribute("id")) const attribute = el.getAttribute("validate") || ""; if (!attribute.length) return; const validators = attribute.split(" "); @@ -214,8 +207,6 @@ function handleInputValidation(e) { /** On button click, handles running any associated validators. */ function handleValidationClick(e) { - console.log('validating dotgov domain') - const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; @@ -226,8 +217,6 @@ function handleValidationClick(e) { function handleFormsetValidationClick(e) { // Check availability for alternative domains - - console.log('validating alternative domains') const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); @@ -264,13 +253,13 @@ function handleFormsetValidationClick(e) { for(const input of needsValidation) { input.addEventListener('input', handleInputValidation); } - const dotgovDomainsAvailability = document.getElementById('check-availability-for-dotgov-domain'); const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); const activatesValidation = document.querySelectorAll('[validate-for]'); + for(const button of activatesValidation) { - if (alternativeDomainsAvailability) { - alternativeDomainsAvailability.addEventListener('click', handleFormsetValidationClick); - dotgovDomainsAvailability.addEventListener('click', handleValidationClick); + // Adds multi-field validation for alternative domains + if (button === alternativeDomainsAvailability) { + button.addEventListener('click', handleFormsetValidationClick); } else { button.addEventListener('click', handleValidationClick); } diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 74c6dce06..af5e60751 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -82,15 +82,14 @@ Add another alternative -
    - -

    If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

    +

    If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

    From 65ff4ece76778adb6a0c0aac8d8bab7d74e17367 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:46:53 -0700 Subject: [PATCH 047/120] Add notes field --- src/registrar/admin.py | 29 ++++++++++++++++++- ...64_domain_notes_domainapplication_notes.py | 22 ++++++++++++++ src/registrar/models/domain.py | 6 ++++ src/registrar/models/domain_application.py | 6 ++++ src/registrar/models/domain_information.py | 21 +++++++++----- 5 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 src/registrar/migrations/0064_domain_notes_domainapplication_notes.py diff --git a/src/registrar/admin.py b/src/registrar/admin.py index bd5555805..d3f5b49f1 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -737,7 +737,18 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view form = DomainApplicationAdminForm fieldsets = [ - (None, {"fields": ["status", "investigator", "creator", "approved_domain"]}), + ( + None, + { + "fields": [ + "status", + "investigator", + "creator", + "approved_domain", + "notes" + ] + } + ), ( "Type of organization", { @@ -991,6 +1002,22 @@ class DomainAdmin(ListHeaderAdmin): "deleted", ] + fieldsets = ( + ( + None, + { + "fields": [ + "name", + "state", + "expiration_date", + "first_ready", + "deleted", + "notes" + ] + }, + ), + ) + # this ordering effects the ordering of results # in autocomplete_fields for domain ordering = ["name"] diff --git a/src/registrar/migrations/0064_domain_notes_domainapplication_notes.py b/src/registrar/migrations/0064_domain_notes_domainapplication_notes.py new file mode 100644 index 000000000..24dfaf4e3 --- /dev/null +++ b/src/registrar/migrations/0064_domain_notes_domainapplication_notes.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.7 on 2024-01-24 20:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0063_veryimportantperson"), + ] + + operations = [ + migrations.AddField( + model_name="domain", + name="notes", + field=models.TextField(blank=True, help_text="Notes about this domain", null=True), + ), + migrations.AddField( + model_name="domainapplication", + name="notes", + field=models.TextField(blank=True, help_text="Notes about this application", null=True), + ), + ] diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..f84e61a80 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,6 +992,12 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) + notes = models.TextField( + null=True, + blank=True, + help_text="Notes about this domain", + ) + def isActive(self): return self.state == Domain.State.CREATED diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 196449bfa..301d1b42b 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -556,6 +556,12 @@ class DomainApplication(TimeStampedModel): help_text="Date submitted", ) + notes = models.TextField( + null=True, + blank=True, + help_text="Notes about this application", + ) + def __str__(self): try: if self.requested_domain and self.requested_domain.name: diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index bdff6061b..fa3b78e1a 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -223,13 +223,20 @@ class DomainInformation(TimeStampedModel): if domain_info: return domain_info # the following information below is not needed in the domain information: - da_dict.pop("status", None) - da_dict.pop("current_websites", None) - da_dict.pop("investigator", None) - da_dict.pop("alternative_domains", None) - da_dict.pop("requested_domain", None) - da_dict.pop("approved_domain", None) - da_dict.pop("submission_date", None) + unused_one_to_one_fields = [ + "status", + "current_websites", + "investigator", + "alternative_domains", + "requested_domain", + "approved_domain", + "submission_date", + "other_contacts", + "notes", + ] + for field in unused_one_to_one_fields: + da_dict.pop(field, None) + other_contacts = da_dict.pop("other_contacts", []) domain_info = cls(**da_dict) domain_info.domain_application = domain_application From c345ab90ecfea086ded4341d80be4efb83fb8827 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:49:16 -0700 Subject: [PATCH 048/120] Linting --- src/registrar/admin.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d3f5b49f1..67a2b9ed2 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -737,18 +737,7 @@ class DomainApplicationAdmin(ListHeaderAdmin): # Detail view form = DomainApplicationAdminForm fieldsets = [ - ( - None, - { - "fields": [ - "status", - "investigator", - "creator", - "approved_domain", - "notes" - ] - } - ), + (None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}), ( "Type of organization", { @@ -1005,16 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - { - "fields": [ - "name", - "state", - "expiration_date", - "first_ready", - "deleted", - "notes" - ] - }, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "notes"]}, ), ) From 8578f22a096043d26f36e0349732c42ddb570375 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:24:02 -0700 Subject: [PATCH 049/120] create_from_da refactor --- src/registrar/models/domain_information.py | 54 +++++++++++++--------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index fa3b78e1a..8995cce30 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -215,30 +215,9 @@ class DomainInformation(TimeStampedModel): def create_from_da(cls, domain_application, domain=None): """Takes in a DomainApplication dict and converts it into DomainInformation""" da_dict = domain_application.to_dict() - # remove the id so one can be assinged on creation - da_id = da_dict.pop("id", None) - # check if we have a record that corresponds with the domain - # application, if so short circuit the create - domain_info = cls.objects.filter(domain_application__id=da_id).first() - if domain_info: - return domain_info - # the following information below is not needed in the domain information: - unused_one_to_one_fields = [ - "status", - "current_websites", - "investigator", - "alternative_domains", - "requested_domain", - "approved_domain", - "submission_date", - "other_contacts", - "notes", - ] - for field in unused_one_to_one_fields: - da_dict.pop(field, None) - other_contacts = da_dict.pop("other_contacts", []) - domain_info = cls(**da_dict) + + domain_info = cls._get_domain_info_from_da_dict(da_dict) domain_info.domain_application = domain_application # Save so the object now have PK # (needed to process the manytomany below before, first) @@ -251,5 +230,34 @@ class DomainInformation(TimeStampedModel): domain_info.save() return domain_info + @classmethod + def _get_domain_info_from_da_dict(cls, da_dict): + """Given a domain_application dict, generate a DomainInformation object. + Copy any existing fields, and purge any fields that don't exist + on the DomainInformation definition.""" + + # remove the id so one can be assigned on creation + da_id = da_dict.pop("id", None) + + # check if we have a record that corresponds with the domain + # application, if so short circuit the create + domain_info = cls.objects.filter(domain_application__id=da_id).first() + if domain_info: + return domain_info + + # Get a list of the existing fields on DomainApplication and DomainInformation + domain_app_fields = set(f.name for f in DomainApplication._meta.get_fields()) + domain_info_fields = set(f.name for f in DomainInformation._meta.get_fields()) + + # Get the fields that only exist on DomainApplication, but not DomainInformation + unused_one_to_one_fields = domain_app_fields - domain_info_fields + + # Remove unusable fields from the dictionary + for field in unused_one_to_one_fields: + da_dict.pop(field, None) + + domain_info = cls(**da_dict) + return domain_info + class Meta: verbose_name_plural = "Domain information" From c2c60cd53094815b94f55e0fb460fed97453ceb9 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:32:49 -0800 Subject: [PATCH 050/120] Restyle availability button --- src/registrar/templates/application_dotgov_domain.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 74c6dce06..15f5d2d33 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -82,13 +82,14 @@ Add another alternative -
    - - + >Check availability +
    +

    If you’re not sure this is the domain you want, that’s ok. You can change the domain later.

    From fd53cc9241c9de14b8a8eca0fce25156a6da8495 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 24 Jan 2024 13:44:56 -0800 Subject: [PATCH 051/120] Shorten tag name --- src/registrar/assets/js/get-gov.js | 4 ++-- src/registrar/templates/application_dotgov_domain.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 3c49f6098..16402b056 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -218,7 +218,7 @@ function handleValidationClick(e) { function handleFormsetValidationClick(e) { // Check availability for alternative domains - const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); + const alternativeDomainsAvailability = document.getElementById('check-avail-for-alt-domains'); // Collect input IDs from the repeatable forms let inputIds = Array.from(document.querySelectorAll('.repeatable-form input')).map(input => input.id); @@ -253,7 +253,7 @@ function handleFormsetValidationClick(e) { for(const input of needsValidation) { input.addEventListener('input', handleInputValidation); } - const alternativeDomainsAvailability = document.getElementById('check-availability-for-alternative-domains'); + const alternativeDomainsAvailability = document.getElementById('check-avail-for-alt-domains'); const activatesValidation = document.querySelectorAll('[validate-for]'); for(const button of activatesValidation) { diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index b6b708e39..3af94941a 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -84,7 +84,7 @@
    From e3fc6082027026c1a72bfc0b52133a024980ef21 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 24 Jan 2024 13:53:45 -0800 Subject: [PATCH 052/120] Remove unused classes and comments --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/models/domain.py | 13 +++++++------ .../templates/application_dotgov_domain.html | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 16402b056..4a7891fbd 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -210,7 +210,7 @@ function handleValidationClick(e) { const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; - const input = document.getElementById(attribute); // You might need to define 'attribute' + const input = document.getElementById(attribute); runValidators(input); } diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..6b5f0d19c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -173,12 +173,13 @@ class Domain(TimeStampedModel, DomainHelper): @classmethod def available(cls, domain: str) -> bool: - """Check if a domain is available.""" - if not cls.string_could_be_domain(domain): - raise ValueError("Not a valid domain: %s" % str(domain)) - domain_name = domain.lower() - req = commands.CheckDomain([domain_name]) - return registry.send(req, cleaned=True).res_data[0].avail + return True + # """Check if a domain is available.""" + # if not cls.string_could_be_domain(domain): + # raise ValueError("Not a valid domain: %s" % str(domain)) + # domain_name = domain.lower() + # req = commands.CheckDomain([domain_name]) + # return registry.send(req, cleaned=True).res_data[0].avail @classmethod def registered(cls, domain: str) -> bool: diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 3af94941a..67206b272 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -48,7 +48,6 @@ {% endwith %} {% endwith %}
    From f82ce53345287f8bfafcca8a3b918617ee79c813 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Wed, 24 Jan 2024 13:54:40 -0800 Subject: [PATCH 053/120] Add back in EPP availability --- src/registrar/models/domain.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 6b5f0d19c..1a581a4ec 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -173,13 +173,12 @@ class Domain(TimeStampedModel, DomainHelper): @classmethod def available(cls, domain: str) -> bool: - return True - # """Check if a domain is available.""" - # if not cls.string_could_be_domain(domain): - # raise ValueError("Not a valid domain: %s" % str(domain)) - # domain_name = domain.lower() - # req = commands.CheckDomain([domain_name]) - # return registry.send(req, cleaned=True).res_data[0].avail + """Check if a domain is available.""" + if not cls.string_could_be_domain(domain): + raise ValueError("Not a valid domain: %s" % str(domain)) + domain_name = domain.lower() + req = commands.CheckDomain([domain_name]) + return registry.send(req, cleaned=True).res_data[0].avail @classmethod def registered(cls, domain: str) -> bool: From 947b347513c4186485b0711cf0c426c1dcadbc9f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 14:59:22 -0700 Subject: [PATCH 054/120] Make create_from_da generic --- src/registrar/models/domain_information.py | 82 +++++++++++++--------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 8995cce30..68bb3a973 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -1,4 +1,5 @@ from __future__ import annotations +from django.db import transaction from .domain_application import DomainApplication from .utility.time_stamped_model import TimeStampedModel @@ -212,51 +213,62 @@ class DomainInformation(TimeStampedModel): return "" @classmethod - def create_from_da(cls, domain_application, domain=None): - """Takes in a DomainApplication dict and converts it into DomainInformation""" - da_dict = domain_application.to_dict() - other_contacts = da_dict.pop("other_contacts", []) + def create_from_da(cls, domain_application: DomainApplication, domain=None): + """Takes in a DomainApplication and converts it into DomainInformation""" - domain_info = cls._get_domain_info_from_da_dict(da_dict) - domain_info.domain_application = domain_application - # Save so the object now have PK - # (needed to process the manytomany below before, first) - domain_info.save() + # Throw an error if we get None - we can't create something from nothing + if domain_application is None: + raise ValueError("The provided DomainApplication is None") - # Process the remaining "many to many" stuff - domain_info.other_contacts.add(*other_contacts) - if domain: - domain_info.domain = domain - domain_info.save() - return domain_info - - @classmethod - def _get_domain_info_from_da_dict(cls, da_dict): - """Given a domain_application dict, generate a DomainInformation object. - Copy any existing fields, and purge any fields that don't exist - on the DomainInformation definition.""" - - # remove the id so one can be assigned on creation - da_id = da_dict.pop("id", None) + # Throw an error if the da doesn't have an id + if not hasattr(domain_application, "id"): + raise ValueError("The provided DomainApplication has no id") # check if we have a record that corresponds with the domain # application, if so short circuit the create - domain_info = cls.objects.filter(domain_application__id=da_id).first() - if domain_info: - return domain_info + existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first() + if existing_domain_info: + return existing_domain_info # Get a list of the existing fields on DomainApplication and DomainInformation - domain_app_fields = set(f.name for f in DomainApplication._meta.get_fields()) - domain_info_fields = set(f.name for f in DomainInformation._meta.get_fields()) + domain_app_fields = set(field.name for field in DomainApplication._meta.get_fields()) + domain_info_fields = set(field.name for field in DomainInformation._meta.get_fields()) - # Get the fields that only exist on DomainApplication, but not DomainInformation - unused_one_to_one_fields = domain_app_fields - domain_info_fields + # Get a list of all many_to_many relations on DomainInformation (needs to be saved differently) + info_many_to_many_fields = {field.name for field in DomainInformation._meta.many_to_many} - # Remove unusable fields from the dictionary - for field in unused_one_to_one_fields: - da_dict.pop(field, None) + # Get the fields that exist on both DomainApplication and DomainInformation + common_fields = domain_app_fields & domain_info_fields + + # Create a dictionary with only the common fields, and create a DomainInformation from it + da_dict = {} + da_many_to_many_dict = {} + for field in common_fields: + # If the field isn't many_to_many, populate the da_dict. + # If it is, populate da_many_to_many_dict as we need to save this later. + if hasattr(domain_application, field) and field not in info_many_to_many_fields: + da_dict[field] = getattr(domain_application, field) + elif hasattr(domain_application, field): + da_many_to_many_dict[field] = getattr(domain_application, field).all() + + domain_info = DomainInformation(**da_dict) + + # Add the domain_application and domain fields + domain_info.domain_application = domain_application + if domain: + domain_info.domain = domain + + # Create the object + domain_info.save() + + # Save the instance and set the many-to-many fields. + # Lumped under .atomic to ensure we don't make redundant DB calls. + # This bundles them all together, and then saves it in a single call. + with transaction.atomic(): + domain_info.save() + for field, value in da_many_to_many_dict.items(): + getattr(domain_info, field).set(value) - domain_info = cls(**da_dict) return domain_info class Meta: From 2a345cddb9a9e34a4b623ce24ef4a0f4bf72002e Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:15:56 -0700 Subject: [PATCH 055/120] Bug fix --- src/registrar/models/domain_information.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 68bb3a973..3531c6426 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -231,8 +231,8 @@ class DomainInformation(TimeStampedModel): return existing_domain_info # Get a list of the existing fields on DomainApplication and DomainInformation - domain_app_fields = set(field.name for field in DomainApplication._meta.get_fields()) - domain_info_fields = set(field.name for field in DomainInformation._meta.get_fields()) + domain_app_fields = set(field.name for field in DomainApplication._meta.get_fields() if field != "id") + domain_info_fields = set(field.name for field in DomainInformation._meta.get_fields() if field != "id") # Get a list of all many_to_many relations on DomainInformation (needs to be saved differently) info_many_to_many_fields = {field.name for field in DomainInformation._meta.many_to_many} @@ -251,6 +251,7 @@ class DomainInformation(TimeStampedModel): elif hasattr(domain_application, field): da_many_to_many_dict[field] = getattr(domain_application, field).all() + # Create a placeholder DomainInformation object domain_info = DomainInformation(**da_dict) # Add the domain_application and domain fields @@ -258,9 +259,6 @@ class DomainInformation(TimeStampedModel): if domain: domain_info.domain = domain - # Create the object - domain_info.save() - # Save the instance and set the many-to-many fields. # Lumped under .atomic to ensure we don't make redundant DB calls. # This bundles them all together, and then saves it in a single call. From 29621a6184bc5b5cd5cd7f24c6d42a0bb4605ab3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:38:08 -0700 Subject: [PATCH 056/120] Add notes to DomainInformation --- src/registrar/admin.py | 2 +- ...064_domain_notes_domainapplication_notes_and_more.py} | 9 +++++++-- src/registrar/models/domain_application.py | 4 ++-- src/registrar/models/domain_information.py | 6 ++++++ src/registrar/tests/test_admin.py | 1 + 5 files changed, 17 insertions(+), 5 deletions(-) rename src/registrar/migrations/{0064_domain_notes_domainapplication_notes.py => 0064_domain_notes_domainapplication_notes_and_more.py} (65%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 67a2b9ed2..dc2741c49 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -570,7 +570,7 @@ class DomainInformationAdmin(ListHeaderAdmin): search_help_text = "Search by domain." fieldsets = [ - (None, {"fields": ["creator", "domain_application"]}), + (None, {"fields": ["creator", "domain_application", "notes"]}), ( "Type of organization", { diff --git a/src/registrar/migrations/0064_domain_notes_domainapplication_notes.py b/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py similarity index 65% rename from src/registrar/migrations/0064_domain_notes_domainapplication_notes.py rename to src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py index 24dfaf4e3..0ec824233 100644 --- a/src/registrar/migrations/0064_domain_notes_domainapplication_notes.py +++ b/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-24 20:34 +# Generated by Django 4.2.7 on 2024-01-24 22:28 from django.db import migrations, models @@ -17,6 +17,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name="domainapplication", name="notes", - field=models.TextField(blank=True, help_text="Notes about this application", null=True), + field=models.TextField(blank=True, help_text="Notes about this request", null=True), + ), + migrations.AddField( + model_name="domaininformation", + name="notes", + field=models.TextField(blank=True, help_text="Notes about the request", null=True), ), ] diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py index 301d1b42b..dc2dc80c7 100644 --- a/src/registrar/models/domain_application.py +++ b/src/registrar/models/domain_application.py @@ -559,7 +559,7 @@ class DomainApplication(TimeStampedModel): notes = models.TextField( null=True, blank=True, - help_text="Notes about this application", + help_text="Notes about this request", ) def __str__(self): @@ -711,7 +711,7 @@ class DomainApplication(TimeStampedModel): # copy the information from domainapplication into domaininformation DomainInformation = apps.get_model("registrar.DomainInformation") - DomainInformation.create_from_da(self, domain=created_domain) + DomainInformation.create_from_da(domain_application=self, domain=created_domain) # create the permission for the user UserDomainRole = apps.get_model("registrar.UserDomainRole") diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 3531c6426..680a3c155 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -203,6 +203,12 @@ class DomainInformation(TimeStampedModel): help_text="Acknowledged .gov acceptable use policy", ) + notes = models.TextField( + null=True, + blank=True, + help_text="Notes about the request", + ) + def __str__(self): try: if self.domain and self.domain.name: diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 7c9aa8fe4..b02abf0d4 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -624,6 +624,7 @@ class TestDomainApplicationAdmin(MockEppLib): "anything_else", "is_policy_acknowledged", "submission_date", + "notes", "current_websites", "other_contacts", "alternative_domains", From a758bfe51261c75b9da7758295f44252bb9d8176 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:43:48 -0700 Subject: [PATCH 057/120] Change notes -> domain_notes for domain --- src/registrar/admin.py | 2 +- ...4_domain_domain_notes_domainapplication_notes_and_more.py} | 4 ++-- src/registrar/models/domain.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename src/registrar/migrations/{0064_domain_notes_domainapplication_notes_and_more.py => 0064_domain_domain_notes_domainapplication_notes_and_more.py} (90%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index dc2741c49..691481e94 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -994,7 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "notes"]}, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "domain_notes"]}, ), ) diff --git a/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py b/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py similarity index 90% rename from src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py rename to src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py index 0ec824233..92165f814 100644 --- a/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py +++ b/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-24 22:28 +# Generated by Django 4.2.7 on 2024-01-24 22:41 from django.db import migrations, models @@ -11,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="domain", - name="notes", + name="domain_notes", field=models.TextField(blank=True, help_text="Notes about this domain", null=True), ), migrations.AddField( diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f84e61a80..161f9dddd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,7 +992,7 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) - notes = models.TextField( + domain_notes = models.TextField( null=True, blank=True, help_text="Notes about this domain", From 6415c645ee897a3726ce2b291e6244414f3dd449 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 08:17:00 -0700 Subject: [PATCH 058/120] Linting, remove domain notes --- src/registrar/admin.py | 2 +- src/registrar/models/domain.py | 6 ------ src/registrar/models/domain_information.py | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 691481e94..51c208790 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -994,7 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "domain_notes"]}, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]}, ), ) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 161f9dddd..1a581a4ec 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,12 +992,6 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) - domain_notes = models.TextField( - null=True, - blank=True, - help_text="Notes about this domain", - ) - def isActive(self): return self.state == Domain.State.CREATED diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 680a3c155..d812db973 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -241,7 +241,7 @@ class DomainInformation(TimeStampedModel): domain_info_fields = set(field.name for field in DomainInformation._meta.get_fields() if field != "id") # Get a list of all many_to_many relations on DomainInformation (needs to be saved differently) - info_many_to_many_fields = {field.name for field in DomainInformation._meta.many_to_many} + info_many_to_many_fields = {field.name for field in DomainInformation._meta.many_to_many} # type: ignore # Get the fields that exist on both DomainApplication and DomainInformation common_fields = domain_app_fields & domain_info_fields From 5039aff9aac4c9913b869e6c702c248e4671b9cb Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 08:39:13 -0700 Subject: [PATCH 059/120] Add unit test --- src/registrar/tests/test_models.py | 32 ++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ef6522747..0dafd9618 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -548,9 +548,9 @@ class TestPermissions(TestCase): self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain)) -class TestDomainInfo(TestCase): +class TestDomainInformation(TestCase): - """Test creation of Domain Information when approved.""" + """Test the DomainInformation model, when approved or otherwise""" def setUp(self): super().setUp() @@ -559,12 +559,18 @@ class TestDomainInfo(TestCase): def tearDown(self): super().tearDown() self.mock_client.EMAILS_SENT.clear() + Domain.objects.all().delete() + DomainInformation.objects.all().delete() + DomainApplication.objects.all().delete() + User.objects.all().delete() + DraftDomain.objects.all().delete() @boto3_mocking.patching def test_approval_creates_info(self): + self.maxDiff = None draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov") user, _ = User.objects.get_or_create() - application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain) + application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain, notes="test notes") with boto3_mocking.clients.handler_for("sesv2", self.mock_client): with less_console_noise(): @@ -574,7 +580,25 @@ class TestDomainInfo(TestCase): # should be an information present for this domain domain = Domain.objects.get(name="igorville.gov") - self.assertTrue(DomainInformation.objects.get(domain=domain)) + domain_information = DomainInformation.objects.filter(domain=domain) + self.assertTrue(domain_information.exists()) + + # Test that both objects are what we expect + current_domain_information = domain_information.get().__dict__ + expected_domain_information = DomainInformation( + creator=user, + domain=domain, + notes="test notes", + domain_application=application, + ).__dict__ + + # Test the two records for consistency + self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information)) + + def clean_dict(self, dict_obj): + """Cleans dynamic fields in a dictionary""" + bad_fields = ["_state", "created_at", "id", "updated_at"] + return {k: v for k, v in dict_obj.items() if k not in bad_fields} class TestInvitations(TestCase): From 5c16a2b5e8d99a1c56ddf30f28e29d863e058af4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 08:57:35 -0700 Subject: [PATCH 060/120] Migrations --- ...064_domainapplication_notes_domaininformation_notes.py} | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) rename src/registrar/migrations/{0064_domain_domain_notes_domainapplication_notes_and_more.py => 0064_domainapplication_notes_domaininformation_notes.py} (70%) diff --git a/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py b/src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py similarity index 70% rename from src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py rename to src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py index 92165f814..35fe3f3e7 100644 --- a/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py +++ b/src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-24 22:41 +# Generated by Django 4.2.7 on 2024-01-25 15:57 from django.db import migrations, models @@ -9,11 +9,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name="domain", - name="domain_notes", - field=models.TextField(blank=True, help_text="Notes about this domain", null=True), - ), migrations.AddField( model_name="domainapplication", name="notes", From 76f3704de73dc9c421d51a2cc1679fc09260956a Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:14:58 -0700 Subject: [PATCH 061/120] Readd domain notes --- src/registrar/admin.py | 2 +- src/registrar/models/domain.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 51c208790..691481e94 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -994,7 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - {"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]}, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "domain_notes"]}, ), ) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..161f9dddd 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,6 +992,12 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) + domain_notes = models.TextField( + null=True, + blank=True, + help_text="Notes about this domain", + ) + def isActive(self): return self.state == Domain.State.CREATED From dc5e0293f63ff0be4aa2c33e399de32db5998925 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:18:47 -0700 Subject: [PATCH 062/120] Fix migrations (again) --- ...omain_domain_notes_domainapplication_notes_and_more.py} | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) rename src/registrar/migrations/{0064_domainapplication_notes_domaininformation_notes.py => 0064_domain_domain_notes_domainapplication_notes_and_more.py} (70%) diff --git a/src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py b/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py similarity index 70% rename from src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py rename to src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py index 35fe3f3e7..515f2eda4 100644 --- a/src/registrar/migrations/0064_domainapplication_notes_domaininformation_notes.py +++ b/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-25 15:57 +# Generated by Django 4.2.7 on 2024-01-25 16:18 from django.db import migrations, models @@ -9,6 +9,11 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name="domain", + name="domain_notes", + field=models.TextField(blank=True, help_text="Notes about this domain", null=True), + ), migrations.AddField( model_name="domainapplication", name="notes", From 974c5f5a9eb09a4bd4d71d2bc6d23fb85dfd865d Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 25 Jan 2024 09:40:14 -0800 Subject: [PATCH 063/120] Remove unused debounce --- src/registrar/assets/js/get-gov.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4a7891fbd..e8e5e57e5 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -154,9 +154,6 @@ function checkDomainAvailability(el) { fetchJSON(`available/?domain=${el.value}`, callback); } -/** Call the API to see if the domain is good. */ -// const checkDomainAvailability = debounce(_checkDomainAvailability); - /** Hides the toast message and clears the aira live region. */ function clearDomainAvailability(el) { el.classList.remove('usa-input--success'); From dac472cab209a9002eacaef51356bd754adbf039 Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 25 Jan 2024 09:46:56 -0800 Subject: [PATCH 064/120] Swap back to green error messaging instead of blue informative --- src/registrar/assets/js/get-gov.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index e8e5e57e5..69ba50cd4 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -138,9 +138,9 @@ function checkDomainAvailability(el) { // Determines if we ignore the field if it is just blank ignore_blank = el.classList.contains("blank-ok") if (el.validity.valid) { - el.classList.add('usa-input--info'); + el.classList.add('usa-input--success'); // use of `parentElement` due to .gov inputs being wrapped in www/.gov decoration - inlineToast(el.parentElement, el.id, INFORMATIVE, response.message); + inlineToast(el.parentElement, el.id, SUCCESS, response.message); } else if (ignore_blank && response.code == "required"){ // Visually remove the error error = "usa-input--error" From 694ae50e34990158bce63fec5e42f0b145d07bce Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 25 Jan 2024 11:58:38 -0800 Subject: [PATCH 065/120] Variable cleanup and function refactoring --- src/registrar/assets/js/get-gov.js | 19 +++++++++---------- src/registrar/models/domain.py | 13 +++++++------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 69ba50cd4..fba043888 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -212,23 +212,20 @@ function handleValidationClick(e) { } -function handleFormsetValidationClick(e) { - // Check availability for alternative domains - - const alternativeDomainsAvailability = document.getElementById('check-avail-for-alt-domains'); - +function handleFormsetValidationClick(e, availabilityButton) { + // Collect input IDs from the repeatable forms - let inputIds = Array.from(document.querySelectorAll('.repeatable-form input')).map(input => input.id); + let inputs = Array.from(document.querySelectorAll('.repeatable-form input')) // Run validators for each input - inputIds.forEach(inputId => { - const input = document.getElementById(inputId); + inputs.forEach(input => { runValidators(input); }); // Set the validate-for attribute on the button with the collected input IDs // Not needed for functionality but nice for accessibility - alternativeDomainsAvailability.setAttribute('validate-for', inputIds.join(', ')); + inputs = inputs.map(input => input.id).join(', '); + availabilityButton.setAttribute('validate-for', inputs); } // <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>> @@ -256,7 +253,9 @@ function handleFormsetValidationClick(e) { for(const button of activatesValidation) { // Adds multi-field validation for alternative domains if (button === alternativeDomainsAvailability) { - button.addEventListener('click', handleFormsetValidationClick); + button.addEventListener('click', (e) => { + handleFormsetValidationClick(e, alternativeDomainsAvailability) + }); } else { button.addEventListener('click', handleValidationClick); } diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 1a581a4ec..6b5f0d19c 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -173,12 +173,13 @@ class Domain(TimeStampedModel, DomainHelper): @classmethod def available(cls, domain: str) -> bool: - """Check if a domain is available.""" - if not cls.string_could_be_domain(domain): - raise ValueError("Not a valid domain: %s" % str(domain)) - domain_name = domain.lower() - req = commands.CheckDomain([domain_name]) - return registry.send(req, cleaned=True).res_data[0].avail + return True + # """Check if a domain is available.""" + # if not cls.string_could_be_domain(domain): + # raise ValueError("Not a valid domain: %s" % str(domain)) + # domain_name = domain.lower() + # req = commands.CheckDomain([domain_name]) + # return registry.send(req, cleaned=True).res_data[0].avail @classmethod def registered(cls, domain: str) -> bool: From 29af5d4ac67a0e960ca83f6342b3d868a97193ee Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 25 Jan 2024 12:43:40 -0800 Subject: [PATCH 066/120] Update spacing --- src/registrar/assets/js/get-gov.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index fba043888..af968b842 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -206,7 +206,6 @@ function handleInputValidation(e) { function handleValidationClick(e) { const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; - const input = document.getElementById(attribute); runValidators(input); } From c75c4bb7a8e02f64e69cf4937759f0665da4a766 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:45:45 -0700 Subject: [PATCH 067/120] Implement PR suggestions --- src/registrar/admin.py | 2 +- ...notes_domainapplication_notes_and_more.py} | 4 +-- src/registrar/models/domain.py | 2 +- src/registrar/models/domain_information.py | 24 ++++++++------ src/registrar/models/utility/domain_helper.py | 31 +++++++++++++++++-- 5 files changed, 47 insertions(+), 16 deletions(-) rename src/registrar/migrations/{0064_domain_domain_notes_domainapplication_notes_and_more.py => 0064_domain_notes_domainapplication_notes_and_more.py} (90%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 691481e94..dc2741c49 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -994,7 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "domain_notes"]}, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "notes"]}, ), ) diff --git a/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py b/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py similarity index 90% rename from src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py rename to src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py index 515f2eda4..e75054ddb 100644 --- a/src/registrar/migrations/0064_domain_domain_notes_domainapplication_notes_and_more.py +++ b/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-25 16:18 +# Generated by Django 4.2.7 on 2024-01-25 20:43 from django.db import migrations, models @@ -11,7 +11,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name="domain", - name="domain_notes", + name="notes", field=models.TextField(blank=True, help_text="Notes about this domain", null=True), ), migrations.AddField( diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 161f9dddd..f84e61a80 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,7 +992,7 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) - domain_notes = models.TextField( + notes = models.TextField( null=True, blank=True, help_text="Notes about this domain", diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index d812db973..6303b2036 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -1,5 +1,7 @@ from __future__ import annotations from django.db import transaction + +from registrar.models.utility.domain_helper import DomainHelper from .domain_application import DomainApplication from .utility.time_stamped_model import TimeStampedModel @@ -236,15 +238,11 @@ class DomainInformation(TimeStampedModel): if existing_domain_info: return existing_domain_info - # Get a list of the existing fields on DomainApplication and DomainInformation - domain_app_fields = set(field.name for field in DomainApplication._meta.get_fields() if field != "id") - domain_info_fields = set(field.name for field in DomainInformation._meta.get_fields() if field != "id") + # Get the fields that exist on both DomainApplication and DomainInformation + common_fields = DomainHelper.get_common_fields(DomainApplication, DomainInformation) # Get a list of all many_to_many relations on DomainInformation (needs to be saved differently) - info_many_to_many_fields = {field.name for field in DomainInformation._meta.many_to_many} # type: ignore - - # Get the fields that exist on both DomainApplication and DomainInformation - common_fields = domain_app_fields & domain_info_fields + info_many_to_many_fields = DomainInformation._get_many_to_many_fields() # Create a dictionary with only the common fields, and create a DomainInformation from it da_dict = {} @@ -253,9 +251,10 @@ class DomainInformation(TimeStampedModel): # If the field isn't many_to_many, populate the da_dict. # If it is, populate da_many_to_many_dict as we need to save this later. if hasattr(domain_application, field) and field not in info_many_to_many_fields: - da_dict[field] = getattr(domain_application, field) - elif hasattr(domain_application, field): - da_many_to_many_dict[field] = getattr(domain_application, field).all() + if field not in info_many_to_many_fields: + da_dict[field] = getattr(domain_application, field) + else: + da_many_to_many_dict[field] = getattr(domain_application, field).all() # Create a placeholder DomainInformation object domain_info = DomainInformation(**da_dict) @@ -275,5 +274,10 @@ class DomainInformation(TimeStampedModel): return domain_info + @staticmethod + 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/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index a808ef803..31a5af4d3 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -1,5 +1,6 @@ import re - +from typing import Type +from django.db import models from django import forms from django.http import JsonResponse @@ -29,7 +30,7 @@ class DomainHelper: @classmethod def validate(cls, domain: str, blank_ok=False) -> str: """Attempt to determine if a domain name could be requested.""" - + return domain # Split into pieces for the linter domain = cls._validate_domain_string(domain, blank_ok) @@ -158,3 +159,29 @@ class DomainHelper: """Get the top level domain. Example: `gsa.gov` -> `gov`.""" parts = domain.rsplit(".") return parts[-1] if len(parts) > 1 else "" + + @staticmethod + def get_common_fields(model_1: Type[models.Model], model_2: Type[models.Model]): + """ + Returns a set of field names that two Django models have in common, excluding the 'id' field. + + Args: + model_1 (Type[models.Model]): The first Django model class. + model_2 (Type[models.Model]): The second Django model class. + + Returns: + Set[str]: A set of field names that both models share. + + Example: + If model_1 has fields {"id", "name", "color"} and model_2 has fields {"id", "color"}, + the function will return {"color"}. + """ + + # Get a list of the existing fields on model_1 and model_2 + model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id") + model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id") + + # Get the fields that exist on both DomainApplication and DomainInformation + common_fields = model_1_fields & model_2_fields + + return common_fields From 7ec51e3b9aa1c9f0ae7b8b93b2e3e89b42e898d3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:46:17 -0700 Subject: [PATCH 068/120] Undo test change --- src/registrar/models/utility/domain_helper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/registrar/models/utility/domain_helper.py b/src/registrar/models/utility/domain_helper.py index 31a5af4d3..bdb5afdca 100644 --- a/src/registrar/models/utility/domain_helper.py +++ b/src/registrar/models/utility/domain_helper.py @@ -30,7 +30,6 @@ class DomainHelper: @classmethod def validate(cls, domain: str, blank_ok=False) -> str: """Attempt to determine if a domain name could be requested.""" - return domain # Split into pieces for the linter domain = cls._validate_domain_string(domain, blank_ok) From a2dece7e5bd063d78e352f8c3612c64bd3fac604 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 25 Jan 2024 13:48:55 -0700 Subject: [PATCH 069/120] Fix migrations after mege --- ... => 0065_domain_notes_domainapplication_notes_and_more.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/registrar/migrations/{0064_domain_notes_domainapplication_notes_and_more.py => 0065_domain_notes_domainapplication_notes_and_more.py} (85%) diff --git a/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py b/src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py similarity index 85% rename from src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py rename to src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py index e75054ddb..2fdd64bdf 100644 --- a/src/registrar/migrations/0064_domain_notes_domainapplication_notes_and_more.py +++ b/src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py @@ -1,11 +1,11 @@ -# Generated by Django 4.2.7 on 2024-01-25 20:43 +# Generated by Django 4.2.7 on 2024-01-25 20:48 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0063_veryimportantperson"), + ("registrar", "0064_alter_domainapplication_address_line1_and_more"), ] operations = [ From ff44b2c4d9eab07f59292ebd208312a31826de8e Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Thu, 25 Jan 2024 12:49:01 -0800 Subject: [PATCH 070/120] Add in EPP fix my bad --- src/registrar/models/domain.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 6b5f0d19c..1a581a4ec 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -173,13 +173,12 @@ class Domain(TimeStampedModel, DomainHelper): @classmethod def available(cls, domain: str) -> bool: - return True - # """Check if a domain is available.""" - # if not cls.string_could_be_domain(domain): - # raise ValueError("Not a valid domain: %s" % str(domain)) - # domain_name = domain.lower() - # req = commands.CheckDomain([domain_name]) - # return registry.send(req, cleaned=True).res_data[0].avail + """Check if a domain is available.""" + if not cls.string_could_be_domain(domain): + raise ValueError("Not a valid domain: %s" % str(domain)) + domain_name = domain.lower() + req = commands.CheckDomain([domain_name]) + return registry.send(req, cleaned=True).res_data[0].avail @classmethod def registered(cls, domain: str) -> bool: From 3d703b221769ecc34e6620442aaedc2802e629e2 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:00:48 -0700 Subject: [PATCH 071/120] Remove old code --- src/registrar/models/domain_information.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py index 6303b2036..65d099e5a 100644 --- a/src/registrar/models/domain_information.py +++ b/src/registrar/models/domain_information.py @@ -250,7 +250,7 @@ class DomainInformation(TimeStampedModel): for field in common_fields: # If the field isn't many_to_many, populate the da_dict. # If it is, populate da_many_to_many_dict as we need to save this later. - if hasattr(domain_application, field) and field not in info_many_to_many_fields: + if hasattr(domain_application, field): if field not in info_many_to_many_fields: da_dict[field] = getattr(domain_application, field) else: From d66899164d2f8660e71201e65c7f4003861a2b3b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:41:45 -0700 Subject: [PATCH 072/120] Improve speed of load_transition_domain script Slightly decrease the wait time for the load transitoin domain script for testing purposes --- .../utility/extra_transition_domain_helper.py | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index 755c9b98a..926dc4553 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -182,8 +182,6 @@ class LoadExtraTransitionDomain: # STEP 5: Parse creation and expiration data updated_transition_domain = self.parse_creation_expiration_data(domain_name, transition_domain) - # Check if the instance has changed before saving - updated_transition_domain.save() updated_transition_domains.append(updated_transition_domain) logger.info(f"{TerminalColors.OKCYAN}" f"Successfully updated {domain_name}" f"{TerminalColors.ENDC}") @@ -198,6 +196,28 @@ class LoadExtraTransitionDomain: f"{TerminalColors.ENDC}" ) failed_transition_domains.append(domain_name) + + updated_fields = [ + "organization_name", + "organization_type", + "federal_type", + "federal_agency", + "first_name", + "middle_name", + "last_name", + "email", + "phone", + "epp_creation_date", + "epp_expiration_date", + ] + + batch_size = 1000 + # Create a Paginator object. Bulk_update on the full dataset + # is too memory intensive for our current app config, so we can chunk this data instead. + paginator = Paginator(updated_transition_domains, batch_size) + for page_num in paginator.page_range: + page = paginator.page(page_num) + TransitionDomain.objects.bulk_update(page.object_list, updated_fields) failed_count = len(failed_transition_domains) if failed_count == 0: From 18c3c0d01e03c179248987ee5976fed143fc6247 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:53:52 -0700 Subject: [PATCH 073/120] Add back important --- src/registrar/assets/sass/_theme/_buttons.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index b168551b5..0576f8b47 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -47,8 +47,8 @@ a.usa-button.disabled-link:focus { a.usa-button--unstyled.disabled-link, a.usa-button--unstyled.disabled-link:hover, a.usa-button--unstyled.disabled-link:focus { - cursor: not-allowed; - outline: none; + cursor: not-allowed !important; + outline: none !important; } a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { From c60f5bb6bcab59ee242ab9fb6afbd19916d01e90 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 10:54:18 -0700 Subject: [PATCH 074/120] Undo important (my bad) --- src/registrar/assets/sass/_theme/_buttons.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 0576f8b47..b168551b5 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -47,8 +47,8 @@ a.usa-button.disabled-link:focus { a.usa-button--unstyled.disabled-link, a.usa-button--unstyled.disabled-link:hover, a.usa-button--unstyled.disabled-link:focus { - cursor: not-allowed !important; - outline: none !important; + cursor: not-allowed; + outline: none; } a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { From 30f6c93aaa2498d22b727335c8005b3c5d1e679c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 11:15:34 -0700 Subject: [PATCH 075/120] Update _buttons.scss --- src/registrar/assets/sass/_theme/_buttons.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index b168551b5..0576f8b47 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -47,8 +47,8 @@ a.usa-button.disabled-link:focus { a.usa-button--unstyled.disabled-link, a.usa-button--unstyled.disabled-link:hover, a.usa-button--unstyled.disabled-link:focus { - cursor: not-allowed; - outline: none; + cursor: not-allowed !important; + outline: none !important; } a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { From d9610ae12dac880997837cc2656495203b1bab00 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 12:13:09 -0700 Subject: [PATCH 076/120] UI changes --- src/registrar/assets/sass/_theme/_buttons.scss | 1 + src/registrar/templates/domain_users.html | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 0576f8b47..5148456e5 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -49,6 +49,7 @@ a.usa-button--unstyled.disabled-link:hover, a.usa-button--unstyled.disabled-link:focus { cursor: not-allowed !important; outline: none !important; + text-decoration: none !important; } a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index e22800677..b3d52bdd4 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -53,7 +53,7 @@ {# Display a custom message if the user is trying to delete themselves #} {% if permission.user.email == current_user_email %}
    {% with domain_name=domain.name|force_escape %} - {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager for "|add:domain_name|add:"?"|safe modal_description="You will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button_self|safe %} {% endwith %}
    {% else %}
    {% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %} - {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove <"|add:email|add:">?"|safe modal_description="<"|add:email|add:"> will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value="<"|add:email|add:">?" modal_description="<"|add:email|add:"> will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} {% endwith %}
    From bc7ba2f7521d8cceb481331795ab4db6050b5fe0 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 13:10:47 -0700 Subject: [PATCH 077/120] Remove notes --- src/registrar/admin.py | 2 +- ...065_domainapplication_notes_domaininformation_notes.py} | 7 +------ src/registrar/models/domain.py | 6 ------ 3 files changed, 2 insertions(+), 13 deletions(-) rename src/registrar/migrations/{0065_domain_notes_domainapplication_notes_and_more.py => 0065_domainapplication_notes_domaininformation_notes.py} (72%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index dc2741c49..51c208790 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -994,7 +994,7 @@ class DomainAdmin(ListHeaderAdmin): fieldsets = ( ( None, - {"fields": ["name", "state", "expiration_date", "first_ready", "deleted", "notes"]}, + {"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]}, ), ) diff --git a/src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py b/src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py similarity index 72% rename from src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py rename to src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py index 2fdd64bdf..71f1021d8 100644 --- a/src/registrar/migrations/0065_domain_notes_domainapplication_notes_and_more.py +++ b/src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.7 on 2024-01-25 20:48 +# Generated by Django 4.2.7 on 2024-01-26 20:09 from django.db import migrations, models @@ -9,11 +9,6 @@ class Migration(migrations.Migration): ] operations = [ - migrations.AddField( - model_name="domain", - name="notes", - field=models.TextField(blank=True, help_text="Notes about this domain", null=True), - ), migrations.AddField( model_name="domainapplication", name="notes", diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index f84e61a80..1a581a4ec 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -992,12 +992,6 @@ class Domain(TimeStampedModel, DomainHelper): help_text="The last time this domain moved into the READY state", ) - notes = models.TextField( - null=True, - blank=True, - help_text="Notes about this domain", - ) - def isActive(self): return self.state == Domain.State.CREATED From dcfe19dc7cc7ef8f455918c0b8f5329c304fe58d Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Fri, 26 Jan 2024 15:42:09 -0500 Subject: [PATCH 078/120] Add VIP table access to staff --- .../migrations/0065_create_groups_v06.py | 37 +++++++++++++++++++ src/registrar/models/user_group.py | 5 +++ src/registrar/tests/test_migrations.py | 3 ++ 3 files changed, 45 insertions(+) create mode 100644 src/registrar/migrations/0065_create_groups_v06.py diff --git a/src/registrar/migrations/0065_create_groups_v06.py b/src/registrar/migrations/0065_create_groups_v06.py new file mode 100644 index 000000000..d2cb32cee --- /dev/null +++ b/src/registrar/migrations/0065_create_groups_v06.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0064_alter_domainapplication_address_line1_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index a733239c7..2658ec52d 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -66,6 +66,11 @@ class UserGroup(Group): "model": "userdomainrole", "permissions": ["view_userdomainrole", "delete_userdomainrole"], }, + { + "app_label": "registrar", + "model": "veryimportantperson", + "permissions": ["add_veryimportantperson", "change_veryimportantperson", "delete_veryimportantperson"], + }, ] # Avoid error: You can't execute queries until the end diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index cc9d379e5..cf28aa81a 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -43,6 +43,9 @@ class TestGroups(TestCase): "change_user", "delete_userdomainrole", "view_userdomainrole", + "add_veryimportantperson", + "change_veryimportantperson", + "delete_veryimportantperson", "change_website", ] From 8b6d2ea2c76b5322d279df95845ff08493fa987f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Fri, 26 Jan 2024 15:02:52 -0700 Subject: [PATCH 079/120] Add back id --- src/registrar/assets/js/get-gov.js | 11 ++++------- .../templates/application_dotgov_domain.html | 2 +- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 4a1ce005f..375b9738f 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -245,13 +245,10 @@ function handleValidationClick(e) { const alternateDomainsInputs = document.querySelectorAll('[auto-validate]'); if (alternateDomainsInputs) { for (const domainInput of alternateDomainsInputs){ - // Only apply this logic to alternate domains input - if (domainInput.classList.contains('alternate-domain-input')){ - domainInput.addEventListener('input', function() { - removeFormErrors(domainInput, true); - } - ); - } + domainInput.addEventListener('input', function() { + removeFormErrors(domainInput, true); + } + ); } } })(); diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index bd3c4a473..223fa8179 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -79,7 +79,7 @@ {% endwith %} {% endwith %} - ' ) context["modal_button"] = modal_button # Create HTML for the modal button when deleting yourself - modal_button_self = self._create_modal_button_html( - button_name="delete_domain_manager_self", - button_text_content="Yes, remove myself", - classes=["usa-button", "usa-button--secondary"], + modal_button_self = ( + '' ) context["modal_button_self"] = modal_button_self return context - def _create_modal_button_html(self, button_name: str, button_text_content: str, classes: List[str] | str): - """Template for modal submit buttons""" - - if isinstance(classes, list): - class_list = " ".join(classes) - elif isinstance(classes, str): - class_list = classes - - html_class = f'class="{class_list}"' if class_list else None - modal_button = '' - return modal_button - class DomainAddUserView(DomainFormBaseView): """Inside of a domain's user management, a form for adding users. diff --git a/src/registrar/views/utility/mixins.py b/src/registrar/views/utility/mixins.py index 980c0dad5..b2c4cb364 100644 --- a/src/registrar/views/utility/mixins.py +++ b/src/registrar/views/utility/mixins.py @@ -286,7 +286,7 @@ class DomainApplicationPermission(PermissionsLoginMixin): return True -class UserDomainRolePermission(PermissionsLoginMixin): +class UserDeleteDomainRolePermission(PermissionsLoginMixin): """Permission mixin for UserDomainRole 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 a274db0d9..54c96d602 100644 --- a/src/registrar/views/utility/permission_views.py +++ b/src/registrar/views/utility/permission_views.py @@ -12,7 +12,7 @@ from .mixins import ( DomainApplicationPermissionWithdraw, DomainInvitationPermission, ApplicationWizardPermission, - UserDomainRolePermission, + UserDeleteDomainRolePermission, ) import logging @@ -134,21 +134,7 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV object: DomainApplication -class UserDomainRolePermissionView(UserDomainRolePermission, DetailView, abc.ABC): - - """Abstract base view for UserDomainRole that enforces permissions. - - This abstract view cannot be instantiated. Actual views must specify - `template_name`. - """ - - # DetailView property for what model this is viewing - model = UserDomainRole - # variable name in template context for the model object - context_object_name = "userdomainrole" - - -class UserDomainRolePermissionDeleteView(UserDomainRolePermissionView, DeleteView, abc.ABC): +class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC): """Abstract base view for deleting a UserDomainRole. From 619cadb953820ec03d0f5a43f1e2399dde8be08f Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 10:52:38 -0700 Subject: [PATCH 083/120] Grab sec emails from dict --- src/registrar/utility/csv_export.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 68dc126db..d87f97ad7 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -41,7 +41,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): +def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, skip_epp_call=True): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -50,11 +50,17 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): domain = domain_info.domain # type: ignore - cached_sec_email = domain.get_security_email(skip_epp_call) - security_email = cached_sec_email if cached_sec_email is not None else " " + # Grab the security email from a preset dictionary. + # If nothing exists in the dictionary, grab from get_security_email + if security_emails_dict is not None and domain.name in security_emails_dict: + _email = security_emails_dict.get(domain.name) + security_email = _email if _email is not None else " " + else: + cached_sec_email = domain.get_security_email(skip_epp_call) + security_email = cached_sec_email if cached_sec_email is not None else " " - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report + invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} if security_email.lower() in invalid_emails: security_email = "(blank)" @@ -98,6 +104,16 @@ def write_body( # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) + + # Populate a dictionary of domain names and their security contacts + security_emails_dict = {} + for domain_info in all_domain_infos: + if domain_info not in security_emails_dict: + domain: Domain = domain_info.domain + if domain is not None: + security_emails_dict[domain.name] = domain.security_contact_registry_id + else: + logger.warning("csv_export -> Duplicate domain object found") # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) @@ -106,7 +122,7 @@ def write_body( rows = [] for domain_info in page.object_list: try: - row = parse_row(columns, domain_info) + row = parse_row(columns, domain_info, security_emails_dict) rows.append(row) except ValueError: # This should not happen. If it does, just skip this row. From e420db52f22ad6006583e6a7dadc91e2ab4323f4 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:07:37 -0700 Subject: [PATCH 084/120] Revert "Grab sec emails from dict" This reverts commit 619cadb953820ec03d0f5a43f1e2399dde8be08f. --- src/registrar/utility/csv_export.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index d87f97ad7..68dc126db 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -41,7 +41,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, skip_epp_call=True): +def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -50,17 +50,11 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None domain = domain_info.domain # type: ignore - # Grab the security email from a preset dictionary. - # If nothing exists in the dictionary, grab from get_security_email - if security_emails_dict is not None and domain.name in security_emails_dict: - _email = security_emails_dict.get(domain.name) - security_email = _email if _email is not None else " " - else: - cached_sec_email = domain.get_security_email(skip_epp_call) - security_email = cached_sec_email if cached_sec_email is not None else " " + cached_sec_email = domain.get_security_email(skip_epp_call) + security_email = cached_sec_email if cached_sec_email is not None else " " - # These are default emails that should not be displayed in the csv report invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} + # These are default emails that should not be displayed in the csv report if security_email.lower() in invalid_emails: security_email = "(blank)" @@ -104,16 +98,6 @@ def write_body( # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) - - # Populate a dictionary of domain names and their security contacts - security_emails_dict = {} - for domain_info in all_domain_infos: - if domain_info not in security_emails_dict: - domain: Domain = domain_info.domain - if domain is not None: - security_emails_dict[domain.name] = domain.security_contact_registry_id - else: - logger.warning("csv_export -> Duplicate domain object found") # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) @@ -122,7 +106,7 @@ def write_body( rows = [] for domain_info in page.object_list: try: - row = parse_row(columns, domain_info, security_emails_dict) + row = parse_row(columns, domain_info) rows.append(row) except ValueError: # This should not happen. If it does, just skip this row. From 148ccbdf2a907270ddaa1933e0547b33312499c3 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:18:47 -0700 Subject: [PATCH 085/120] Revert "Revert "Grab sec emails from dict"" This reverts commit e420db52f22ad6006583e6a7dadc91e2ab4323f4. --- src/registrar/utility/csv_export.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 68dc126db..d87f97ad7 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -41,7 +41,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): +def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, skip_epp_call=True): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -50,11 +50,17 @@ def parse_row(columns, domain_info: DomainInformation, skip_epp_call=True): domain = domain_info.domain # type: ignore - cached_sec_email = domain.get_security_email(skip_epp_call) - security_email = cached_sec_email if cached_sec_email is not None else " " + # Grab the security email from a preset dictionary. + # If nothing exists in the dictionary, grab from get_security_email + if security_emails_dict is not None and domain.name in security_emails_dict: + _email = security_emails_dict.get(domain.name) + security_email = _email if _email is not None else " " + else: + cached_sec_email = domain.get_security_email(skip_epp_call) + security_email = cached_sec_email if cached_sec_email is not None else " " - invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} # These are default emails that should not be displayed in the csv report + invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} if security_email.lower() in invalid_emails: security_email = "(blank)" @@ -98,6 +104,16 @@ def write_body( # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) + + # Populate a dictionary of domain names and their security contacts + security_emails_dict = {} + for domain_info in all_domain_infos: + if domain_info not in security_emails_dict: + domain: Domain = domain_info.domain + if domain is not None: + security_emails_dict[domain.name] = domain.security_contact_registry_id + else: + logger.warning("csv_export -> Duplicate domain object found") # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) @@ -106,7 +122,7 @@ def write_body( rows = [] for domain_info in page.object_list: try: - row = parse_row(columns, domain_info) + row = parse_row(columns, domain_info, security_emails_dict) rows.append(row) except ValueError: # This should not happen. If it does, just skip this row. From 75e687f13b7feccd3c360ca183cb814ab1f69f95 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:38:56 -0700 Subject: [PATCH 086/120] Remove skip epp call, use dict instead --- src/registrar/models/domain.py | 22 +++++++------------- src/registrar/utility/csv_export.py | 32 ++++++++++++++++++----------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 0cbe8c071..27a8364bc 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -909,17 +909,11 @@ class Domain(TimeStampedModel, DomainHelper): """Time to renew. Not implemented.""" raise NotImplementedError() - def get_security_email(self, skip_epp_call=False): + def get_security_email(self): logger.info("get_security_email-> getting the contact") - # If specified, skip the epp call outright. - # Otherwise, proceed as normal. - if skip_epp_call: - logger.info("get_security_email-> skipping epp call") - security = PublicContact.ContactTypeChoices.SECURITY - security_contact = self.generic_contact_getter(security, skip_epp_call) - else: - security_contact = self.security_contact + security = PublicContact.ContactTypeChoices.SECURITY + security_contact = self.generic_contact_getter(security) # If we get a valid value for security_contact, pull its email # Otherwise, just return nothing @@ -1121,9 +1115,7 @@ class Domain(TimeStampedModel, DomainHelper): ) raise error - def generic_contact_getter( - self, contact_type_choice: PublicContact.ContactTypeChoices, skip_epp_call=False - ) -> PublicContact | None: + def generic_contact_getter(self, contact_type_choice: PublicContact.ContactTypeChoices) -> PublicContact | None: """Retrieves the desired PublicContact from the registry. This abstracts the caching and EPP retrieval for all contact items and thus may result in EPP calls being sent. @@ -1143,7 +1135,7 @@ class Domain(TimeStampedModel, DomainHelper): try: # Grab from cache - contacts = self._get_property(desired_property, skip_epp_call) + contacts = self._get_property(desired_property) except KeyError as error: # if contact type is security, attempt to retrieve registry id # for the security contact from domain.security_contact_registry_id @@ -1878,9 +1870,9 @@ class Domain(TimeStampedModel, DomainHelper): """Remove cache data when updates are made.""" self._cache = {} - def _get_property(self, property, skip_epp_call=False): + def _get_property(self, property): """Get some piece of info about a domain.""" - if property not in self._cache and not skip_epp_call: + if property not in self._cache: self._fetch_cache( fetch_hosts=(property == "hosts"), fetch_contacts=(property == "contacts"), diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index d87f97ad7..e2e31bc0f 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -8,6 +8,8 @@ from django.core.paginator import Paginator from django.db.models import F, Value, CharField from django.db.models.functions import Concat, Coalesce +from registrar.models.public_contact import PublicContact + logger = logging.getLogger(__name__) @@ -41,7 +43,7 @@ def get_domain_infos(filter_condition, sort_fields): return domain_infos_cleaned -def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None, skip_epp_call=True): +def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None): """Given a set of columns, generate a new row from cleaned column data""" # Domain should never be none when parsing this information @@ -51,13 +53,16 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None domain = domain_info.domain # type: ignore # Grab the security email from a preset dictionary. - # If nothing exists in the dictionary, grab from get_security_email + # If nothing exists in the dictionary, grab from .contacts. if security_emails_dict is not None and domain.name in security_emails_dict: _email = security_emails_dict.get(domain.name) security_email = _email if _email is not None else " " else: - cached_sec_email = domain.get_security_email(skip_epp_call) - security_email = cached_sec_email if cached_sec_email is not None else " " + # If the dictionary doesn't contain that data, lets filter for it manually. + # This is a last resort as this is a more expensive operation. + security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) + _email = security_contacts[0].email + security_email = _email if _email is not None else " " # These are default emails that should not be displayed in the csv report invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} @@ -104,16 +109,19 @@ def write_body( # Get the domainInfos all_domain_infos = get_domain_infos(filter_condition, sort_fields) - - # Populate a dictionary of domain names and their security contacts + + # Store all security emails to avoid epp calls or excessive filters + sec_contact_ids = list(all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)) security_emails_dict = {} - for domain_info in all_domain_infos: - if domain_info not in security_emails_dict: - domain: Domain = domain_info.domain - if domain is not None: - security_emails_dict[domain.name] = domain.security_contact_registry_id + public_contacts = PublicContact.objects.filter(registry_id__in=sec_contact_ids) + + # Populate a dictionary of domain names and their security contacts + for contact in public_contacts: + domain: Domain = domain_info.domain + if domain is not None and domain.name not in security_emails_dict: + security_emails_dict[domain.name] = contact.email else: - logger.warning("csv_export -> Duplicate domain object found") + logger.warning("csv_export -> Domain was none for PublicContact") # Reduce the memory overhead when performing the write operation paginator = Paginator(all_domain_infos, 1000) From acbef25ec18effcd71eb8787e306c6849917ff83 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 12:54:56 -0700 Subject: [PATCH 087/120] Bug fix --- src/registrar/utility/csv_export.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index e2e31bc0f..4d9bd6869 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -60,9 +60,10 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None else: # If the dictionary doesn't contain that data, lets filter for it manually. # This is a last resort as this is a more expensive operation. - security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) - _email = security_contacts[0].email + security_contacts = domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) + _email = security_contacts[0].email if security_contacts else None security_email = _email if _email is not None else " " + print("in else statement....") # These are default emails that should not be displayed in the csv report invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} @@ -117,7 +118,7 @@ def write_body( # Populate a dictionary of domain names and their security contacts for contact in public_contacts: - domain: Domain = domain_info.domain + domain: Domain = contact.domain if domain is not None and domain.name not in security_emails_dict: security_emails_dict[domain.name] = contact.email else: From d8d7f6381935ba7706e9de05a2c3da83f497ec40 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:00:41 -0700 Subject: [PATCH 088/120] Fix typo in unit test --- src/registrar/tests/test_views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ffe94199c..3bbcfcf01 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -2781,7 +2781,7 @@ class TestDomainManagers(TestDomainOverview): self.assertTrue(role_2_exists) # Make sure that the current user wasn't deleted for some reason - current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() + current_user_exists = UserDomainRole.objects.filter(user=dummy_user_1.id, domain=vip_domain.id).exists() self.assertTrue(current_user_exists) def test_domain_user_delete_denied_if_last_man_standing(self): @@ -2810,7 +2810,7 @@ class TestDomainManagers(TestDomainOverview): self.assertEqual(response.status_code, 403) # Make sure that the current user wasn't deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=vip_domain.id).exists() self.assertTrue(current_user_exists) def test_domain_user_delete_self_redirects_home(self): From 11542c9ca0353e7c7bef2e4df248f11970662c4b Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:18:22 -0700 Subject: [PATCH 089/120] Minor performance enhancement --- src/registrar/utility/csv_export.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 4d9bd6869..29fc2fc54 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -63,7 +63,6 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None security_contacts = domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY) _email = security_contacts[0].email if security_contacts else None security_email = _email if _email is not None else " " - print("in else statement....") # These are default emails that should not be displayed in the csv report invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"} @@ -112,9 +111,9 @@ def write_body( all_domain_infos = get_domain_infos(filter_condition, sort_fields) # Store all security emails to avoid epp calls or excessive filters - sec_contact_ids = list(all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)) + sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True) security_emails_dict = {} - public_contacts = PublicContact.objects.filter(registry_id__in=sec_contact_ids) + public_contacts = PublicContact.objects.only('email', 'domain__name').select_related("domain").filter(registry_id__in=sec_contact_ids) # Populate a dictionary of domain names and their security contacts for contact in public_contacts: From d2a8ab16c8804aa4718ae85fa690657138abd8db Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:21:44 -0700 Subject: [PATCH 090/120] Linting --- src/registrar/utility/csv_export.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py index 29fc2fc54..f9608f553 100644 --- a/src/registrar/utility/csv_export.py +++ b/src/registrar/utility/csv_export.py @@ -52,7 +52,7 @@ def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None domain = domain_info.domain # type: ignore - # Grab the security email from a preset dictionary. + # Grab the security email from a preset dictionary. # If nothing exists in the dictionary, grab from .contacts. if security_emails_dict is not None and domain.name in security_emails_dict: _email = security_emails_dict.get(domain.name) @@ -113,7 +113,11 @@ def write_body( # Store all security emails to avoid epp calls or excessive filters sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True) security_emails_dict = {} - public_contacts = PublicContact.objects.only('email', 'domain__name').select_related("domain").filter(registry_id__in=sec_contact_ids) + public_contacts = ( + PublicContact.objects.only("email", "domain__name") + .select_related("domain") + .filter(registry_id__in=sec_contact_ids) + ) # Populate a dictionary of domain names and their security contacts for contact in public_contacts: From 07bcff952273fcae9bd45c204e3a5c0b3465a005 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:22:35 -0700 Subject: [PATCH 091/120] Linting 2 --- .../utility/extra_transition_domain_helper.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/registrar/management/commands/utility/extra_transition_domain_helper.py b/src/registrar/management/commands/utility/extra_transition_domain_helper.py index 926dc4553..c082552eb 100644 --- a/src/registrar/management/commands/utility/extra_transition_domain_helper.py +++ b/src/registrar/management/commands/utility/extra_transition_domain_helper.py @@ -196,18 +196,18 @@ class LoadExtraTransitionDomain: f"{TerminalColors.ENDC}" ) failed_transition_domains.append(domain_name) - + updated_fields = [ - "organization_name", - "organization_type", - "federal_type", - "federal_agency", - "first_name", - "middle_name", - "last_name", - "email", - "phone", - "epp_creation_date", + "organization_name", + "organization_type", + "federal_type", + "federal_agency", + "first_name", + "middle_name", + "last_name", + "email", + "phone", + "epp_creation_date", "epp_expiration_date", ] From cca111bdec9ea0294d0072920fa58ec508125304 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 14:39:26 -0800 Subject: [PATCH 092/120] change very import person to verfied by staff --- src/registrar/admin.py | 2 +- ...mportantperson_verifiedbystaff_and_more.py | 20 ++++++++++ .../migrations/0067_create_groups_v07.py | 37 +++++++++++++++++++ src/registrar/models/__init__.py | 6 +-- src/registrar/models/user.py | 4 +- src/registrar/models/user_group.py | 4 +- ...portant_person.py => verified_by_staff.py} | 6 ++- src/registrar/tests/test_admin.py | 2 +- src/registrar/tests/test_models.py | 2 +- 9 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py create mode 100644 src/registrar/migrations/0067_create_groups_v07.py rename src/registrar/models/{very_important_person.py => verified_by_staff.py} (84%) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index 0e9849370..b8e08284f 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1338,4 +1338,4 @@ admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) -admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin) +admin.site.register(models.VerifiedByStaff, VeryImportantPersonAdmin) diff --git a/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py b/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py new file mode 100644 index 000000000..d167334a0 --- /dev/null +++ b/src/registrar/migrations/0066_rename_veryimportantperson_verifiedbystaff_and_more.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.7 on 2024-01-29 22:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0065_create_groups_v06"), + ] + + operations = [ + migrations.RenameModel( + old_name="VeryImportantPerson", + new_name="VerifiedByStaff", + ), + migrations.AlterModelOptions( + name="verifiedbystaff", + options={"verbose_name_plural": "Verified by staff"}, + ), + ] diff --git a/src/registrar/migrations/0067_create_groups_v07.py b/src/registrar/migrations/0067_create_groups_v07.py new file mode 100644 index 000000000..b6fbd3e5c --- /dev/null +++ b/src/registrar/migrations/0067_create_groups_v07.py @@ -0,0 +1,37 @@ +# This migration creates the create_full_access_group and create_cisa_analyst_group groups +# It is dependent on 0035 (which populates ContentType and Permissions) +# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS +# in the user_group model then: +# [NOT RECOMMENDED] +# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions +# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups +# step 3: fake run the latest migration in the migrations list +# [RECOMMENDED] +# Alternatively: +# step 1: duplicate the migration that loads data +# step 2: docker-compose exec app ./manage.py migrate + +from django.db import migrations +from registrar.models import UserGroup +from typing import Any + + +# For linting: RunPython expects a function reference, +# so let's give it one +def create_groups(apps, schema_editor) -> Any: + UserGroup.create_cisa_analyst_group(apps, schema_editor) + UserGroup.create_full_access_group(apps, schema_editor) + + +class Migration(migrations.Migration): + dependencies = [ + ("registrar", "0066_rename_veryimportperson_verifiedbystaff_and_more"), + ] + + operations = [ + migrations.RunPython( + create_groups, + reverse_code=migrations.RunPython.noop, + atomic=True, + ), + ] diff --git a/src/registrar/models/__init__.py b/src/registrar/models/__init__.py index 90cb2e286..d9ccd64cb 100644 --- a/src/registrar/models/__init__.py +++ b/src/registrar/models/__init__.py @@ -13,7 +13,7 @@ from .user import User from .user_group import UserGroup from .website import Website from .transition_domain import TransitionDomain -from .very_important_person import VeryImportantPerson +from .verified_by_staff import VerifiedByStaff __all__ = [ "Contact", @@ -30,7 +30,7 @@ __all__ = [ "UserGroup", "Website", "TransitionDomain", - "VeryImportantPerson", + "VerifiedByStaff", ] auditlog.register(Contact) @@ -47,4 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"]) auditlog.register(UserGroup, m2m_fields=["permissions"]) auditlog.register(Website) auditlog.register(TransitionDomain) -auditlog.register(VeryImportantPerson) +auditlog.register(VerifiedByStaff) diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index 269569bfe..bf904a044 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -7,7 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole from .domain_invitation import DomainInvitation from .transition_domain import TransitionDomain -from .very_important_person import VeryImportantPerson +from .verified_by_staff import VerifiedByStaff from .domain import Domain from phonenumber_field.modelfields import PhoneNumberField # type: ignore @@ -91,7 +91,7 @@ class User(AbstractUser): return False # New users flagged by Staff to bypass ial2 - if VeryImportantPerson.objects.filter(email=email).exists(): + if VerifiedByStaff.objects.filter(email=email).exists(): return False # A new incoming user who is being invited to be a domain manager (that is, diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py index 2658ec52d..a32406a05 100644 --- a/src/registrar/models/user_group.py +++ b/src/registrar/models/user_group.py @@ -68,8 +68,8 @@ class UserGroup(Group): }, { "app_label": "registrar", - "model": "veryimportantperson", - "permissions": ["add_veryimportantperson", "change_veryimportantperson", "delete_veryimportantperson"], + "model": "verifiedbystaff", + "permissions": ["add_verifiedbystaff", "change_verifiedbystaff", "delete_verifiedbystaff"], }, ] diff --git a/src/registrar/models/very_important_person.py b/src/registrar/models/verified_by_staff.py similarity index 84% rename from src/registrar/models/very_important_person.py rename to src/registrar/models/verified_by_staff.py index 9134cb893..5ebbc8598 100644 --- a/src/registrar/models/very_important_person.py +++ b/src/registrar/models/verified_by_staff.py @@ -3,7 +3,7 @@ from django.db import models from .utility.time_stamped_model import TimeStampedModel -class VeryImportantPerson(TimeStampedModel): +class VerifiedByStaff(TimeStampedModel): """emails that get added to this table will bypass ial2 on login.""" @@ -27,6 +27,8 @@ class VeryImportantPerson(TimeStampedModel): blank=False, help_text="Notes", ) - + + class Meta: + verbose_name_plural ="Verified by staff" def __str__(self): return self.email diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index ebf3dfed9..5ca7866f7 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -18,7 +18,7 @@ from registrar.admin import ( ) from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole -from registrar.models.very_important_person import VeryImportantPerson +from registrar.models.verified_by_staff import VeryImportantPerson from .common import ( MockSESClient, AuditedAdminMockData, diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index ef6522747..d84b241ec 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -16,7 +16,7 @@ from registrar.models import ( import boto3_mocking from registrar.models.transition_domain import TransitionDomain -from registrar.models.very_important_person import VeryImportantPerson # type: ignore +from registrar.models.verified_by_staff import VeryImportantPerson # type: ignore from .common import MockSESClient, less_console_noise, completed_application from django_fsm import TransitionNotAllowed From 7c63d9c21bfec343bc26d0091ddee3ed3f32c606 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 14:40:53 -0800 Subject: [PATCH 093/120] fixed typo --- src/registrar/migrations/0067_create_groups_v07.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/migrations/0067_create_groups_v07.py b/src/registrar/migrations/0067_create_groups_v07.py index b6fbd3e5c..85138d4af 100644 --- a/src/registrar/migrations/0067_create_groups_v07.py +++ b/src/registrar/migrations/0067_create_groups_v07.py @@ -25,7 +25,7 @@ def create_groups(apps, schema_editor) -> Any: class Migration(migrations.Migration): dependencies = [ - ("registrar", "0066_rename_veryimportperson_verifiedbystaff_and_more"), + ("registrar", "0066_rename_veryimportantperson_verifiedbystaff_and_more"), ] operations = [ From f85730af25398bf67b063bc717a0f834a42bd654 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 14:54:39 -0800 Subject: [PATCH 094/120] changed VIP to verified by staff --- src/registrar/admin.py | 4 ++-- src/registrar/tests/test_admin.py | 14 +++++++------- src/registrar/tests/test_migrations.py | 6 +++--- src/registrar/tests/test_models.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/registrar/admin.py b/src/registrar/admin.py index b8e08284f..325081575 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1295,7 +1295,7 @@ class DraftDomainAdmin(ListHeaderAdmin): search_help_text = "Search by draft domain name." -class VeryImportantPersonAdmin(ListHeaderAdmin): +class VerifiedByStaffAdmin(ListHeaderAdmin): list_display = ("email", "requestor", "truncated_notes", "created_at") search_fields = ["email"] search_help_text = "Search by email." @@ -1338,4 +1338,4 @@ admin.site.register(models.Website, WebsiteAdmin) admin.site.register(models.PublicContact, AuditedAdmin) admin.site.register(models.DomainApplication, DomainApplicationAdmin) admin.site.register(models.TransitionDomain, TransitionDomainAdmin) -admin.site.register(models.VerifiedByStaff, VeryImportantPersonAdmin) +admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 5ca7866f7..0a99c039a 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -14,11 +14,11 @@ from registrar.admin import ( ContactAdmin, DomainInformationAdmin, UserDomainRoleAdmin, - VeryImportantPersonAdmin, + VerifiedByStaff, ) from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole -from registrar.models.verified_by_staff import VeryImportantPerson +from registrar.models.verified_by_staff import VerifiedByStaffAdmin from .common import ( MockSESClient, AuditedAdminMockData, @@ -1820,7 +1820,7 @@ class ContactAdminTest(TestCase): User.objects.all().delete() -class VeryImportantPersonAdminTestCase(TestCase): +class VerifiedByStaffAdminTestCase(TestCase): def setUp(self): self.superuser = create_superuser() self.factory = RequestFactory() @@ -1829,13 +1829,13 @@ class VeryImportantPersonAdminTestCase(TestCase): self.client.force_login(self.superuser) # Create an instance of the admin class - admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None) + admin_instance = VerifiedByStaffAdmin(model=VerifiedB, admin_site=None) - # Create a VeryImportantPerson instance - vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes") + # Create a VerifiedByStaff instance + vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") # Create a request object - request = self.factory.post("/admin/yourapp/veryimportantperson/add/") + request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/") request.user = self.superuser # Call the save_model method diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index cf28aa81a..98c9c1271 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -43,9 +43,9 @@ class TestGroups(TestCase): "change_user", "delete_userdomainrole", "view_userdomainrole", - "add_veryimportantperson", - "change_veryimportantperson", - "delete_veryimportantperson", + "add_verfiedbystaff", + "change_verfiedbystaff", + "delete_verfiedbystaff", "change_website", ] diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py index d84b241ec..d0005cbd5 100644 --- a/src/registrar/tests/test_models.py +++ b/src/registrar/tests/test_models.py @@ -16,7 +16,7 @@ from registrar.models import ( import boto3_mocking from registrar.models.transition_domain import TransitionDomain -from registrar.models.verified_by_staff import VeryImportantPerson # type: ignore +from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore from .common import MockSESClient, less_console_noise, completed_application from django_fsm import TransitionNotAllowed @@ -656,7 +656,7 @@ class TestUser(TestCase): def test_identity_verification_with_very_important_person(self): """A Very Important Person should return False when tested with class method needs_identity_verification""" - VeryImportantPerson.objects.get_or_create(email=self.user.email) + VerifiedByStaff.objects.get_or_create(email=self.user.email) self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username)) def test_identity_verification_with_invited_user(self): From ad2d9e6f2d49f51fcbc677199e228103230ff717 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jan 2024 14:58:05 -0800 Subject: [PATCH 095/120] removeFormErrors on availability button click --- src/registrar/assets/js/get-gov.js | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index d92554baa..337086522 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -207,6 +207,7 @@ function handleValidationClick(e) { const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; const input = document.getElementById(attribute); + removeFormErrors(input, true); runValidators(input); } @@ -262,16 +263,16 @@ function handleFormsetValidationClick(e, availabilityButton) { // Add event listener to the "Check availability" button - const checkAvailabilityButton = document.getElementById('check-availability-button'); - if (checkAvailabilityButton) { - const targetId = checkAvailabilityButton.getAttribute('validate-for'); - const checkAvailabilityInput = document.getElementById(targetId); - checkAvailabilityButton.addEventListener('click', - function() { - removeFormErrors(checkAvailabilityInput, true); - } - ); - } + // const checkAvailabilityButton = document.getElementById('check-availability-button'); + // if (checkAvailabilityButton) { + // const targetId = checkAvailabilityButton.getAttribute('validate-for'); + // const checkAvailabilityInput = document.getElementById(targetId); + // checkAvailabilityButton.addEventListener('click', + // function() { + // removeFormErrors(checkAvailabilityInput, true); + // } + // ); + // } // Add event listener to the alternate domains input const alternateDomainsInputs = document.querySelectorAll('[auto-validate]'); From 35536327e37487be0b3bc2506359ebfa9078ba14 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 29 Jan 2024 16:06:18 -0700 Subject: [PATCH 096/120] Fixed color of domain growth report heading to match colors used in model links --- src/registrar/templates/admin/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/admin/index.html b/src/registrar/templates/admin/index.html index 04601ef32..020e258a5 100644 --- a/src/registrar/templates/admin/index.html +++ b/src/registrar/templates/admin/index.html @@ -5,7 +5,7 @@ {% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}

    Reports

    -

    Domain growth report

    +

    Domain growth report

    {% comment %} Inputs of type date suck for accessibility. From 8a91643cd08a903ca9692700845d1aebfaf3bbfd Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 15:08:43 -0800 Subject: [PATCH 097/120] linter --- src/registrar/models/verified_by_staff.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/registrar/models/verified_by_staff.py b/src/registrar/models/verified_by_staff.py index 5ebbc8598..4c9e76e9d 100644 --- a/src/registrar/models/verified_by_staff.py +++ b/src/registrar/models/verified_by_staff.py @@ -27,8 +27,9 @@ class VerifiedByStaff(TimeStampedModel): blank=False, help_text="Notes", ) - + class Meta: - verbose_name_plural ="Verified by staff" + verbose_name_plural = "Verified by staff" + def __str__(self): return self.email From 5e27ec571d07397180b0ce1850a92b8987ffe861 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:11:48 -0800 Subject: [PATCH 098/120] Remove unused function --- src/registrar/assets/js/get-gov.js | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 337086522..2c6b7adf2 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -261,19 +261,6 @@ function handleFormsetValidationClick(e, availabilityButton) { } } - - // Add event listener to the "Check availability" button - // const checkAvailabilityButton = document.getElementById('check-availability-button'); - // if (checkAvailabilityButton) { - // const targetId = checkAvailabilityButton.getAttribute('validate-for'); - // const checkAvailabilityInput = document.getElementById(targetId); - // checkAvailabilityButton.addEventListener('click', - // function() { - // removeFormErrors(checkAvailabilityInput, true); - // } - // ); - // } - // Add event listener to the alternate domains input const alternateDomainsInputs = document.querySelectorAll('[auto-validate]'); if (alternateDomainsInputs) { From 865d8e2175763a6387e4d64d3f208d8e33e402ed Mon Sep 17 00:00:00 2001 From: CocoByte Date: Mon, 29 Jan 2024 16:23:25 -0700 Subject: [PATCH 099/120] Moved styling to sass --- src/registrar/assets/sass/_theme/_admin.scss | 2 +- src/registrar/templates/admin/index.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 6d9ab550f..760c4f13a 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -143,7 +143,7 @@ h1, h2, h3, .module h3 { padding: 0; - color: var(--primary); + color: var(--link-fg); margin: units(2) 0 units(1) 0; } diff --git a/src/registrar/templates/admin/index.html b/src/registrar/templates/admin/index.html index 020e258a5..04601ef32 100644 --- a/src/registrar/templates/admin/index.html +++ b/src/registrar/templates/admin/index.html @@ -5,7 +5,7 @@ {% include "admin/app_list.html" with app_list=app_list show_changelinks=True %}

    Reports

    -

    Domain growth report

    +

    Domain growth report

    {% comment %} Inputs of type date suck for accessibility. From 284c3107226ba8a6b6aa15edb2b13c529e90df6e Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 15:25:58 -0800 Subject: [PATCH 100/120] added missing i in verified --- src/registrar/tests/test_migrations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py index 98c9c1271..773a885c1 100644 --- a/src/registrar/tests/test_migrations.py +++ b/src/registrar/tests/test_migrations.py @@ -43,9 +43,9 @@ class TestGroups(TestCase): "change_user", "delete_userdomainrole", "view_userdomainrole", - "add_verfiedbystaff", - "change_verfiedbystaff", - "delete_verfiedbystaff", + "add_verifiedbystaff", + "change_verifiedbystaff", + "delete_verifiedbystaff", "change_website", ] From 6746947c2eb08e945a30c1fcaa25492778f0b75f Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 15:27:42 -0800 Subject: [PATCH 101/120] fixed typo in test --- src/registrar/tests/test_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 0a99c039a..8c75855dc 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -14,11 +14,11 @@ from registrar.admin import ( ContactAdmin, DomainInformationAdmin, UserDomainRoleAdmin, - VerifiedByStaff, + VerifiedByStaffAdmin, ) from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website from registrar.models.user_domain_role import UserDomainRole -from registrar.models.verified_by_staff import VerifiedByStaffAdmin +from registrar.models.verified_by_staff import VerifiedByStaff from .common import ( MockSESClient, AuditedAdminMockData, From c850e1d0841cee37fd1a90bb353894266dd49657 Mon Sep 17 00:00:00 2001 From: Alysia Broddrick Date: Mon, 29 Jan 2024 15:32:55 -0800 Subject: [PATCH 102/120] removed accidental delete --- src/registrar/tests/test_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index 8c75855dc..f88e25c2f 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -1829,7 +1829,7 @@ class VerifiedByStaffAdminTestCase(TestCase): self.client.force_login(self.superuser) # Create an instance of the admin class - admin_instance = VerifiedByStaffAdmin(model=VerifiedB, admin_site=None) + admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None) # Create a VerifiedByStaff instance vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes") From d2f6988bd8678721680ef65aded5ccb3b15e1f94 Mon Sep 17 00:00:00 2001 From: Erin <121973038+erinysong@users.noreply.github.com> Date: Tue, 30 Jan 2024 10:46:45 -0800 Subject: [PATCH 103/120] Rename functions for clarity --- src/registrar/assets/js/get-gov.js | 22 +++++++++---------- .../templates/application_dotgov_domain.html | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 2c6b7adf2..53ab5e06b 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -203,7 +203,7 @@ function handleInputValidation(e) { } /** On button click, handles running any associated validators. */ -function handleValidationClick(e) { +function validateFieldInput(e) { const attribute = e.target.getAttribute("validate-for") || ""; if (!attribute.length) return; const input = document.getElementById(attribute); @@ -212,7 +212,7 @@ function handleValidationClick(e) { } -function handleFormsetValidationClick(e, availabilityButton) { +function validateFormsetInputs(e, availabilityButton) { // Collect input IDs from the repeatable forms let inputs = Array.from(document.querySelectorAll('.repeatable-form input')) @@ -247,26 +247,26 @@ function handleFormsetValidationClick(e, availabilityButton) { for(const input of needsValidation) { input.addEventListener('input', handleInputValidation); } - const alternativeDomainsAvailability = document.getElementById('check-avail-for-alt-domains'); + const alternativeDomainsAvailability = document.getElementById('validate-alt-domains-availability'); const activatesValidation = document.querySelectorAll('[validate-for]'); for(const button of activatesValidation) { // Adds multi-field validation for alternative domains if (button === alternativeDomainsAvailability) { button.addEventListener('click', (e) => { - handleFormsetValidationClick(e, alternativeDomainsAvailability) + validateFormsetInputs(e, alternativeDomainsAvailability) }); } else { - button.addEventListener('click', handleValidationClick); + button.addEventListener('click', validateField); } } - // Add event listener to the alternate domains input - const alternateDomainsInputs = document.querySelectorAll('[auto-validate]'); - if (alternateDomainsInputs) { - for (const domainInput of alternateDomainsInputs){ - domainInput.addEventListener('input', function() { - removeFormErrors(domainInput, true); + // Clear errors on auto-validated inputs when user reselects input + const autoValidateInputs = document.querySelectorAll('[auto-validate]'); + if (autoValidateInputs) { + for (const input of autoValidateInputs){ + input.addEventListener('input', function() { + removeFormErrors(input, true); } ); } diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index ac59d4629..f5b31fb15 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -84,7 +84,7 @@
    From f85dc682a3c3e30ac5efcdf059887193eed0c879 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Tue, 30 Jan 2024 13:16:38 -0700 Subject: [PATCH 107/120] Hotfix --- src/registrar/templates/domain_users.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index 349e4125a..544cb7249 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -75,7 +75,7 @@ >
    {% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %} - {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value="email|add:"?" modal_description=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} + {% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description=""|add:email|add:" will no longer be able to manage the domain "|add:domain_name|add:"."|safe modal_button=modal_button|safe %} {% endwith %}
    From 6f5a1cf5e9bbd057d4ed91ed8d3152716235618f Mon Sep 17 00:00:00 2001 From: Rebecca Hsieh Date: Tue, 30 Jan 2024 13:06:04 -0800 Subject: [PATCH 108/120] Address quick comments --- src/registrar/assets/js/get-gov.js | 2 +- src/registrar/templates/application_dotgov_domain.html | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 52f88bb1d..587b95305 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -219,8 +219,8 @@ function validateFormsetInputs(e, availabilityButton) { // Run validators for each input inputs.forEach(input => { - runValidators(input); removeFormErrors(input, true); + runValidators(input); }); // Set the validate-for attribute on the button with the collected input IDs diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index f5b31fb15..39f9935c2 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -48,7 +48,6 @@ {% endwith %} {% endwith %}
    {% endif %} {% else %} - - Remove - + data-tooltip="true"> {% endif %} From 161bd30e1f5e674ed7072a5323ad9cb5cbb0b80c Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Wed, 31 Jan 2024 08:34:05 -0700 Subject: [PATCH 114/120] Add style for disabled button --- src/registrar/assets/sass/_theme/_buttons.scss | 8 ++++++++ src/registrar/templates/domain_users.html | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss index 5148456e5..2f4121399 100644 --- a/src/registrar/assets/sass/_theme/_buttons.scss +++ b/src/registrar/assets/sass/_theme/_buttons.scss @@ -52,6 +52,14 @@ a.usa-button--unstyled.disabled-link:focus { text-decoration: none !important; } +.usa-button--unstyled.disabled-button, +.usa-button--unstyled.disabled-link:hover, +.usa-button--unstyled.disabled-link:focus { + cursor: not-allowed !important; + outline: none !important; + text-decoration: none !important; +} + a.usa-button:not(.usa-button--unstyled, .usa-button--outline) { color: color('white'); } diff --git a/src/registrar/templates/domain_users.html b/src/registrar/templates/domain_users.html index beed4a462..e295d2f7e 100644 --- a/src/registrar/templates/domain_users.html +++ b/src/registrar/templates/domain_users.html @@ -84,12 +84,14 @@ {% else %} + role="button" + > {% endif %} From 9dffa049b8001d8e9af46780b921ce2f60004808 Mon Sep 17 00:00:00 2001 From: Rachid Mrad Date: Wed, 31 Jan 2024 12:06:18 -0500 Subject: [PATCH 115/120] move the start and end date assignemnets inside check for elements --- src/registrar/assets/js/get-gov-admin.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index cdbbc83ee..866c7bd7d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -283,19 +283,20 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk, (function (){ // Get the current date in the format YYYY-MM-DD - var currentDate = new Date().toISOString().split('T')[0]; + let currentDate = new Date().toISOString().split('T')[0]; // Default the value of the start date input field to the current date let startDateInput =document.getElementById('start'); - startDateInput.value = currentDate; - + // Default the value of the end date input field to the current date let endDateInput =document.getElementById('end'); - endDateInput.value = currentDate; let exportGrowthReportButton = document.getElementById('exportLink'); if (exportGrowthReportButton) { + startDateInput.value = currentDate; + endDateInput.value = currentDate; + exportGrowthReportButton.addEventListener('click', function() { // Get the selected start and end dates let startDate = startDateInput.value; From c51ddcfff89426d2b1825e1de93e402effde4d81 Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 31 Jan 2024 16:16:20 -0700 Subject: [PATCH 116/120] Hide State verbage for federal applicants --- src/registrar/templates/application_dotgov_domain.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index 39f9935c2..da0442f60 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -11,7 +11,8 @@

    -

    Names that uniquely apply to your organization are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your state’s two-letter abbreviation.

    +

    Names that uniquely apply to your organization are likely to be approved over names that could also apply to other organizations. + {% if is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}

    Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.

    From a6d9cefa598411df456720b774c4d64f4480997a Mon Sep 17 00:00:00 2001 From: CocoByte Date: Wed, 31 Jan 2024 16:38:50 -0700 Subject: [PATCH 117/120] FIxed logic to be the right way around --- src/registrar/templates/application_dotgov_domain.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registrar/templates/application_dotgov_domain.html b/src/registrar/templates/application_dotgov_domain.html index da0442f60..155fdbeb4 100644 --- a/src/registrar/templates/application_dotgov_domain.html +++ b/src/registrar/templates/application_dotgov_domain.html @@ -12,7 +12,7 @@

    Names that uniquely apply to your organization are likely to be approved over names that could also apply to other organizations. - {% if is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}

    + {% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}

    Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.

    From e5bbd3e523800c2f7d06e6febc101184c76f8f32 Mon Sep 17 00:00:00 2001 From: zandercymatics <141044360+zandercymatics@users.noreply.github.com> Date: Thu, 1 Feb 2024 10:36:11 -0700 Subject: [PATCH 118/120] Fix merge stuff --- ... => 0068_domainapplication_notes_domaininformation_notes.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/registrar/migrations/{0065_domainapplication_notes_domaininformation_notes.py => 0068_domainapplication_notes_domaininformation_notes.py} (88%) diff --git a/src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py b/src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py similarity index 88% rename from src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py rename to src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py index 71f1021d8..ea94be77e 100644 --- a/src/registrar/migrations/0065_domainapplication_notes_domaininformation_notes.py +++ b/src/registrar/migrations/0068_domainapplication_notes_domaininformation_notes.py @@ -5,7 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ("registrar", "0064_alter_domainapplication_address_line1_and_more"), + ("registrar", "0067_create_groups_v07"), ] operations = [ From f46a91f5e4adfa587018ed8ae7004ebf1a23e17f Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 2 Feb 2024 13:59:23 -0500 Subject: [PATCH 119/120] reorganized test_views into three files --- src/registrar/tests/test_views.py | 3740 ----------------- src/registrar/tests/test_views_application.py | 2199 ++++++++++ src/registrar/tests/test_views_domain.py | 1611 +++++++ 3 files changed, 3810 insertions(+), 3740 deletions(-) create mode 100644 src/registrar/tests/test_views_application.py create mode 100644 src/registrar/tests/test_views_domain.py diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index dcb6ba9e0..ac0ec0cdb 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -333,3743 +333,3 @@ class LoggedInTests(TestWithUser): self.assertEqual(response.status_code, 403) -class DomainApplicationTests(TestWithUser, WebTest): - - """Webtests for domain application to test filling and submitting.""" - - # Doesn't work with CSRF checking - # hypothesis is that CSRF_USE_SESSIONS is incompatible with WebTest - csrf_checks = False - - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.TITLES = ApplicationWizard.TITLES - - def test_application_form_intro_acknowledgement(self): - """Tests that user is presented with intro acknowledgement page""" - intro_page = self.app.get(reverse("application:")) - self.assertContains(intro_page, "You’re about to start your .gov domain request") - - def test_application_form_intro_is_skipped_when_edit_access(self): - """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" - completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Edit" link - detail_page = home_page.click("Edit", index=0) - # Check that the response is a redirect - self.assertEqual(detail_page.status_code, 302) - # You can access the 'Location' header to get the redirect URL - redirect_url = detail_page.url - self.assertEqual(redirect_url, "/request/organization_type/") - - def test_application_form_empty_submit(self): - """Tests empty submit on the first page after the acknowledgement page""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # submitting should get back the same page if the required field is empty - result = type_page.forms[0].submit() - self.assertIn("What kind of U.S.-based government organization do you represent?", result) - - def test_application_multiple_applications_exist(self): - """Test that an info message appears when user has multiple applications already""" - # create and submit an application - application = completed_application(user=self.user) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - application.submit() - application.save() - - # now, attempt to create another one - with less_console_noise(): - intro_page = self.app.get(reverse("application:")) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - self.assertContains(type_page, "You cannot submit this request yet") - - @boto3_mocking.patching - def test_application_form_submission(self): - """ - Can fill out the entire form and submit. - As we add additional form pages, we need to include them here to make - this test work. - - This test also looks for the long organization name on the summary page. - - This also tests for the presence of a modal trigger and the dynamic test - in the modal header on the submit page. - """ - num_pages_tested = 0 - # elections, type_of_work, tribal_government - SKIPPED_PAGES = 3 - num_pages = len(self.TITLES) - SKIPPED_PAGES - - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - # test next button and validate data - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # should see results in db - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.organization_type, "federal") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_federal/") - num_pages_tested += 1 - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.federal_type, "executive") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(federal_result.status_code, 302) - self.assertEqual(federal_result["Location"], "/request/organization_contact/") - num_pages_tested += 1 - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - 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) - org_contact_result = org_contact_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.organization_name, "Testorg") - self.assertEqual(application.address_line1, "address 1") - self.assertEqual(application.address_line2, "address 2") - self.assertEqual(application.city, "NYC") - self.assertEqual(application.state_territory, "NY") - self.assertEqual(application.zipcode, "10002") - self.assertEqual(application.urbanization, "URB Royal Oaks") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(org_contact_result.status_code, 302) - self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") - num_pages_tested += 1 - - # ---- 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" - - # 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 - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.authorizing_official.first_name, "Testy ATO") - self.assertEqual(application.authorizing_official.last_name, "Tester ATO") - self.assertEqual(application.authorizing_official.title, "Chief Tester") - self.assertEqual(application.authorizing_official.email, "testy@town.com") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(ao_result.status_code, 302) - self.assertEqual(ao_result["Location"], "/request/current_sites/") - 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" - - # 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 - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.current_websites.filter(website="http://www.city.com").count(), - 1, - ) - # the post request should return a redirect to the next form in - # the application - self.assertEqual(current_sites_result.status_code, 302) - self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") - num_pages_tested += 1 - - # ---- 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" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_result = dotgov_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.requested_domain.name, "city.gov") - self.assertEqual(application.alternative_domains.filter(website="city1.gov").count(), 1) - # the post request should return a redirect to the next form in - # the application - 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." - - # 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 - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.purpose, "For all kinds of things.") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(purpose_result.status_code, 302) - self.assertEqual(purpose_result["Location"], "/request/your_contact/") - 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] - - 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) - your_contact_result = your_contact_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.submitter.first_name, "Testy you") - self.assertEqual(application.submitter.last_name, "Tester you") - self.assertEqual(application.submitter.title, "Admin Tester") - self.assertEqual(application.submitter.email, "testy-admin@town.com") - self.assertEqual(application.submitter.phone, "(201) 555 5556") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(your_contact_result.status_code, 302) - self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") - 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() - - # 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] - - other_contacts_form["other_contacts-has_other_contacts"] = "True" - - 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 - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.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 application - self.assertEqual(other_contacts_result.status_code, 302) - self.assertEqual(other_contacts_result["Location"], "/request/anything_else/") - num_pages_tested += 1 - - # ---- ANYTHING ELSE PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_page = other_contacts_result.follow() - anything_else_form = anything_else_page.forms[0] - - anything_else_form["anything_else-anything_else"] = "Nothing else." - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - anything_else_result = anything_else_form.submit() - # validate that data from this step are being saved - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.anything_else, "Nothing else.") - # the post request should return a redirect to the next form in - # the application - self.assertEqual(anything_else_result.status_code, 302) - self.assertEqual(anything_else_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 = anything_else_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 - application = DomainApplication.objects.get() # there's only one - self.assertEqual(application.is_policy_acknowledged, True) - # the post request should return a redirect to the next form in - # the application - 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] - - # Review page contains all the previously entered data - # Let's make sure the long org name is displayed - self.assertContains(review_page, "Federal") - self.assertContains(review_page, "Executive") - self.assertContains(review_page, "Testorg") - self.assertContains(review_page, "address 1") - self.assertContains(review_page, "address 2") - self.assertContains(review_page, "NYC") - self.assertContains(review_page, "NY") - self.assertContains(review_page, "10002") - self.assertContains(review_page, "URB Royal Oaks") - self.assertContains(review_page, "Testy ATO") - self.assertContains(review_page, "Tester ATO") - self.assertContains(review_page, "Chief Tester") - self.assertContains(review_page, "testy@town.com") - self.assertContains(review_page, "city.com") - self.assertContains(review_page, "city.gov") - self.assertContains(review_page, "city1.gov") - self.assertContains(review_page, "For all kinds of things.") - self.assertContains(review_page, "Testy you") - self.assertContains(review_page, "Tester you") - self.assertContains(review_page, "Admin Tester") - self.assertContains(review_page, "testy-admin@town.com") - self.assertContains(review_page, "(201) 555-5556") - self.assertContains(review_page, "Testy2") - self.assertContains(review_page, "Tester2") - self.assertContains(review_page, "Another Tester") - self.assertContains(review_page, "testy2@town.com") - self.assertContains(review_page, "(201) 555-5557") - self.assertContains(review_page, "Nothing else.") - - # We can't test the modal itself as it relies on JS for init and triggering, - # but we can test for the existence of its trigger: - self.assertContains(review_page, "toggle-submit-domain-request") - # 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 > application_form > modal - self.assertContains(review_page, "You are about to submit a domain request for city.gov") - - # 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!") - - # check that any new pages are added to this test - self.assertEqual(num_pages, num_pages_tested) - - # This is the start of a test to check an existing application, it currently - # does not work and results in errors as noted in: - # https://github.com/cisagov/getgov/pull/728 - @skip("WIP") - def test_application_form_started_allsteps(self): - num_pages_tested = 0 - # elections, type_of_work, tribal_government - SKIPPED_PAGES = 3 - DASHBOARD_PAGE = 1 - num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE - - application = completed_application(user=self.user) - application.save() - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - self.assertContains(home_page, "Started") - num_pages_tested += 1 - - # TODO: For some reason this click results in a new application being generated - # This appraoch is an alternatie to using get as is being done below - # - # type_page = home_page.click("Edit") - - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - url = reverse("edit-application", kwargs={"id": application.pk}) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # TODO: The following line results in a django error on middleware - response = self.client.get(url, follow=True) - self.assertContains(response, "Type of organization") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # TODO: Step through the remaining pages - - self.assertEqual(num_pages, num_pages_tested) - - def test_application_form_conditional_federal(self): - """Federal branch question is shown for federal organizations.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - - # the conditional step titles shouldn't appear initially - self.assertNotContains(type_page, self.TITLES["organization_federal"]) - self.assertNotContains(type_page, self.TITLES["organization_election"]) - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # set the session ID before .submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # the post request should return a redirect to the federal branch - # question - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_federal/") - - # and the step label should appear in the sidebar of the resulting page - # but the step label for the elections page should not appear - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - self.assertContains(federal_page, self.TITLES["organization_federal"]) - self.assertNotContains(federal_page, self.TITLES["organization_election"]) - - # continuing on in the flow we need to see top-level agency on the - # contact page - federal_page.forms[0]["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_page.forms[0].submit() - # the post request should return a redirect to the contact - # question - self.assertEqual(federal_result.status_code, 302) - self.assertEqual(federal_result["Location"], "/request/organization_contact/") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = federal_result.follow() - self.assertContains(contact_page, "Federal agency") - - def test_application_form_conditional_elections(self): - """Election question is shown for other organizations.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - - # the conditional step titles shouldn't appear initially - self.assertNotContains(type_page, self.TITLES["organization_federal"]) - self.assertNotContains(type_page, self.TITLES["organization_election"]) - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "county" - - # set the session ID before .submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # the post request should return a redirect to the elections question - self.assertEqual(type_result.status_code, 302) - self.assertEqual(type_result["Location"], "/request/organization_election/") - - # and the step label should appear in the sidebar of the resulting page - # but the step label for the elections page should not appear - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - self.assertContains(election_page, self.TITLES["organization_election"]) - self.assertNotContains(election_page, self.TITLES["organization_federal"]) - - # continuing on in the flow we need to NOT see top-level agency on the - # contact page - election_page.forms[0]["organization_election-is_election_board"] = "True" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_result = election_page.forms[0].submit() - # the post request should return a redirect to the contact - # question - self.assertEqual(election_result.status_code, 302) - self.assertEqual(election_result["Location"], "/request/organization_contact/") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = election_result.follow() - self.assertNotContains(contact_page, "Federal agency") - - def test_application_form_section_skipping(self): - """Can skip forward and back in sections""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - - # Now on federal type page, click back to the organization type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0) - - # Should be a link to the organization_federal page - self.assertGreater( - len(new_page.html.find_all("a", href="/request/organization_federal/")), - 0, - ) - - def test_application_form_nonfederal(self): - """Non-federal organizations don't have to provide their federal agency.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - org_contact_form = contact_page.forms[0] - - self.assertNotIn("federal_agency", org_contact_form.fields) - - # minimal fields that must be filled out - org_contact_form["organization_contact-organization_name"] = "Testorg" - org_contact_form["organization_contact-address_line1"] = "address 1" - org_contact_form["organization_contact-city"] = "NYC" - org_contact_form["organization_contact-state_territory"] = "NY" - org_contact_form["organization_contact-zipcode"] = "10002" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_result = org_contact_form.submit() - - # the post request should return a redirect to the - # about your organization page if it was successful. - self.assertEqual(contact_result.status_code, 302) - self.assertEqual(contact_result["Location"], "/request/about_your_organization/") - - def test_application_about_your_organization_special(self): - """Special districts have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_page.forms[0].submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - - self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - - def test_yes_no_form_inits_blank_for_new_application(self): - """On the Other Contacts page, the yes/no form gets initialized with nothing selected for - new applications""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) - - def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): - """On the Other Contacts page, the yes/no form gets initialized with YES selected if the - application has other contacts""" - # Application has other contacts by default - application = completed_application(user=self.user) - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): - """On the Other Contacts page, the yes/no form gets initialized with NO selected if the - application has no other contacts""" - # Application has other contacts by default - application = completed_application(user=self.user, has_other_contacts=False) - application.no_other_contacts_rationale = "Hello!" - application.save() - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - - def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): - """When a user submits the Other Contacts form with other contacts selected, the application's - no other contacts rationale gets deleted""" - # Application has other contacts by default - application = completed_application(user=self.user, has_other_contacts=False) - application.no_other_contacts_rationale = "Hello!" - application.save() - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") - - other_contacts_form["other_contacts-has_other_contacts"] = "True" - - other_contacts_form["other_contacts-0-first_name"] = "Testy" - other_contacts_form["other_contacts-0-middle_name"] = "" - other_contacts_form["other_contacts-0-last_name"] = "McTesterson" - other_contacts_form["other_contacts-0-title"] = "Lord" - other_contacts_form["other_contacts-0-email"] = "testy@abc.org" - other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 1, - ) - - self.assertEquals( - application.no_other_contacts_rationale, - None, - ) - - def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): - """When a user submits the Other Contacts form with no other contacts selected, the application's - other contacts get deleted for other contacts that exist and are not joined to other objects - """ - # Application has other contacts by default - application = completed_application(user=self.user) - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - other_contacts_form["other_contacts-has_other_contacts"] = "False" - - other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 0, - ) - - self.assertEquals( - application.no_other_contacts_rationale, - "Hello again!", - ) - - def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): - """When a user submits the Other Contacts form with no other contacts selected, the application's - other contacts references get removed for other contacts that exist and are joined to other objects""" - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(555) 555 5555", - ) - 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", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # Now let's join the other contact to another object - domain_info = DomainInformation.objects.create(creator=self.user) - domain_info.other_contacts.set([other]) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") - - other_contacts_form["other_contacts-has_other_contacts"] = "False" - - other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" - - # Submit the now empty form - other_contacts_form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application - application = DomainApplication.objects.get() - self.assertEqual( - application.other_contacts.count(), - 0, - ) - - # Verify that the 'other' contact object still exists - domain_info = DomainInformation.objects.get() - self.assertEqual( - domain_info.other_contacts.count(), - 1, - ) - self.assertEqual( - domain_info.other_contacts.all()[0].first_name, - "Testy2", - ) - - self.assertEquals( - application.no_other_contacts_rationale, - "Hello again!", - ) - - def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): - """Applicants with no other contacts have to give a reason.""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - other_contacts_form["other_contacts-has_other_contacts"] = "False" - response = other_contacts_page.forms[0].submit() - - # The textarea for no other contacts returns this error message - # Assert that it is returned, ie the no other contacts form is required - self.assertContains(response, "Rationale for no other employees is required.") - - # The first name field for other contacts returns this error message - # Assert that it is not returned, ie the contacts form is not required - self.assertNotContains(response, "Enter the first name / given name of this contact.") - - def test_if_yes_no_form_is_yes_then_other_contacts_required(self): - """Applicants with other contacts do not have to give a reason.""" - other_contacts_page = self.app.get(reverse("application:other_contacts")) - other_contacts_form = other_contacts_page.forms[0] - other_contacts_form["other_contacts-has_other_contacts"] = "True" - response = other_contacts_page.forms[0].submit() - - # The textarea for no other contacts returns this error message - # Assert that it is not returned, ie the no other contacts form is not required - self.assertNotContains(response, "Rationale for no other employees is required.") - - # The first name field for other contacts returns this error message - # Assert that it is returned, ie the contacts form is required - self.assertContains(response, "Enter the first name / given name of this contact.") - - def test_delete_other_contact(self): - """Other contacts can be deleted after being saved to database. - - This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application, - loading the form and marking one contact up for deletion.""" - # Populate the database with a domain application that - # has 2 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - other2, _ = Contact.objects.get_or_create( - first_name="Testy3", - last_name="Tester3", - title="Another Tester", - email="testy3@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - application.other_contacts.add(other2) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded with both other contacts - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3") - - # Mark the first dude for deletion - other_contacts_form.set("other_contacts-0-DELETE", "on") - - # Submit the form - other_contacts_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the first dude was deleted - application = DomainApplication.objects.get() - self.assertEqual(application.other_contacts.count(), 1) - self.assertEqual(application.other_contacts.first().first_name, "Testy3") - - def test_delete_other_contact_does_not_allow_zero_contacts(self): - """Delete Other Contact does not allow submission with zero contacts.""" - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # Mark the first dude for deletion - other_contacts_form.set("other_contacts-0-DELETE", "on") - - # Submit the form - other_contacts_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - # Verify that the contact was not deleted - application = DomainApplication.objects.get() - self.assertEqual(application.other_contacts.count(), 1) - self.assertEqual(application.other_contacts.first().first_name, "Testy2") - - def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self): - """When you: - 1. add an empty contact, - 2. delete existing contacts, - 3. then submit, - The forms on page reload shows all the required fields and their errors.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch so we can reuse the other contact - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # Set total forms to 2 indicating an additional formset was added. - # Submit no data though for the second formset. - # Set the first formset to be deleted. - other_contacts_form["other_contacts-TOTAL_FORMS"] = "2" - other_contacts_form.set("other_contacts-0-DELETE", "on") - - response = other_contacts_form.submit() - - # Assert that the response presents errors to the user, including to - # Enter the first name ... - self.assertContains(response, "Enter the first name / given name of this contact.") - - def test_edit_other_contact_in_place(self): - """When you: - 1. edit an existing contact which is not joined to another model, - 2. then submit, - The application is linked to the existing contact, and the existing contact updated.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - other, _ = Contact.objects.get_or_create( - first_name="Testy2", - last_name="Tester2", - title="Another Tester", - email="testy2@town.com", - phone="(201) 555 5557", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(other) - - # other_contact_pk is the initial pk of the other contact. set it before update - # to be able to verify after update that the same contact object is in place - other_contact_pk = other.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") - - # update the first name of the contact - other_contacts_form["other_contacts-0-first_name"] = "Testy3" - - # Submit the updated form - other_contacts_form.submit() - - application.refresh_from_db() - - # assert that the Other Contact is updated "in place" - other_contact = application.other_contacts.all()[0] - self.assertEquals(other_contact_pk, other_contact.id) - self.assertEquals("Testy3", other_contact.first_name) - - def test_edit_other_contact_creates_new(self): - """When you: - 1. edit an existing contact which IS joined to another model, - 2. then submit, - The application is linked to a new contact, and the new contact is updated.""" - - # Populate the database with a domain application that - # has 1 "other contact" assigned to it, the other contact is also - # the authorizing official initially - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - you, _ = Contact.objects.get_or_create( - first_name="Testy you", - last_name="Tester you", - title="Admin Tester", - email="testy-admin@town.com", - phone="(201) 555 5556", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - submitter=you, - creator=self.user, - status="started", - ) - application.other_contacts.add(ao) - - # other_contact_pk is the initial pk of the other contact. set it before update - # to be able to verify after update that the ao contact is still in place - # and not updated, and that the new contact has a new id - other_contact_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - other_contacts_page = self.app.get(reverse("application:other_contacts")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - other_contacts_form = other_contacts_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy") - - # update the first name of the contact - other_contacts_form["other_contacts-0-first_name"] = "Testy2" - - # Submit the updated form - other_contacts_form.submit() - - application.refresh_from_db() - - # assert that other contact info is updated, and that a new Contact - # is created for the other contact - other_contact = application.other_contacts.all()[0] - self.assertNotEquals(other_contact_pk, other_contact.id) - self.assertEquals("Testy2", other_contact.first_name) - # assert that the authorizing official is not updated - authorizing_official = application.authorizing_official - self.assertEquals("Testy", authorizing_official.first_name) - - def test_edit_authorizing_official_in_place(self): - """When you: - 1. edit an authorizing official which is not joined to another model, - 2. then submit, - The application is linked to the existing ao, and the ao updated.""" - - # Populate the database with a domain application that - # has an authorizing_official (ao) - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - creator=self.user, - status="started", - ) - - # ao_pk is the initial pk of the Authorizing Official. set it before update - # to be able to verify after update that the same Contact object is in place - ao_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - ao_page = self.app.get(reverse("application:authorizing_official")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_form = ao_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") - - # update the first name of the contact - ao_form["authorizing_official-first_name"] = "Testy2" - - # Submit the updated form - ao_form.submit() - - application.refresh_from_db() - - # assert AO is updated "in place" - updated_ao = application.authorizing_official - self.assertEquals(ao_pk, updated_ao.id) - self.assertEquals("Testy2", updated_ao.first_name) - - def test_edit_authorizing_official_creates_new(self): - """When you: - 1. edit an existing authorizing official which IS joined to another model, - 2. then submit, - The application is linked to a new Contact, and the new Contact is updated.""" - - # Populate the database with a domain application that - # has authorizing official assigned to it, the authorizing offical is also - # an other contact initially - # We'll do it from scratch - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - creator=self.user, - status="started", - ) - application.other_contacts.add(ao) - - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the other contact is still in place - # and not updated, and that the new ao has a new id - ao_pk = ao.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - ao_page = self.app.get(reverse("application:authorizing_official")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - ao_form = ao_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") - - # update the first name of the contact - ao_form["authorizing_official-first_name"] = "Testy2" - - # Submit the updated form - ao_form.submit() - - application.refresh_from_db() - - # assert that the other contact is not updated - other_contacts = application.other_contacts.all() - other_contact = other_contacts[0] - self.assertEquals(ao_pk, other_contact.id) - self.assertEquals("Testy", other_contact.first_name) - # assert that the authorizing official is updated - authorizing_official = application.authorizing_official - self.assertEquals("Testy2", authorizing_official.first_name) - - def test_edit_submitter_in_place(self): - """When you: - 1. edit a submitter (your contact) which is not joined to another model, - 2. then submit, - The application is linked to the existing submitter, and the submitter updated.""" - - # Populate the database with a domain application that - # has a submitter - # We'll do it from scratch - you, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - submitter=you, - creator=self.user, - status="started", - ) - - # submitter_pk is the initial pk of the submitter. set it before update - # to be able to verify after update that the same contact object is in place - submitter_pk = you.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - your_contact_page = self.app.get(reverse("application:your_contact")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_form = your_contact_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") - - # update the first name of the contact - your_contact_form["your_contact-first_name"] = "Testy2" - - # Submit the updated form - your_contact_form.submit() - - application.refresh_from_db() - - updated_submitter = application.submitter - self.assertEquals(submitter_pk, updated_submitter.id) - self.assertEquals("Testy2", updated_submitter.first_name) - - def test_edit_submitter_creates_new(self): - """When you: - 1. edit an existing your contact which IS joined to another model, - 2. then submit, - The application is linked to a new Contact, and the new Contact is updated.""" - - # Populate the database with a domain application that - # has submitter assigned to it, the submitter is also - # an other contact initially - # We'll do it from scratch - submitter, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(201) 555 5555", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - submitter=submitter, - creator=self.user, - status="started", - ) - application.other_contacts.add(submitter) - - # submitter_pk is the initial pk of the your contact. set it before update - # to be able to verify after update that the other contact is still in place - # and not updated, and that the new submitter has a new id - submitter_pk = submitter.id - - # prime the form by visiting /edit - self.app.get(reverse("edit-application", kwargs={"id": application.pk})) - # 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) - - your_contact_page = self.app.get(reverse("application:your_contact")) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - your_contact_form = your_contact_page.forms[0] - - # Minimal check to ensure the form is loaded - self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") - - # update the first name of the contact - your_contact_form["your_contact-first_name"] = "Testy2" - - # Submit the updated form - your_contact_form.submit() - - application.refresh_from_db() - - # assert that the other contact is not updated - other_contacts = application.other_contacts.all() - other_contact = other_contacts[0] - self.assertEquals(submitter_pk, other_contact.id) - self.assertEquals("Testy", other_contact.first_name) - # assert that the submitter is updated - submitter = application.submitter - self.assertEquals("Testy2", submitter.first_name) - - def test_application_about_your_organiztion_interstate(self): - """Special districts have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - contact_page = type_result.follow() - - self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) - - def test_application_tribal_government(self): - """Tribal organizations have to answer an additional question.""" - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = DomainApplication.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 - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - tribal_government_page = type_result.follow() - - # and the step is on the sidebar list. - self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) - - def test_application_ao_dynamic_text(self): - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - 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" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - - # ---- AO CONTACT PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - self.assertContains(ao_page, "Executive branch federal agencies") - - # Go back to organization type page and change type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page.click(str(self.TITLES["organization_type"]), index=0) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" - type_result = type_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - - # Go back to AO page and test the dynamic text changed - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0) - self.assertContains(ao_page, "Domain requests from cities") - - def test_application_dotgov_domain_dynamic_text(self): - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - # ---- TYPE PAGE ---- - type_form = type_page.forms[0] - type_form["organization_type-organization_type"] = "federal" - - # test next button - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_result = type_form.submit() - - # ---- FEDERAL BRANCH PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_page = type_result.follow() - federal_form = federal_page.forms[0] - federal_form["organization_federal-federal_type"] = "executive" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - federal_result = federal_form.submit() - - # ---- ORG CONTACT PAGE ---- - # Follow the redirect to the next form page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_page = federal_result.follow() - org_contact_form = org_contact_page.forms[0] - # federal agency so we have to fill in federal_agency - org_contact_form["organization_contact-federal_agency"] = "General Services Administration" - 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" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - org_contact_result = org_contact_form.submit() - - # ---- AO CONTACT PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_page = org_contact_result.follow() - - # ---- 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" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_result = ao_form.submit() - - # ---- 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" - - # test saving the page - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit() - - # ---- DOTGOV DOMAIN PAGE ---- - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = current_sites_result.follow() - - self.assertContains(dotgov_page, "medicare.gov") - - # Go back to organization type page and change type - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page.click(str(self.TITLES["organization_type"]), index=0) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_form["organization_type-organization_type"] = "city" - type_result = type_form.submit() - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - election_page = type_result.follow() - - # Go back to dotgov domain page to test the dynamic text changed - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0) - self.assertContains(dotgov_page, "CityofEudoraKS.gov") - self.assertNotContains(dotgov_page, "medicare.gov") - - def test_application_formsets(self): - """Users are able to add more than one of some fields.""" - current_sites_page = self.app.get(reverse("application:current_sites")) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - # fill in the form field - current_sites_form = current_sites_page.forms[0] - self.assertIn("current_sites-0-website", current_sites_form.fields) - self.assertNotIn("current_sites-1-website", current_sites_form.fields) - current_sites_form["current_sites-0-website"] = "https://example.com" - - # click "Add another" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_result = current_sites_form.submit("submit_button", value="save") - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - current_sites_form = current_sites_result.follow().forms[0] - - # verify that there are two form fields - value = current_sites_form["current_sites-0-website"].value - self.assertEqual(value, "https://example.com") - self.assertIn("current_sites-1-website", current_sites_form.fields) - # and it is correctly referenced in the ManyToOne relationship - application = DomainApplication.objects.get() # there's only one - self.assertEqual( - application.current_websites.filter(website="https://example.com").count(), - 1, - ) - - @skip("WIP") - def test_application_edit_restore(self): - """ - Test that a previously saved application is available at the /edit endpoint. - """ - ao, _ = Contact.objects.get_or_create( - first_name="Testy", - last_name="Tester", - title="Chief Tester", - email="testy@town.com", - phone="(555) 555 5555", - ) - domain, _ = Domain.objects.get_or_create(name="city.gov") - alt, _ = Website.objects.get_or_create(website="city1.gov") - current, _ = Website.objects.get_or_create(website="city.com") - 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", - ) - application, _ = DomainApplication.objects.get_or_create( - organization_type="federal", - federal_type="executive", - purpose="Purpose of the site", - anything_else="No", - is_policy_acknowledged=True, - organization_name="Testorg", - address_line1="address 1", - state_territory="NY", - zipcode="10002", - authorizing_official=ao, - requested_domain=domain, - submitter=you, - creator=self.user, - ) - application.other_contacts.add(other) - application.current_websites.add(current) - application.alternative_domains.add(alt) - - # prime the form by visiting /edit - url = reverse("edit-application", kwargs={"id": application.pk}) - response = self.client.get(url) - - # TODO: this is a sketch of each page in the wizard which needs to be tested - # Django does not have tools sufficient for real end to end integration testing - # (for example, USWDS moves radio buttons off screen and replaces them with - # CSS styled "fakes" -- Django cannot determine if those are visually correct) - # -- the best that can/should be done here is to ensure the correct values - # are being passed to the templating engine - - url = reverse("application:organization_type") - response = self.client.get(url, follow=True) - self.assertContains(response, "") - # choices = response.context['wizard']['form']['organization_type'].subwidgets - # radio = [ x for x in choices if x.data["value"] == "federal" ][0] - # checked = radio.data["selected"] - # self.assertTrue(checked) - - # url = reverse("application:organization_federal") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:organization_contact") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:authorizing_official") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:current_sites") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:dotgov_domain") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:purpose") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:your_contact") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:other_contacts") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:other_contacts") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:security_email") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:anything_else") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - # url = reverse("application:requirements") - # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # page = self.app.get(url) - # self.assertNotContains(page, "VALUE") - - def test_long_org_name_in_application(self): - """ - Make sure the long name is displaying in the application form, - org step - """ - intro_page = self.app.get(reverse("application:")) - # 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] - - intro_form = intro_page.forms[0] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - intro_result = intro_form.submit() - - # follow first redirect - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - type_page = intro_result.follow() - - self.assertContains(type_page, "Federal: an agency of the U.S. government") - - def test_submit_modal_no_domain_text_fallback(self): - """When user clicks on submit your domain request and the requested domain - is null (possible through url direct access to the review page), present - fallback copy in the modal's header. - - NOTE: This may be a moot point if we implement a more solid pattern in the - future, like not a submit action at all on the review page.""" - - review_page = self.app.get(reverse("application:review")) - self.assertContains(review_page, "toggle-submit-domain-request") - self.assertContains(review_page, "You are about to submit an incomplete request") - - -class TestWithDomainPermissions(TestWithUser): - def setUp(self): - super().setUp() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov") - self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") - self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") - self.domain_on_hold, _ = Domain.objects.get_or_create( - name="on-hold.gov", - state=Domain.State.ON_HOLD, - expiration_date=timezone.make_aware( - datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) - ), - ) - self.domain_deleted, _ = Domain.objects.get_or_create( - name="deleted.gov", - state=Domain.State.DELETED, - expiration_date=timezone.make_aware( - datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) - ), - ) - - self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") - self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") - # We could simply use domain (igorville) but this will be more readable in tests - # that inherit this setUp - self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) - DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) - - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_multdsdata, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_dnssec_none, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_with_ip, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, - domain=self.domain_just_nameserver, - role=UserDomainRole.Roles.MANAGER, - ) - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER - ) - UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER - ) - - def tearDown(self): - try: - UserDomainRole.objects.all().delete() - if hasattr(self.domain, "contacts"): - self.domain.contacts.all().delete() - DomainApplication.objects.all().delete() - DomainInformation.objects.all().delete() - PublicContact.objects.all().delete() - HostIP.objects.all().delete() - Host.objects.all().delete() - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - except ValueError: # pass if already deleted - pass - super().tearDown() - - -class TestDomainPermissions(TestWithDomainPermissions): - def test_not_logged_in(self): - """Not logged in gets a redirect to Login.""" - for view_name in [ - "domain", - "domain-users", - "domain-users-add", - "domain-dns-nameservers", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - with self.subTest(view_name=view_name): - response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 302) - - def test_no_domain_role(self): - """Logged in but no role gets 403 Forbidden.""" - self.client.force_login(self.user) - self.role.delete() # user no longer has a role on this domain - - for view_name in [ - "domain", - "domain-users", - "domain-users-add", - "domain-dns-nameservers", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - with self.subTest(view_name=view_name): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - def test_domain_pages_blocked_for_on_hold_and_deleted(self): - """Test that the domain pages are blocked for on hold and deleted domains""" - - self.client.force_login(self.user) - for view_name in [ - "domain-users", - "domain-users-add", - "domain-dns", - "domain-dns-nameservers", - "domain-dns-dnssec", - "domain-dns-dnssec-dsdata", - "domain-org-name-address", - "domain-authorizing-official", - "domain-your-contact-information", - "domain-security-email", - ]: - for domain in [ - self.domain_on_hold, - self.domain_deleted, - ]: - with self.subTest(view_name=view_name, domain=domain): - with less_console_noise(): - response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) - self.assertEqual(response.status_code, 403) - - -class TestDomainOverview(TestWithDomainPermissions, WebTest): - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.client.force_login(self.user) - - -class TestDomainDetail(TestDomainOverview): - @skip("Assertion broke for no reason, why? Need to fix") - def test_domain_detail_link_works(self): - home_page = self.app.get("/") - logger.info(f"This is the value of home_page: {home_page}") - self.assertContains(home_page, "igorville.gov") - # click the "Edit" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "igorville.gov") - self.assertContains(detail_page, "Status") - - def test_unknown_domain_does_not_show_as_expired_on_homepage(self): - """An UNKNOWN domain does not show as expired on the homepage. - It shows as 'DNS needed'""" - # At the time of this test's writing, there are 6 UNKNOWN domains inherited - # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - home_page = self.app.get("/") - self.assertNotContains(home_page, "igorville.gov") - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - self.assertNotContains(home_page, "Expired") - self.assertContains(home_page, "DNS needed") - - def test_unknown_domain_does_not_show_as_expired_on_detail_page(self): - """An UNKNOWN domain does not show as expired on the detail page. - It shows as 'DNS needed'""" - # At the time of this test's writing, there are 6 UNKNOWN domains inherited - # from constructors. Let's reset. - Domain.objects.all().delete() - UserDomainRole.objects.all().delete() - - self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - self.role, _ = UserDomainRole.objects.get_or_create( - user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER - ) - - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - igorville = Domain.objects.get(name="igorville.gov") - self.assertEquals(igorville.state, Domain.State.UNKNOWN) - detail_page = home_page.click("Manage", index=0) - self.assertNotContains(detail_page, "Expired") - - self.assertContains(detail_page, "DNS needed") - - def test_domain_detail_blocked_for_ineligible_user(self): - """We could easily duplicate this test for all domain management - views, but a single url test should be solid enough since all domain - management pages share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - def test_domain_detail_allowed_for_on_hold(self): - """Test that the domain overview page displays for on hold domain""" - home_page = self.app.get("/") - self.assertContains(home_page, "on-hold.gov") - - # View domain overview page - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id})) - self.assertNotContains(detail_page, "Edit") - - def test_domain_detail_see_just_nameserver(self): - home_page = self.app.get("/") - self.assertContains(home_page, "justnameserver.com") - - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) - - self.assertContains(detail_page, "justnameserver.com") - self.assertContains(detail_page, "ns1.justnameserver.com") - self.assertContains(detail_page, "ns2.justnameserver.com") - - def test_domain_detail_see_nameserver_and_ip(self): - home_page = self.app.get("/") - self.assertContains(home_page, "nameserverwithip.gov") - - # View nameserver on Domain Overview page - detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) - - self.assertContains(detail_page, "nameserverwithip.gov") - - self.assertContains(detail_page, "ns1.nameserverwithip.gov") - self.assertContains(detail_page, "ns2.nameserverwithip.gov") - self.assertContains(detail_page, "ns3.nameserverwithip.gov") - # Splitting IP addresses bc there is odd whitespace and can't strip text - self.assertContains(detail_page, "(1.2.3.4,") - self.assertContains(detail_page, "2.3.4.5)") - - def test_domain_detail_with_no_information_or_application(self): - """Test that domain management page returns 200 and displays error - when no domain information or domain application exist""" - # have to use staff user for this test - staff_user = create_user() - # staff_user.save() - self.client.force_login(staff_user) - - # need to set the analyst_action and analyst_action_location - # in the session to emulate user clicking Manage Domain - # in the admin interface - session = self.client.session - session["analyst_action"] = "foo" - session["analyst_action_location"] = self.domain_no_information.id - session.save() - - detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) - - self.assertContains(detail_page, "noinformation.gov") - self.assertContains(detail_page, "Domain missing domain information") - - -class TestDomainManagers(TestDomainOverview): - def tearDown(self): - """Ensure that the user has its original permissions""" - super().tearDown() - self.user.is_staff = False - self.user.save() - User.objects.all().delete() - - def test_domain_managers(self): - response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - self.assertContains(response, "Domain managers") - - def test_domain_managers_add_link(self): - """Button to get to user add page works.""" - management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - add_page = management_page.click("Add a domain manager") - self.assertContains(add_page, "Add a domain manager") - - def test_domain_user_add(self): - response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - self.assertContains(response, "Add a domain manager") - - def test_domain_user_delete(self): - """Tests if deleting a domain manager works""" - - # Add additional users - dummy_user_1 = User.objects.create( - username="macncheese", - email="cheese@igorville.com", - ) - dummy_user_2 = User.objects.create( - username="pastapizza", - email="pasta@igorville.com", - ) - - role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - - response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - - # Make sure we're on the right page - self.assertContains(response, "Domain managers") - - # Make sure the desired user exists - self.assertContains(response, "cheese@igorville.com") - - # Delete dummy_user_1 - response = self.client.post( - reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": dummy_user_1.id}), follow=True - ) - - # Grab the displayed messages - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - - # Ensure the error we recieve is in line with what we expect - message = messages[0] - self.assertEqual(message.message, "Removed cheese@igorville.com as a manager for this domain.") - self.assertEqual(message.tags, "success") - - # Check that role_1 deleted in the DB after the post - deleted_user_exists = UserDomainRole.objects.filter(id=role_1.id).exists() - self.assertFalse(deleted_user_exists) - - # Ensure that the current user wasn't deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() - self.assertTrue(current_user_exists) - - # Ensure that the other userdomainrole was not deleted - role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() - self.assertTrue(role_2_exists) - - def test_domain_user_delete_denied_if_no_permission(self): - """Deleting a domain manager is denied if the user has no permission to do so""" - - # Create a domain object - vip_domain = Domain.objects.create(name="freeman.gov") - - # Add users - dummy_user_1 = User.objects.create( - username="bagel", - email="bagel@igorville.com", - ) - dummy_user_2 = User.objects.create( - username="pastapizza", - email="pasta@igorville.com", - ) - - role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) - role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) - - response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id})) - - # Make sure that we can't access the domain manager page normally - self.assertEqual(response.status_code, 403) - - # Try to delete dummy_user_1 - response = self.client.post( - reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": dummy_user_1.id}), follow=True - ) - - # Ensure that we are denied access - self.assertEqual(response.status_code, 403) - - # Ensure that the user wasn't deleted - role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists() - self.assertTrue(role_1_exists) - - # Ensure that the other userdomainrole was not deleted - role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() - self.assertTrue(role_2_exists) - - # Make sure that the current user wasn't deleted for some reason - current_user_exists = UserDomainRole.objects.filter(user=dummy_user_1.id, domain=vip_domain.id).exists() - self.assertTrue(current_user_exists) - - def test_domain_user_delete_denied_if_last_man_standing(self): - """Deleting a domain manager is denied if the user is the only manager""" - - # Create a domain object - vip_domain = Domain.objects.create(name="olive-oil.gov") - - # Add the requesting user as the only manager on the domain - UserDomainRole.objects.create(user=self.user, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) - - response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id})) - - # Make sure that we can still access the domain manager page normally - self.assertContains(response, "Domain managers") - - # Make sure that the logged in user exists - self.assertContains(response, "info@example.com") - - # Try to delete the current user - response = self.client.post( - reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": self.user.id}), follow=True - ) - - # Ensure that we are denied access - self.assertEqual(response.status_code, 403) - - # Make sure that the current user wasn't deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=vip_domain.id).exists() - self.assertTrue(current_user_exists) - - def test_domain_user_delete_self_redirects_home(self): - """Tests if deleting yourself redirects to home""" - # Add additional users - dummy_user_1 = User.objects.create( - username="macncheese", - email="cheese@igorville.com", - ) - dummy_user_2 = User.objects.create( - username="pastapizza", - email="pasta@igorville.com", - ) - - role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) - - response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) - - # Make sure we're on the right page - self.assertContains(response, "Domain managers") - - # Make sure the desired user exists - self.assertContains(response, "info@example.com") - - # Make sure more than one UserDomainRole exists on this object - self.assertContains(response, "cheese@igorville.com") - - # Delete the current user - response = self.client.post( - reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True - ) - - # Check if we've been redirected to the home page - self.assertContains(response, "Manage your domains") - - # Grab the displayed messages - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - - # Ensure the error we recieve is in line with what we expect - message = messages[0] - self.assertEqual(message.message, "You are no longer managing the domain igorville.gov.") - self.assertEqual(message.tags, "success") - - # Ensure that the current user was deleted - current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() - self.assertFalse(current_user_exists) - - # Ensure that the other userdomainroles are not deleted - role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists() - self.assertTrue(role_1_exists) - - role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() - self.assertTrue(role_2_exists) - - @boto3_mocking.patching - def test_domain_user_add_form(self): - """Adding an existing user works.""" - other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - add_page.form["email"] = "mayor@igorville.gov" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.assertEqual(success_result.status_code, 302) - self.assertEqual( - success_result["Location"], - reverse("domain-users", kwargs={"pk": self.domain.id}), - ) - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - self.assertContains(success_page, "mayor@igorville.gov") - - @boto3_mocking.patching - def test_domain_invitation_created(self): - """Add user on a nonexistent email creates an invitation. - - Adding a non-existent user sends an email as a side-effect, so mock - out the boto3 SES email sending here. - """ - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - - self.assertContains(success_page, email_address) - self.assertContains(success_page, "Cancel") # link to cancel invitation - self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) - - @boto3_mocking.patching - def test_domain_invitation_created_for_caps_email(self): - """Add user on a nonexistent email with CAPS creates an invitation to lowercase email. - - Adding a non-existent user sends an email as a side-effect, so mock - out the boto3 SES email sending here. - """ - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - caps_email_address = "MAYOR@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = caps_email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - success_result = add_page.form.submit() - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = success_result.follow() - - self.assertContains(success_page, email_address) - self.assertContains(success_page, "Cancel") # link to cancel invitation - self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) - - @boto3_mocking.patching - def test_domain_invitation_email_sent(self): - """Inviting a non-existent user sends them an email.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requestor_non_existent(self): - """Inviting a non existent user sends them an email, with email as the name.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("info@example.com", email_content) - - # Check that the requestors first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requestor(self): - """Inviting a user sends them an email, with email as the name.""" - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("info@example.com", email_content) - - # Check that the requestors first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_has_email_as_requestor_staff(self): - """Inviting a user sends them an email, with email as the name.""" - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - # Make sure the user is staff - self.user.is_staff = True - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_client_instance = mock_client.return_value - - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit() - - # check the mock instance to see if `send_email` was called right - mock_client_instance.send_email.assert_called_once_with( - FromEmailAddress=settings.DEFAULT_FROM_EMAIL, - Destination={"ToAddresses": [email_address]}, - Content=ANY, - ) - - # Check the arguments passed to send_email method - _, kwargs = mock_client_instance.send_email.call_args - - # Extract the email content, and check that the message is as we expect - email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] - self.assertIn("help@get.gov", email_content) - - # Check that the requestors first/last name do not exist - self.assertNotIn("First", email_content) - self.assertNotIn("Last", email_content) - self.assertNotIn("First Last", email_content) - - @boto3_mocking.patching - def test_domain_invitation_email_displays_error_non_existent(self): - """Inviting a non existent user sends them an email, with email as the name.""" - # make sure there is no user with this email - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - # Give the user who is sending the email an invalid email address - self.user.email = "" - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - mock_error_message = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() - - expected_message_content = "Can't send invitation email. No email is associated with your account." - - # Grab the message content - returned_error_message = mock_error_message.call_args[0][1] - - # Check that the message content is what we expect - self.assertEqual(expected_message_content, returned_error_message) - - @boto3_mocking.patching - def test_domain_invitation_email_displays_error(self): - """When the requesting user has no email, an error is displayed""" - # make sure there is no user with this email - # Create a fake user object - email_address = "mayor@igorville.gov" - User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") - - # Give the user who is sending the email an invalid email address - self.user.email = "" - self.user.save() - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - mock_client = MagicMock() - - mock_error_message = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with patch("django.contrib.messages.error") as mock_error_message: - with less_console_noise(): - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - add_page.form.submit().follow() - - expected_message_content = "Can't send invitation email. No email is associated with your account." - - # Grab the message content - returned_error_message = mock_error_message.call_args[0][1] - - # Check that the message content is what we expect - self.assertEqual(expected_message_content, returned_error_message) - - def test_domain_invitation_cancel(self): - """Posting to the delete view deletes an invitation.""" - email_address = "mayor@igorville.gov" - invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) - mock_client.EMAILS_SENT.clear() - with self.assertRaises(DomainInvitation.DoesNotExist): - DomainInvitation.objects.get(id=invitation.id) - - def test_domain_invitation_cancel_no_permissions(self): - """Posting to the delete view as a different user should fail.""" - email_address = "mayor@igorville.gov" - invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) - - other_user = User() - other_user.save() - self.client.force_login(other_user) - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # permission denied makes console errors - result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) - - self.assertEqual(result.status_code, 403) - - @boto3_mocking.patching - def test_domain_invitation_flow(self): - """Send an invitation to a new user, log in and load the dashboard.""" - email_address = "mayor@igorville.gov" - User.objects.filter(email=email_address).delete() - - add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) - - self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) - - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - add_page.form["email"] = email_address - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - add_page.form.submit() - - # user was invited, create them - new_user = User.objects.create(username=email_address, email=email_address) - # log them in to `self.app` - self.app.set_user(new_user.username) - # and manually call the on each login callback - new_user.on_each_login() - - # Now load the home page and make sure our domain appears there - home_page = self.app.get(reverse("home")) - self.assertContains(home_page, self.domain.name) - - -class TestDomainNameservers(TestDomainOverview): - def test_domain_nameservers(self): - """Can load domain's nameservers page.""" - page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - self.assertContains(page, "DNS name servers") - - def test_domain_nameservers_form_submit_one_nameserver(self): - """Nameserver form submitted with one nameserver throws error. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form with only one nameserver, should error - # regarding required fields - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. form requires a minimum of 2 name servers - self.assertContains( - result, - "At least two name servers are required.", - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_subdomain_missing_ip(self): - """Nameserver form catches missing ip error on subdomain. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = "ns2.igorville.gov" - - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. subdomain missing an ip - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_missing_host(self): - """Nameserver form catches error when host is missing. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-ip"] = "127.0.0.1" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_duplicate_host(self): - """Nameserver form catches error when host is duplicated. - - Uses self.app WebTest because we need to interact with forms. - """ - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form with duplicate host names of fake.host.com - nameservers_page.form["form-0-ip"] = "" - nameservers_page.form["form-1-server"] = "fake.host.com" - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. remove duplicate entry - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_whitespace(self): - """Nameserver form removes whitespace from ip. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.gov" - valid_ip = "1.1. 1.1" - # initial nameservers page has one server with two ips - # have to throw an error in order to test that the whitespace has been stripped from ip - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without one host and an ip with whitespace - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-ip"] = valid_ip - nameservers_page.form["form-1-server"] = nameserver2 - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an ip address which has been stripped of whitespace, - # response should be a 302 to success page - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - # in the event of a generic nameserver error from registry error, there will be a 302 - # with an error message displayed, so need to follow 302 and test for success message - self.assertContains(page, "The name servers for this domain have been updated") - - def test_domain_nameservers_form_submit_glue_record_not_allowed(self): - """Nameserver form catches error when IP is present - but host not subdomain. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.com" - valid_ip = "127.0.0.1" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_invalid_ip(self): - """Nameserver form catches invalid IP on submission. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver = "ns2.igorville.gov" - invalid_ip = "123" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = nameserver - nameservers_page.form["form-1-ip"] = invalid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has ip but missing host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submit_invalid_host(self): - """Nameserver form catches invalid host on submission. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver = "invalid-nameserver.gov" - valid_ip = "123.2.45.111" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-1-server"] = nameserver - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the required field. nameserver has invalid host - self.assertContains( - result, - str(NameserverError(code=NameserverErrorCodes.INVALID_HOST, nameserver=nameserver)), - count=2, - status_code=200, - ) - - def test_domain_nameservers_form_submits_successfully(self): - """Nameserver form submits successfully with valid input. - - Uses self.app WebTest because we need to interact with forms. - """ - nameserver1 = "ns1.igorville.gov" - nameserver2 = "ns2.igorville.gov" - valid_ip = "127.0.0.1" - # initial nameservers page has one server with two ips - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # attempt to submit the form without two hosts, both subdomains, - # only one has ips - nameservers_page.form["form-0-server"] = nameserver1 - nameservers_page.form["form-1-server"] = nameserver2 - nameservers_page.form["form-1-ip"] = valid_ip - with less_console_noise(): # swallow log warning message - result = nameservers_page.form.submit() - # form submission was a successful post, response should be a 302 - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - self.assertContains(page, "The name servers for this domain have been updated") - - def test_domain_nameservers_form_invalid(self): - """Nameserver form does not submit with invalid data. - - Uses self.app WebTest because we need to interact with forms. - """ - nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - nameservers_page.form["form-0-server"] = "" - with less_console_noise(): # swallow logged warning message - result = nameservers_page.form.submit() - # form submission was a post with an error, response should be a 200 - # error text appears four times, twice at the top of the page, - # once around each required field. - self.assertContains( - result, - "At least two name servers are required.", - count=4, - status_code=200, - ) - - -class TestDomainAuthorizingOfficial(TestDomainOverview): - def test_domain_authorizing_official(self): - """Can load domain's authorizing official page.""" - page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - # once on the sidebar, once in the title - self.assertContains(page, "Authorizing official", count=2) - - def test_domain_authorizing_official_content(self): - """Authorizing official information appears on the page.""" - self.domain_information.authorizing_official = Contact(first_name="Testy") - self.domain_information.authorizing_official.save() - self.domain_information.save() - page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Testy") - - def test_domain_edit_authorizing_official_in_place(self): - """When editing an authorizing official for domain information and AO is not - joined to any other objects""" - self.domain_information.authorizing_official = Contact( - first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" - ) - self.domain_information.authorizing_official.save() - self.domain_information.save() - ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_form = ao_page.forms[0] - self.assertEqual(ao_form["first_name"].value, "Testy") - ao_form["first_name"] = "Testy2" - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the same contact object is in place - ao_pk = self.domain_information.authorizing_official.id - ao_form.submit() - - # refresh domain information - self.domain_information.refresh_from_db() - self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) - self.assertEqual(ao_pk, self.domain_information.authorizing_official.id) - - def test_domain_edit_authorizing_official_creates_new(self): - """When editing an authorizing official for domain information and AO IS - joined to another object""" - # set AO and Other Contact to the same Contact object - self.domain_information.authorizing_official = Contact( - first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" - ) - self.domain_information.authorizing_official.save() - self.domain_information.save() - self.domain_information.other_contacts.add(self.domain_information.authorizing_official) - self.domain_information.save() - # load the Authorizing Official in the web form - ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - ao_form = ao_page.forms[0] - # verify the first name is "Testy" and then change it to "Testy2" - self.assertEqual(ao_form["first_name"].value, "Testy") - ao_form["first_name"] = "Testy2" - # ao_pk is the initial pk of the authorizing official. set it before update - # to be able to verify after update that the same contact object is in place - ao_pk = self.domain_information.authorizing_official.id - ao_form.submit() - - # refresh domain information - self.domain_information.refresh_from_db() - # assert that AO information is updated, and that the AO is a new Contact - self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) - self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id) - # assert that the Other Contact information is not updated and that the Other Contact - # is the original Contact object - other_contact = self.domain_information.other_contacts.all()[0] - self.assertEqual("Testy", other_contact.first_name) - self.assertEqual(ao_pk, other_contact.id) - - -class TestDomainOrganization(TestDomainOverview): - def test_domain_org_name_address(self): - """Can load domain's org name and mailing address page.""" - page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - # once on the sidebar, once in the page title, once as H1 - self.assertContains(page, "Organization name and mailing address", count=3) - - def test_domain_org_name_address_content(self): - """Org name and address information appears on the page.""" - self.domain_information.organization_name = "Town of Igorville" - self.domain_information.save() - page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Town of Igorville") - - def test_domain_org_name_address_form(self): - """Submitting changes works on the org name address page.""" - self.domain_information.organization_name = "Town of Igorville" - self.domain_information.save() - org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - - org_name_page.form["organization_name"] = "Not igorville" - org_name_page.form["city"] = "Faketown" - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_result_page = org_name_page.form.submit() - self.assertEqual(success_result_page.status_code, 200) - - self.assertContains(success_result_page, "Not igorville") - self.assertContains(success_result_page, "Faketown") - - -class TestDomainContactInformation(TestDomainOverview): - def test_domain_your_contact_information(self): - """Can load domain's your contact information page.""" - page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Your contact information") - - def test_domain_your_contact_information_content(self): - """Logged-in user's contact information appears on the page.""" - self.user.contact.first_name = "Testy" - self.user.contact.save() - page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Testy") - - -class TestDomainSecurityEmail(TestDomainOverview): - def test_domain_security_email_existing_security_contact(self): - """Can load domain's security email page.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend - - domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") - # Add current user to this domain - _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() - page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) - - # Loads correctly - self.assertContains(page, "Security email") - self.assertContains(page, "security@mail.gov") - self.mockSendPatch.stop() - - def test_domain_security_email_no_security_contact(self): - """Loads a domain with no defined security email. - We should not show the default.""" - self.mockSendPatch = patch("registrar.models.domain.registry.send") - self.mockedSendFunction = self.mockSendPatch.start() - self.mockedSendFunction.side_effect = self.mockSend - - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - - # Loads correctly - self.assertContains(page, "Security email") - self.assertNotContains(page, "dotgov@cisa.dhs.gov") - self.mockSendPatch.stop() - - def test_domain_security_email(self): - """Can load domain's security email page.""" - page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Security email") - - def test_domain_security_email_form(self): - """Adding a security email works. - Uses self.app WebTest because we need to interact with forms. - """ - security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - security_email_page.form["security_email"] = "mayor@igorville.gov" - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - mock_client = MagicMock() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): # swallow log warning message - result = security_email_page.form.submit() - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - ) - - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - success_page = result.follow() - self.assertContains(success_page, "The security email for this domain has been updated") - - def test_security_email_form_messages(self): - """ - Test against the success and error messages that are defined in the view - """ - p = "adminpass" - self.client.login(username="superuser", password=p) - - form_data_registry_error = { - "security_email": "test@failCreate.gov", - } - - form_data_contact_error = { - "security_email": "test@contactError.gov", - } - - form_data_success = { - "security_email": "test@something.gov", - } - - test_cases = [ - ( - "RegistryError", - form_data_registry_error, - str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), - ), - ( - "ContactError", - form_data_contact_error, - str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), - ), - ( - "RegistrySuccess", - form_data_success, - "The security email for this domain has been updated.", - ), - # Add more test cases with different scenarios here - ] - - for test_name, data, expected_message in test_cases: - response = self.client.post( - reverse("domain-security-email", kwargs={"pk": self.domain.id}), - data=data, - follow=True, - ) - - # Check the response status code, content, or any other relevant assertions - self.assertEqual(response.status_code, 200) - - # Check if the expected message tag is set - if test_name == "RegistryError" or test_name == "ContactError": - message_tag = "error" - elif test_name == "RegistrySuccess": - message_tag = "success" - else: - # Handle other cases if needed - message_tag = "info" # Change to the appropriate default - - # Check the message tag - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - message = messages[0] - self.assertEqual(message.tags, message_tag) - self.assertEqual(message.message.strip(), expected_message.strip()) - - def test_domain_overview_blocked_for_ineligible_user(self): - """We could easily duplicate this test for all domain management - views, but a single url test should be solid enough since all domain - management pages share the same permissions class""" - self.user.status = User.RESTRICTED - self.user.save() - home_page = self.app.get("/") - self.assertContains(home_page, "igorville.gov") - with less_console_noise(): - response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) - self.assertEqual(response.status_code, 403) - - -class TestDomainDNSSEC(TestDomainOverview): - - """MockEPPLib is already inherited.""" - - def test_dnssec_page_refreshes_enable_button(self): - """DNSSEC overview page loads when domain has no DNSSEC data - and shows a 'Enable DNSSEC' button.""" - - page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) - self.assertContains(page, "Enable DNSSEC") - - def test_dnssec_page_loads_with_data_in_domain(self): - """DNSSEC overview page loads when domain has DNSSEC data - and the template contains a button to disable DNSSEC.""" - - page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})) - self.assertContains(page, "Disable DNSSEC") - - # Prepare the data for the POST request - post_data = { - "disable_dnssec": "Disable DNSSEC", - } - updated_page = self.client.post( - reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), - post_data, - follow=True, - ) - - self.assertEqual(updated_page.status_code, 200) - - self.assertContains(updated_page, "Enable DNSSEC") - - def test_ds_form_loads_with_no_domain_data(self): - """DNSSEC Add DS data page loads when there is no - domain DNSSEC data and shows a button to Add new record""" - - page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id})) - self.assertContains(page, "You have no DS data added") - self.assertContains(page, "Add new record") - - def test_ds_form_loads_with_ds_data(self): - """DNSSEC Add DS data page loads when there is - domain DNSSEC DS data and shows the data""" - - page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - self.assertContains(page, "DS data record 1") - - def test_ds_data_form_modal(self): - """When user clicks on save, a modal pops up.""" - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - # Assert that a hidden trigger for the modal does not exist. - # This hidden trigger will pop on the page when certain condition are met: - # 1) Initial form contained DS data, 2) All data is deleted and form is - # submitted. - self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") - # Simulate a delete all data - form_data = {} - response = self.client.post( - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), - data=form_data, - ) - self.assertEqual(response.status_code, 200) # Adjust status code as needed - # Now check to see whether the JS trigger for the modal is present on the page - self.assertContains(response, "Trigger Disable DNSSEC Modal") - - def test_ds_data_form_submits(self): - """DS data form submits successfully - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - with less_console_noise(): # swallow log warning message - result = add_data_page.forms[0].submit() - # form submission was a post, response should be a redirect - self.assertEqual(result.status_code, 302) - self.assertEqual( - result["Location"], - reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), - ) - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - page = result.follow() - self.assertContains(page, "The DS data records for this domain have been updated.") - - def test_ds_data_form_invalid(self): - """DS data form errors with invalid data (missing required fields) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # all four form fields are required, so will test with each blank - add_data_page.forms[0]["form-0-key_tag"] = "" - add_data_page.forms[0]["form-0-algorithm"] = "" - add_data_page.forms[0]["form-0-digest_type"] = "" - add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains(result, "Key tag is required", count=2, status_code=200) - self.assertContains(result, "Algorithm is required", count=2, status_code=200) - self.assertContains(result, "Digest type is required", count=2, status_code=200) - self.assertContains(result, "Digest is required", count=2, status_code=200) - - def test_ds_data_form_invalid_keytag(self): - """DS data form errors with invalid data (key tag too large) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "65536" # > 65535 - add_data_page.forms[0]["form-0-algorithm"] = "" - add_data_page.forms[0]["form-0-digest_type"] = "" - add_data_page.forms[0]["form-0-digest"] = "" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_chars(self): - """DS data form errors with invalid data (digest contains non hexadecimal chars) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "1" - add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_sha1(self): - """DS data form errors with invalid data (digest is invalid sha-1) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1 - add_data_page.forms[0]["form-0-digest"] = "A123" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200 - ) - - def test_ds_data_form_invalid_digest_sha256(self): - """DS data form errors with invalid data (digest is invalid sha-256) - - Uses self.app WebTest because we need to interact with forms. - """ - add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) - session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] - self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) - # first two nameservers are required, so if we empty one out we should - # get a form error - add_data_page.forms[0]["form-0-key_tag"] = "1234" - add_data_page.forms[0]["form-0-algorithm"] = "3" - add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256 - add_data_page.forms[0]["form-0-digest"] = "GG1234" - with less_console_noise(): # swallow logged warning message - result = add_data_page.forms[0].submit() - # form submission was a post with an error, response should be a 200 - # error text appears twice, once at the top of the page, once around - # the field. - self.assertContains( - result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256)), count=2, status_code=200 - ) - - -class TestApplicationStatus(TestWithUser, WebTest): - def setUp(self): - super().setUp() - self.app.set_user(self.user.username) - self.client.force_login(self.user) - - def test_application_status(self): - """Checking application status page""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - - def test_application_status_with_ineligible_user(self): - """Checking application status page whith a blocked user. - The user should still have access to view.""" - self.user.status = "ineligible" - self.user.save() - - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - - def test_application_withdraw(self): - """Checking application status page""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - # click the "Withdraw request" button - mock_client = MockSESClient() - with boto3_mocking.clients.handler_for("sesv2", mock_client): - with less_console_noise(): - withdraw_page = detail_page.click("Withdraw request") - self.assertContains(withdraw_page, "Withdraw request for") - home_page = withdraw_page.click("Withdraw request") - # confirm that it has redirected, and the status has been updated to withdrawn - self.assertRedirects( - home_page, - "/", - status_code=302, - target_status_code=200, - fetch_redirect_response=True, - ) - home_page = self.app.get("/") - self.assertContains(home_page, "Withdrawn") - - def test_application_withdraw_no_permissions(self): - """Can't withdraw applications as a restricted user.""" - self.user.status = User.RESTRICTED - self.user.save() - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - application.save() - - home_page = self.app.get("/") - self.assertContains(home_page, "city.gov") - # click the "Manage" link - detail_page = home_page.click("Manage", index=0) - self.assertContains(detail_page, "city.gov") - self.assertContains(detail_page, "city1.gov") - self.assertContains(detail_page, "Chief Tester") - self.assertContains(detail_page, "testy@town.com") - self.assertContains(detail_page, "Admin Tester") - self.assertContains(detail_page, "Status:") - # Restricted user trying to withdraw results in 403 error - with less_console_noise(): - for url_name in [ - "application-withdraw-confirmation", - "application-withdrawn", - ]: - with self.subTest(url_name=url_name): - page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) - self.assertEqual(page.status_code, 403) - - def test_application_status_no_permissions(self): - """Can't access applications without being the creator.""" - application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) - other_user = User() - other_user.save() - application.creator = other_user - application.save() - - # PermissionDeniedErrors make lots of noise in test output - with less_console_noise(): - for url_name in [ - "application-status", - "application-withdraw-confirmation", - "application-withdrawn", - ]: - with self.subTest(url_name=url_name): - page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) - self.assertEqual(page.status_code, 403) - - def test_approved_application_not_in_active_requests(self): - """An approved application is not shown in the Active - Requests table on home.html.""" - application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user) - application.save() - - home_page = self.app.get("/") - # This works in our test environment because creating - # an approved application here does not generate a - # domain object, so we do not expect to see 'city.gov' - # in either the Domains or Requests tables. - self.assertNotContains(home_page, "city.gov") diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py new file mode 100644 index 000000000..cdeacaf27 --- /dev/null +++ b/src/registrar/tests/test_views_application.py @@ -0,0 +1,2199 @@ +from unittest import skip + +from django.conf import settings +from django.urls import reverse + +from .common import MockSESClient, completed_application # type: ignore +from django_webtest import WebTest # type: ignore +import boto3_mocking # type: ignore + +from registrar.models import ( + DomainApplication, + Domain, + DomainInformation, + Contact, + User, + Website, +) +from registrar.views.application import ApplicationWizard, Step + +from .common import less_console_noise +from .test_views import TestWithUser + +import logging + +logger = logging.getLogger(__name__) + + +class DomainApplicationTests(TestWithUser, WebTest): + + """Webtests for domain application to test filling and submitting.""" + + # Doesn't work with CSRF checking + # hypothesis is that CSRF_USE_SESSIONS is incompatible with WebTest + csrf_checks = False + + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.TITLES = ApplicationWizard.TITLES + + def test_application_form_intro_acknowledgement(self): + """Tests that user is presented with intro acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + self.assertContains(intro_page, "You’re about to start your .gov domain request") + + def test_application_form_intro_is_skipped_when_edit_access(self): + """Tests that user is NOT presented with intro acknowledgement page when accessed through 'edit'""" + completed_application(status=DomainApplication.ApplicationStatus.STARTED, user=self.user) + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Edit" link + detail_page = home_page.click("Edit", index=0) + # Check that the response is a redirect + self.assertEqual(detail_page.status_code, 302) + # You can access the 'Location' header to get the redirect URL + redirect_url = detail_page.url + self.assertEqual(redirect_url, "/request/organization_type/") + + def test_application_form_empty_submit(self): + """Tests empty submit on the first page after the acknowledgement page""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # submitting should get back the same page if the required field is empty + result = type_page.forms[0].submit() + self.assertIn("What kind of U.S.-based government organization do you represent?", result) + + def test_application_multiple_applications_exist(self): + """Test that an info message appears when user has multiple applications already""" + # create and submit an application + application = completed_application(user=self.user) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + application.submit() + application.save() + + # now, attempt to create another one + with less_console_noise(): + intro_page = self.app.get(reverse("application:")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + self.assertContains(type_page, "You cannot submit this request yet") + + @boto3_mocking.patching + def test_application_form_submission(self): + """ + Can fill out the entire form and submit. + As we add additional form pages, we need to include them here to make + this test work. + + This test also looks for the long organization name on the summary page. + + This also tests for the presence of a modal trigger and the dynamic test + in the modal header on the submit page. + """ + num_pages_tested = 0 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 + num_pages = len(self.TITLES) - SKIPPED_PAGES + + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + # test next button and validate data + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + # should see results in db + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.organization_type, "federal") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_federal/") + num_pages_tested += 1 + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.federal_type, "executive") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(federal_result.status_code, 302) + self.assertEqual(federal_result["Location"], "/request/organization_contact/") + num_pages_tested += 1 + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + 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) + org_contact_result = org_contact_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.organization_name, "Testorg") + self.assertEqual(application.address_line1, "address 1") + self.assertEqual(application.address_line2, "address 2") + self.assertEqual(application.city, "NYC") + self.assertEqual(application.state_territory, "NY") + self.assertEqual(application.zipcode, "10002") + self.assertEqual(application.urbanization, "URB Royal Oaks") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(org_contact_result.status_code, 302) + self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/") + num_pages_tested += 1 + + # ---- 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" + + # 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 + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.authorizing_official.first_name, "Testy ATO") + self.assertEqual(application.authorizing_official.last_name, "Tester ATO") + self.assertEqual(application.authorizing_official.title, "Chief Tester") + self.assertEqual(application.authorizing_official.email, "testy@town.com") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(ao_result.status_code, 302) + self.assertEqual(ao_result["Location"], "/request/current_sites/") + 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" + + # 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 + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.current_websites.filter(website="http://www.city.com").count(), + 1, + ) + # the post request should return a redirect to the next form in + # the application + self.assertEqual(current_sites_result.status_code, 302) + self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/") + num_pages_tested += 1 + + # ---- 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" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_result = dotgov_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.requested_domain.name, "city.gov") + self.assertEqual(application.alternative_domains.filter(website="city1.gov").count(), 1) + # the post request should return a redirect to the next form in + # the application + 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." + + # 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 + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.purpose, "For all kinds of things.") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(purpose_result.status_code, 302) + self.assertEqual(purpose_result["Location"], "/request/your_contact/") + 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] + + 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) + your_contact_result = your_contact_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.submitter.first_name, "Testy you") + self.assertEqual(application.submitter.last_name, "Tester you") + self.assertEqual(application.submitter.title, "Admin Tester") + self.assertEqual(application.submitter.email, "testy-admin@town.com") + self.assertEqual(application.submitter.phone, "(201) 555 5556") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(your_contact_result.status_code, 302) + self.assertEqual(your_contact_result["Location"], "/request/other_contacts/") + 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() + + # 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] + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + + 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 + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.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 application + self.assertEqual(other_contacts_result.status_code, 302) + self.assertEqual(other_contacts_result["Location"], "/request/anything_else/") + num_pages_tested += 1 + + # ---- ANYTHING ELSE PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + anything_else_page = other_contacts_result.follow() + anything_else_form = anything_else_page.forms[0] + + anything_else_form["anything_else-anything_else"] = "Nothing else." + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + anything_else_result = anything_else_form.submit() + # validate that data from this step are being saved + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.anything_else, "Nothing else.") + # the post request should return a redirect to the next form in + # the application + self.assertEqual(anything_else_result.status_code, 302) + self.assertEqual(anything_else_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 = anything_else_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 + application = DomainApplication.objects.get() # there's only one + self.assertEqual(application.is_policy_acknowledged, True) + # the post request should return a redirect to the next form in + # the application + 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] + + # Review page contains all the previously entered data + # Let's make sure the long org name is displayed + self.assertContains(review_page, "Federal") + self.assertContains(review_page, "Executive") + self.assertContains(review_page, "Testorg") + self.assertContains(review_page, "address 1") + self.assertContains(review_page, "address 2") + self.assertContains(review_page, "NYC") + self.assertContains(review_page, "NY") + self.assertContains(review_page, "10002") + self.assertContains(review_page, "URB Royal Oaks") + self.assertContains(review_page, "Testy ATO") + self.assertContains(review_page, "Tester ATO") + self.assertContains(review_page, "Chief Tester") + self.assertContains(review_page, "testy@town.com") + self.assertContains(review_page, "city.com") + self.assertContains(review_page, "city.gov") + self.assertContains(review_page, "city1.gov") + self.assertContains(review_page, "For all kinds of things.") + self.assertContains(review_page, "Testy you") + self.assertContains(review_page, "Tester you") + self.assertContains(review_page, "Admin Tester") + self.assertContains(review_page, "testy-admin@town.com") + self.assertContains(review_page, "(201) 555-5556") + self.assertContains(review_page, "Testy2") + self.assertContains(review_page, "Tester2") + self.assertContains(review_page, "Another Tester") + self.assertContains(review_page, "testy2@town.com") + self.assertContains(review_page, "(201) 555-5557") + self.assertContains(review_page, "Nothing else.") + + # We can't test the modal itself as it relies on JS for init and triggering, + # but we can test for the existence of its trigger: + self.assertContains(review_page, "toggle-submit-domain-request") + # 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 > application_form > modal + self.assertContains(review_page, "You are about to submit a domain request for city.gov") + + # 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!") + + # check that any new pages are added to this test + self.assertEqual(num_pages, num_pages_tested) + + # This is the start of a test to check an existing application, it currently + # does not work and results in errors as noted in: + # https://github.com/cisagov/getgov/pull/728 + @skip("WIP") + def test_application_form_started_allsteps(self): + num_pages_tested = 0 + # elections, type_of_work, tribal_government + SKIPPED_PAGES = 3 + DASHBOARD_PAGE = 1 + num_pages = len(self.TITLES) - SKIPPED_PAGES + DASHBOARD_PAGE + + application = completed_application(user=self.user) + application.save() + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + self.assertContains(home_page, "Started") + num_pages_tested += 1 + + # TODO: For some reason this click results in a new application being generated + # This appraoch is an alternatie to using get as is being done below + # + # type_page = home_page.click("Edit") + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + url = reverse("edit-application", kwargs={"id": application.pk}) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # TODO: The following line results in a django error on middleware + response = self.client.get(url, follow=True) + self.assertContains(response, "Type of organization") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # TODO: Step through the remaining pages + + self.assertEqual(num_pages, num_pages_tested) + + def test_application_form_conditional_federal(self): + """Federal branch question is shown for federal organizations.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + + # the conditional step titles shouldn't appear initially + self.assertNotContains(type_page, self.TITLES["organization_federal"]) + self.assertNotContains(type_page, self.TITLES["organization_election"]) + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # set the session ID before .submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # the post request should return a redirect to the federal branch + # question + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_federal/") + + # and the step label should appear in the sidebar of the resulting page + # but the step label for the elections page should not appear + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + self.assertContains(federal_page, self.TITLES["organization_federal"]) + self.assertNotContains(federal_page, self.TITLES["organization_election"]) + + # continuing on in the flow we need to see top-level agency on the + # contact page + federal_page.forms[0]["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_page.forms[0].submit() + # the post request should return a redirect to the contact + # question + self.assertEqual(federal_result.status_code, 302) + self.assertEqual(federal_result["Location"], "/request/organization_contact/") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = federal_result.follow() + self.assertContains(contact_page, "Federal agency") + + def test_application_form_conditional_elections(self): + """Election question is shown for other organizations.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + + # the conditional step titles shouldn't appear initially + self.assertNotContains(type_page, self.TITLES["organization_federal"]) + self.assertNotContains(type_page, self.TITLES["organization_election"]) + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "county" + + # set the session ID before .submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # the post request should return a redirect to the elections question + self.assertEqual(type_result.status_code, 302) + self.assertEqual(type_result["Location"], "/request/organization_election/") + + # and the step label should appear in the sidebar of the resulting page + # but the step label for the elections page should not appear + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + self.assertContains(election_page, self.TITLES["organization_election"]) + self.assertNotContains(election_page, self.TITLES["organization_federal"]) + + # continuing on in the flow we need to NOT see top-level agency on the + # contact page + election_page.forms[0]["organization_election-is_election_board"] = "True" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_result = election_page.forms[0].submit() + # the post request should return a redirect to the contact + # question + self.assertEqual(election_result.status_code, 302) + self.assertEqual(election_result["Location"], "/request/organization_contact/") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = election_result.follow() + self.assertNotContains(contact_page, "Federal agency") + + def test_application_form_section_skipping(self): + """Can skip forward and back in sections""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + + # Now on federal type page, click back to the organization type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + new_page = federal_page.click(str(self.TITLES["organization_type"]), index=0) + + # Should be a link to the organization_federal page + self.assertGreater( + len(new_page.html.find_all("a", href="/request/organization_federal/")), + 0, + ) + + def test_application_form_nonfederal(self): + """Non-federal organizations don't have to provide their federal agency.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + org_contact_form = contact_page.forms[0] + + self.assertNotIn("federal_agency", org_contact_form.fields) + + # minimal fields that must be filled out + org_contact_form["organization_contact-organization_name"] = "Testorg" + org_contact_form["organization_contact-address_line1"] = "address 1" + org_contact_form["organization_contact-city"] = "NYC" + org_contact_form["organization_contact-state_territory"] = "NY" + org_contact_form["organization_contact-zipcode"] = "10002" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_result = org_contact_form.submit() + + # the post request should return a redirect to the + # about your organization page if it was successful. + self.assertEqual(contact_result.status_code, 302) + self.assertEqual(contact_result["Location"], "/request/about_your_organization/") + + def test_application_about_your_organization_special(self): + """Special districts have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.SPECIAL_DISTRICT + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_page.forms[0].submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + + def test_yes_no_form_inits_blank_for_new_application(self): + """On the Other Contacts page, the yes/no form gets initialized with nothing selected for + new applications""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, None) + + def test_yes_no_form_inits_yes_for_application_with_other_contacts(self): + """On the Other Contacts page, the yes/no form gets initialized with YES selected if the + application has other contacts""" + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + def test_yes_no_form_inits_no_for_application_with_no_other_contacts_rationale(self): + """On the Other Contacts page, the yes/no form gets initialized with NO selected if the + application has no other contacts""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + def test_submitting_other_contacts_deletes_no_other_contacts_rationale(self): + """When a user submits the Other Contacts form with other contacts selected, the application's + no other contacts rationale gets deleted""" + # Application has other contacts by default + application = completed_application(user=self.user, has_other_contacts=False) + application.no_other_contacts_rationale = "Hello!" + application.save() + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "False") + + other_contacts_form["other_contacts-has_other_contacts"] = "True" + + other_contacts_form["other_contacts-0-first_name"] = "Testy" + other_contacts_form["other_contacts-0-middle_name"] = "" + other_contacts_form["other_contacts-0-last_name"] = "McTesterson" + other_contacts_form["other_contacts-0-title"] = "Lord" + other_contacts_form["other_contacts-0-email"] = "testy@abc.org" + other_contacts_form["other_contacts-0-phone"] = "(201) 555-0123" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 1, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + None, + ) + + def test_submitting_no_other_contacts_rationale_deletes_other_contacts(self): + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts get deleted for other contacts that exist and are not joined to other objects + """ + # Application has other contacts by default + application = completed_application(user=self.user) + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier has been removed from the database + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + + def test_submitting_no_other_contacts_rationale_removes_reference_other_contacts_when_joined(self): + """When a user submits the Other Contacts form with no other contacts selected, the application's + other contacts references get removed for other contacts that exist and are joined to other objects""" + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + 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", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # Now let's join the other contact to another object + domain_info = DomainInformation.objects.create(creator=self.user) + domain_info.other_contacts.set([other]) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + self.assertEquals(other_contacts_form["other_contacts-has_other_contacts"].value, "True") + + other_contacts_form["other_contacts-has_other_contacts"] = "False" + + other_contacts_form["other_contacts-no_other_contacts_rationale"] = "Hello again!" + + # Submit the now empty form + other_contacts_form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the no_other_contacts_rationale we saved earlier is no longer associated with the application + application = DomainApplication.objects.get() + self.assertEqual( + application.other_contacts.count(), + 0, + ) + + # Verify that the 'other' contact object still exists + domain_info = DomainInformation.objects.get() + self.assertEqual( + domain_info.other_contacts.count(), + 1, + ) + self.assertEqual( + domain_info.other_contacts.all()[0].first_name, + "Testy2", + ) + + self.assertEquals( + application.no_other_contacts_rationale, + "Hello again!", + ) + + def test_if_yes_no_form_is_no_then_no_other_contacts_required(self): + """Applicants with no other contacts have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "False" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is returned, ie the no other contacts form is required + self.assertContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is not returned, ie the contacts form is not required + self.assertNotContains(response, "Enter the first name / given name of this contact.") + + def test_if_yes_no_form_is_yes_then_other_contacts_required(self): + """Applicants with other contacts do not have to give a reason.""" + other_contacts_page = self.app.get(reverse("application:other_contacts")) + other_contacts_form = other_contacts_page.forms[0] + other_contacts_form["other_contacts-has_other_contacts"] = "True" + response = other_contacts_page.forms[0].submit() + + # The textarea for no other contacts returns this error message + # Assert that it is not returned, ie the no other contacts form is not required + self.assertNotContains(response, "Rationale for no other employees is required.") + + # The first name field for other contacts returns this error message + # Assert that it is returned, ie the contacts form is required + self.assertContains(response, "Enter the first name / given name of this contact.") + + def test_delete_other_contact(self): + """Other contacts can be deleted after being saved to database. + + This formset uses the DJANGO DELETE widget. We'll test that by setting 2 contacts on an application, + loading the form and marking one contact up for deletion.""" + # Populate the database with a domain application that + # has 2 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + other2, _ = Contact.objects.get_or_create( + first_name="Testy3", + last_name="Tester3", + title="Another Tester", + email="testy3@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + application.other_contacts.add(other2) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded with both other contacts + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + self.assertEqual(other_contacts_form["other_contacts-1-first_name"].value, "Testy3") + + # Mark the first dude for deletion + other_contacts_form.set("other_contacts-0-DELETE", "on") + + # Submit the form + other_contacts_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the first dude was deleted + application = DomainApplication.objects.get() + self.assertEqual(application.other_contacts.count(), 1) + self.assertEqual(application.other_contacts.first().first_name, "Testy3") + + def test_delete_other_contact_does_not_allow_zero_contacts(self): + """Delete Other Contact does not allow submission with zero contacts.""" + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # Mark the first dude for deletion + other_contacts_form.set("other_contacts-0-DELETE", "on") + + # Submit the form + other_contacts_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + # Verify that the contact was not deleted + application = DomainApplication.objects.get() + self.assertEqual(application.other_contacts.count(), 1) + self.assertEqual(application.other_contacts.first().first_name, "Testy2") + + def test_delete_other_contact_sets_visible_empty_form_as_required_after_failed_submit(self): + """When you: + 1. add an empty contact, + 2. delete existing contacts, + 3. then submit, + The forms on page reload shows all the required fields and their errors.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch so we can reuse the other contact + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # Set total forms to 2 indicating an additional formset was added. + # Submit no data though for the second formset. + # Set the first formset to be deleted. + other_contacts_form["other_contacts-TOTAL_FORMS"] = "2" + other_contacts_form.set("other_contacts-0-DELETE", "on") + + response = other_contacts_form.submit() + + # Assert that the response presents errors to the user, including to + # Enter the first name ... + self.assertContains(response, "Enter the first name / given name of this contact.") + + def test_edit_other_contact_in_place(self): + """When you: + 1. edit an existing contact which is not joined to another model, + 2. then submit, + The application is linked to the existing contact, and the existing contact updated.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + other, _ = Contact.objects.get_or_create( + first_name="Testy2", + last_name="Tester2", + title="Another Tester", + email="testy2@town.com", + phone="(201) 555 5557", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(other) + + # other_contact_pk is the initial pk of the other contact. set it before update + # to be able to verify after update that the same contact object is in place + other_contact_pk = other.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy2") + + # update the first name of the contact + other_contacts_form["other_contacts-0-first_name"] = "Testy3" + + # Submit the updated form + other_contacts_form.submit() + + application.refresh_from_db() + + # assert that the Other Contact is updated "in place" + other_contact = application.other_contacts.all()[0] + self.assertEquals(other_contact_pk, other_contact.id) + self.assertEquals("Testy3", other_contact.first_name) + + def test_edit_other_contact_creates_new(self): + """When you: + 1. edit an existing contact which IS joined to another model, + 2. then submit, + The application is linked to a new contact, and the new contact is updated.""" + + # Populate the database with a domain application that + # has 1 "other contact" assigned to it, the other contact is also + # the authorizing official initially + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + you, _ = Contact.objects.get_or_create( + first_name="Testy you", + last_name="Tester you", + title="Admin Tester", + email="testy-admin@town.com", + phone="(201) 555 5556", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + submitter=you, + creator=self.user, + status="started", + ) + application.other_contacts.add(ao) + + # other_contact_pk is the initial pk of the other contact. set it before update + # to be able to verify after update that the ao contact is still in place + # and not updated, and that the new contact has a new id + other_contact_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + other_contacts_page = self.app.get(reverse("application:other_contacts")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + other_contacts_form = other_contacts_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(other_contacts_form["other_contacts-0-first_name"].value, "Testy") + + # update the first name of the contact + other_contacts_form["other_contacts-0-first_name"] = "Testy2" + + # Submit the updated form + other_contacts_form.submit() + + application.refresh_from_db() + + # assert that other contact info is updated, and that a new Contact + # is created for the other contact + other_contact = application.other_contacts.all()[0] + self.assertNotEquals(other_contact_pk, other_contact.id) + self.assertEquals("Testy2", other_contact.first_name) + # assert that the authorizing official is not updated + authorizing_official = application.authorizing_official + self.assertEquals("Testy", authorizing_official.first_name) + + def test_edit_authorizing_official_in_place(self): + """When you: + 1. edit an authorizing official which is not joined to another model, + 2. then submit, + The application is linked to the existing ao, and the ao updated.""" + + # Populate the database with a domain application that + # has an authorizing_official (ao) + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + creator=self.user, + status="started", + ) + + # ao_pk is the initial pk of the Authorizing Official. set it before update + # to be able to verify after update that the same Contact object is in place + ao_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + ao_page = self.app.get(reverse("application:authorizing_official")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_form = ao_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") + + # update the first name of the contact + ao_form["authorizing_official-first_name"] = "Testy2" + + # Submit the updated form + ao_form.submit() + + application.refresh_from_db() + + # assert AO is updated "in place" + updated_ao = application.authorizing_official + self.assertEquals(ao_pk, updated_ao.id) + self.assertEquals("Testy2", updated_ao.first_name) + + def test_edit_authorizing_official_creates_new(self): + """When you: + 1. edit an existing authorizing official which IS joined to another model, + 2. then submit, + The application is linked to a new Contact, and the new Contact is updated.""" + + # Populate the database with a domain application that + # has authorizing official assigned to it, the authorizing offical is also + # an other contact initially + # We'll do it from scratch + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + creator=self.user, + status="started", + ) + application.other_contacts.add(ao) + + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the other contact is still in place + # and not updated, and that the new ao has a new id + ao_pk = ao.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + ao_page = self.app.get(reverse("application:authorizing_official")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + ao_form = ao_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(ao_form["authorizing_official-first_name"].value, "Testy") + + # update the first name of the contact + ao_form["authorizing_official-first_name"] = "Testy2" + + # Submit the updated form + ao_form.submit() + + application.refresh_from_db() + + # assert that the other contact is not updated + other_contacts = application.other_contacts.all() + other_contact = other_contacts[0] + self.assertEquals(ao_pk, other_contact.id) + self.assertEquals("Testy", other_contact.first_name) + # assert that the authorizing official is updated + authorizing_official = application.authorizing_official + self.assertEquals("Testy2", authorizing_official.first_name) + + def test_edit_submitter_in_place(self): + """When you: + 1. edit a submitter (your contact) which is not joined to another model, + 2. then submit, + The application is linked to the existing submitter, and the submitter updated.""" + + # Populate the database with a domain application that + # has a submitter + # We'll do it from scratch + you, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + submitter=you, + creator=self.user, + status="started", + ) + + # submitter_pk is the initial pk of the submitter. set it before update + # to be able to verify after update that the same contact object is in place + submitter_pk = you.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + your_contact_page = self.app.get(reverse("application:your_contact")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_form = your_contact_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") + + # update the first name of the contact + your_contact_form["your_contact-first_name"] = "Testy2" + + # Submit the updated form + your_contact_form.submit() + + application.refresh_from_db() + + updated_submitter = application.submitter + self.assertEquals(submitter_pk, updated_submitter.id) + self.assertEquals("Testy2", updated_submitter.first_name) + + def test_edit_submitter_creates_new(self): + """When you: + 1. edit an existing your contact which IS joined to another model, + 2. then submit, + The application is linked to a new Contact, and the new Contact is updated.""" + + # Populate the database with a domain application that + # has submitter assigned to it, the submitter is also + # an other contact initially + # We'll do it from scratch + submitter, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(201) 555 5555", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + submitter=submitter, + creator=self.user, + status="started", + ) + application.other_contacts.add(submitter) + + # submitter_pk is the initial pk of the your contact. set it before update + # to be able to verify after update that the other contact is still in place + # and not updated, and that the new submitter has a new id + submitter_pk = submitter.id + + # prime the form by visiting /edit + self.app.get(reverse("edit-application", kwargs={"id": application.pk})) + # 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) + + your_contact_page = self.app.get(reverse("application:your_contact")) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + your_contact_form = your_contact_page.forms[0] + + # Minimal check to ensure the form is loaded + self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy") + + # update the first name of the contact + your_contact_form["your_contact-first_name"] = "Testy2" + + # Submit the updated form + your_contact_form.submit() + + application.refresh_from_db() + + # assert that the other contact is not updated + other_contacts = application.other_contacts.all() + other_contact = other_contacts[0] + self.assertEquals(submitter_pk, other_contact.id) + self.assertEquals("Testy", other_contact.first_name) + # assert that the submitter is updated + submitter = application.submitter + self.assertEquals("Testy2", submitter.first_name) + + def test_application_about_your_organiztion_interstate(self): + """Special districts have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.OrganizationChoices.INTERSTATE + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + contact_page = type_result.follow() + + self.assertContains(contact_page, self.TITLES[Step.ABOUT_YOUR_ORGANIZATION]) + + def test_application_tribal_government(self): + """Tribal organizations have to answer an additional question.""" + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = DomainApplication.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 + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + tribal_government_page = type_result.follow() + + # and the step is on the sidebar list. + self.assertContains(tribal_government_page, self.TITLES[Step.TRIBAL_GOVERNMENT]) + + def test_application_ao_dynamic_text(self): + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + 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" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_result = org_contact_form.submit() + + # ---- AO CONTACT PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + self.assertContains(ao_page, "Executive branch federal agencies") + + # Go back to organization type page and change type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page.click(str(self.TITLES["organization_type"]), index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_form["organization_type-organization_type"] = "city" + type_result = type_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + + # Go back to AO page and test the dynamic text changed + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = election_page.click(str(self.TITLES["authorizing_official"]), index=0) + self.assertContains(ao_page, "Domain requests from cities") + + def test_application_dotgov_domain_dynamic_text(self): + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + # ---- TYPE PAGE ---- + type_form = type_page.forms[0] + type_form["organization_type-organization_type"] = "federal" + + # test next button + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_result = type_form.submit() + + # ---- FEDERAL BRANCH PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_page = type_result.follow() + federal_form = federal_page.forms[0] + federal_form["organization_federal-federal_type"] = "executive" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + federal_result = federal_form.submit() + + # ---- ORG CONTACT PAGE ---- + # Follow the redirect to the next form page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_page = federal_result.follow() + org_contact_form = org_contact_page.forms[0] + # federal agency so we have to fill in federal_agency + org_contact_form["organization_contact-federal_agency"] = "General Services Administration" + 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" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + org_contact_result = org_contact_form.submit() + + # ---- AO CONTACT PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_page = org_contact_result.follow() + + # ---- 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" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_result = ao_form.submit() + + # ---- 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" + + # test saving the page + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_result = current_sites_form.submit() + + # ---- DOTGOV DOMAIN PAGE ---- + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page = current_sites_result.follow() + + self.assertContains(dotgov_page, "medicare.gov") + + # Go back to organization type page and change type + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page.click(str(self.TITLES["organization_type"]), index=0) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_form["organization_type-organization_type"] = "city" + type_result = type_form.submit() + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + election_page = type_result.follow() + + # Go back to dotgov domain page to test the dynamic text changed + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + dotgov_page = election_page.click(str(self.TITLES["dotgov_domain"]), index=0) + self.assertContains(dotgov_page, "CityofEudoraKS.gov") + self.assertNotContains(dotgov_page, "medicare.gov") + + def test_application_formsets(self): + """Users are able to add more than one of some fields.""" + current_sites_page = self.app.get(reverse("application:current_sites")) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + # fill in the form field + current_sites_form = current_sites_page.forms[0] + self.assertIn("current_sites-0-website", current_sites_form.fields) + self.assertNotIn("current_sites-1-website", current_sites_form.fields) + current_sites_form["current_sites-0-website"] = "https://example.com" + + # click "Add another" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_result = current_sites_form.submit("submit_button", value="save") + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + current_sites_form = current_sites_result.follow().forms[0] + + # verify that there are two form fields + value = current_sites_form["current_sites-0-website"].value + self.assertEqual(value, "https://example.com") + self.assertIn("current_sites-1-website", current_sites_form.fields) + # and it is correctly referenced in the ManyToOne relationship + application = DomainApplication.objects.get() # there's only one + self.assertEqual( + application.current_websites.filter(website="https://example.com").count(), + 1, + ) + + @skip("WIP") + def test_application_edit_restore(self): + """ + Test that a previously saved application is available at the /edit endpoint. + """ + ao, _ = Contact.objects.get_or_create( + first_name="Testy", + last_name="Tester", + title="Chief Tester", + email="testy@town.com", + phone="(555) 555 5555", + ) + domain, _ = Domain.objects.get_or_create(name="city.gov") + alt, _ = Website.objects.get_or_create(website="city1.gov") + current, _ = Website.objects.get_or_create(website="city.com") + 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", + ) + application, _ = DomainApplication.objects.get_or_create( + organization_type="federal", + federal_type="executive", + purpose="Purpose of the site", + anything_else="No", + is_policy_acknowledged=True, + organization_name="Testorg", + address_line1="address 1", + state_territory="NY", + zipcode="10002", + authorizing_official=ao, + requested_domain=domain, + submitter=you, + creator=self.user, + ) + application.other_contacts.add(other) + application.current_websites.add(current) + application.alternative_domains.add(alt) + + # prime the form by visiting /edit + url = reverse("edit-application", kwargs={"id": application.pk}) + response = self.client.get(url) + + # TODO: this is a sketch of each page in the wizard which needs to be tested + # Django does not have tools sufficient for real end to end integration testing + # (for example, USWDS moves radio buttons off screen and replaces them with + # CSS styled "fakes" -- Django cannot determine if those are visually correct) + # -- the best that can/should be done here is to ensure the correct values + # are being passed to the templating engine + + url = reverse("application:organization_type") + response = self.client.get(url, follow=True) + self.assertContains(response, "") + # choices = response.context['wizard']['form']['organization_type'].subwidgets + # radio = [ x for x in choices if x.data["value"] == "federal" ][0] + # checked = radio.data["selected"] + # self.assertTrue(checked) + + # url = reverse("application:organization_federal") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:organization_contact") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:authorizing_official") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:current_sites") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:dotgov_domain") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:purpose") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:your_contact") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:other_contacts") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:other_contacts") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:security_email") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:anything_else") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + # url = reverse("application:requirements") + # self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # page = self.app.get(url) + # self.assertNotContains(page, "VALUE") + + def test_long_org_name_in_application(self): + """ + Make sure the long name is displaying in the application form, + org step + """ + intro_page = self.app.get(reverse("application:")) + # 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] + + intro_form = intro_page.forms[0] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + intro_result = intro_form.submit() + + # follow first redirect + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + type_page = intro_result.follow() + + self.assertContains(type_page, "Federal: an agency of the U.S. government") + + def test_submit_modal_no_domain_text_fallback(self): + """When user clicks on submit your domain request and the requested domain + is null (possible through url direct access to the review page), present + fallback copy in the modal's header. + + NOTE: This may be a moot point if we implement a more solid pattern in the + future, like not a submit action at all on the review page.""" + + review_page = self.app.get(reverse("application:review")) + self.assertContains(review_page, "toggle-submit-domain-request") + self.assertContains(review_page, "You are about to submit an incomplete request") + + +class DomainApplicationTestDifferentStatuses(TestWithUser, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.client.force_login(self.user) + + def test_application_status(self): + """Checking application status page""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + + def test_application_status_with_ineligible_user(self): + """Checking application status page whith a blocked user. + The user should still have access to view.""" + self.user.status = "ineligible" + self.user.save() + + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + + def test_application_withdraw(self): + """Checking application status page""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # click the "Withdraw request" button + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + withdraw_page = detail_page.click("Withdraw request") + self.assertContains(withdraw_page, "Withdraw request for") + home_page = withdraw_page.click("Withdraw request") + # confirm that it has redirected, and the status has been updated to withdrawn + self.assertRedirects( + home_page, + "/", + status_code=302, + target_status_code=200, + fetch_redirect_response=True, + ) + home_page = self.app.get("/") + self.assertContains(home_page, "Withdrawn") + + def test_application_withdraw_no_permissions(self): + """Can't withdraw applications as a restricted user.""" + self.user.status = User.RESTRICTED + self.user.save() + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + application.save() + + home_page = self.app.get("/") + self.assertContains(home_page, "city.gov") + # click the "Manage" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "city.gov") + self.assertContains(detail_page, "city1.gov") + self.assertContains(detail_page, "Chief Tester") + self.assertContains(detail_page, "testy@town.com") + self.assertContains(detail_page, "Admin Tester") + self.assertContains(detail_page, "Status:") + # Restricted user trying to withdraw results in 403 error + with less_console_noise(): + for url_name in [ + "application-withdraw-confirmation", + "application-withdrawn", + ]: + with self.subTest(url_name=url_name): + page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) + self.assertEqual(page.status_code, 403) + + def test_application_status_no_permissions(self): + """Can't access applications without being the creator.""" + application = completed_application(status=DomainApplication.ApplicationStatus.SUBMITTED, user=self.user) + other_user = User() + other_user.save() + application.creator = other_user + application.save() + + # PermissionDeniedErrors make lots of noise in test output + with less_console_noise(): + for url_name in [ + "application-status", + "application-withdraw-confirmation", + "application-withdrawn", + ]: + with self.subTest(url_name=url_name): + page = self.client.get(reverse(url_name, kwargs={"pk": application.pk})) + self.assertEqual(page.status_code, 403) + + def test_approved_application_not_in_active_requests(self): + """An approved application is not shown in the Active + Requests table on home.html.""" + application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED, user=self.user) + application.save() + + home_page = self.app.get("/") + # This works in our test environment because creating + # an approved application here does not generate a + # domain object, so we do not expect to see 'city.gov' + # in either the Domains or Requests tables. + self.assertNotContains(home_page, "city.gov") diff --git a/src/registrar/tests/test_views_domain.py b/src/registrar/tests/test_views_domain.py new file mode 100644 index 000000000..df035b13e --- /dev/null +++ b/src/registrar/tests/test_views_domain.py @@ -0,0 +1,1611 @@ +from unittest import skip +from unittest.mock import MagicMock, ANY, patch + +from django.conf import settings +from django.urls import reverse +from django.contrib.auth import get_user_model + +from .common import MockSESClient, create_user # type: ignore +from django_webtest import WebTest # type: ignore +import boto3_mocking # type: ignore + +from registrar.utility.errors import ( + NameserverError, + NameserverErrorCodes, + SecurityEmailError, + SecurityEmailErrorCodes, + GenericError, + GenericErrorCodes, + DsDataError, + DsDataErrorCodes, +) + +from registrar.models import ( + DomainApplication, + Domain, + DomainInformation, + DomainInvitation, + Contact, + PublicContact, + Host, + HostIP, + UserDomainRole, + User, +) +from datetime import date, datetime, timedelta +from django.utils import timezone + +from .common import less_console_noise +from .test_views import TestWithUser + +import logging + +logger = logging.getLogger(__name__) + + +class TestWithDomainPermissions(TestWithUser): + def setUp(self): + super().setUp() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_with_ip, _ = Domain.objects.get_or_create(name="nameserverwithip.gov") + self.domain_just_nameserver, _ = Domain.objects.get_or_create(name="justnameserver.com") + self.domain_no_information, _ = Domain.objects.get_or_create(name="noinformation.gov") + self.domain_on_hold, _ = Domain.objects.get_or_create( + name="on-hold.gov", + state=Domain.State.ON_HOLD, + expiration_date=timezone.make_aware( + datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) + ), + ) + self.domain_deleted, _ = Domain.objects.get_or_create( + name="deleted.gov", + state=Domain.State.DELETED, + expiration_date=timezone.make_aware( + datetime.combine(date.today() + timedelta(days=1), datetime.min.time()) + ), + ) + + self.domain_dsdata, _ = Domain.objects.get_or_create(name="dnssec-dsdata.gov") + self.domain_multdsdata, _ = Domain.objects.get_or_create(name="dnssec-multdsdata.gov") + # We could simply use domain (igorville) but this will be more readable in tests + # that inherit this setUp + self.domain_dnssec_none, _ = Domain.objects.get_or_create(name="dnssec-none.gov") + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dsdata) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_multdsdata) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_dnssec_none) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_with_ip) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_just_nameserver) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_on_hold) + DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain_deleted) + + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_multdsdata, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_dnssec_none, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_with_ip, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, + domain=self.domain_just_nameserver, + role=UserDomainRole.Roles.MANAGER, + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_on_hold, role=UserDomainRole.Roles.MANAGER + ) + UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain_deleted, role=UserDomainRole.Roles.MANAGER + ) + + def tearDown(self): + try: + UserDomainRole.objects.all().delete() + if hasattr(self.domain, "contacts"): + self.domain.contacts.all().delete() + DomainApplication.objects.all().delete() + DomainInformation.objects.all().delete() + PublicContact.objects.all().delete() + HostIP.objects.all().delete() + Host.objects.all().delete() + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + except ValueError: # pass if already deleted + pass + super().tearDown() + + +class TestDomainPermissions(TestWithDomainPermissions): + def test_not_logged_in(self): + """Not logged in gets a redirect to Login.""" + for view_name in [ + "domain", + "domain-users", + "domain-users-add", + "domain-dns-nameservers", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + with self.subTest(view_name=view_name): + response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 302) + + def test_no_domain_role(self): + """Logged in but no role gets 403 Forbidden.""" + self.client.force_login(self.user) + self.role.delete() # user no longer has a role on this domain + + for view_name in [ + "domain", + "domain-users", + "domain-users-add", + "domain-dns-nameservers", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + with self.subTest(view_name=view_name): + with less_console_noise(): + response = self.client.get(reverse(view_name, kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + def test_domain_pages_blocked_for_on_hold_and_deleted(self): + """Test that the domain pages are blocked for on hold and deleted domains""" + + self.client.force_login(self.user) + for view_name in [ + "domain-users", + "domain-users-add", + "domain-dns", + "domain-dns-nameservers", + "domain-dns-dnssec", + "domain-dns-dnssec-dsdata", + "domain-org-name-address", + "domain-authorizing-official", + "domain-your-contact-information", + "domain-security-email", + ]: + for domain in [ + self.domain_on_hold, + self.domain_deleted, + ]: + with self.subTest(view_name=view_name, domain=domain): + with less_console_noise(): + response = self.client.get(reverse(view_name, kwargs={"pk": domain.id})) + self.assertEqual(response.status_code, 403) + + +class TestDomainOverview(TestWithDomainPermissions, WebTest): + def setUp(self): + super().setUp() + self.app.set_user(self.user.username) + self.client.force_login(self.user) + + +class TestDomainDetail(TestDomainOverview): + @skip("Assertion broke for no reason, why? Need to fix") + def test_domain_detail_link_works(self): + home_page = self.app.get("/") + logger.info(f"This is the value of home_page: {home_page}") + self.assertContains(home_page, "igorville.gov") + # click the "Edit" link + detail_page = home_page.click("Manage", index=0) + self.assertContains(detail_page, "igorville.gov") + self.assertContains(detail_page, "Status") + + def test_unknown_domain_does_not_show_as_expired_on_homepage(self): + """An UNKNOWN domain does not show as expired on the homepage. + It shows as 'DNS needed'""" + # At the time of this test's writing, there are 6 UNKNOWN domains inherited + # from constructors. Let's reset. + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + home_page = self.app.get("/") + self.assertNotContains(home_page, "igorville.gov") + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + self.assertNotContains(home_page, "Expired") + self.assertContains(home_page, "DNS needed") + + def test_unknown_domain_does_not_show_as_expired_on_detail_page(self): + """An UNKNOWN domain does not show as expired on the detail page. + It shows as 'DNS needed'""" + # At the time of this test's writing, there are 6 UNKNOWN domains inherited + # from constructors. Let's reset. + Domain.objects.all().delete() + UserDomainRole.objects.all().delete() + + self.domain, _ = Domain.objects.get_or_create(name="igorville.gov") + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + self.role, _ = UserDomainRole.objects.get_or_create( + user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER + ) + + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + igorville = Domain.objects.get(name="igorville.gov") + self.assertEquals(igorville.state, Domain.State.UNKNOWN) + detail_page = home_page.click("Manage", index=0) + self.assertNotContains(detail_page, "Expired") + + self.assertContains(detail_page, "DNS needed") + + def test_domain_detail_blocked_for_ineligible_user(self): + """We could easily duplicate this test for all domain management + views, but a single url test should be solid enough since all domain + management pages share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + with less_console_noise(): + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + def test_domain_detail_allowed_for_on_hold(self): + """Test that the domain overview page displays for on hold domain""" + home_page = self.app.get("/") + self.assertContains(home_page, "on-hold.gov") + + # View domain overview page + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_on_hold.id})) + self.assertNotContains(detail_page, "Edit") + + def test_domain_detail_see_just_nameserver(self): + home_page = self.app.get("/") + self.assertContains(home_page, "justnameserver.com") + + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_just_nameserver.id})) + + self.assertContains(detail_page, "justnameserver.com") + self.assertContains(detail_page, "ns1.justnameserver.com") + self.assertContains(detail_page, "ns2.justnameserver.com") + + def test_domain_detail_see_nameserver_and_ip(self): + home_page = self.app.get("/") + self.assertContains(home_page, "nameserverwithip.gov") + + # View nameserver on Domain Overview page + detail_page = self.app.get(reverse("domain", kwargs={"pk": self.domain_with_ip.id})) + + self.assertContains(detail_page, "nameserverwithip.gov") + + self.assertContains(detail_page, "ns1.nameserverwithip.gov") + self.assertContains(detail_page, "ns2.nameserverwithip.gov") + self.assertContains(detail_page, "ns3.nameserverwithip.gov") + # Splitting IP addresses bc there is odd whitespace and can't strip text + self.assertContains(detail_page, "(1.2.3.4,") + self.assertContains(detail_page, "2.3.4.5)") + + def test_domain_detail_with_no_information_or_application(self): + """Test that domain management page returns 200 and displays error + when no domain information or domain application exist""" + # have to use staff user for this test + staff_user = create_user() + # staff_user.save() + self.client.force_login(staff_user) + + # need to set the analyst_action and analyst_action_location + # in the session to emulate user clicking Manage Domain + # in the admin interface + session = self.client.session + session["analyst_action"] = "foo" + session["analyst_action_location"] = self.domain_no_information.id + session.save() + + detail_page = self.client.get(reverse("domain", kwargs={"pk": self.domain_no_information.id})) + + self.assertContains(detail_page, "noinformation.gov") + self.assertContains(detail_page, "Domain missing domain information") + + +class TestDomainManagers(TestDomainOverview): + def tearDown(self): + """Ensure that the user has its original permissions""" + super().tearDown() + self.user.is_staff = False + self.user.save() + User.objects.all().delete() + + def test_domain_managers(self): + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + self.assertContains(response, "Domain managers") + + def test_domain_managers_add_link(self): + """Button to get to user add page works.""" + management_page = self.app.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + add_page = management_page.click("Add a domain manager") + self.assertContains(add_page, "Add a domain manager") + + def test_domain_user_add(self): + response = self.client.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + self.assertContains(response, "Add a domain manager") + + def test_domain_user_delete(self): + """Tests if deleting a domain manager works""" + + # Add additional users + dummy_user_1 = User.objects.create( + username="macncheese", + email="cheese@igorville.com", + ) + dummy_user_2 = User.objects.create( + username="pastapizza", + email="pasta@igorville.com", + ) + + role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + + # Make sure we're on the right page + self.assertContains(response, "Domain managers") + + # Make sure the desired user exists + self.assertContains(response, "cheese@igorville.com") + + # Delete dummy_user_1 + response = self.client.post( + reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": dummy_user_1.id}), follow=True + ) + + # Grab the displayed messages + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + + # Ensure the error we recieve is in line with what we expect + message = messages[0] + self.assertEqual(message.message, "Removed cheese@igorville.com as a manager for this domain.") + self.assertEqual(message.tags, "success") + + # Check that role_1 deleted in the DB after the post + deleted_user_exists = UserDomainRole.objects.filter(id=role_1.id).exists() + self.assertFalse(deleted_user_exists) + + # Ensure that the current user wasn't deleted + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() + self.assertTrue(current_user_exists) + + # Ensure that the other userdomainrole was not deleted + role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() + self.assertTrue(role_2_exists) + + def test_domain_user_delete_denied_if_no_permission(self): + """Deleting a domain manager is denied if the user has no permission to do so""" + + # Create a domain object + vip_domain = Domain.objects.create(name="freeman.gov") + + # Add users + dummy_user_1 = User.objects.create( + username="bagel", + email="bagel@igorville.com", + ) + dummy_user_2 = User.objects.create( + username="pastapizza", + email="pasta@igorville.com", + ) + + role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) + role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) + + response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id})) + + # Make sure that we can't access the domain manager page normally + self.assertEqual(response.status_code, 403) + + # Try to delete dummy_user_1 + response = self.client.post( + reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": dummy_user_1.id}), follow=True + ) + + # Ensure that we are denied access + self.assertEqual(response.status_code, 403) + + # Ensure that the user wasn't deleted + role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists() + self.assertTrue(role_1_exists) + + # Ensure that the other userdomainrole was not deleted + role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() + self.assertTrue(role_2_exists) + + # Make sure that the current user wasn't deleted for some reason + current_user_exists = UserDomainRole.objects.filter(user=dummy_user_1.id, domain=vip_domain.id).exists() + self.assertTrue(current_user_exists) + + def test_domain_user_delete_denied_if_last_man_standing(self): + """Deleting a domain manager is denied if the user is the only manager""" + + # Create a domain object + vip_domain = Domain.objects.create(name="olive-oil.gov") + + # Add the requesting user as the only manager on the domain + UserDomainRole.objects.create(user=self.user, domain=vip_domain, role=UserDomainRole.Roles.MANAGER) + + response = self.client.get(reverse("domain-users", kwargs={"pk": vip_domain.id})) + + # Make sure that we can still access the domain manager page normally + self.assertContains(response, "Domain managers") + + # Make sure that the logged in user exists + self.assertContains(response, "info@example.com") + + # Try to delete the current user + response = self.client.post( + reverse("domain-user-delete", kwargs={"pk": vip_domain.id, "user_pk": self.user.id}), follow=True + ) + + # Ensure that we are denied access + self.assertEqual(response.status_code, 403) + + # Make sure that the current user wasn't deleted + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=vip_domain.id).exists() + self.assertTrue(current_user_exists) + + def test_domain_user_delete_self_redirects_home(self): + """Tests if deleting yourself redirects to home""" + # Add additional users + dummy_user_1 = User.objects.create( + username="macncheese", + email="cheese@igorville.com", + ) + dummy_user_2 = User.objects.create( + username="pastapizza", + email="pasta@igorville.com", + ) + + role_1 = UserDomainRole.objects.create(user=dummy_user_1, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + role_2 = UserDomainRole.objects.create(user=dummy_user_2, domain=self.domain, role=UserDomainRole.Roles.MANAGER) + + response = self.client.get(reverse("domain-users", kwargs={"pk": self.domain.id})) + + # Make sure we're on the right page + self.assertContains(response, "Domain managers") + + # Make sure the desired user exists + self.assertContains(response, "info@example.com") + + # Make sure more than one UserDomainRole exists on this object + self.assertContains(response, "cheese@igorville.com") + + # Delete the current user + response = self.client.post( + reverse("domain-user-delete", kwargs={"pk": self.domain.id, "user_pk": self.user.id}), follow=True + ) + + # Check if we've been redirected to the home page + self.assertContains(response, "Manage your domains") + + # Grab the displayed messages + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + + # Ensure the error we recieve is in line with what we expect + message = messages[0] + self.assertEqual(message.message, "You are no longer managing the domain igorville.gov.") + self.assertEqual(message.tags, "success") + + # Ensure that the current user was deleted + current_user_exists = UserDomainRole.objects.filter(user=self.user.id, domain=self.domain).exists() + self.assertFalse(current_user_exists) + + # Ensure that the other userdomainroles are not deleted + role_1_exists = UserDomainRole.objects.filter(id=role_1.id).exists() + self.assertTrue(role_1_exists) + + role_2_exists = UserDomainRole.objects.filter(id=role_2.id).exists() + self.assertTrue(role_2_exists) + + @boto3_mocking.patching + def test_domain_user_add_form(self): + """Adding an existing user works.""" + other_user, _ = get_user_model().objects.get_or_create(email="mayor@igorville.gov") + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + add_page.form["email"] = "mayor@igorville.gov" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.assertEqual(success_result.status_code, 302) + self.assertEqual( + success_result["Location"], + reverse("domain-users", kwargs={"pk": self.domain.id}), + ) + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + self.assertContains(success_page, "mayor@igorville.gov") + + @boto3_mocking.patching + def test_domain_invitation_created(self): + """Add user on a nonexistent email creates an invitation. + + Adding a non-existent user sends an email as a side-effect, so mock + out the boto3 SES email sending here. + """ + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + + self.assertContains(success_page, email_address) + self.assertContains(success_page, "Cancel") # link to cancel invitation + self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) + + @boto3_mocking.patching + def test_domain_invitation_created_for_caps_email(self): + """Add user on a nonexistent email with CAPS creates an invitation to lowercase email. + + Adding a non-existent user sends an email as a side-effect, so mock + out the boto3 SES email sending here. + """ + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + caps_email_address = "MAYOR@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = caps_email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + success_result = add_page.form.submit() + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = success_result.follow() + + self.assertContains(success_page, email_address) + self.assertContains(success_page, "Cancel") # link to cancel invitation + self.assertTrue(DomainInvitation.objects.filter(email=email_address).exists()) + + @boto3_mocking.patching + def test_domain_invitation_email_sent(self): + """Inviting a non-existent user sends them an email.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor_non_existent(self): + """Inviting a non existent user sends them an email, with email as the name.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("info@example.com", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor(self): + """Inviting a user sends them an email, with email as the name.""" + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("info@example.com", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_has_email_as_requestor_staff(self): + """Inviting a user sends them an email, with email as the name.""" + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + # Make sure the user is staff + self.user.is_staff = True + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_client_instance = mock_client.return_value + + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit() + + # check the mock instance to see if `send_email` was called right + mock_client_instance.send_email.assert_called_once_with( + FromEmailAddress=settings.DEFAULT_FROM_EMAIL, + Destination={"ToAddresses": [email_address]}, + Content=ANY, + ) + + # Check the arguments passed to send_email method + _, kwargs = mock_client_instance.send_email.call_args + + # Extract the email content, and check that the message is as we expect + email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"] + self.assertIn("help@get.gov", email_content) + + # Check that the requestors first/last name do not exist + self.assertNotIn("First", email_content) + self.assertNotIn("Last", email_content) + self.assertNotIn("First Last", email_content) + + @boto3_mocking.patching + def test_domain_invitation_email_displays_error_non_existent(self): + """Inviting a non existent user sends them an email, with email as the name.""" + # make sure there is no user with this email + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + # Give the user who is sending the email an invalid email address + self.user.email = "" + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + mock_error_message = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch("django.contrib.messages.error") as mock_error_message: + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() + + expected_message_content = "Can't send invitation email. No email is associated with your account." + + # Grab the message content + returned_error_message = mock_error_message.call_args[0][1] + + # Check that the message content is what we expect + self.assertEqual(expected_message_content, returned_error_message) + + @boto3_mocking.patching + def test_domain_invitation_email_displays_error(self): + """When the requesting user has no email, an error is displayed""" + # make sure there is no user with this email + # Create a fake user object + email_address = "mayor@igorville.gov" + User.objects.get_or_create(email=email_address, username="fakeuser@fakeymail.com") + + # Give the user who is sending the email an invalid email address + self.user.email = "" + self.user.save() + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + mock_client = MagicMock() + + mock_error_message = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with patch("django.contrib.messages.error") as mock_error_message: + with less_console_noise(): + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + add_page.form.submit().follow() + + expected_message_content = "Can't send invitation email. No email is associated with your account." + + # Grab the message content + returned_error_message = mock_error_message.call_args[0][1] + + # Check that the message content is what we expect + self.assertEqual(expected_message_content, returned_error_message) + + def test_domain_invitation_cancel(self): + """Posting to the delete view deletes an invitation.""" + email_address = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) + mock_client = MockSESClient() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + mock_client.EMAILS_SENT.clear() + with self.assertRaises(DomainInvitation.DoesNotExist): + DomainInvitation.objects.get(id=invitation.id) + + def test_domain_invitation_cancel_no_permissions(self): + """Posting to the delete view as a different user should fail.""" + email_address = "mayor@igorville.gov" + invitation, _ = DomainInvitation.objects.get_or_create(domain=self.domain, email=email_address) + + other_user = User() + other_user.save() + self.client.force_login(other_user) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # permission denied makes console errors + result = self.client.post(reverse("invitation-delete", kwargs={"pk": invitation.id})) + + self.assertEqual(result.status_code, 403) + + @boto3_mocking.patching + def test_domain_invitation_flow(self): + """Send an invitation to a new user, log in and load the dashboard.""" + email_address = "mayor@igorville.gov" + User.objects.filter(email=email_address).delete() + + add_page = self.app.get(reverse("domain-users-add", kwargs={"pk": self.domain.id})) + + self.domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user, domain=self.domain) + + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + add_page.form["email"] = email_address + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): + add_page.form.submit() + + # user was invited, create them + new_user = User.objects.create(username=email_address, email=email_address) + # log them in to `self.app` + self.app.set_user(new_user.username) + # and manually call the on each login callback + new_user.on_each_login() + + # Now load the home page and make sure our domain appears there + home_page = self.app.get(reverse("home")) + self.assertContains(home_page, self.domain.name) + + +class TestDomainNameservers(TestDomainOverview): + def test_domain_nameservers(self): + """Can load domain's nameservers page.""" + page = self.client.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + self.assertContains(page, "DNS name servers") + + def test_domain_nameservers_form_submit_one_nameserver(self): + """Nameserver form submitted with one nameserver throws error. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form with only one nameserver, should error + # regarding required fields + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. form requires a minimum of 2 name servers + self.assertContains( + result, + "At least two name servers are required.", + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_subdomain_missing_ip(self): + """Nameserver form catches missing ip error on subdomain. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = "ns2.igorville.gov" + + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. subdomain missing an ip + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_IP)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_missing_host(self): + """Nameserver form catches error when host is missing. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-ip"] = "127.0.0.1" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.MISSING_HOST)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_duplicate_host(self): + """Nameserver form catches error when host is duplicated. + + Uses self.app WebTest because we need to interact with forms. + """ + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form with duplicate host names of fake.host.com + nameservers_page.form["form-0-ip"] = "" + nameservers_page.form["form-1-server"] = "fake.host.com" + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. remove duplicate entry + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.DUPLICATE_HOST)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_whitespace(self): + """Nameserver form removes whitespace from ip. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.gov" + valid_ip = "1.1. 1.1" + # initial nameservers page has one server with two ips + # have to throw an error in order to test that the whitespace has been stripped from ip + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without one host and an ip with whitespace + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-ip"] = valid_ip + nameservers_page.form["form-1-server"] = nameserver2 + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an ip address which has been stripped of whitespace, + # response should be a 302 to success page + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + # in the event of a generic nameserver error from registry error, there will be a 302 + # with an error message displayed, so need to follow 302 and test for success message + self.assertContains(page, "The name servers for this domain have been updated") + + def test_domain_nameservers_form_submit_glue_record_not_allowed(self): + """Nameserver form catches error when IP is present + but host not subdomain. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.com" + valid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.GLUE_RECORD_NOT_ALLOWED)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_invalid_ip(self): + """Nameserver form catches invalid IP on submission. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver = "ns2.igorville.gov" + invalid_ip = "123" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = nameserver + nameservers_page.form["form-1-ip"] = invalid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has ip but missing host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.INVALID_IP, nameserver=nameserver)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submit_invalid_host(self): + """Nameserver form catches invalid host on submission. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver = "invalid-nameserver.gov" + valid_ip = "123.2.45.111" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-1-server"] = nameserver + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the required field. nameserver has invalid host + self.assertContains( + result, + str(NameserverError(code=NameserverErrorCodes.INVALID_HOST, nameserver=nameserver)), + count=2, + status_code=200, + ) + + def test_domain_nameservers_form_submits_successfully(self): + """Nameserver form submits successfully with valid input. + + Uses self.app WebTest because we need to interact with forms. + """ + nameserver1 = "ns1.igorville.gov" + nameserver2 = "ns2.igorville.gov" + valid_ip = "127.0.0.1" + # initial nameservers page has one server with two ips + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # attempt to submit the form without two hosts, both subdomains, + # only one has ips + nameservers_page.form["form-0-server"] = nameserver1 + nameservers_page.form["form-1-server"] = nameserver2 + nameservers_page.form["form-1-ip"] = valid_ip + with less_console_noise(): # swallow log warning message + result = nameservers_page.form.submit() + # form submission was a successful post, response should be a 302 + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The name servers for this domain have been updated") + + def test_domain_nameservers_form_invalid(self): + """Nameserver form does not submit with invalid data. + + Uses self.app WebTest because we need to interact with forms. + """ + nameservers_page = self.app.get(reverse("domain-dns-nameservers", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + nameservers_page.form["form-0-server"] = "" + with less_console_noise(): # swallow logged warning message + result = nameservers_page.form.submit() + # form submission was a post with an error, response should be a 200 + # error text appears four times, twice at the top of the page, + # once around each required field. + self.assertContains( + result, + "At least two name servers are required.", + count=4, + status_code=200, + ) + + +class TestDomainAuthorizingOfficial(TestDomainOverview): + def test_domain_authorizing_official(self): + """Can load domain's authorizing official page.""" + page = self.client.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + # once on the sidebar, once in the title + self.assertContains(page, "Authorizing official", count=2) + + def test_domain_authorizing_official_content(self): + """Authorizing official information appears on the page.""" + self.domain_information.authorizing_official = Contact(first_name="Testy") + self.domain_information.authorizing_official.save() + self.domain_information.save() + page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Testy") + + def test_domain_edit_authorizing_official_in_place(self): + """When editing an authorizing official for domain information and AO is not + joined to any other objects""" + self.domain_information.authorizing_official = Contact( + first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" + ) + self.domain_information.authorizing_official.save() + self.domain_information.save() + ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_form = ao_page.forms[0] + self.assertEqual(ao_form["first_name"].value, "Testy") + ao_form["first_name"] = "Testy2" + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the same contact object is in place + ao_pk = self.domain_information.authorizing_official.id + ao_form.submit() + + # refresh domain information + self.domain_information.refresh_from_db() + self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) + self.assertEqual(ao_pk, self.domain_information.authorizing_official.id) + + def test_domain_edit_authorizing_official_creates_new(self): + """When editing an authorizing official for domain information and AO IS + joined to another object""" + # set AO and Other Contact to the same Contact object + self.domain_information.authorizing_official = Contact( + first_name="Testy", last_name="Tester", title="CIO", email="nobody@igorville.gov" + ) + self.domain_information.authorizing_official.save() + self.domain_information.save() + self.domain_information.other_contacts.add(self.domain_information.authorizing_official) + self.domain_information.save() + # load the Authorizing Official in the web form + ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + ao_form = ao_page.forms[0] + # verify the first name is "Testy" and then change it to "Testy2" + self.assertEqual(ao_form["first_name"].value, "Testy") + ao_form["first_name"] = "Testy2" + # ao_pk is the initial pk of the authorizing official. set it before update + # to be able to verify after update that the same contact object is in place + ao_pk = self.domain_information.authorizing_official.id + ao_form.submit() + + # refresh domain information + self.domain_information.refresh_from_db() + # assert that AO information is updated, and that the AO is a new Contact + self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name) + self.assertNotEqual(ao_pk, self.domain_information.authorizing_official.id) + # assert that the Other Contact information is not updated and that the Other Contact + # is the original Contact object + other_contact = self.domain_information.other_contacts.all()[0] + self.assertEqual("Testy", other_contact.first_name) + self.assertEqual(ao_pk, other_contact.id) + + +class TestDomainOrganization(TestDomainOverview): + def test_domain_org_name_address(self): + """Can load domain's org name and mailing address page.""" + page = self.client.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + # once on the sidebar, once in the page title, once as H1 + self.assertContains(page, "Organization name and mailing address", count=3) + + def test_domain_org_name_address_content(self): + """Org name and address information appears on the page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Town of Igorville") + + def test_domain_org_name_address_form(self): + """Submitting changes works on the org name address page.""" + self.domain_information.organization_name = "Town of Igorville" + self.domain_information.save() + org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + + org_name_page.form["organization_name"] = "Not igorville" + org_name_page.form["city"] = "Faketown" + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_result_page = org_name_page.form.submit() + self.assertEqual(success_result_page.status_code, 200) + + self.assertContains(success_result_page, "Not igorville") + self.assertContains(success_result_page, "Faketown") + + +class TestDomainContactInformation(TestDomainOverview): + def test_domain_your_contact_information(self): + """Can load domain's your contact information page.""" + page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Your contact information") + + def test_domain_your_contact_information_content(self): + """Logged-in user's contact information appears on the page.""" + self.user.contact.first_name = "Testy" + self.user.contact.save() + page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Testy") + + +class TestDomainSecurityEmail(TestDomainOverview): + def test_domain_security_email_existing_security_contact(self): + """Can load domain's security email page.""" + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend + + domain_contact, _ = Domain.objects.get_or_create(name="freeman.gov") + # Add current user to this domain + _ = UserDomainRole(user=self.user, domain=domain_contact, role="admin").save() + page = self.client.get(reverse("domain-security-email", kwargs={"pk": domain_contact.id})) + + # Loads correctly + self.assertContains(page, "Security email") + self.assertContains(page, "security@mail.gov") + self.mockSendPatch.stop() + + def test_domain_security_email_no_security_contact(self): + """Loads a domain with no defined security email. + We should not show the default.""" + self.mockSendPatch = patch("registrar.models.domain.registry.send") + self.mockedSendFunction = self.mockSendPatch.start() + self.mockedSendFunction.side_effect = self.mockSend + + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + + # Loads correctly + self.assertContains(page, "Security email") + self.assertNotContains(page, "dotgov@cisa.dhs.gov") + self.mockSendPatch.stop() + + def test_domain_security_email(self): + """Can load domain's security email page.""" + page = self.client.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Security email") + + def test_domain_security_email_form(self): + """Adding a security email works. + Uses self.app WebTest because we need to interact with forms. + """ + security_email_page = self.app.get(reverse("domain-security-email", kwargs={"pk": self.domain.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + security_email_page.form["security_email"] = "mayor@igorville.gov" + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + mock_client = MagicMock() + with boto3_mocking.clients.handler_for("sesv2", mock_client): + with less_console_noise(): # swallow log warning message + result = security_email_page.form.submit() + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + ) + + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + success_page = result.follow() + self.assertContains(success_page, "The security email for this domain has been updated") + + def test_security_email_form_messages(self): + """ + Test against the success and error messages that are defined in the view + """ + p = "adminpass" + self.client.login(username="superuser", password=p) + + form_data_registry_error = { + "security_email": "test@failCreate.gov", + } + + form_data_contact_error = { + "security_email": "test@contactError.gov", + } + + form_data_success = { + "security_email": "test@something.gov", + } + + test_cases = [ + ( + "RegistryError", + form_data_registry_error, + str(GenericError(code=GenericErrorCodes.CANNOT_CONTACT_REGISTRY)), + ), + ( + "ContactError", + form_data_contact_error, + str(SecurityEmailError(code=SecurityEmailErrorCodes.BAD_DATA)), + ), + ( + "RegistrySuccess", + form_data_success, + "The security email for this domain has been updated.", + ), + # Add more test cases with different scenarios here + ] + + for test_name, data, expected_message in test_cases: + response = self.client.post( + reverse("domain-security-email", kwargs={"pk": self.domain.id}), + data=data, + follow=True, + ) + + # Check the response status code, content, or any other relevant assertions + self.assertEqual(response.status_code, 200) + + # Check if the expected message tag is set + if test_name == "RegistryError" or test_name == "ContactError": + message_tag = "error" + elif test_name == "RegistrySuccess": + message_tag = "success" + else: + # Handle other cases if needed + message_tag = "info" # Change to the appropriate default + + # Check the message tag + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + message = messages[0] + self.assertEqual(message.tags, message_tag) + self.assertEqual(message.message.strip(), expected_message.strip()) + + def test_domain_overview_blocked_for_ineligible_user(self): + """We could easily duplicate this test for all domain management + views, but a single url test should be solid enough since all domain + management pages share the same permissions class""" + self.user.status = User.RESTRICTED + self.user.save() + home_page = self.app.get("/") + self.assertContains(home_page, "igorville.gov") + with less_console_noise(): + response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id})) + self.assertEqual(response.status_code, 403) + + +class TestDomainDNSSEC(TestDomainOverview): + + """MockEPPLib is already inherited.""" + + def test_dnssec_page_refreshes_enable_button(self): + """DNSSEC overview page loads when domain has no DNSSEC data + and shows a 'Enable DNSSEC' button.""" + + page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id})) + self.assertContains(page, "Enable DNSSEC") + + def test_dnssec_page_loads_with_data_in_domain(self): + """DNSSEC overview page loads when domain has DNSSEC data + and the template contains a button to disable DNSSEC.""" + + page = self.client.get(reverse("domain-dns-dnssec", kwargs={"pk": self.domain_multdsdata.id})) + self.assertContains(page, "Disable DNSSEC") + + # Prepare the data for the POST request + post_data = { + "disable_dnssec": "Disable DNSSEC", + } + updated_page = self.client.post( + reverse("domain-dns-dnssec", kwargs={"pk": self.domain.id}), + post_data, + follow=True, + ) + + self.assertEqual(updated_page.status_code, 200) + + self.assertContains(updated_page, "Enable DNSSEC") + + def test_ds_form_loads_with_no_domain_data(self): + """DNSSEC Add DS data page loads when there is no + domain DNSSEC data and shows a button to Add new record""" + + page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dnssec_none.id})) + self.assertContains(page, "You have no DS data added") + self.assertContains(page, "Add new record") + + def test_ds_form_loads_with_ds_data(self): + """DNSSEC Add DS data page loads when there is + domain DNSSEC DS data and shows the data""" + + page = self.client.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + self.assertContains(page, "DS data record 1") + + def test_ds_data_form_modal(self): + """When user clicks on save, a modal pops up.""" + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + # Assert that a hidden trigger for the modal does not exist. + # This hidden trigger will pop on the page when certain condition are met: + # 1) Initial form contained DS data, 2) All data is deleted and form is + # submitted. + self.assertNotContains(add_data_page, "Trigger Disable DNSSEC Modal") + # Simulate a delete all data + form_data = {} + response = self.client.post( + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + data=form_data, + ) + self.assertEqual(response.status_code, 200) # Adjust status code as needed + # Now check to see whether the JS trigger for the modal is present on the page + self.assertContains(response, "Trigger Disable DNSSEC Modal") + + def test_ds_data_form_submits(self): + """DS data form submits successfully + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + with less_console_noise(): # swallow log warning message + result = add_data_page.forms[0].submit() + # form submission was a post, response should be a redirect + self.assertEqual(result.status_code, 302) + self.assertEqual( + result["Location"], + reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id}), + ) + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + page = result.follow() + self.assertContains(page, "The DS data records for this domain have been updated.") + + def test_ds_data_form_invalid(self): + """DS data form errors with invalid data (missing required fields) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # all four form fields are required, so will test with each blank + add_data_page.forms[0]["form-0-key_tag"] = "" + add_data_page.forms[0]["form-0-algorithm"] = "" + add_data_page.forms[0]["form-0-digest_type"] = "" + add_data_page.forms[0]["form-0-digest"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains(result, "Key tag is required", count=2, status_code=200) + self.assertContains(result, "Algorithm is required", count=2, status_code=200) + self.assertContains(result, "Digest type is required", count=2, status_code=200) + self.assertContains(result, "Digest is required", count=2, status_code=200) + + def test_ds_data_form_invalid_keytag(self): + """DS data form errors with invalid data (key tag too large) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "65536" # > 65535 + add_data_page.forms[0]["form-0-algorithm"] = "" + add_data_page.forms[0]["form-0-digest_type"] = "" + add_data_page.forms[0]["form-0-digest"] = "" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_KEYTAG_SIZE)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_chars(self): + """DS data form errors with invalid data (digest contains non hexadecimal chars) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "1" + add_data_page.forms[0]["form-0-digest"] = "GG1234" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_CHARS)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_sha1(self): + """DS data form errors with invalid data (digest is invalid sha-1) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "1" # SHA-1 + add_data_page.forms[0]["form-0-digest"] = "A123" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA1)), count=2, status_code=200 + ) + + def test_ds_data_form_invalid_digest_sha256(self): + """DS data form errors with invalid data (digest is invalid sha-256) + + Uses self.app WebTest because we need to interact with forms. + """ + add_data_page = self.app.get(reverse("domain-dns-dnssec-dsdata", kwargs={"pk": self.domain_dsdata.id})) + session_id = self.app.cookies[settings.SESSION_COOKIE_NAME] + self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id) + # first two nameservers are required, so if we empty one out we should + # get a form error + add_data_page.forms[0]["form-0-key_tag"] = "1234" + add_data_page.forms[0]["form-0-algorithm"] = "3" + add_data_page.forms[0]["form-0-digest_type"] = "2" # SHA-256 + add_data_page.forms[0]["form-0-digest"] = "GG1234" + with less_console_noise(): # swallow logged warning message + result = add_data_page.forms[0].submit() + # form submission was a post with an error, response should be a 200 + # error text appears twice, once at the top of the page, once around + # the field. + self.assertContains( + result, str(DsDataError(code=DsDataErrorCodes.INVALID_DIGEST_SHA256)), count=2, status_code=200 + ) From 3c0927e73a31844fcfb6f6253c9e4b0d9487a9c0 Mon Sep 17 00:00:00 2001 From: David Kennedy Date: Fri, 2 Feb 2024 14:04:13 -0500 Subject: [PATCH 120/120] lint --- src/registrar/tests/test_views.py | 31 +------------------ src/registrar/tests/test_views_application.py | 2 +- 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index ac0ec0cdb..915659f51 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1,44 +1,17 @@ -from unittest import skip -from unittest.mock import MagicMock, ANY, patch - -from django.conf import settings from django.test import Client, TestCase from django.urls import reverse from django.contrib.auth import get_user_model -from .common import MockEppLib, MockSESClient, completed_application, create_user # type: ignore -from django_webtest import WebTest # type: ignore -import boto3_mocking # type: ignore +from .common import MockEppLib # type: ignore -from registrar.utility.errors import ( - NameserverError, - NameserverErrorCodes, - SecurityEmailError, - SecurityEmailErrorCodes, - GenericError, - GenericErrorCodes, - DsDataError, - DsDataErrorCodes, -) from registrar.models import ( DomainApplication, - Domain, DomainInformation, DraftDomain, - DomainInvitation, Contact, - PublicContact, - Host, - HostIP, - Website, - UserDomainRole, User, ) -from registrar.views.application import ApplicationWizard, Step -from datetime import date, datetime, timedelta -from django.utils import timezone - from .common import less_console_noise import logging @@ -331,5 +304,3 @@ class LoggedInTests(TestWithUser): response = self.client.get("/request/", follow=True) print(response.status_code) self.assertEqual(response.status_code, 403) - - diff --git a/src/registrar/tests/test_views_application.py b/src/registrar/tests/test_views_application.py index cdeacaf27..62485fa86 100644 --- a/src/registrar/tests/test_views_application.py +++ b/src/registrar/tests/test_views_application.py @@ -37,7 +37,7 @@ class DomainApplicationTests(TestWithUser, WebTest): super().setUp() self.app.set_user(self.user.username) self.TITLES = ApplicationWizard.TITLES - + def test_application_form_intro_acknowledgement(self): """Tests that user is presented with intro acknowledgement page""" intro_page = self.app.get(reverse("application:"))