diff --git a/src/registrar/admin.py b/src/registrar/admin.py index eccfa1750..174500f28 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -138,24 +138,12 @@ class MyUserAdmin(BaseUserAdmin): "email", "first_name", "last_name", + # Group is a custom property defined within this file, + # rather than in a model like the other properties "group", "status", ) - list_filter = ( - "is_active", - "groups", - ) - - # Let's define First group - # (which should in theory be the ONLY group) - def group(self, obj): - if obj.groups.filter(name="full_access_group").exists(): - return "Full access" - elif obj.groups.filter(name="cisa_analysts_group").exists(): - return "Analyst" - return "" - fieldsets = ( ( None, @@ -222,6 +210,20 @@ class MyUserAdmin(BaseUserAdmin): "date_joined", ] + list_filter = ( + "is_active", + "groups", + ) + + # Let's define First group + # (which should in theory be the ONLY group) + def group(self, obj): + if obj.groups.filter(name="full_access_group").exists(): + return "Full access" + elif obj.groups.filter(name="cisa_analysts_group").exists(): + return "Analyst" + return "" + def get_list_display(self, request): # The full_access_permission perm will load onto the full_access_group # which is equivalent to superuser. The other group we use to manage @@ -811,7 +813,8 @@ class DomainAdmin(ListHeaderAdmin): else: self.message_user( request, - ("Domain statuses are %s" ". Thanks!") % statuses, + f"The registry statuses are {statuses}. " + "These statuses are from the provider of the .gov registry.", ) return HttpResponseRedirect(".") diff --git a/src/registrar/forms/domain.py b/src/registrar/forms/domain.py index f14448bcf..2f9aa1976 100644 --- a/src/registrar/forms/domain.py +++ b/src/registrar/forms/domain.py @@ -64,7 +64,7 @@ class DomainSecurityEmailForm(forms.Form): """Form for adding or editing a security email to a domain.""" - security_email = forms.EmailField(label="Security email") + security_email = forms.EmailField(label="Security email", required=False) class DomainOrgNameAddressForm(forms.ModelForm): diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py index 59edb707a..649eaed07 100644 --- a/src/registrar/models/domain.py +++ b/src/registrar/models/domain.py @@ -15,7 +15,7 @@ from epplibwrapper import ( RegistryError, ErrorCode, ) -from registrar.models.utility.contact_error import ContactError +from registrar.models.utility.contact_error import ContactError, ContactErrorCodes from .utility.domain_field import DomainField from .utility.domain_helper import DomainHelper @@ -625,7 +625,10 @@ class Domain(TimeStampedModel, DomainHelper): def get_security_email(self): logger.info("get_security_email-> getting the contact ") secContact = self.security_contact - return secContact.email + if secContact is not None: + return secContact.email + else: + return None def clientHoldStatus(self): return epp.Status(state=self.Status.CLIENT_HOLD, description="", lang="en") @@ -698,10 +701,10 @@ class Domain(TimeStampedModel, DomainHelper): return None if contact_type is None: - raise ContactError("contact_type is None") + raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE) if contact_id is None: - raise ContactError("contact_id is None") + raise ContactError(code=ContactErrorCodes.CONTACT_ID_NONE) # Since contact_id is registry_id, # check that its the right length @@ -710,14 +713,10 @@ class Domain(TimeStampedModel, DomainHelper): contact_id_length > PublicContact.get_max_id_length() or contact_id_length < 1 ): - raise ContactError( - "contact_id is of invalid length. " - "Cannot exceed 16 characters, " - f"got {contact_id} with a length of {contact_id_length}" - ) + raise ContactError(code=ContactErrorCodes.CONTACT_ID_INVALID_LENGTH) if not isinstance(contact, eppInfo.InfoContactResultData): - raise ContactError("Contact must be of type InfoContactResultData") + raise ContactError(code=ContactErrorCodes.CONTACT_INVALID_TYPE) auth_info = contact.auth_info postal_info = contact.postal_info @@ -881,8 +880,8 @@ class Domain(TimeStampedModel, DomainHelper): return self._handle_registrant_contact(desired_contact) - _registry_id: str - if contact_type in contacts: + _registry_id: str = "" + if contacts is not None and contact_type in contacts: _registry_id = contacts.get(contact_type) desired = PublicContact.objects.filter( diff --git a/src/registrar/models/utility/contact_error.py b/src/registrar/models/utility/contact_error.py index 93084eca2..cf392cb6e 100644 --- a/src/registrar/models/utility/contact_error.py +++ b/src/registrar/models/utility/contact_error.py @@ -1,2 +1,51 @@ +from enum import IntEnum + + +class ContactErrorCodes(IntEnum): + """Used in the ContactError class for + error mapping. + + Overview of contact error codes: + - 2000 CONTACT_TYPE_NONE + - 2001 CONTACT_ID_NONE + - 2002 CONTACT_ID_INVALID_LENGTH + - 2003 CONTACT_INVALID_TYPE + """ + + CONTACT_TYPE_NONE = 2000 + CONTACT_ID_NONE = 2001 + CONTACT_ID_INVALID_LENGTH = 2002 + CONTACT_INVALID_TYPE = 2003 + CONTACT_NOT_FOUND = 2004 + + class ContactError(Exception): - ... + """ + Overview of contact error codes: + - 2000 CONTACT_TYPE_NONE + - 2001 CONTACT_ID_NONE + - 2002 CONTACT_ID_INVALID_LENGTH + - 2003 CONTACT_INVALID_TYPE + - 2004 CONTACT_NOT_FOUND + """ + + # For linter + _contact_id_error = "contact_id has an invalid length. Cannot exceed 16 characters." + _contact_invalid_error = "Contact must be of type InfoContactResultData" + _contact_not_found_error = "No contact was found in cache or the registry" + _error_mapping = { + ContactErrorCodes.CONTACT_TYPE_NONE: "contact_type is None", + ContactErrorCodes.CONTACT_ID_NONE: "contact_id is None", + ContactErrorCodes.CONTACT_ID_INVALID_LENGTH: _contact_id_error, + ContactErrorCodes.CONTACT_INVALID_TYPE: _contact_invalid_error, + ContactErrorCodes.CONTACT_NOT_FOUND: _contact_not_found_error, + } + + def __init__(self, *args, code=None, **kwargs): + super().__init__(*args, **kwargs) + self.code = code + if self.code in self._error_mapping: + self.message = self._error_mapping.get(self.code) + + def __str__(self): + return f"{self.message}" diff --git a/src/registrar/templates/django/admin/domain_change_form.html b/src/registrar/templates/django/admin/domain_change_form.html index ac26fc922..2ed3d7532 100644 --- a/src/registrar/templates/django/admin/domain_change_form.html +++ b/src/registrar/templates/django/admin/domain_change_form.html @@ -13,10 +13,10 @@ {% elif original.state == original.State.ON_HOLD %} {% endif %} - - + + {% if original.state != original.State.DELETED %} - + {% endif %} {{ block.super }} diff --git a/src/registrar/templates/domain_detail.html b/src/registrar/templates/domain_detail.html index 6a700b393..bcf775fe5 100644 --- a/src/registrar/templates/domain_detail.html +++ b/src/registrar/templates/domain_detail.html @@ -46,8 +46,11 @@ {% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url %} {% url 'domain-security-email' pk=domain.id as url %} - {% include "includes/summary_item.html" with title='Security email' value=domain.security_email edit_link=url %} - + {% if security_email is not None and security_email != default_security_email%} + {% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url %} + {% else %} + {% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url %} + {% endif %} {% url 'domain-users' pk=domain.id as url %} {% include "includes/summary_item.html" with title='User management' users='true' list=True value=domain.permissions.all edit_link=url %} diff --git a/src/registrar/templates/domain_nameservers.html b/src/registrar/templates/domain_nameservers.html index 2dabac1af..a7371ee0b 100644 --- a/src/registrar/templates/domain_nameservers.html +++ b/src/registrar/templates/domain_nameservers.html @@ -1,7 +1,7 @@ {% extends "domain_base.html" %} {% load static field_helpers%} -{% block title %}Domain name servers | {{ domain.name }} | {% endblock %} +{% block title %}DNS name servers | {{ domain.name }} | {% endblock %} {% block domain_content %} {# this is right after the messages block in the parent template #} @@ -9,7 +9,7 @@ {% include "includes/form_errors.html" with form=form %} {% endfor %} -

