From f918720383561bcad51f6ad6a66a3fe8245fa885 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 21 Nov 2024 14:57:48 -0800
Subject: [PATCH 001/135] Add screenreader text to no other contacts form
---
src/registrar/forms/domain_request_wizard.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index c7f5571af..a619aec26 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -733,7 +733,14 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
required=True,
# label has to end in a space to get the label_suffix to show
label=("No other employees rationale"),
- widget=forms.Textarea(),
+ widget=forms.Textarea(
+ attrs={
+ "aria-label": "You don’t need to provide names of other employees now, \
+ but it may slow down our assessment of your eligibility. Describe \
+ why there are no other employees who can help verify your request. \
+ You can enter up to 1000 characters."
+ }
+ ),
validators=[
MaxLengthValidator(
1000,
From e76571eb50d4eead1429aa936ff96b48259172d2 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 21 Nov 2024 17:05:08 -0800
Subject: [PATCH 002/135] Remove unused legends from radio groups
---
src/registrar/forms/domain_request_wizard.py | 1 +
.../forms/utility/wizard_form_helper.py | 10 +++++-
.../templates/django/forms/label.html | 36 +++++++++++--------
.../templates/includes/input_with_errors.html | 6 +++-
src/registrar/templatetags/field_helpers.py | 1 +
5 files changed, 37 insertions(+), 17 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index a619aec26..5d8f23057 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -544,6 +544,7 @@ class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
+ title_label = "Are there other employees who can help verify your request?"
field_name = "has_other_contacts"
@property
diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py
index eedf5839b..22c70b010 100644
--- a/src/registrar/forms/utility/wizard_form_helper.py
+++ b/src/registrar/forms/utility/wizard_form_helper.py
@@ -239,6 +239,10 @@ class BaseYesNoForm(RegistrarForm):
# Default form choice mapping. Default is suitable for most cases.
form_choices = ((True, "Yes"), (False, "No"))
+ # Option to append question to aria label for screenreader accessibility.
+ # Not added by default.
+ aria_label = ""
+
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
@@ -256,7 +260,11 @@ class BaseYesNoForm(RegistrarForm):
coerce=lambda x: x.lower() == "true" if x is not None else None,
choices=self.form_choices,
initial=self.get_initial_value(),
- widget=forms.RadioSelect,
+ widget=forms.RadioSelect(
+ attrs={
+ "aria-label": self.aria_label
+ }
+ ),
error_messages={
"required": self.required_error_message,
},
diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html
index 545ccf781..e4d7842fa 100644
--- a/src/registrar/templates/django/forms/label.html
+++ b/src/registrar/templates/django/forms/label.html
@@ -1,19 +1,25 @@
-<{{ label_tag }}
- class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
- {% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
->
- {% if span_for_text %}
- {{ field.label }}
- {% else %}
- {{ field.label }}
- {% endif %}
+
+{% if not type == "radio" and not label_tag == "legend" %}
+ <{{ label_tag }}
+ class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
+ {% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
+ >
+{% endif %}
- {% if widget.attrs.required %}
-
- {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
+ {% if span_for_text %}
+ {{ field.label }}
{% else %}
- *
+ {{ field.label }}
{% endif %}
- {% endif %}
-{{ label_tag }}>
+ {% if widget.attrs.required %}
+
+ {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
+ {% else %}
+ *
+ {% endif %}
+ {% endif %}
+
+{% if not type == "radio" and not label_tag == "legend" %}
+ {{ label_tag }}>
+{% endif %}
diff --git a/src/registrar/templates/includes/input_with_errors.html b/src/registrar/templates/includes/input_with_errors.html
index d1e53968e..d239954a0 100644
--- a/src/registrar/templates/includes/input_with_errors.html
+++ b/src/registrar/templates/includes/input_with_errors.html
@@ -68,7 +68,11 @@ error messages, if necessary.
{% endif %}
{# this is the input field, itself #}
- {% include widget.template_name %}
+ {% with aria_label=aria_label %}
+ {% include widget.template_name %}
+ {% endwith %}
+
+
{% if append_gov %}
.gov
diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py
index 8a80a75b9..d2bca13fb 100644
--- a/src/registrar/templatetags/field_helpers.py
+++ b/src/registrar/templatetags/field_helpers.py
@@ -169,5 +169,6 @@ def input_with_errors(context, field=None): # noqa: C901
) # -> {"widget": {"name": ...}}
context["widget"] = widget["widget"]
+ print("context: ", context)
return context
From 7cf8b8a82ea0a0d3df4885eaae45b1c4d0082dfc Mon Sep 17 00:00:00 2001
From: Matthew Spence
Date: Fri, 22 Nov 2024 10:51:13 -0600
Subject: [PATCH 003/135] Delete contacts and subdomains on delete domain
---
src/registrar/models/domain.py | 44 ++++++++++++++++++++++++++++++++++
1 file changed, 44 insertions(+)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 7fdc56971..03a969471 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1026,6 +1026,26 @@ class Domain(TimeStampedModel, DomainHelper):
# if registry error occurs, log the error, and raise it as well
logger.error(f"registry error removing client hold: {err}")
raise (err)
+
+ def _delete_contacts(self):
+ """Contacts associated with this domain will be deleted.
+ RegistryErrors will be logged and raised. Additional
+ error handling should be provided by the caller.
+ """
+ contacts = self._cache.get("contacts")
+ for contact in contacts:
+ self._delete_contact(contact)
+
+ def _delete_subdomains(self):
+ """Subdomains of this domain should be deleted from the registry.
+ Subdomains which are used by other domains (eg as a hostname) will
+ not be deleted.
+
+ Supresses registry error, as registry can disallow delete for various reasons
+ """
+ nameservers = [n[0] for n in self.nameservers]
+ hostsToDelete = self.createDeleteHostList(nameservers)
+ self._delete_hosts_if_not_used(hostsToDelete)
def _delete_domain(self):
"""This domain should be deleted from the registry
@@ -1431,6 +1451,8 @@ class Domain(TimeStampedModel, DomainHelper):
@transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
def deletedInEpp(self):
"""Domain is deleted in epp but is saved in our database.
+ Subdomains will be deleted first if not in use by another domain.
+ Contacts for this domain will also be deleted.
Error handling should be provided by the caller."""
# While we want to log errors, we want to preserve
# that information when this function is called.
@@ -1438,6 +1460,8 @@ class Domain(TimeStampedModel, DomainHelper):
# as doing everything here would reduce reliablity.
try:
logger.info("deletedInEpp()-> inside _delete_domain")
+ self._delete_subdomains()
+ self._delete_contacts()
self._delete_domain()
self.deleted = timezone.now()
except RegistryError as err:
@@ -1639,6 +1663,26 @@ class Domain(TimeStampedModel, DomainHelper):
)
raise e
+
+ def _delete_contact(self, contact: PublicContact):
+ """Try to delete a contact. RegistryErrors will be logged.
+
+ raises:
+ RegistryError: if the registry is unable to delete the contact
+ """
+ logger.info("_delete_contact() -> Attempting to delete contact for %s from domain %s", contact.name, contact.domain)
+ try:
+ req = commands.DeletContact(id=contact.registry_id)
+ return registry.send(req, cleaned=True).res_data[0]
+ except RegistryError as error:
+ logger.error(
+ "Registry threw error when trying to delete contact id %s contact type is %s, error code is\n %s full error is %s", # noqa
+ contact.registry_id,
+ contact.contact_type,
+ error.code,
+ error,
+ )
+ raise error
def is_ipv6(self, ip: str):
ip_addr = ipaddress.ip_address(ip)
From 36b78e9cea8207cf14b64c11271a3bbe6033464a Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Fri, 22 Nov 2024 11:43:31 -0800
Subject: [PATCH 004/135] Refactor legends from two separate legends to one
combined legend
---
.../forms/utility/wizard_form_helper.py | 5 +++--
.../templates/django/forms/label.html | 21 +++++++++----------
.../domain_request_other_contacts.html | 11 +++-------
src/registrar/templatetags/field_helpers.py | 6 ++++++
4 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py
index 22c70b010..d48f7af64 100644
--- a/src/registrar/forms/utility/wizard_form_helper.py
+++ b/src/registrar/forms/utility/wizard_form_helper.py
@@ -241,7 +241,8 @@ class BaseYesNoForm(RegistrarForm):
# Option to append question to aria label for screenreader accessibility.
# Not added by default.
- aria_label = ""
+ title_label = ""
+ aria_label = title_label.join("")
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
@@ -262,7 +263,7 @@ class BaseYesNoForm(RegistrarForm):
initial=self.get_initial_value(),
widget=forms.RadioSelect(
attrs={
- "aria-label": self.aria_label
+ # "aria-label": self.title_label
}
),
error_messages={
diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html
index e4d7842fa..eb9604dec 100644
--- a/src/registrar/templates/django/forms/label.html
+++ b/src/registrar/templates/django/forms/label.html
@@ -1,25 +1,24 @@
-{% if not type == "radio" and not label_tag == "legend" %}
- <{{ label_tag }}
- class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
- {% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
- >
-{% endif %}
-
+<{{ label_tag }}
+ class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
+ {% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
+>
+ {% if legend_label %}
+
{{ legend_label }}
+ {% else %}
{% if span_for_text %}
{{ field.label }}
{% else %}
{{ field.label }}
{% endif %}
+ {% endif %}
{% if widget.attrs.required %}
- {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
+ {% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." or field.label == "Has other contacts" %}
{% else %}
*
{% endif %}
{% endif %}
-{% if not type == "radio" and not label_tag == "legend" %}
- {{ label_tag }}>
-{% endif %}
+{{ label_tag }}>
diff --git a/src/registrar/templates/domain_request_other_contacts.html b/src/registrar/templates/domain_request_other_contacts.html
index 72e4abd8b..93fafe872 100644
--- a/src/registrar/templates/domain_request_other_contacts.html
+++ b/src/registrar/templates/domain_request_other_contacts.html
@@ -17,17 +17,12 @@
{% endblock %}
{% block form_fields %}
-
- {% include "includes/required_fields.html" %}
{{ forms.1.management_form }}
{# forms.1 is a formset and this iterates over its forms #}
{% for form in forms.1.forms %}
From 8154d25873208a08234c34490253ecdbb8d867a3 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Tue, 3 Dec 2024 14:55:25 -0800
Subject: [PATCH 013/135] Add aria label for additional details form
---
src/registrar/forms/domain_request_wizard.py | 7 ++++++-
src/registrar/templates/django/forms/label.html | 2 +-
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 5d8f23057..5ce50dc0c 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -789,7 +789,12 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
anything_else = forms.CharField(
required=True,
label="Anything else?",
- widget=forms.Textarea(),
+ widget=forms.Textarea(
+ attrs={
+ "aria-label": "Is there anything else you’d like us to know about your domain request? Provide details below. \
+ You can enter up to 2000 characters"
+ }
+ ),
validators=[
MaxLengthValidator(
2000,
diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html
index 422186522..2852ce2ba 100644
--- a/src/registrar/templates/django/forms/label.html
+++ b/src/registrar/templates/django/forms/label.html
@@ -3,7 +3,7 @@
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
>
{% if legend_label %}
-
{{ legend_label }}
+
{{ legend_label }}
{% if widget.attrs.id == 'id_additional_details-has_cisa_representative' %}
.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has 10 regions that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.
{% endif %}
From 27868a0aed8f1fe6fad6566788b281ca761dc163 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 11:24:49 -0600
Subject: [PATCH 014/135] minor fixes to tests
---
src/registrar/models/domain.py | 10 ++--
src/registrar/tests/common.py | 11 ++--
src/registrar/tests/test_models_domain.py | 71 ++++++++++++-----------
3 files changed, 46 insertions(+), 46 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 2f5524ab4..9c954b073 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -161,12 +161,12 @@ class Domain(TimeStampedModel, DomainHelper):
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
help_texts = {
# For now, unknown has the same message as DNS_NEEDED
- cls.UNKNOWN: ("Before this domain can be used, " "you’ll need to add name server addresses."),
- cls.DNS_NEEDED: ("Before this domain can be used, " "you’ll need to add name server addresses."),
+ cls.UNKNOWN: ("Before this domain can be used, " "you'll need to add name server addresses."),
+ cls.DNS_NEEDED: ("Before this domain can be used, " "you'll need to add name server addresses."),
cls.READY: "This domain has name servers and is ready for use.",
cls.ON_HOLD: (
"This domain is administratively paused, "
- "so it can’t be edited and won’t resolve in DNS. "
+ "so it can't be edited and won't resolve in DNS. "
"Contact help@get.gov for details."
),
cls.DELETED: ("This domain has been removed and " "is no longer registered to your organization."),
@@ -1060,11 +1060,11 @@ class Domain(TimeStampedModel, DomainHelper):
"""
logger.info("Deleting nameservers for %s", self.name)
nameservers = [n[0] for n in self.nameservers]
- logger.info("Nameservers found: %s", nameservers)
hostsToDelete, _ = self.createDeleteHostList(nameservers)
logger.debug("HostsToDelete from %s inside _delete_subdomains -> %s", self.name, hostsToDelete)
- self._delete_hosts_if_not_used(hostsToDelete)
+ for objSet in hostsToDelete:
+ self._delete_hosts_if_not_used(objSet.hosts)
def _delete_domain(self):
"""This domain should be deleted from the registry
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index ac444c8aa..72a315e9b 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1620,7 +1620,7 @@ class MockEppLib(TestCase):
case commands.UpdateHost:
return self.mockUpdateHostCommands(_request, cleaned)
case commands.DeleteHost:
- return self.mockDeletHostCommands(_request, cleaned)
+ return self.mockDeleteHostCommands(_request, cleaned)
case commands.CheckDomain:
return self.mockCheckDomainCommand(_request, cleaned)
case commands.DeleteDomain:
@@ -1673,11 +1673,10 @@ class MockEppLib(TestCase):
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
- def mockDeletHostCommands(self, _request, cleaned):
- hosts = getattr(_request, "name", None).hosts
- for host in hosts:
- if "sharedhost.com" in host:
- raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
+ def mockDeleteHostCommands(self, _request, cleaned):
+ host = getattr(_request, "name", None)
+ if "sharedhost.com" in host:
+ raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
return MagicMock(
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 73691bb69..b013c7811 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -1422,40 +1422,41 @@ class TestRegistrantNameservers(MockEppLib):
And `domain.is_active` returns False
"""
- with less_console_noise():
- self.domainWithThreeNS.nameservers = [(self.nameserver1,)]
- expectedCalls = [
- call(
- commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
- cleaned=True,
+ # with less_console_noise():
+ self.domainWithThreeNS.nameservers = [(self.nameserver1,)]
+ expectedCalls = [
+ call(
+ commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
+ cleaned=True,
+ ),
+ call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True),
+ call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True),
+ call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True),
+ call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True),
+ call(
+ commands.UpdateDomain(
+ name=self.domainWithThreeNS.name,
+ add=[],
+ rem=[
+ common.HostObjSet(
+ hosts=[
+ "ns1.my-nameserver-2.com",
+ "ns1.cats-are-superior3.com",
+ ]
+ ),
+ ],
+ nsset=None,
+ keyset=None,
+ registrant=None,
+ auth_info=None,
),
- call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True),
- call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True),
- call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True),
- call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True),
- call(
- commands.UpdateDomain(
- name=self.domainWithThreeNS.name,
- add=[],
- rem=[
- common.HostObjSet(
- hosts=[
- "ns1.my-nameserver-2.com",
- "ns1.cats-are-superior3.com",
- ]
- ),
- ],
- nsset=None,
- keyset=None,
- registrant=None,
- auth_info=None,
- ),
- cleaned=True,
- ),
- call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
- ]
- self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
- self.assertFalse(self.domainWithThreeNS.is_active())
+ cleaned=True,
+ ),
+ call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
+ ]
+
+ self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
+ self.assertFalse(self.domainWithThreeNS.is_active())
def test_user_replaces_nameservers(self):
"""
@@ -2642,7 +2643,7 @@ class TestAnalystDelete(MockEppLib):
self.mockedSendFunction.assert_has_calls(
[
call(
- commands.DeleteHost(name=common.HostObjSet(hosts=['ns1.sharedhost.com'])),
+ commands.DeleteHost(name='ns1.sharedhost.com'),
cleaned=True,
),
]
@@ -2674,7 +2675,7 @@ class TestAnalystDelete(MockEppLib):
# Check that the host and contacts are deleted, order doesn't matter
self.mockedSendFunction.assert_has_calls(
[
- call(commands.DeleteHost(name=common.HostObjSet(hosts=['fake.host.com'])), cleaned=True),
+ call(commands.DeleteHost(name='fake.host.com'), cleaned=True),
call(commands.DeleteContact(id="securityContact"), cleaned=True),
call(commands.DeleteContact(id="technicalContact"), cleaned=True),
call(commands.DeleteContact(id="adminContact"),cleaned=True,)
From 9437b732c8a475d3b5216ca9c805942e3507b586 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 12:50:28 -0600
Subject: [PATCH 015/135] minor test fix
---
src/registrar/tests/test_admin_domain.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py
index f02b59a91..ee275741c 100644
--- a/src/registrar/tests/test_admin_domain.py
+++ b/src/registrar/tests/test_admin_domain.py
@@ -16,6 +16,7 @@ from registrar.models import (
Host,
Portfolio,
)
+from registrar.models.public_contact import PublicContact
from registrar.models.user_domain_role import UserDomainRole
from .common import (
MockSESClient,
@@ -59,6 +60,7 @@ class TestDomainAdminAsStaff(MockEppLib):
def tearDown(self):
super().tearDown()
Host.objects.all().delete()
+ PublicContact.objects.all().delete()
Domain.objects.all().delete()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
From 89253a1626f9e4cf6a24f0d8ed4ef203822a8e30 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 13:37:23 -0600
Subject: [PATCH 016/135] linter fixes
---
src/registrar/models/domain.py | 25 ++++++++---------------
src/registrar/tests/common.py | 10 ++++-----
src/registrar/tests/test_models_domain.py | 13 +++++++-----
3 files changed, 22 insertions(+), 26 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 9c954b073..64d29a21a 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -747,11 +747,7 @@ class Domain(TimeStampedModel, DomainHelper):
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
- try:
- self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
- except:
- # in this case we don't care if there's an error, and it will be logged in the function.
- pass
+ self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
if successTotalNameservers < 2:
try:
@@ -1034,10 +1030,10 @@ class Domain(TimeStampedModel, DomainHelper):
# if registry error occurs, log the error, and raise it as well
logger.error(f"registry error removing client hold: {err}")
raise (err)
-
+
def _delete_nonregistrant_contacts(self):
"""Non-registrant contacts associated with this domain will be deleted.
- RegistryErrors will be logged and raised. Error
+ RegistryErrors will be logged and raised. Error
handling should be provided by the caller.
"""
logger.info("Deleting contacts for %s", self.name)
@@ -1048,8 +1044,7 @@ class Domain(TimeStampedModel, DomainHelper):
# registrants have to be deleted after the domain
if contact != PublicContact.ContactTypeChoices.REGISTRANT:
self._delete_contact(contact, id)
-
-
+
def _delete_subdomains(self):
"""Subdomains of this domain should be deleted from the registry.
Subdomains which are used by other domains (eg as a hostname) will
@@ -1690,10 +1685,10 @@ class Domain(TimeStampedModel, DomainHelper):
)
raise e
-
+
def _delete_contact(self, contact_name: str, registry_id: str):
"""Try to delete a contact from the registry.
-
+
raises:
RegistryError: if the registry is unable to delete the contact
"""
@@ -1703,10 +1698,8 @@ class Domain(TimeStampedModel, DomainHelper):
return registry.send(req, cleaned=True).res_data[0]
except RegistryError as error:
logger.error(
- "Registry threw error when trying to delete contact id %s contact type is %s, error code is\n %s full error is %s", # noqa
- contact.registry_id,
- contact.contact_type,
- error.code,
+ "Registry threw error when trying to delete contact %s, error: %s", # noqa
+ contact_name,
error,
)
raise error
@@ -1834,7 +1827,7 @@ class Domain(TimeStampedModel, DomainHelper):
logger.info("Did not remove host %s because it is in use on another domain." % nameserver)
else:
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
-
+
raise e
def _fix_unknown_state(self, cleaned):
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 72a315e9b..79c262cb9 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1672,7 +1672,7 @@ class MockEppLib(TestCase):
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
-
+
def mockDeleteHostCommands(self, _request, cleaned):
host = getattr(_request, "name", None)
if "sharedhost.com" in host:
@@ -1814,15 +1814,15 @@ class MockEppLib(TestCase):
# mocks a contact error on creation
raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
return MagicMock(res_data=[self.mockDataInfoHosts])
-
+
def mockDeleteContactCommands(self, _request, cleaned):
if getattr(_request, "id", None) == "fail":
raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
else:
return MagicMock(
- res_data=[self.mockDataInfoContact],
- code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
- )
+ res_data=[self.mockDataInfoContact],
+ code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
+ )
def setUp(self):
"""mock epp send function as this will fail locally"""
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index b013c7811..e381a06fe 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -2643,7 +2643,7 @@ class TestAnalystDelete(MockEppLib):
self.mockedSendFunction.assert_has_calls(
[
call(
- commands.DeleteHost(name='ns1.sharedhost.com'),
+ commands.DeleteHost(name="ns1.sharedhost.com"),
cleaned=True,
),
]
@@ -2664,7 +2664,7 @@ class TestAnalystDelete(MockEppLib):
And `state` is set to `DELETED`
"""
# with less_console_noise():
- # Desired domain
+ # Desired domain
domain, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.ON_HOLD)
# Put the domain in client hold
domain.place_client_hold()
@@ -2675,12 +2675,15 @@ class TestAnalystDelete(MockEppLib):
# Check that the host and contacts are deleted, order doesn't matter
self.mockedSendFunction.assert_has_calls(
[
- call(commands.DeleteHost(name='fake.host.com'), cleaned=True),
+ call(commands.DeleteHost(name="fake.host.com"), cleaned=True),
call(commands.DeleteContact(id="securityContact"), cleaned=True),
call(commands.DeleteContact(id="technicalContact"), cleaned=True),
- call(commands.DeleteContact(id="adminContact"),cleaned=True,)
+ call(
+ commands.DeleteContact(id="adminContact"),
+ cleaned=True,
+ ),
],
- any_order=True
+ any_order=True,
)
# These calls need to be in order
From f25bb9be055835866c004a827e7241ef0485c1cd Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 16:28:33 -0600
Subject: [PATCH 017/135] include hostname in error messages for shared hosts
---
src/registrar/models/domain.py | 23 ++++++-
src/registrar/tests/common.py | 1 +
src/registrar/tests/test_models_domain.py | 79 +++++++++++------------
3 files changed, 59 insertions(+), 44 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 64d29a21a..61cc539b0 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -231,6 +231,14 @@ class Domain(TimeStampedModel, DomainHelper):
"""Called during delete. Example: `del domain.registrant`."""
super().__delete__(obj)
+ def save(
+ self, force_insert=False, force_update=False, using=None, update_fields=None
+ ):
+ # If the domain is deleted we don't want the expiration date to be set
+ if self.state == self.State.DELETED and self.expiration_date:
+ self.expiration_date = None
+ super().save(force_insert, force_update, using, update_fields)
+
@classmethod
def available(cls, domain: str) -> bool:
"""Check if a domain is available.
@@ -1054,6 +1062,13 @@ class Domain(TimeStampedModel, DomainHelper):
RegistryError: if any subdomain cannot be deleted
"""
logger.info("Deleting nameservers for %s", self.name)
+ # check if any nameservers are in use by another domain
+ hosts = Host.objects.filter(name__regex=r'.+{}'.format(self.name))
+ for host in hosts:
+ if host.domain != self:
+ logger.error("Host %s in use by another domain: %s", host.name, host.domain)
+ raise RegistryError("Host in use by another domain: {}".format(host.domain), code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
+
nameservers = [n[0] for n in self.nameservers]
hostsToDelete, _ = self.createDeleteHostList(nameservers)
logger.debug("HostsToDelete from %s inside _delete_subdomains -> %s", self.name, hostsToDelete)
@@ -1070,9 +1085,10 @@ class Domain(TimeStampedModel, DomainHelper):
def _delete_domain_registrant(self):
"""This domain's registrant should be deleted from the registry
may raises RegistryError, should be caught or handled correctly by caller"""
- registrantID = self.registrant_contact.registry_id
- request = commands.DeleteContact(id=registrantID)
- registry.send(request, cleaned=True)
+ if self.registrant_contact:
+ registrantID = self.registrant_contact.registry_id
+ request = commands.DeleteContact(id=registrantID)
+ registry.send(request, cleaned=True)
def __str__(self) -> str:
return self.name
@@ -1486,6 +1502,7 @@ class Domain(TimeStampedModel, DomainHelper):
self._delete_domain()
self._delete_domain_registrant()
self.deleted = timezone.now()
+ self.expiration_date = None
except RegistryError as err:
logger.error(f"Could not delete domain. Registry returned error: {err}")
raise err
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 79c262cb9..16fa58104 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1676,6 +1676,7 @@ class MockEppLib(TestCase):
def mockDeleteHostCommands(self, _request, cleaned):
host = getattr(_request, "name", None)
if "sharedhost.com" in host:
+ print("raising registry error")
raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
return MagicMock(
res_data=[self.mockDataHostChange],
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index e381a06fe..8dfb764e3 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -2584,6 +2584,7 @@ class TestAnalystDelete(MockEppLib):
super().setUp()
self.domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
self.domain_on_hold, _ = Domain.objects.get_or_create(name="fake-on-hold.gov", state=Domain.State.ON_HOLD)
+ Host.objects.create(name="ns1.sharingiscaring.gov", domain=self.domain_on_hold)
def tearDown(self):
Host.objects.all().delete()
@@ -2639,15 +2640,9 @@ class TestAnalystDelete(MockEppLib):
with self.assertRaises(RegistryError) as err:
domain.deletedInEpp()
domain.save()
+
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
- self.mockedSendFunction.assert_has_calls(
- [
- call(
- commands.DeleteHost(name="ns1.sharedhost.com"),
- cleaned=True,
- ),
- ]
- )
+ self.assertEqual(err.msg, "Host in use by another domain: fake-on-hold.gov")
# Domain itself should not be deleted
self.assertNotEqual(domain, None)
# State should not have changed
@@ -2663,41 +2658,43 @@ class TestAnalystDelete(MockEppLib):
Then `commands.DeleteContact` is sent to the registry for the registrant contact
And `state` is set to `DELETED`
"""
- # with less_console_noise():
- # Desired domain
- domain, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.ON_HOLD)
- # Put the domain in client hold
- domain.place_client_hold()
- # Delete it
- domain.deletedInEpp()
- domain.save()
+ with less_console_noise():
+ # Desired domain
+ domain, _ = Domain.objects.get_or_create(name="freeman.gov", state=Domain.State.ON_HOLD)
+ # Put the domain in client hold
+ domain.place_client_hold()
+ # Delete it
+ domain.deletedInEpp()
+ domain.save()
- # Check that the host and contacts are deleted, order doesn't matter
- self.mockedSendFunction.assert_has_calls(
- [
- call(commands.DeleteHost(name="fake.host.com"), cleaned=True),
- call(commands.DeleteContact(id="securityContact"), cleaned=True),
- call(commands.DeleteContact(id="technicalContact"), cleaned=True),
- call(
- commands.DeleteContact(id="adminContact"),
- cleaned=True,
- ),
- ],
- any_order=True,
- )
+ # Check that the host and contacts are deleted, order doesn't matter
+ self.mockedSendFunction.assert_has_calls(
+ [
+ call(commands.DeleteHost(name="fake.host.com"), cleaned=True),
+ call(commands.DeleteContact(id="securityContact"), cleaned=True),
+ call(commands.DeleteContact(id="technicalContact"), cleaned=True),
+ call(
+ commands.DeleteContact(id="adminContact"),
+ cleaned=True,
+ ),
+ ],
+ any_order=True,
+ )
+ actual_calls = self.mockedSendFunction.call_args_list
+ print("actual_calls", actual_calls)
- # These calls need to be in order
- self.mockedSendFunction.assert_has_calls(
- [
- call(commands.DeleteDomain(name="freeman.gov"), cleaned=True),
- call(commands.InfoContact(id="regContact"), cleaned=True),
- call(commands.DeleteContact(id="regContact"), cleaned=True),
- ],
- )
- # Domain itself should not be deleted
- self.assertNotEqual(domain, None)
- # State should have changed
- self.assertEqual(domain.state, Domain.State.DELETED)
+ # These calls need to be in order
+ self.mockedSendFunction.assert_has_calls(
+ [
+ call(commands.DeleteDomain(name="freeman.gov"), cleaned=True),
+ call(commands.InfoContact(id="regContact"), cleaned=True),
+ call(commands.DeleteContact(id="regContact"), cleaned=True),
+ ],
+ )
+ # Domain itself should not be deleted
+ self.assertNotEqual(domain, None)
+ # State should have changed
+ self.assertEqual(domain.state, Domain.State.DELETED)
def test_deletion_ready_fsm_failure(self):
"""
From dad42264bf6293765c107dac17861bec078b7357 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 16:32:17 -0600
Subject: [PATCH 018/135] add back in less console noise decorator
---
src/registrar/tests/test_models_domain.py | 110 +++++++++++-----------
1 file changed, 55 insertions(+), 55 deletions(-)
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 8dfb764e3..8fd2b5411 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -1422,41 +1422,41 @@ class TestRegistrantNameservers(MockEppLib):
And `domain.is_active` returns False
"""
- # with less_console_noise():
- self.domainWithThreeNS.nameservers = [(self.nameserver1,)]
- expectedCalls = [
- call(
- commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
- cleaned=True,
- ),
- call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True),
- call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True),
- call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True),
- call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True),
- call(
- commands.UpdateDomain(
- name=self.domainWithThreeNS.name,
- add=[],
- rem=[
- common.HostObjSet(
- hosts=[
- "ns1.my-nameserver-2.com",
- "ns1.cats-are-superior3.com",
- ]
- ),
- ],
- nsset=None,
- keyset=None,
- registrant=None,
- auth_info=None,
+ with less_console_noise():
+ self.domainWithThreeNS.nameservers = [(self.nameserver1,)]
+ expectedCalls = [
+ call(
+ commands.InfoDomain(name=self.domainWithThreeNS.name, auth_info=None),
+ cleaned=True,
),
- cleaned=True,
- ),
- call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
- ]
+ call(commands.InfoHost(name="ns1.my-nameserver-1.com"), cleaned=True),
+ call(commands.InfoHost(name="ns1.my-nameserver-2.com"), cleaned=True),
+ call(commands.InfoHost(name="ns1.cats-are-superior3.com"), cleaned=True),
+ call(commands.DeleteHost(name="ns1.my-nameserver-2.com"), cleaned=True),
+ call(
+ commands.UpdateDomain(
+ name=self.domainWithThreeNS.name,
+ add=[],
+ rem=[
+ common.HostObjSet(
+ hosts=[
+ "ns1.my-nameserver-2.com",
+ "ns1.cats-are-superior3.com",
+ ]
+ ),
+ ],
+ nsset=None,
+ keyset=None,
+ registrant=None,
+ auth_info=None,
+ ),
+ cleaned=True,
+ ),
+ call(commands.DeleteHost(name="ns1.cats-are-superior3.com"), cleaned=True),
+ ]
- self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
- self.assertFalse(self.domainWithThreeNS.is_active())
+ self.mockedSendFunction.assert_has_calls(expectedCalls, any_order=True)
+ self.assertFalse(self.domainWithThreeNS.is_active())
def test_user_replaces_nameservers(self):
"""
@@ -2601,28 +2601,28 @@ class TestAnalystDelete(MockEppLib):
The deleted date is set.
"""
- # with less_console_noise():
- # Put the domain in client hold
- self.domain.place_client_hold()
- # Delete it...
- self.domain.deletedInEpp()
- self.domain.save()
- self.mockedSendFunction.assert_has_calls(
- [
- call(
- commands.DeleteDomain(name="fake.gov"),
- cleaned=True,
- )
- ]
- )
- # Domain itself should not be deleted
- self.assertNotEqual(self.domain, None)
- # Domain should have the right state
- self.assertEqual(self.domain.state, Domain.State.DELETED)
- # Domain should have a deleted
- self.assertNotEqual(self.domain.deleted, None)
- # Cache should be invalidated
- self.assertEqual(self.domain._cache, {})
+ with less_console_noise():
+ # Put the domain in client hold
+ self.domain.place_client_hold()
+ # Delete it...
+ self.domain.deletedInEpp()
+ self.domain.save()
+ self.mockedSendFunction.assert_has_calls(
+ [
+ call(
+ commands.DeleteDomain(name="fake.gov"),
+ cleaned=True,
+ )
+ ]
+ )
+ # Domain itself should not be deleted
+ self.assertNotEqual(self.domain, None)
+ # Domain should have the right state
+ self.assertEqual(self.domain.state, Domain.State.DELETED)
+ # Domain should have a deleted
+ self.assertNotEqual(self.domain.deleted, None)
+ # Cache should be invalidated
+ self.assertEqual(self.domain._cache, {})
def test_deletion_is_unsuccessful(self):
"""
From 6fdb763c0249bdc46684a0d8d2e3928a442fae43 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Wed, 4 Dec 2024 17:10:45 -0600
Subject: [PATCH 019/135] admin fix
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 40d4befb5..042666619 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1570,7 +1570,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"is_policy_acknowledged",
]
- # For each filter_horizontal, init in admin js initFilterHorizontalWidget
+ # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
From e8fdf0c5d376b2b94647ff782d59adfdf6d957f5 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 10:16:40 -0600
Subject: [PATCH 020/135] revert accidental admin change
---
src/registrar/admin.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 042666619..40d4befb5 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -1570,7 +1570,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"is_policy_acknowledged",
]
- # For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
+ # For each filter_horizontal, init in admin js initFilterHorizontalWidget
# to activate the edit/delete/view buttons
filter_horizontal = ("other_contacts",)
From 3f79b562bd9db55af9eb5aac5bf08c3aca61a962 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 10:58:12 -0600
Subject: [PATCH 021/135] temp test changes
---
src/registrar/models/domain.py | 6 +++---
src/registrar/tests/test_views.py | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 61cc539b0..6ca3676f7 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -161,12 +161,12 @@ class Domain(TimeStampedModel, DomainHelper):
"""Returns a help message for a desired state. If none is found, an empty string is returned"""
help_texts = {
# For now, unknown has the same message as DNS_NEEDED
- cls.UNKNOWN: ("Before this domain can be used, " "you'll need to add name server addresses."),
- cls.DNS_NEEDED: ("Before this domain can be used, " "you'll need to add name server addresses."),
+ cls.UNKNOWN: ("Before this domain can be used, " "you’ll need to add name server addresses."),
+ cls.DNS_NEEDED: ("Before this domain can be used, " "you’ll need to add name server addresses."),
cls.READY: "This domain has name servers and is ready for use.",
cls.ON_HOLD: (
"This domain is administratively paused, "
- "so it can't be edited and won't resolve in DNS. "
+ "so it can’t be edited and won’t resolve in DNS. "
"Contact help@get.gov for details."
),
cls.DELETED: ("This domain has been removed and " "is no longer registered to your organization."),
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index f46e417be..3c1f1959e 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -169,7 +169,7 @@ class HomeTests(TestWithUser):
self.assertContains(response, "You don't have any registered domains.")
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
- @less_console_noise_decorator
+ # @less_console_noise_decorator
def test_state_help_text(self):
"""Tests if each domain state has help text"""
From 2e841711e112cf0d1482dd42e19d839d86cfbbac Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 11:30:16 -0600
Subject: [PATCH 022/135] fix a test
---
src/registrar/models/domain.py | 2 +-
src/registrar/tests/test_views.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6ca3676f7..661e958e6 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1154,7 +1154,7 @@ class Domain(TimeStampedModel, DomainHelper):
Returns True if expired, False otherwise.
"""
if self.expiration_date is None:
- return True
+ return self.state != self.State.DELETED
now = timezone.now().date()
return self.expiration_date < now
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 3c1f1959e..f46e417be 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -169,7 +169,7 @@ class HomeTests(TestWithUser):
self.assertContains(response, "You don't have any registered domains.")
self.assertContains(response, "Why don't I see my domain when I sign in to the registrar?")
- # @less_console_noise_decorator
+ @less_console_noise_decorator
def test_state_help_text(self):
"""Tests if each domain state has help text"""
From aaaa4f21d238e1e46b0010741cf7be55a7a41822 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 12:50:25 -0600
Subject: [PATCH 023/135] fix broken test
---
src/registrar/models/domain.py | 13 +++++++------
src/registrar/tests/test_models_domain.py | 2 +-
src/registrar/tests/test_reports.py | 8 ++++----
src/registrar/utility/csv_export.py | 3 ++-
4 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 661e958e6..348ccf3ad 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -231,9 +231,7 @@ class Domain(TimeStampedModel, DomainHelper):
"""Called during delete. Example: `del domain.registrant`."""
super().__delete__(obj)
- def save(
- self, force_insert=False, force_update=False, using=None, update_fields=None
- ):
+ def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
# If the domain is deleted we don't want the expiration date to be set
if self.state == self.State.DELETED and self.expiration_date:
self.expiration_date = None
@@ -1063,12 +1061,15 @@ class Domain(TimeStampedModel, DomainHelper):
"""
logger.info("Deleting nameservers for %s", self.name)
# check if any nameservers are in use by another domain
- hosts = Host.objects.filter(name__regex=r'.+{}'.format(self.name))
+ hosts = Host.objects.filter(name__regex=r".+{}".format(self.name))
for host in hosts:
if host.domain != self:
logger.error("Host %s in use by another domain: %s", host.name, host.domain)
- raise RegistryError("Host in use by another domain: {}".format(host.domain), code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
-
+ raise RegistryError(
+ "Host in use by another domain: {}".format(host.domain),
+ code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
+ )
+
nameservers = [n[0] for n in self.nameservers]
hostsToDelete, _ = self.createDeleteHostList(nameservers)
logger.debug("HostsToDelete from %s inside _delete_subdomains -> %s", self.name, hostsToDelete)
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index 8fd2b5411..e5df19d82 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -2640,7 +2640,7 @@ class TestAnalystDelete(MockEppLib):
with self.assertRaises(RegistryError) as err:
domain.deletedInEpp()
domain.save()
-
+
self.assertTrue(err.is_client_error() and err.code == ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
self.assertEqual(err.msg, "Host in use by another domain: fake-on-hold.gov")
# Domain itself should not be deleted
diff --git a/src/registrar/tests/test_reports.py b/src/registrar/tests/test_reports.py
index 377216aa4..0c3fad51a 100644
--- a/src/registrar/tests/test_reports.py
+++ b/src/registrar/tests/test_reports.py
@@ -880,18 +880,18 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
"Email,Organization admin,Invited by,Joined date,Last active,Domain requests,"
"Member management,Domain management,Number of domains,Domains\n"
# Content
+ "big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n"
+ "cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer,Viewer,False,0,\n"
+ "icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
"meoward@rocks.com,False,big_lebowski@dude.co,2022-04-01,Invalid date,None,"
'Manager,True,2,"adomain2.gov,cdomain1.gov"\n'
- "big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,Viewer,True,1,cdomain1.gov\n"
- "tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
- "icy_superuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer Requester,Manager,False,0,\n"
- "cozy_staffuser@igorville.gov,True,help@get.gov,2022-04-01,2024-02-01,Viewer,Viewer,False,0,\n"
"nonexistentmember_1@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Manager,False,0,\n"
"nonexistentmember_2@igorville.gov,False,help@get.gov,Unretrieved,Invited,None,Viewer,False,0,\n"
"nonexistentmember_3@igorville.gov,False,help@get.gov,Unretrieved,Invited,Viewer,None,False,0,\n"
"nonexistentmember_4@igorville.gov,True,help@get.gov,Unretrieved,"
"Invited,Viewer Requester,Manager,False,0,\n"
"nonexistentmember_5@igorville.gov,True,help@get.gov,Unretrieved,Invited,Viewer,Viewer,False,0,\n"
+ "tired_sleepy@igorville.gov,False,System,2022-04-01,Invalid date,Viewer,None,False,0,\n"
)
# Normalize line endings and remove commas,
# spaces and leading/trailing whitespace
diff --git a/src/registrar/utility/csv_export.py b/src/registrar/utility/csv_export.py
index a03e51de5..48a5f9e2d 100644
--- a/src/registrar/utility/csv_export.py
+++ b/src/registrar/utility/csv_export.py
@@ -415,7 +415,8 @@ class MemberExport(BaseExport):
.values(*shared_columns)
)
- return convert_queryset_to_dict(permissions.union(invitations), is_model=False)
+ members = permissions.union(invitations).order_by("email_display")
+ return convert_queryset_to_dict(members, is_model=False)
@classmethod
def get_invited_by_query(cls, object_id_query):
From b9ea3d884665f863f18cc3852af6f1de6b97a469 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 11:31:58 -0800
Subject: [PATCH 024/135] Save changes
---
src/registrar/forms/domain_request_wizard.py | 7 +------
.../templates/domain_request_additional_details.html | 2 +-
2 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 5ce50dc0c..5d8f23057 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -789,12 +789,7 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
anything_else = forms.CharField(
required=True,
label="Anything else?",
- widget=forms.Textarea(
- attrs={
- "aria-label": "Is there anything else you’d like us to know about your domain request? Provide details below. \
- You can enter up to 2000 characters"
- }
- ),
+ widget=forms.Textarea(),
validators=[
MaxLengthValidator(
2000,
diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html
index 386a7a4af..6e28e5869 100644
--- a/src/registrar/templates/domain_request_additional_details.html
+++ b/src/registrar/templates/domain_request_additional_details.html
@@ -34,7 +34,7 @@
Provide details below. *
- {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" add_legend_class="usa-sr-only" add_legend_label="Is there anything else you’d like us to know about your domain request?" add_aria_label="Provide details below. You can enter up to 2000 characters" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
From 8b473d5e1846d80d4495ff5b375a2136c8b14f53 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 13:55:32 -0600
Subject: [PATCH 025/135] add error message to registry errors
---
src/epplibwrapper/errors.py | 3 ++-
src/registrar/admin.py | 5 ++---
src/registrar/models/domain.py | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py
index 2b7bdd255..4ded1e5a7 100644
--- a/src/epplibwrapper/errors.py
+++ b/src/epplibwrapper/errors.py
@@ -62,9 +62,10 @@ class RegistryError(Exception):
- 2501 - 2502 Something malicious or abusive may have occurred
"""
- def __init__(self, *args, code=None, **kwargs):
+ def __init__(self, *args, code=None, msg=None,**kwargs):
super().__init__(*args, **kwargs)
self.code = code
+ self.msg = msg
def should_retry(self):
return self.code == ErrorCode.COMMAND_FAILED
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 40d4befb5..6bafbab08 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -2916,18 +2916,17 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
except RegistryError as err:
# Using variables to get past the linter
message1 = f"Cannot delete Domain when in state {obj.state}"
- message2 = "This subdomain is being used as a hostname on another domain"
# Human-readable mappings of ErrorCodes. Can be expanded.
error_messages = {
# noqa on these items as black wants to reformat to an invalid length
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
- ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
+ ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: err.msg,
}
message = "Cannot connect to the registry"
if not err.is_connection_error():
# If nothing is found, will default to returned err
- message = error_messages.get(err.code, err)
+ message = error_messages[err.code]
self.message_user(request, f"Error deleting this Domain: {message}", messages.ERROR)
except TransitionNotAllowed:
if obj.state == Domain.State.DELETED:
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 348ccf3ad..f4922bfdd 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1066,7 +1066,7 @@ class Domain(TimeStampedModel, DomainHelper):
if host.domain != self:
logger.error("Host %s in use by another domain: %s", host.name, host.domain)
raise RegistryError(
- "Host in use by another domain: {}".format(host.domain),
+ msg="Host in use by another domain: {}".format(host.domain),
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
)
From 3dbafb52207d2c64af201a736f53e510b123b5c8 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 14:21:02 -0600
Subject: [PATCH 026/135] up log level
---
ops/manifests/manifest-ms.yaml | 2 +-
src/epplibwrapper/errors.py | 4 ++--
src/registrar/admin.py | 1 +
src/registrar/models/domain.py | 2 +-
4 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/ops/manifests/manifest-ms.yaml b/ops/manifests/manifest-ms.yaml
index 153ee5f08..ac46f5d92 100644
--- a/ops/manifests/manifest-ms.yaml
+++ b/ops/manifests/manifest-ms.yaml
@@ -20,7 +20,7 @@ applications:
# Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-ms.app.cloud.gov
# Tell Django how much stuff to log
- DJANGO_LOG_LEVEL: INFO
+ DJANGO_LOG_LEVEL: DEBUG
# default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments
diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py
index 4ded1e5a7..d30ae93ea 100644
--- a/src/epplibwrapper/errors.py
+++ b/src/epplibwrapper/errors.py
@@ -62,10 +62,10 @@ class RegistryError(Exception):
- 2501 - 2502 Something malicious or abusive may have occurred
"""
- def __init__(self, *args, code=None, msg=None,**kwargs):
+ def __init__(self, *args, code=None, note=None,**kwargs):
super().__init__(*args, **kwargs)
self.code = code
- self.msg = msg
+ self.note = note
def should_retry(self):
return self.code == ErrorCode.COMMAND_FAILED
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 6bafbab08..81e4772e5 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -2916,6 +2916,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
except RegistryError as err:
# Using variables to get past the linter
message1 = f"Cannot delete Domain when in state {obj.state}"
+ message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
# Human-readable mappings of ErrorCodes. Can be expanded.
error_messages = {
# noqa on these items as black wants to reformat to an invalid length
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index f4922bfdd..e3a2c910a 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1066,8 +1066,8 @@ class Domain(TimeStampedModel, DomainHelper):
if host.domain != self:
logger.error("Host %s in use by another domain: %s", host.name, host.domain)
raise RegistryError(
- msg="Host in use by another domain: {}".format(host.domain),
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
+ note=host.domain,
)
nameservers = [n[0] for n in self.nameservers]
From a9710dafde51eb48b09327f9ac6a786861c56b46 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 14:50:49 -0600
Subject: [PATCH 027/135] more debugging
---
src/epplibwrapper/errors.py | 1 +
src/registrar/admin.py | 2 +-
src/registrar/models/domain.py | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py
index d30ae93ea..0f6ee2722 100644
--- a/src/epplibwrapper/errors.py
+++ b/src/epplibwrapper/errors.py
@@ -65,6 +65,7 @@ class RegistryError(Exception):
def __init__(self, *args, code=None, note=None,**kwargs):
super().__init__(*args, **kwargs)
self.code = code
+ # note is a string that can be used to provide additional context
self.note = note
def should_retry(self):
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 81e4772e5..d26566c63 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -2921,7 +2921,7 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
error_messages = {
# noqa on these items as black wants to reformat to an invalid length
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
- ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: err.msg,
+ ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
}
message = "Cannot connect to the registry"
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index e3a2c910a..19c4f6a8d 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1505,7 +1505,7 @@ class Domain(TimeStampedModel, DomainHelper):
self.deleted = timezone.now()
self.expiration_date = None
except RegistryError as err:
- logger.error(f"Could not delete domain. Registry returned error: {err}")
+ logger.error(f"Could not delete domain. Registry returned error: {err}. Additional context: {err.note}")
raise err
except TransitionNotAllowed as err:
logger.error("Could not delete domain. FSM failure: {err}")
From 5e7823a6ecd3390703691063a84134363720afd3 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Thu, 5 Dec 2024 15:10:16 -0600
Subject: [PATCH 028/135] more debugging
---
src/registrar/models/domain.py | 2 +-
src/registrar/tests/common.py | 2 +-
src/registrar/tests/test_admin_domain.py | 51 +++++++++++++++++++++++-
3 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 19c4f6a8d..6596232f6 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1486,7 +1486,7 @@ class Domain(TimeStampedModel, DomainHelper):
self._remove_client_hold()
# TODO -on the client hold ticket any additional error handling here
- @transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED], target=State.DELETED)
+ @transition(field="state", source=[State.ON_HOLD, State.DNS_NEEDED, State.UNKNOWN], target=State.DELETED)
def deletedInEpp(self):
"""Domain is deleted in epp but is saved in our database.
Subdomains will be deleted first if not in use by another domain.
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 16fa58104..0f7923083 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -1677,7 +1677,7 @@ class MockEppLib(TestCase):
host = getattr(_request, "name", None)
if "sharedhost.com" in host:
print("raising registry error")
- raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION)
+ raise RegistryError(code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION, note="otherdomain.gov")
return MagicMock(
res_data=[self.mockDataHostChange],
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py
index ee275741c..57961605d 100644
--- a/src/registrar/tests/test_admin_domain.py
+++ b/src/registrar/tests/test_admin_domain.py
@@ -172,7 +172,7 @@ class TestDomainAdminAsStaff(MockEppLib):
@less_console_noise_decorator
def test_deletion_is_successful(self):
"""
- Scenario: Domain deletion is unsuccessful
+ Scenario: Domain deletion is successful
When the domain is deleted
Then a user-friendly success message is returned for displaying on the web
And `state` is set to `DELETED`
@@ -223,6 +223,55 @@ class TestDomainAdminAsStaff(MockEppLib):
self.assertEqual(domain.state, Domain.State.DELETED)
+ # @less_console_noise_decorator
+ def test_deletion_is_unsuccessful(self):
+ """
+ Scenario: Domain deletion is unsuccessful
+ When the domain is deleted and has shared subdomains
+ Then a user-friendly success message is returned for displaying on the web
+ And `state` is not set to `DELETED`
+ """
+ domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
+ # Put in client hold
+ domain.place_client_hold()
+ # Ensure everything is displaying correctly
+ response = self.client.get(
+ "/admin/registrar/domain/{}/change/".format(domain.pk),
+ follow=True,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, domain.name)
+ self.assertContains(response, "Remove from registry")
+
+ # The contents of the modal should exist before and after the post.
+ # Check for the header
+ self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
+
+ # Check for some of its body
+ self.assertContains(response, "When a domain is removed from the registry:")
+
+ # Check for some of the button content
+ self.assertContains(response, "Yes, remove from registry")
+
+ # Test the info dialog
+ request = self.factory.post(
+ "/admin/registrar/domain/{}/change/".format(domain.pk),
+ {"_delete_domain": "Remove from registry", "name": domain.name},
+ follow=True,
+ )
+ request.user = self.client
+ with patch("django.contrib.messages.add_message") as mock_add_message:
+ self.admin.do_delete_domain(request, domain)
+ mock_add_message.assert_called_once_with(
+ request,
+ messages.ERROR,
+ "Error deleting this Domain: This subdomain is being used as a hostname on another domain: otherdomain.gov",
+ extra_tags="",
+ fail_silently=False,
+ )
+
+ self.assertEqual(domain.state, Domain.State.ON_HOLD)
+
@less_console_noise_decorator
def test_deletion_ready_fsm_failure(self):
"""
From 6ff3901c91f021e123feba9ef04328c35988e3b1 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 14:43:41 -0800
Subject: [PATCH 029/135] Add aria label to anything else text field
---
src/registrar/forms/domain_request_wizard.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 5d8f23057..ca1313184 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -789,7 +789,12 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
anything_else = forms.CharField(
required=True,
label="Anything else?",
- widget=forms.Textarea(),
+ widget=forms.Textarea(
+ attrs={
+ "aria-label": "Is there anything else you’d like us to know about your domain request? \
+ Provide details below. You can enter up to 2000 characters"
+ }
+ ),
validators=[
MaxLengthValidator(
2000,
From 4671c8967fcbf1d3ae98c8b53e2425300abe9f95 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 15:10:57 -0800
Subject: [PATCH 030/135] Refactor legend heading
---
src/registrar/assets/sass/_theme/_base.scss | 8 ++++++++
src/registrar/assets/sass/_theme/_typography.scss | 3 +--
src/registrar/templates/django/forms/label.html | 4 ++--
.../templates/domain_request_additional_details.html | 8 ++++----
.../templates/domain_request_other_contacts.html | 2 +-
src/registrar/templatetags/field_helpers.py | 10 +++++-----
6 files changed, 21 insertions(+), 14 deletions(-)
diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss
index db1599621..9a6818f09 100644
--- a/src/registrar/assets/sass/_theme/_base.scss
+++ b/src/registrar/assets/sass/_theme/_base.scss
@@ -149,6 +149,14 @@ footer {
color: color('primary');
}
+.usa-footer {
+ margin-bottom: 0.5rem;
+}
+
+.usa-radio {
+ margin-top: 1rem;
+}
+
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
diff --git a/src/registrar/assets/sass/_theme/_typography.scss b/src/registrar/assets/sass/_theme/_typography.scss
index c60b7d802..952fc6dad 100644
--- a/src/registrar/assets/sass/_theme/_typography.scss
+++ b/src/registrar/assets/sass/_theme/_typography.scss
@@ -27,9 +27,8 @@ h2 {
.usa-form,
.usa-form fieldset {
font-size: 1rem;
- legend em {
+ .usa-legend {
font-size: 1rem;
- margin-bottom: 0.5rem;
}
}
diff --git a/src/registrar/templates/django/forms/label.html b/src/registrar/templates/django/forms/label.html
index 2852ce2ba..3783c0fef 100644
--- a/src/registrar/templates/django/forms/label.html
+++ b/src/registrar/templates/django/forms/label.html
@@ -2,8 +2,8 @@
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
>
- {% if legend_label %}
-
{{ legend_label }}
+ {% if legend_heading %}
+
{{ legend_heading }}
{% if widget.attrs.id == 'id_additional_details-has_cisa_representative' %}
.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has 10 regions that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.
- {% with add_class="usa-radio__input--tile" add_legend_label="Are you working with a CISA regional representative on your domain request?" %}
+ {% with add_class="usa-radio__input--tile" add_legend_heading="Are you working with a CISA regional representative on your domain request?" %}
{% input_with_errors forms.0.has_cisa_representative %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
@@ -26,7 +26,7 @@
- {% with add_class="usa-radio__input--tile" add_legend_label="Is there anything else you’d like us to know about your domain request?" %}
+ {% with add_class="usa-radio__input--tile" add_legend_heading="Is there anything else you’d like us to know about your domain request?" %}
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
@@ -34,7 +34,7 @@
Provide details below. *
- {% with attr_maxlength=2000 add_label_class="usa-sr-only" add_legend_class="usa-sr-only" add_legend_label="Is there anything else you’d like us to know about your domain request?" add_aria_label="Provide details below. You can enter up to 2000 characters" %}
+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" add_legend_class="usa-sr-only" add_legend_heading="Is there anything else you’d like us to know about your domain request?" add_aria_label="Provide details below. You can enter up to 2000 characters" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}
diff --git a/src/registrar/templates/domain_request_other_contacts.html b/src/registrar/templates/domain_request_other_contacts.html
index 035fe442b..b3c1be8b4 100644
--- a/src/registrar/templates/domain_request_other_contacts.html
+++ b/src/registrar/templates/domain_request_other_contacts.html
@@ -18,7 +18,7 @@
{% block form_fields %}
- {% with add_class="usa-radio__input--tile" add_legend_label="Are there other employees who can help verify your request?" %}
+ {% with add_class="usa-radio__input--tile" add_legend_heading="Are there other employees who can help verify your request?" %}
{% input_with_errors forms.0.has_other_contacts %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py
index 6cf671931..a1f662fba 100644
--- a/src/registrar/templatetags/field_helpers.py
+++ b/src/registrar/templatetags/field_helpers.py
@@ -57,7 +57,7 @@ def input_with_errors(context, field=None): # noqa: C901
legend_classes = []
group_classes = []
aria_labels = []
- legend_labels = []
+ legend_headings = []
# this will be converted to an attribute string
described_by = []
@@ -91,8 +91,8 @@ def input_with_errors(context, field=None): # noqa: C901
label_classes.append(value)
elif key == "add_legend_class":
legend_classes.append(value)
- elif key == "add_legend_label":
- legend_labels.append(value)
+ elif key == "add_legend_heading":
+ legend_headings.append(value)
elif key == "add_group_class":
group_classes.append(value)
@@ -152,8 +152,8 @@ def input_with_errors(context, field=None): # noqa: C901
if legend_classes:
context["legend_classes"] = " ".join(legend_classes)
- if legend_labels:
- context["legend_label"] = " ".join(legend_labels)
+ if legend_headings:
+ context["legend_heading"] = " ".join(legend_headings)
if group_classes:
context["group_classes"] = " ".join(group_classes)
From 6854310449f81c28678e88022441cb44a3f8921c Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 15:55:16 -0800
Subject: [PATCH 031/135] Refactor how usa-legend is assigned
---
src/registrar/assets/sass/_theme/_base.scss | 1 +
src/registrar/templates/domain_request_additional_details.html | 2 +-
src/registrar/templatetags/field_helpers.py | 3 ---
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/src/registrar/assets/sass/_theme/_base.scss b/src/registrar/assets/sass/_theme/_base.scss
index 9a6818f09..f6a9eef90 100644
--- a/src/registrar/assets/sass/_theme/_base.scss
+++ b/src/registrar/assets/sass/_theme/_base.scss
@@ -155,6 +155,7 @@ footer {
.usa-radio {
margin-top: 1rem;
+ font-size: 1.06rem;
}
abbr[title] {
diff --git a/src/registrar/templates/domain_request_additional_details.html b/src/registrar/templates/domain_request_additional_details.html
index ba7a7f441..86fa79fa3 100644
--- a/src/registrar/templates/domain_request_additional_details.html
+++ b/src/registrar/templates/domain_request_additional_details.html
@@ -11,7 +11,7 @@
- {% with add_class="usa-radio__input--tile" add_legend_heading="Are you working with a CISA regional representative on your domain request?" %}
+ {% with add_class="usa-radio__input--tile" add_legend_class="margin-top-0" add_legend_heading="Are you working with a CISA regional representative on your domain request?" %}
{% input_with_errors forms.0.has_cisa_representative %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
diff --git a/src/registrar/templatetags/field_helpers.py b/src/registrar/templatetags/field_helpers.py
index a1f662fba..426caf9bc 100644
--- a/src/registrar/templatetags/field_helpers.py
+++ b/src/registrar/templatetags/field_helpers.py
@@ -119,9 +119,6 @@ def input_with_errors(context, field=None): # noqa: C901
else:
context["label_tag"] = "label"
- if field.use_fieldset:
- label_classes.append("usa-legend")
-
if field.widget_type == "checkbox":
label_classes.append("usa-checkbox__label")
elif not field.use_fieldset:
From 5b2ecbcf8520c50927bd8d6c17881e7b1842f5cc Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 16:08:59 -0800
Subject: [PATCH 032/135] Remove unused param
---
src/registrar/forms/domain_request_wizard.py | 1 -
src/registrar/forms/utility/wizard_form_helper.py | 11 +----------
2 files changed, 1 insertion(+), 11 deletions(-)
diff --git a/src/registrar/forms/domain_request_wizard.py b/src/registrar/forms/domain_request_wizard.py
index 4a0403e08..5ec8deb24 100644
--- a/src/registrar/forms/domain_request_wizard.py
+++ b/src/registrar/forms/domain_request_wizard.py
@@ -547,7 +547,6 @@ class OtherContactsYesNoForm(BaseYesNoForm):
"""The yes/no field for the OtherContacts form."""
form_choices = ((True, "Yes, I can name other employees."), (False, "No. (We’ll ask you to explain why.)"))
- title_label = "Are there other employees who can help verify your request?"
field_name = "has_other_contacts"
@property
diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py
index d48f7af64..e87998372 100644
--- a/src/registrar/forms/utility/wizard_form_helper.py
+++ b/src/registrar/forms/utility/wizard_form_helper.py
@@ -239,11 +239,6 @@ class BaseYesNoForm(RegistrarForm):
# Default form choice mapping. Default is suitable for most cases.
form_choices = ((True, "Yes"), (False, "No"))
- # Option to append question to aria label for screenreader accessibility.
- # Not added by default.
- title_label = ""
- aria_label = title_label.join("")
-
def __init__(self, *args, **kwargs):
"""Extend the initialization of the form from RegistrarForm __init__"""
super().__init__(*args, **kwargs)
@@ -261,11 +256,7 @@ class BaseYesNoForm(RegistrarForm):
coerce=lambda x: x.lower() == "true" if x is not None else None,
choices=self.form_choices,
initial=self.get_initial_value(),
- widget=forms.RadioSelect(
- attrs={
- # "aria-label": self.title_label
- }
- ),
+ widget=forms.RadioSelect(),
error_messages={
"required": self.required_error_message,
},
From 762bda225ad85ecb7154fc44c1d58045c2162c46 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 16:10:41 -0800
Subject: [PATCH 033/135] Remove unused change
---
src/registrar/assets/src/sass/_theme/_base.scss | 4 ----
1 file changed, 4 deletions(-)
diff --git a/src/registrar/assets/src/sass/_theme/_base.scss b/src/registrar/assets/src/sass/_theme/_base.scss
index dd1c375d9..62f9f436e 100644
--- a/src/registrar/assets/src/sass/_theme/_base.scss
+++ b/src/registrar/assets/src/sass/_theme/_base.scss
@@ -149,10 +149,6 @@ footer {
color: color('primary');
}
-.usa-footer {
- margin-bottom: 0.5rem;
-}
-
.usa-radio {
margin-top: 1rem;
font-size: 1.06rem;
From 904629734c4fec4cce0dea0ec6b5b4ddb222b786 Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Thu, 5 Dec 2024 16:13:41 -0800
Subject: [PATCH 034/135] Remove unused parentheses
---
src/registrar/forms/utility/wizard_form_helper.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/registrar/forms/utility/wizard_form_helper.py b/src/registrar/forms/utility/wizard_form_helper.py
index e87998372..eedf5839b 100644
--- a/src/registrar/forms/utility/wizard_form_helper.py
+++ b/src/registrar/forms/utility/wizard_form_helper.py
@@ -256,7 +256,7 @@ class BaseYesNoForm(RegistrarForm):
coerce=lambda x: x.lower() == "true" if x is not None else None,
choices=self.form_choices,
initial=self.get_initial_value(),
- widget=forms.RadioSelect(),
+ widget=forms.RadioSelect,
error_messages={
"required": self.required_error_message,
},
From 8dfb183ce08e384e4dc35664b969ad0fab54a9c9 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 9 Dec 2024 10:38:46 -0700
Subject: [PATCH 035/135] Copy template
---
.../portfolio_member_permissions.html | 143 +++++++++++++++++-
1 file changed, 142 insertions(+), 1 deletion(-)
diff --git a/src/registrar/templates/portfolio_member_permissions.html b/src/registrar/templates/portfolio_member_permissions.html
index 02d120360..ca816ee2d 100644
--- a/src/registrar/templates/portfolio_member_permissions.html
+++ b/src/registrar/templates/portfolio_member_permissions.html
@@ -1,4 +1,145 @@
{% extends 'portfolio_base.html' %}
+{% load static url_helpers %}
+{% load field_helpers %}
+
+{% block title %}Organization member{% endblock %}
+
+{% block wrapper_class %}
+ {{ block.super }} dashboard--grey-1
+{% endblock %}
+
+{% block portfolio_content %}
+
+
+{% include "includes/form_errors.html" with form=form %}
+{% block messages %}
+ {% include "includes/form_messages.html" %}
+{% endblock messages%}
+
+
+
+
+
+{% block new_member_header %}
+
-{% endblock %}
+{% endblock %} {% endcomment %}
From e87c4f78f111553ab0d185d9e242a2714703ee26 Mon Sep 17 00:00:00 2001
From: matthewswspence
Date: Mon, 9 Dec 2024 13:34:34 -0600
Subject: [PATCH 036/135] use update function to delete hosts
---
src/registrar/models/domain.py | 5 +++--
src/registrar/tests/test_admin_domain.py | 2 +-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6596232f6..c768838d5 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -1074,8 +1074,9 @@ class Domain(TimeStampedModel, DomainHelper):
hostsToDelete, _ = self.createDeleteHostList(nameservers)
logger.debug("HostsToDelete from %s inside _delete_subdomains -> %s", self.name, hostsToDelete)
- for objSet in hostsToDelete:
- self._delete_hosts_if_not_used(objSet.hosts)
+ self.addAndRemoveHostsFromDomain(None, hostsToDelete=nameservers)
+ # for objSet in hostsToDelete:
+ # self._delete_hosts_if_not_used(objSet.hosts)
def _delete_domain(self):
"""This domain should be deleted from the registry
diff --git a/src/registrar/tests/test_admin_domain.py b/src/registrar/tests/test_admin_domain.py
index 57961605d..aed4795a6 100644
--- a/src/registrar/tests/test_admin_domain.py
+++ b/src/registrar/tests/test_admin_domain.py
@@ -228,7 +228,7 @@ class TestDomainAdminAsStaff(MockEppLib):
"""
Scenario: Domain deletion is unsuccessful
When the domain is deleted and has shared subdomains
- Then a user-friendly success message is returned for displaying on the web
+ Then a user-friendly error message is returned for displaying on the web
And `state` is not set to `DELETED`
"""
domain, _ = Domain.objects.get_or_create(name="sharingiscaring.gov", state=Domain.State.ON_HOLD)
From dffae9163e7ccaa88388bd0c3ffd082c28c994fe Mon Sep 17 00:00:00 2001
From: Erin Song <121973038+erinysong@users.noreply.github.com>
Date: Mon, 9 Dec 2024 12:20:58 -0800
Subject: [PATCH 037/135] Update portfolio screenreader additional details form
---
.../portfolio_domain_request_additional_details.html | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/registrar/templates/portfolio_domain_request_additional_details.html b/src/registrar/templates/portfolio_domain_request_additional_details.html
index 5bc529243..1adc4e308 100644
--- a/src/registrar/templates/portfolio_domain_request_additional_details.html
+++ b/src/registrar/templates/portfolio_domain_request_additional_details.html
@@ -6,15 +6,9 @@
{% endblock %}
{% block form_fields %}
-
-
-
Is there anything else you’d like us to know about your domain request?
-
-
-
This question is optional.
- {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
+ {% with attr_maxlength=2000 add_legend_heading="Is there anything else you’d like us to know about your domain request?" add_aria_label="This question is optional." %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
Is there anything else you’d like us to know about your domain request?
+
+
+
This question is optional.
- {% with attr_maxlength=2000 add_legend_heading="Is there anything else you’d like us to know about your domain request?" add_aria_label="This question is optional." %}
+ {% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% input_with_errors forms.0.anything_else %}
{% endwith %}
From ed9d21557793f13f268577c719eaaf6f4dffd250 Mon Sep 17 00:00:00 2001
From: zandercymatics <141044360+zandercymatics@users.noreply.github.com>
Date: Mon, 9 Dec 2024 15:03:39 -0700
Subject: [PATCH 039/135] Form structure
---
src/registrar/forms/portfolio.py | 179 +++++++++++++++---
.../models/utility/portfolio_helper.py | 17 ++
.../django/forms/widgets/multiple_input.html | 14 +-
.../portfolio_member_permissions.html | 54 ++----
src/registrar/templatetags/custom_filters.py | 8 +
5 files changed, 201 insertions(+), 71 deletions(-)
diff --git a/src/registrar/forms/portfolio.py b/src/registrar/forms/portfolio.py
index 5309f7263..65911200b 100644
--- a/src/registrar/forms/portfolio.py
+++ b/src/registrar/forms/portfolio.py
@@ -110,52 +110,169 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
return cleaned_data
-class PortfolioMemberForm(forms.ModelForm):
+class BasePortfolioMemberForm(forms.ModelForm):
+ role = forms.ChoiceField(
+ label="Select permission",
+ choices=[
+ (UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "Admin Access"),
+ (UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "Basic Access")
+ ],
+ widget=forms.RadioSelect,
+ required=True,
+ error_messages={
+ "required": "Member access level is required",
+ },
+ )
+ # Permissions for admins
+ domain_request_permissions_admin = forms.ChoiceField(
+ label="Select permission",
+ choices=[
+ (UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
+ (UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Create and edit requests")
+ ],
+ widget=forms.RadioSelect,
+ required=False,
+ error_messages={
+ "required": "Admin domain request permission is required",
+ },
+ )
+ member_permissions_admin = forms.ChoiceField(
+ label="Select permission",
+ choices=[
+ (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
+ (UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "Create and edit members")
+ ],
+ widget=forms.RadioSelect,
+ required=False,
+ error_messages={
+ "required": "Admin member permission is required",
+ },
+ )
+ domain_request_permissions_member = forms.ChoiceField(
+ label="Select permission",
+ choices=[
+ (UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
+ (UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "Create and edit members")
+ ],
+ widget=forms.RadioSelect,
+ required=False,
+ error_messages={
+ "required": "Basic member permission is required",
+ },
+ )
+
+ # this form dynamically shows/hides some fields, depending on what
+ # was selected prior. This toggles which field is required or not.
+ ROLE_REQUIRED_FIELDS = {
+ UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
+ "domain_request_permissions_admin",
+ "member_permissions_admin",
+ ],
+ UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
+ "domain_request_permissions_member",
+ ],
+ }
+
+ def _map_instance_to_form(self, instance):
+ """Maps model instance data to form fields"""
+ if not instance:
+ return {}
+ mapped_data = {}
+ # Map roles with priority for admin
+ if instance.roles:
+ if UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value in instance.roles:
+ mapped_data['role'] = UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value
+ else:
+ mapped_data['role'] = UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value
+
+ perms = UserPortfolioPermission.get_portfolio_permissions(instance.roles, instance.additional_permissions)
+ # Map permissions with priority for edit permissions
+ if perms:
+ if UserPortfolioPermissionChoices.EDIT_REQUESTS.value in perms:
+ mapped_data['domain_request_permissions_admin'] = UserPortfolioPermissionChoices.EDIT_REQUESTS.value
+ elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value in perms:
+ mapped_data['domain_request_permissions_admin'] = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value
+
+ if UserPortfolioPermissionChoices.EDIT_MEMBERS.value in perms:
+ mapped_data['member_permissions_admin'] = UserPortfolioPermissionChoices.EDIT_MEMBERS.value
+ elif UserPortfolioPermissionChoices.VIEW_MEMBERS.value in perms:
+ mapped_data['member_permissions_admin'] = UserPortfolioPermissionChoices.VIEW_MEMBERS.value
+
+ return mapped_data
+
+ def _map_form_to_instance(self, instance):
+ """Maps form data to model instance"""
+ if not self.is_valid():
+ return
+
+ role = self.cleaned_data.get("role")
+ domain_request_permissions_member = self.cleaned_data.get("domain_request_permissions_member")
+ domain_request_permissions_admin = self.cleaned_data.get('domain_request_permissions_admin')
+ member_permissions_admin = self.cleaned_data.get('member_permissions_admin')
+
+ instance.roles = [role]
+ additional_permissions = []
+ if domain_request_permissions_member:
+ additional_permissions.append(domain_request_permissions_member)
+ elif domain_request_permissions_admin:
+ additional_permissions.append(domain_request_permissions_admin)
+
+ if member_permissions_admin:
+ additional_permissions.append(member_permissions_admin)
+
+ instance.additional_permissions = additional_permissions
+ return instance
+
+ def clean(self):
+ cleaned_data = super().clean()
+ role = cleaned_data.get("role")
+
+ # Get required fields for the selected role.
+ # Then validate all required fields for the role.
+ required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
+ for field_name in required_fields:
+ if not cleaned_data.get(field_name):
+ self.add_error(
+ field_name,
+ self.fields.get(field_name).error_messages.get("required")
+ )
+
+ return cleaned_data
+
+
+class PortfolioMemberForm(BasePortfolioMemberForm):
"""
Form for updating a portfolio member.
"""
-
- roles = forms.MultipleChoiceField(
- choices=UserPortfolioRoleChoices.choices,
- widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
- required=False,
- label="Roles",
- )
-
- additional_permissions = forms.MultipleChoiceField(
- choices=UserPortfolioPermissionChoices.choices,
- widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
- required=False,
- label="Additional Permissions",
- )
-
class Meta:
model = UserPortfolioPermission
fields = [
"roles",
"additional_permissions",
]
+ def __init__(self, *args, instance=None, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields['role'].descriptions = {
+ "organization_admin": UserPortfolioRoleChoices.get_role_description(UserPortfolioRoleChoices.ORGANIZATION_ADMIN),
+ "organization_member": UserPortfolioRoleChoices.get_role_description(UserPortfolioRoleChoices.ORGANIZATION_MEMBER)
+ }
+ self.instance = instance
+ self.initial = self._map_instance_to_form(self.instance)
+
+ def save(self):
+ """Save form data to instance"""
+ if not self.instance:
+ self.instance = self.Meta.model()
+ self._map_form_to_instance(self.instance)
+ self.instance.save()
+ return self.instance
-class PortfolioInvitedMemberForm(forms.ModelForm):
+class PortfolioInvitedMemberForm(BasePortfolioMemberForm):
"""
Form for updating a portfolio invited member.
"""
- roles = forms.MultipleChoiceField(
- choices=UserPortfolioRoleChoices.choices,
- widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
- required=False,
- label="Roles",
- )
-
- additional_permissions = forms.MultipleChoiceField(
- choices=UserPortfolioPermissionChoices.choices,
- widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
- required=False,
- label="Additional Permissions",
- )
-
class Meta:
model = PortfolioInvitation
fields = [
diff --git a/src/registrar/models/utility/portfolio_helper.py b/src/registrar/models/utility/portfolio_helper.py
index 3768aa77a..60fa2170a 100644
--- a/src/registrar/models/utility/portfolio_helper.py
+++ b/src/registrar/models/utility/portfolio_helper.py
@@ -17,6 +17,23 @@ class UserPortfolioRoleChoices(models.TextChoices):
@classmethod
def get_user_portfolio_role_label(cls, user_portfolio_role):
return cls(user_portfolio_role).label if user_portfolio_role else None
+
+ @classmethod
+ def get_role_description(cls, user_portfolio_role):
+ """Returns a detailed description for a given role."""
+ descriptions = {
+ cls.ORGANIZATION_ADMIN: (
+ "Grants this member access to the organization-wide information "
+ "on domains, domain requests, and members. Domain management can be assigned separately."
+ ),
+ cls.ORGANIZATION_MEMBER: (
+ "Grants this member access to the organization. They can be given extra permissions to view all "
+ "organization domain requests and submit domain requests on behalf of the organization. Basic access "
+ "members can’t view all members of an organization or manage them. "
+ "Domain management can be assigned separately."
+ )
+ }
+ return descriptions.get(user_portfolio_role)
class UserPortfolioPermissionChoices(models.TextChoices):
diff --git a/src/registrar/templates/django/forms/widgets/multiple_input.html b/src/registrar/templates/django/forms/widgets/multiple_input.html
index 90c241366..76e19b169 100644
--- a/src/registrar/templates/django/forms/widgets/multiple_input.html
+++ b/src/registrar/templates/django/forms/widgets/multiple_input.html
@@ -1,3 +1,5 @@
+{% load static custom_filters %}
+
{% for group, options, index in widget.optgroups %}
{% if group %}
{% endif %}
@@ -13,7 +15,17 @@
+ >
+ {{ option.label }}
+ {% comment %} Add a description on each, if available {% endcomment %}
+ {% if field and field.field and field.field.descriptions %}
+ {% with description=field.field.descriptions|get_dict_value:option.value %}
+ {% if description %}
+
{{ description }}
+ {% endif %}
+ {% endwith %}
+ {% endif %}
+
{% endfor %}
{% if group %}
-{% endblock new_member_header %}
{% include "includes/required_fields.html" %}
@@ -45,7 +37,6 @@
- {% comment %} TODO should default to name {% endcomment %}
{% if member %}
{{ member.email }}
@@ -64,24 +55,15 @@
Select the level of access for this member. *
- {% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
-
- {% for radio in form.member_access_level %}
- {{ radio.tag }}
-
- {% endfor %}
-
+ {% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
+ {% input_with_errors form.role %}
{% endwith %}
+ {% comment %} {% if radio.value == "organization_admin" %}
+ Grants this member access to the organization-wide information on domains, domain requests, and members. Domain management can be assigned separately.
+ {% elif radio.value == "organization_member" %}
+ Grants this member access to the organization. They can be given extra permissions to view all organization domain requests and submit domain requests on behalf of the organization. Basic access members can’t view all members of an organization or manage them. Domain management can be assigned separately.
+ {% endif %} {% endcomment %}
@@ -93,7 +75,7 @@
text-primary-dark
margin-bottom-0">Organization domain requests
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
- {% input_with_errors form.admin_org_domain_request_permissions %}
+ {% input_with_errors form.domain_request_permissions_admin %}
{% endwith %}