Domain name servers

+

DNS name servers

Before your domain can be used we'll need information about your domain name servers.

diff --git a/src/registrar/templates/domain_security_email.html b/src/registrar/templates/domain_security_email.html index 8175fa394..8fb0ccfb0 100644 --- a/src/registrar/templates/domain_security_email.html +++ b/src/registrar/templates/domain_security_email.html @@ -1,18 +1,16 @@ {% extends "domain_base.html" %} {% load static field_helpers url_helpers %} -{% block title %}Domain security email | {{ domain.name }} | {% endblock %} +{% block title %}Security email | {{ domain.name }} | {% endblock %} {% block domain_content %} -

Domain security email

+

Security email

We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the .gov domain data we provide.

A security contact should be capable of evaluating or triaging security reports for your entire domain. Use a team email address, not an individual’s email. We recommend using an alias, like security@domain.gov.

- {% include "includes/required_fields.html" %} -
{% csrf_token %} diff --git a/src/registrar/templates/domain_your_contact_information.html b/src/registrar/templates/domain_your_contact_information.html index 81c62584c..e2cad735f 100644 --- a/src/registrar/templates/domain_your_contact_information.html +++ b/src/registrar/templates/domain_your_contact_information.html @@ -1,11 +1,11 @@ {% extends "domain_base.html" %} {% load static field_helpers %} -{% block title %}Domain contact information | {{ domain.name }} | {% endblock %} +{% block title %}Your contact information | {{ domain.name }} | {% endblock %} {% block domain_content %} -

Domain contact information

+

Your contact information

If you’d like us to use a different name, email, or phone number you can make those changes below. Updating your contact information here will update the contact information for all domains in your account. However, it won’t affect your Login.gov account information.

diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py index dd87a003a..51ace34f7 100644 --- a/src/registrar/tests/test_admin.py +++ b/src/registrar/tests/test_admin.py @@ -109,12 +109,12 @@ class TestDomainAdmin(MockEppLib): ) self.assertEqual(response.status_code, 200) self.assertContains(response, domain.name) - self.assertContains(response, "Delete Domain in Registry") + self.assertContains(response, "Delete domain in registry") # Test the info dialog request = self.factory.post( "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Delete Domain in Registry", "name": domain.name}, + {"_delete_domain": "Delete domain in registry", "name": domain.name}, follow=True, ) request.user = self.client @@ -149,12 +149,12 @@ class TestDomainAdmin(MockEppLib): ) self.assertEqual(response.status_code, 200) self.assertContains(response, domain.name) - self.assertContains(response, "Delete Domain in Registry") + self.assertContains(response, "Delete domain in registry") # Test the error request = self.factory.post( "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Delete Domain in Registry", "name": domain.name}, + {"_delete_domain": "Delete domain in registry", "name": domain.name}, follow=True, ) request.user = self.client @@ -194,12 +194,12 @@ class TestDomainAdmin(MockEppLib): ) self.assertEqual(response.status_code, 200) self.assertContains(response, domain.name) - self.assertContains(response, "Delete Domain in Registry") + self.assertContains(response, "Delete domain in registry") # Test the info dialog request = self.factory.post( "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Delete Domain in Registry", "name": domain.name}, + {"_delete_domain": "Delete domain in registry", "name": domain.name}, follow=True, ) request.user = self.client @@ -221,7 +221,7 @@ class TestDomainAdmin(MockEppLib): # Test the info dialog request = self.factory.post( "/admin/registrar/domain/{}/change/".format(domain.pk), - {"_delete_domain": "Delete Domain in Registry", "name": domain.name}, + {"_delete_domain": "Delete domain in registry", "name": domain.name}, follow=True, ) request.user = self.client diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py index 50456c2d5..46a6de004 100644 --- a/src/registrar/tests/test_models_domain.py +++ b/src/registrar/tests/test_models_domain.py @@ -16,6 +16,7 @@ from registrar.models.domain_information import DomainInformation from registrar.models.draft_domain import DraftDomain from registrar.models.public_contact import PublicContact from registrar.models.user import User +from registrar.models.utility.contact_error import ContactError, ContactErrorCodes from .common import MockEppLib from django_fsm import TransitionNotAllowed # type: ignore from epplibwrapper import ( @@ -193,6 +194,56 @@ class TestDomainCache(MockEppLib): self.assertEqual(cached_contact, in_db.registry_id) self.assertEqual(domain.security_contact.email, "123test@mail.gov") + def test_errors_map_epp_contact_to_public_contact(self): + """ + Scenario: Registrant gets invalid data from EPPLib + When the `map_epp_contact_to_public_contact` function + gets invalid data from EPPLib + Then the function throws the expected ContactErrors + """ + domain, _ = Domain.objects.get_or_create(name="registry.gov") + fakedEpp = self.fakedEppObject() + invalid_length = fakedEpp.dummyInfoContactResultData( + "Cymaticsisasubsetofmodalvibrationalphenomena", "lengthInvalid@mail.gov" + ) + valid_object = fakedEpp.dummyInfoContactResultData("valid", "valid@mail.gov") + + desired_error = ContactErrorCodes.CONTACT_ID_INVALID_LENGTH + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + invalid_length, + invalid_length.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) + + desired_error = ContactErrorCodes.CONTACT_ID_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + None, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) + + desired_error = ContactErrorCodes.CONTACT_INVALID_TYPE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + "bad_object", + valid_object.id, + PublicContact.ContactTypeChoices.SECURITY, + ) + self.assertEqual(context.exception.code, desired_error) + + desired_error = ContactErrorCodes.CONTACT_TYPE_NONE + with self.assertRaises(ContactError) as context: + domain.map_epp_contact_to_public_contact( + valid_object, + valid_object.id, + None, + ) + self.assertEqual(context.exception.code, desired_error) + class TestDomainCreation(MockEppLib): """Rule: An approved domain application must result in a domain""" diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py index 68aaf0ed8..2194b42db 100644 --- a/src/registrar/tests/test_views.py +++ b/src/registrar/tests/test_views.py @@ -1309,7 +1309,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): page = self.client.get( reverse("domain-nameservers", kwargs={"pk": self.domain.id}) ) - self.assertContains(page, "Domain name servers") + self.assertContains(page, "DNS name servers") @skip("Broken by adding registry connection fix in ticket 848") def test_domain_nameservers_form(self): @@ -1414,7 +1414,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): page = self.client.get( reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}) ) - self.assertContains(page, "Domain contact information") + self.assertContains(page, "Your contact information") def test_domain_your_contact_information_content(self): """Logged-in user's contact information appears on the page.""" @@ -1439,7 +1439,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): ) # Loads correctly - self.assertContains(page, "Domain security email") + self.assertContains(page, "Security email") self.assertContains(page, "security@mail.gov") self.mockSendPatch.stop() @@ -1455,7 +1455,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): ) # Loads correctly - self.assertContains(page, "Domain security email") + self.assertContains(page, "Security email") self.assertNotContains(page, "dotgov@cisa.dhs.gov") self.mockSendPatch.stop() @@ -1464,7 +1464,7 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest, MockEppLib): page = self.client.get( reverse("domain-security-email", kwargs={"pk": self.domain.id}) ) - self.assertContains(page, "Domain security email") + self.assertContains(page, "Security email") def test_domain_security_email_form(self): """Adding a security email works. diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py index d8c3c80fa..4ea3d2fbc 100644 --- a/src/registrar/views/domain.py +++ b/src/registrar/views/domain.py @@ -21,6 +21,7 @@ from registrar.models import ( User, UserDomainRole, ) +from registrar.models.public_contact import PublicContact from ..forms import ( ContactForm, @@ -42,6 +43,19 @@ class DomainView(DomainPermissionView): template_name = "domain_detail.html" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + default_email = Domain().get_default_security_contact().email + context["default_security_email"] = default_email + + security_email = self.get_object().get_security_email() + if security_email is None or security_email == default_email: + context["security_email"] = None + return context + context["security_email"] = security_email + return context + class DomainOrgNameAddressView(DomainPermissionView, FormMixin): """Organization name and mailing address view""" @@ -284,10 +298,21 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin): """The form is valid, call setter in model.""" # Set the security email from the form - new_email = form.cleaned_data.get("security_email", "") + new_email: str = form.cleaned_data.get("security_email", "") + + # If we pass nothing for the sec email, set to the default + if new_email is None or new_email.strip() == "": + new_email = PublicContact.get_default_security().email domain = self.get_object() contact = domain.security_contact + + # If no default is created for security_contact, + # then we cannot connect to the registry. + if contact is None: + messages.error(self.request, "Update failed. Cannot contact the registry.") + return redirect(self.get_success_url()) + contact.email = new_email contact.save()