diff --git a/.github/ISSUE_TEMPLATE/issue-default.yml b/.github/ISSUE_TEMPLATE/issue-default.yml
index 27ec10415..3a34b2943 100644
--- a/.github/ISSUE_TEMPLATE/issue-default.yml
+++ b/.github/ISSUE_TEMPLATE/issue-default.yml
@@ -6,13 +6,13 @@ body:
id: title-help
attributes:
value: |
- > Titles should be short, descriptive, and compelling.
+ > Titles should be short, descriptive, and compelling. Use sentence case.
- type: textarea
id: issue-description
attributes:
- label: Issue description and context
+ label: Issue description
description: |
- Describe the issue so that someone who wasn't present for its discovery can understand the problem and why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax). Share desired outcomes or potential next steps. Images or links to other content/context (like documents or Slack discussions) are welcome.
+ Describe the issue so that someone who wasn't present for its discovery can understand why it matters. Use full sentences, plain language, and good [formatting](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax).
validations:
required: true
- type: textarea
@@ -20,16 +20,22 @@ body:
attributes:
label: Acceptance criteria
description: "If known, share 1-3 statements that would need to be true for this issue to be considered resolved. Use a [task list](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/about-task-lists#creating-task-lists) if appropriate."
- placeholder: "- [ ] The button does the thing."
+ placeholder: "- [ ]"
+ - type: textarea
+ id: additional-context
+ attributes:
+ label: Additional context
+ description: "Share any other thoughts, like how this might be implemented or fixed. Screenshots and links to documents/discussions are welcome."
- type: textarea
id: links-to-other-issues
attributes:
label: Links to other issues
description: |
- Add the issue #number of other issues this relates to and how (e.g., 🚧 Blocks, ⛔️ Is blocked by, 🔄 Relates to).
+ "Add issue #numbers this relates to and how (e.g., 🚧 :construction: Blocks, ⛔️ :no_entry: Is blocked by, 🔄 :repeat: Relates to)."
placeholder: 🔄 Relates to...
- type: markdown
id: note
attributes:
value: |
- > We may edit this issue's text to document our understanding and clarify the product work.
+ > We may edit the text in this issue to document our understanding and clarify the product work.
+
diff --git a/docs/architecture/decisions/0023-use-geventconnpool.md b/docs/architecture/decisions/0023-use-geventconnpool.md
new file mode 100644
index 000000000..c24318b4f
--- /dev/null
+++ b/docs/architecture/decisions/0023-use-geventconnpool.md
@@ -0,0 +1,89 @@
+# 22. Use geventconnpool library for Connection Pooling
+
+Date: 2023-13-10
+
+## Status
+
+In Review
+
+## Context
+
+When sending and receiving data from the registry, we use the [EPPLib](https://github.com/cisagov/epplib) library to facilitate that process. To manage these connections within our application, we utilize a module named `epplibwrapper` which serves as a bridge between getgov and the EPPLib library. As part of this process, `epplibwrapper` will instantiate a client that handles sending/receiving data.
+
+At present, each time we need to send a command to the registry, the client establishes a new connection to handle this task. This becomes inefficient when dealing with multiple calls in parallel or in series, as we have to initiate a handshake for each of them. To mitigate this issue, a widely adopted solution is to use a [connection pool](https://en.wikipedia.org/wiki/Connection_pool). In general, a connection pool stores a cache of active connections so that rather than restarting the handshake process when it is unnecessary, we can utilize an existing connection to avoid this problem.
+
+In practice, the lack of a connection pool has resulted in performance issues when dealing with connections to and from the registry. Given the unique nature of our development stack, our options for prebuilt libraries are limited. Out of our available options, a library called [`geventconnpool`](https://github.com/rasky/geventconnpool) was identified that most closely matched our needs.
+
+## Considered Options
+
+**Option 1:** Use the existing connection pool library `geventconnpool`.
+
+➕ Pros
+
+- Saves development time and effort.
+- A tiny library that is easy to audit and understand.
+- Built to be flexible, so every built-in function can be overridden with minimal effort.
+- This library has been used for [EPP before](https://github.com/rasky/geventconnpool/issues/9).
+- Uses [`gevent`](http://www.gevent.org/) for coroutines, which is reliable and well maintained.
+- [`gevent`](http://www.gevent.org/) is used in our WSGI web server.
+- This library is the closest match to our needs that we have found.
+
+
+
+➖ Cons
+
+- Not a well maintained library, could require a fork if a dependency breaks.
+- Heavily reliant on `gevent`.
+
+
+
+**Option 2:** Write our own connection pool logic.
+
+➕ Pros
+
+- Full control over functionality, can be tailored to our specific needs.
+- Highly specific to our stack, could be fine tuned for performance.
+
+
+
+➖ Cons
+
+- Requires significant development time and effort, needs thorough testing.
+- Would require managing with and developing around concurrency.
+- Introduces the potential for many unseen bugs.
+
+
+
+**Option 3:** Modify an existing library which we will then tailor to our needs.
+
+➕ Pros
+
+- Savings in development time and effort, can be tailored to our specific needs.
+- Good middleground between the first two options.
+
+
+
+➖ Cons
+
+- Could introduce complexity, potential issues with maintaining the modified library.
+- May not be necessary if the given library is flexible enough.
+
+
+
+## Decision
+
+We have decided to go with option 1, which is to use the `geventconnpool` library. It closely matches our needs and offers several advantages. Of note, it significantly saves on development time and it is inherently flexible. This allows us to easily change functionality with minimal effort. In addition, the gevent library (which this uses) offers performance benefits due to it being a) written in [cython](https://cython.org/), b) very well maintained and purpose built for tasks such as these, and c) used in our WGSI server.
+
+In summary, this decision was driven by the library's flexibility, simplicity, and compatibility with our tech stack. We acknowledge the risk associated with its maintenance status, but believe that the benefit outweighs the risk.
+
+## Consequences
+
+While its small size makes it easy to work around, `geventconnpool` is not actively maintained. Its last update was in 2021, and as such there is a risk that its dependencies (gevent) will outpace this library and cause it to break. If such an event occurs, it would require that we fork the library and fix those issues. See option 3 pros/cons.
+
+## Mitigation Plan
+To manage this risk, we'll:
+
+1. Monitor the gevent library for updates.
+2. Design the connection pool logic abstractly such that we can easily swap the underlying logic out without needing (or minimizing the need) to rewrite code in `epplibwrapper`.
+3. Document a process for forking and maintaining the library if it becomes necessary, including testing procedures.
+4. Establish a contingency plan for reverting to a previous system state or switching to a different library if significant issues arise with `gevent` or `geventconnpool`.
\ No newline at end of file
diff --git a/docs/developer/user-permissions.md b/docs/developer/user-permissions.md
index 31b69d3b3..f7c41492d 100644
--- a/docs/developer/user-permissions.md
+++ b/docs/developer/user-permissions.md
@@ -42,7 +42,7 @@ as health checks used by our platform).
## Adding roles
The current MVP design uses only a single role called
-`UserDomainRole.Roles.ADMIN` that has all access on a domain. As such, the
+`UserDomainRole.Roles.MANAGER` that has all access on a domain. As such, the
permission mixin doesn't need to examine the `role` field carefully. In the
future, as we add additional roles that our product vision calls for
(read-only? editing only some information?), we need to add conditional
diff --git a/ops/manifests/manifest-stable.yaml b/ops/manifests/manifest-stable.yaml
index bc5e933f6..6295fa63b 100644
--- a/ops/manifests/manifest-stable.yaml
+++ b/ops/manifests/manifest-stable.yaml
@@ -18,7 +18,7 @@ applications:
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
# Tell Django where it is being hosted
- DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov
+ DJANGO_BASE_URL: https://manage.get.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
diff --git a/src/api/tests/test_available.py b/src/api/tests/test_available.py
index 0bbe01f03..9eab17bf7 100644
--- a/src/api/tests/test_available.py
+++ b/src/api/tests/test_available.py
@@ -3,19 +3,27 @@
import json
from django.contrib.auth import get_user_model
-from django.test import TestCase, RequestFactory
+from django.test import RequestFactory
-from ..views import available, _domains, in_domains
+from ..views import available, in_domains
from .common import less_console_noise
+from registrar.tests.common import MockEppLib
+from unittest.mock import call
+
+from epplibwrapper import (
+ commands,
+ RegistryError,
+)
API_BASE_PATH = "/api/v1/available/"
-class AvailableViewTest(TestCase):
+class AvailableViewTest(MockEppLib):
"""Test that the view function works as expected."""
def setUp(self):
+ super().setUp()
self.user = get_user_model().objects.create(username="username")
self.factory = RequestFactory()
@@ -29,26 +37,37 @@ class AvailableViewTest(TestCase):
response_object = json.loads(response.content)
self.assertIn("available", response_object)
- def test_domain_list(self):
- """Test the domain list that is returned from Github.
+ def test_in_domains_makes_calls_(self):
+ """Domain searches successfully make correct mock EPP calls"""
+ gsa_available = in_domains("gsa.gov")
+ igorville_available = in_domains("igorvilleremixed.gov")
- This does not mock out the external file, it is actually fetched from
- the internet.
- """
- domains = _domains()
- self.assertIn("gsa.gov", domains)
- # entries are all lowercase so GSA.GOV is not in the set
- self.assertNotIn("GSA.GOV", domains)
- self.assertNotIn("igorvilleremixed.gov", domains)
- # all the entries have dots
- self.assertNotIn("gsa", domains)
+ """Domain searches successfully make mock EPP calls"""
+ self.mockedSendFunction.assert_has_calls(
+ [
+ call(
+ commands.CheckDomain(
+ ["gsa.gov"],
+ ),
+ cleaned=True,
+ ),
+ call(
+ commands.CheckDomain(
+ ["igorvilleremixed.gov"],
+ ),
+ cleaned=True,
+ ),
+ ]
+ )
+ """Domain searches return correct availability results"""
+ self.assertTrue(gsa_available)
+ self.assertFalse(igorville_available)
- def test_in_domains(self):
+ def test_in_domains_capitalized(self):
+ """Domain searches work without case sensitivity"""
self.assertTrue(in_domains("gsa.gov"))
# input is lowercased so GSA.GOV should be found
- self.assertTrue(in_domains("GSA.GOV"))
- # This domain should not have been registered
- self.assertFalse(in_domains("igorvilleremixed.gov"))
+ self.assertTrue(in_domains("GSA.gov"))
def test_in_domains_dotgov(self):
"""Domain searches work without trailing .gov"""
@@ -86,13 +105,18 @@ class AvailableViewTest(TestCase):
request.user = self.user
response = available(request, domain=bad_string)
self.assertFalse(json.loads(response.content)["available"])
+ # domain set to raise error successfully raises error
+ with self.assertRaises(RegistryError):
+ error_domain_available = available(request, "errordomain.gov")
+ self.assertFalse(json.loads(error_domain_available.content)["available"])
-class AvailableAPITest(TestCase):
+class AvailableAPITest(MockEppLib):
"""Test that the API can be called as expected."""
def setUp(self):
+ super().setUp()
self.user = get_user_model().objects.create(username="username")
def test_available_get(self):
diff --git a/src/api/views.py b/src/api/views.py
index e19e060ef..e8b8431de 100644
--- a/src/api/views.py
+++ b/src/api/views.py
@@ -3,8 +3,6 @@ from django.apps import apps
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse
-from django.contrib.auth.decorators import login_required
-
import requests
from cachetools.func import ttl_cache
@@ -59,16 +57,15 @@ def in_domains(domain):
given domain doesn't end with .gov, ".gov" is added when looking for
a match.
"""
- domain = domain.lower()
+ Domain = apps.get_model("registrar.Domain")
if domain.endswith(".gov"):
- return domain.lower() in _domains()
+ return Domain.available(domain)
else:
# domain search string doesn't end with .gov, add it on here
- return (domain + ".gov") in _domains()
+ return Domain.available(domain + ".gov")
@require_http_methods(["GET"])
-@login_required
def available(request, domain=""):
"""Is a given domain available or not.
diff --git a/src/epplibwrapper/__init__.py b/src/epplibwrapper/__init__.py
index dd6664a3a..d0138d73c 100644
--- a/src/epplibwrapper/__init__.py
+++ b/src/epplibwrapper/__init__.py
@@ -45,7 +45,7 @@ except NameError:
# Attn: these imports should NOT be at the top of the file
try:
from .client import CLIENT, commands
- from .errors import RegistryError, ErrorCode
+ from .errors import RegistryError, ErrorCode, CANNOT_CONTACT_REGISTRY, GENERIC_ERROR
from epplib.models import common, info
from epplib.responses import extensions
from epplib import responses
@@ -61,4 +61,6 @@ __all__ = [
"info",
"ErrorCode",
"RegistryError",
+ "CANNOT_CONTACT_REGISTRY",
+ "GENERIC_ERROR",
]
diff --git a/src/epplibwrapper/errors.py b/src/epplibwrapper/errors.py
index d34ed5e91..dba5f328c 100644
--- a/src/epplibwrapper/errors.py
+++ b/src/epplibwrapper/errors.py
@@ -1,5 +1,8 @@
from enum import IntEnum
+CANNOT_CONTACT_REGISTRY = "Update failed. Cannot contact the registry."
+GENERIC_ERROR = "Value entered was wrong."
+
class ErrorCode(IntEnum):
"""
diff --git a/src/registrar/admin.py b/src/registrar/admin.py
index 8d0ed8c2e..904ce66a4 100644
--- a/src/registrar/admin.py
+++ b/src/registrar/admin.py
@@ -219,9 +219,9 @@ class MyUserAdmin(BaseUserAdmin):
# (which should in theory be the ONLY group)
def group(self, obj):
if obj.groups.filter(name="full_access_group").exists():
- return "Full access"
+ return "full_access_group"
elif obj.groups.filter(name="cisa_analysts_group").exists():
- return "Analyst"
+ return "cisa_analysts_group"
return ""
def get_list_display(self, request):
@@ -294,6 +294,26 @@ class ContactAdmin(ListHeaderAdmin):
contact.admin_order_field = "first_name" # type: ignore
+ # Read only that we'll leverage for CISA Analysts
+ analyst_readonly_fields = [
+ "user",
+ ]
+
+ def get_readonly_fields(self, request, obj=None):
+ """Set the read-only state on form elements.
+ We have 1 conditions that determine which fields are read-only:
+ admin user permissions.
+ """
+
+ readonly_fields = list(self.readonly_fields)
+
+ if request.user.has_perm("registrar.full_access_permission"):
+ return readonly_fields
+ # Return restrictive Read-only fields for analysts and
+ # users who might not belong to groups
+ readonly_fields.extend([field for field in self.analyst_readonly_fields])
+ return readonly_fields # Read-only fields for analysts
+
class WebsiteAdmin(ListHeaderAdmin):
"""Custom website admin class."""
@@ -420,9 +440,6 @@ class DomainInformationAdmin(ListHeaderAdmin):
"creator",
"type_of_work",
"more_organization_information",
- "address_line1",
- "address_line2",
- "zipcode",
"domain",
"submitter",
"no_other_contacts_rationale",
@@ -557,9 +574,6 @@ class DomainApplicationAdmin(ListHeaderAdmin):
analyst_readonly_fields = [
"creator",
"about_your_organization",
- "address_line1",
- "address_line2",
- "zipcode",
"requested_domain",
"alternative_domains",
"purpose",
@@ -721,7 +735,7 @@ class DomainAdmin(ListHeaderAdmin):
]
def organization_type(self, obj):
- return obj.domain_info.organization_type
+ return obj.domain_info.get_organization_type_display()
organization_type.admin_order_field = ( # type: ignore
"domain_info__organization_type"
diff --git a/src/registrar/assets/sass/_theme/_buttons.scss b/src/registrar/assets/sass/_theme/_buttons.scss
index 718bd5792..0857ec603 100644
--- a/src/registrar/assets/sass/_theme/_buttons.scss
+++ b/src/registrar/assets/sass/_theme/_buttons.scss
@@ -22,15 +22,15 @@ a.breadcrumb__back {
}
}
-a.usa-button {
+a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
text-decoration: none;
color: color('white');
}
-a.usa-button:visited,
-a.usa-button:hover,
-a.usa-button:focus,
-a.usa-button:active {
+a.usa-button:not(.usa-button--unstyled, .usa-button--outline):visited,
+a.usa-button:not(.usa-button--unstyled, .usa-button--outline):hover,
+a.usa-button:not(.usa-button--unstyled, .usa-button--outline):focus,
+a.usa-button:not(.usa-button--unstyled, .usa-button--outline):active {
color: color('white');
}
diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py
index ceb215a4d..59f00fe61 100644
--- a/src/registrar/config/settings.py
+++ b/src/registrar/config/settings.py
@@ -581,7 +581,7 @@ ALLOWED_HOSTS = [
"getgov-bl.app.cloud.gov",
"getgov-rjm.app.cloud.gov",
"getgov-dk.app.cloud.gov",
- "get.gov",
+ "manage.get.gov",
]
# Extend ALLOWED_HOSTS.
@@ -652,6 +652,9 @@ SESSION_COOKIE_SAMESITE = "Lax"
# instruct browser to only send cookie via HTTPS
SESSION_COOKIE_SECURE = True
+# session engine to cache session information
+SESSION_ENGINE = "django.contrib.sessions.backends.cache"
+
# ~ Set by django.middleware.clickjacking.XFrameOptionsMiddleware
# prevent clickjacking by instructing the browser not to load
# our site within an iframe
diff --git a/src/registrar/forms/application_wizard.py b/src/registrar/forms/application_wizard.py
index 516683247..2fd78cdd8 100644
--- a/src/registrar/forms/application_wizard.py
+++ b/src/registrar/forms/application_wizard.py
@@ -153,7 +153,8 @@ class RegistrarFormSet(forms.BaseFormSet):
class OrganizationTypeForm(RegistrarForm):
organization_type = forms.ChoiceField(
- choices=DomainApplication.OrganizationChoices.choices,
+ # use the long names in the application form
+ choices=DomainApplication.OrganizationChoicesVerbose.choices,
widget=forms.RadioSelect,
error_messages={"required": "Select the type of organization you represent."},
)
diff --git a/src/registrar/migrations/0040_alter_userdomainrole_role.py b/src/registrar/migrations/0040_alter_userdomainrole_role.py
new file mode 100644
index 000000000..39e539f55
--- /dev/null
+++ b/src/registrar/migrations/0040_alter_userdomainrole_role.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.1 on 2023-10-20 15:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0039_alter_transitiondomain_status"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="userdomainrole",
+ name="role",
+ field=models.TextField(choices=[("manager", "Manager")]),
+ ),
+ ]
diff --git a/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py
new file mode 100644
index 000000000..07cfe0e77
--- /dev/null
+++ b/src/registrar/migrations/0041_alter_domainapplication_organization_type_and_more.py
@@ -0,0 +1,52 @@
+# Generated by Django 4.2.1 on 2023-10-20 21:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0040_alter_userdomainrole_role"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="domainapplication",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ],
+ help_text="Type of organization",
+ max_length=255,
+ null=True,
+ ),
+ ),
+ migrations.AlterField(
+ model_name="domaininformation",
+ name="organization_type",
+ field=models.CharField(
+ blank=True,
+ choices=[
+ ("federal", "Federal"),
+ ("interstate", "Interstate"),
+ ("state_or_territory", "State or territory"),
+ ("tribal", "Tribal"),
+ ("county", "County"),
+ ("city", "City"),
+ ("special_district", "Special district"),
+ ("school_district", "School district"),
+ ],
+ help_text="Type of Organization",
+ max_length=255,
+ null=True,
+ ),
+ ),
+ ]
diff --git a/src/registrar/migrations/0042_create_groups_v03.py b/src/registrar/migrations/0042_create_groups_v03.py
new file mode 100644
index 000000000..01b7985bf
--- /dev/null
+++ b/src/registrar/migrations/0042_create_groups_v03.py
@@ -0,0 +1,37 @@
+# This migration creates the create_full_access_group and create_cisa_analyst_group groups
+# It is dependent on 0035 (which populates ContentType and Permissions)
+# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
+# in the user_group model then:
+# [NOT RECOMMENDED]
+# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
+# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
+# step 3: fake run the latest migration in the migrations list
+# [RECOMMENDED]
+# Alternatively:
+# step 1: duplicate the migration that loads data
+# step 2: docker-compose exec app ./manage.py migrate
+
+from django.db import migrations
+from registrar.models import UserGroup
+from typing import Any
+
+
+# For linting: RunPython expects a function reference,
+# so let's give it one
+def create_groups(apps, schema_editor) -> Any:
+ UserGroup.create_cisa_analyst_group(apps, schema_editor)
+ UserGroup.create_full_access_group(apps, schema_editor)
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("registrar", "0041_alter_domainapplication_organization_type_and_more"),
+ ]
+
+ operations = [
+ migrations.RunPython(
+ create_groups,
+ reverse_code=migrations.RunPython.noop,
+ atomic=True,
+ ),
+ ]
diff --git a/src/registrar/models/domain.py b/src/registrar/models/domain.py
index 6d911c975..d1bda87a4 100644
--- a/src/registrar/models/domain.py
+++ b/src/registrar/models/domain.py
@@ -260,7 +260,6 @@ class Domain(TimeStampedModel, DomainHelper):
"""Creates the host object in the registry
doesn't add the created host to the domain
returns ErrorCode (int)"""
- logger.info("Creating host")
if addrs is not None:
addresses = [epp.Ip(addr=addr) for addr in addrs]
request = commands.CreateHost(name=host, addrs=addresses)
@@ -782,7 +781,7 @@ class Domain(TimeStampedModel, DomainHelper):
and errorCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY
):
# TODO- ticket #433 look here for error handling
- raise Exception("Unable to add contact to registry")
+ raise RegistryError(code=errorCode)
# contact doesn't exist on the domain yet
logger.info("_set_singleton_contact()-> contact has been added to the registry")
@@ -1209,7 +1208,6 @@ class Domain(TimeStampedModel, DomainHelper):
count = 0
while not exitEarly and count < 3:
try:
- logger.info("Getting domain info from epp")
req = commands.InfoDomain(name=self.name)
domainInfoResponse = registry.send(req, cleaned=True)
exitEarly = True
@@ -1376,18 +1374,16 @@ class Domain(TimeStampedModel, DomainHelper):
"""creates a disclose object that can be added to a contact Create using
.disclose= on the command before sending.
if item is security email then make sure email is visable"""
- isSecurity = contact.contact_type == contact.ContactTypeChoices.SECURITY
+ is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
DF = epp.DiscloseField
- fields = {DF.FAX, DF.VOICE, DF.ADDR}
-
- if not isSecurity or (
- isSecurity and contact.email == PublicContact.get_default_security().email
- ):
- fields.add(DF.EMAIL)
+ fields = {DF.EMAIL}
+ disclose = (
+ is_security and contact.email != PublicContact.get_default_security().email
+ )
+ # Will only disclose DF.EMAIL if its not the default
return epp.Disclose(
- flag=False,
+ flag=disclose,
fields=fields,
- types={DF.ADDR: "loc"},
)
def _make_epp_contact_postal_info(self, contact: PublicContact): # type: ignore
@@ -1648,74 +1644,84 @@ class Domain(TimeStampedModel, DomainHelper):
"""Contact registry for info about a domain."""
try:
# get info from registry
- dataResponse = self._get_or_create_domain()
- data = dataResponse.res_data[0]
- # extract properties from response
- # (Ellipsis is used to mean "null")
- cache = {
- "auth_info": getattr(data, "auth_info", ...),
- "_contacts": getattr(data, "contacts", ...),
- "cr_date": getattr(data, "cr_date", ...),
- "ex_date": getattr(data, "ex_date", ...),
- "_hosts": getattr(data, "hosts", ...),
- "name": getattr(data, "name", ...),
- "registrant": getattr(data, "registrant", ...),
- "statuses": getattr(data, "statuses", ...),
- "tr_date": getattr(data, "tr_date", ...),
- "up_date": getattr(data, "up_date", ...),
- }
- # remove null properties (to distinguish between "a value of None" and null)
- cleaned = {k: v for k, v in cache.items() if v is not ...}
+ data_response = self._get_or_create_domain()
+ cache = self._extract_data_from_response(data_response)
+
+ # remove null properties (to distinguish between "a value of None" and null)
+ cleaned = self._remove_null_properties(cache)
- # statuses can just be a list no need to keep the epp object
if "statuses" in cleaned:
cleaned["statuses"] = [status.state for status in cleaned["statuses"]]
- # get extensions info, if there is any
- # DNSSECExtension is one possible extension, make sure to handle
- # only DNSSECExtension and not other type extensions
- returned_extensions = dataResponse.extensions
- cleaned["dnssecdata"] = None
- for extension in returned_extensions:
- if isinstance(extension, extensions.DNSSECExtension):
- cleaned["dnssecdata"] = extension
+ cleaned["dnssecdata"] = self._get_dnssec_data(data_response.extensions)
+
# Capture and store old hosts and contacts from cache if they exist
old_cache_hosts = self._cache.get("hosts")
old_cache_contacts = self._cache.get("contacts")
- # get contact info, if there are any
- if (
- fetch_contacts
- and "_contacts" in cleaned
- and isinstance(cleaned["_contacts"], list)
- and len(cleaned["_contacts"]) > 0
- ):
- cleaned["contacts"] = self._fetch_contacts(cleaned["_contacts"])
- # We're only getting contacts, so retain the old
- # hosts that existed in cache (if they existed)
- # and pass them along.
+ if fetch_contacts:
+ cleaned["contacts"] = self._get_contacts(cleaned.get("_contacts", []))
if old_cache_hosts is not None:
+ logger.debug("resetting cleaned['hosts'] to old_cache_hosts")
cleaned["hosts"] = old_cache_hosts
- # get nameserver info, if there are any
- if (
- fetch_hosts
- and "_hosts" in cleaned
- and isinstance(cleaned["_hosts"], list)
- and len(cleaned["_hosts"])
- ):
- cleaned["hosts"] = self._fetch_hosts(cleaned["_hosts"])
- # We're only getting hosts, so retain the old
- # contacts that existed in cache (if they existed)
- # and pass them along.
+ if fetch_hosts:
+ cleaned["hosts"] = self._get_hosts(cleaned.get("_hosts", []))
if old_cache_contacts is not None:
cleaned["contacts"] = old_cache_contacts
- # replace the prior cache with new data
+
self._cache = cleaned
except RegistryError as e:
logger.error(e)
+ def _extract_data_from_response(self, data_response):
+ data = data_response.res_data[0]
+ return {
+ "auth_info": getattr(data, "auth_info", ...),
+ "_contacts": getattr(data, "contacts", ...),
+ "cr_date": getattr(data, "cr_date", ...),
+ "ex_date": getattr(data, "ex_date", ...),
+ "_hosts": getattr(data, "hosts", ...),
+ "name": getattr(data, "name", ...),
+ "registrant": getattr(data, "registrant", ...),
+ "statuses": getattr(data, "statuses", ...),
+ "tr_date": getattr(data, "tr_date", ...),
+ "up_date": getattr(data, "up_date", ...),
+ }
+
+ def _remove_null_properties(self, cache):
+ return {k: v for k, v in cache.items() if v is not ...}
+
+ def _get_dnssec_data(self, response_extensions):
+ # get extensions info, if there is any
+ # DNSSECExtension is one possible extension, make sure to handle
+ # only DNSSECExtension and not other type extensions
+ dnssec_data = None
+ for extension in response_extensions:
+ if isinstance(extension, extensions.DNSSECExtension):
+ dnssec_data = extension
+ return dnssec_data
+
+ def _get_contacts(self, contacts):
+ choices = PublicContact.ContactTypeChoices
+ # We expect that all these fields get populated,
+ # so we can create these early, rather than waiting.
+ cleaned_contacts = {
+ choices.ADMINISTRATIVE: None,
+ choices.SECURITY: None,
+ choices.TECHNICAL: None,
+ }
+ if contacts and isinstance(contacts, list) and len(contacts) > 0:
+ cleaned_contacts = self._fetch_contacts(contacts)
+ return cleaned_contacts
+
+ def _get_hosts(self, hosts):
+ cleaned_hosts = []
+ if hosts and isinstance(hosts, list):
+ cleaned_hosts = self._fetch_hosts(hosts)
+ return cleaned_hosts
+
def _get_or_create_public_contact(self, public_contact: PublicContact):
"""Tries to find a PublicContact object in our DB.
If it can't, it'll create it. Returns PublicContact"""
diff --git a/src/registrar/models/domain_application.py b/src/registrar/models/domain_application.py
index 68429d381..68fbfab0d 100644
--- a/src/registrar/models/domain_application.py
+++ b/src/registrar/models/domain_application.py
@@ -105,28 +105,57 @@ class DomainApplication(TimeStampedModel):
ARMED_FORCES_AP = "AP", "Armed Forces Pacific (AP)"
class OrganizationChoices(models.TextChoices):
+
+ """
+ Primary organization choices:
+ For use in django admin
+ Keys need to match OrganizationChoicesVerbose
+ """
+
+ FEDERAL = "federal", "Federal"
+ INTERSTATE = "interstate", "Interstate"
+ STATE_OR_TERRITORY = "state_or_territory", "State or territory"
+ TRIBAL = "tribal", "Tribal"
+ COUNTY = "county", "County"
+ CITY = "city", "City"
+ SPECIAL_DISTRICT = "special_district", "Special district"
+ SCHOOL_DISTRICT = "school_district", "School district"
+
+ class OrganizationChoicesVerbose(models.TextChoices):
+
+ """
+ Secondary organization choices
+ For use in the application form and on the templates
+ Keys need to match OrganizationChoices
+ """
+
FEDERAL = (
"federal",
- "Federal: an agency of the U.S. government's executive, legislative, "
- "or judicial branches",
+ "Federal: an agency of the U.S. government's executive, "
+ "legislative, or judicial branches",
)
INTERSTATE = "interstate", "Interstate: an organization of two or more states"
- STATE_OR_TERRITORY = "state_or_territory", (
- "State or territory: one of the 50 U.S. states, the District of "
- "Columbia, American Samoa, Guam, Northern Mariana Islands, "
- "Puerto Rico, or the U.S. Virgin Islands"
+ STATE_OR_TERRITORY = (
+ "state_or_territory",
+ "State or territory: one of the 50 U.S. states, the District of Columbia, "
+ "American Samoa, Guam, Northern Mariana Islands, Puerto Rico, or the U.S. "
+ "Virgin Islands",
)
- TRIBAL = "tribal", (
- "Tribal: a tribal government recognized by the federal or "
- "a state government"
+ TRIBAL = (
+ "tribal",
+ "Tribal: a tribal government recognized by the federal or a state "
+ "government",
)
COUNTY = "county", "County: a county, parish, or borough"
CITY = "city", "City: a city, town, township, village, etc."
- SPECIAL_DISTRICT = "special_district", (
- "Special district: an independent organization within a single state"
+ SPECIAL_DISTRICT = (
+ "special_district",
+ "Special district: an independent organization within a single state",
)
- SCHOOL_DISTRICT = "school_district", (
- "School district: a school district that is not part of a local government"
+ SCHOOL_DISTRICT = (
+ "school_district",
+ "School district: a school district that is not part of a local "
+ "government",
)
class BranchChoices(models.TextChoices):
@@ -297,6 +326,7 @@ class DomainApplication(TimeStampedModel):
# ##### data fields from the initial form #####
organization_type = models.CharField(
max_length=255,
+ # use the short names in Django admin
choices=OrganizationChoices.choices,
null=True,
blank=True,
@@ -582,7 +612,7 @@ class DomainApplication(TimeStampedModel):
# create the permission for the user
UserDomainRole = apps.get_model("registrar.UserDomainRole")
UserDomainRole.objects.get_or_create(
- user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
+ user=self.creator, domain=created_domain, role=UserDomainRole.Roles.MANAGER
)
self._send_status_update_email(
diff --git a/src/registrar/models/domain_information.py b/src/registrar/models/domain_information.py
index 3b93aff48..d2bc5c53d 100644
--- a/src/registrar/models/domain_information.py
+++ b/src/registrar/models/domain_information.py
@@ -21,6 +21,7 @@ class DomainInformation(TimeStampedModel):
StateTerritoryChoices = DomainApplication.StateTerritoryChoices
+ # use the short names in Django admin
OrganizationChoices = DomainApplication.OrganizationChoices
BranchChoices = DomainApplication.BranchChoices
diff --git a/src/registrar/models/domain_invitation.py b/src/registrar/models/domain_invitation.py
index 7cc2a5432..dff03fb87 100644
--- a/src/registrar/models/domain_invitation.py
+++ b/src/registrar/models/domain_invitation.py
@@ -63,7 +63,7 @@ class DomainInvitation(TimeStampedModel):
# and create a role for that user on this domain
_, created = UserDomainRole.objects.get_or_create(
- user=user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
+ user=user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
if not created:
# something strange happened and this role already existed when
diff --git a/src/registrar/models/user_domain_role.py b/src/registrar/models/user_domain_role.py
index e5cb01cc1..7b1f550d3 100644
--- a/src/registrar/models/user_domain_role.py
+++ b/src/registrar/models/user_domain_role.py
@@ -15,7 +15,7 @@ class UserDomainRole(TimeStampedModel):
elsewhere.
"""
- ADMIN = "manager"
+ MANAGER = "manager"
user = models.ForeignKey(
"registrar.User",
diff --git a/src/registrar/models/user_group.py b/src/registrar/models/user_group.py
index 5cdb1f2ec..568741786 100644
--- a/src/registrar/models/user_group.py
+++ b/src/registrar/models/user_group.py
@@ -24,7 +24,7 @@ class UserGroup(Group):
{
"app_label": "registrar",
"model": "contact",
- "permissions": ["view_contact"],
+ "permissions": ["change_contact"],
},
{
"app_label": "registrar",
@@ -56,6 +56,11 @@ class UserGroup(Group):
"model": "domaininvitation",
"permissions": ["add_domaininvitation", "view_domaininvitation"],
},
+ {
+ "app_label": "registrar",
+ "model": "website",
+ "permissions": ["change_website"],
+ },
]
# Avoid error: You can't execute queries until the end
diff --git a/src/registrar/templates/application_review.html b/src/registrar/templates/application_review.html
index be81303b8..6a4dcbffd 100644
--- a/src/registrar/templates/application_review.html
+++ b/src/registrar/templates/application_review.html
@@ -1,5 +1,6 @@
{% extends 'application_form.html' %}
{% load static url_helpers %}
+{% load custom_filters %}
{% block form_required_fields_help_text %}
{# there are no required fields on this page so don't show this #}
@@ -26,7 +27,13 @@
+ Domain managers can update all information related to a domain within the
+ .gov registrar, including contact details, authorizing official, security
+ email, and DNS name servers.
+
+
+
+
There is no limit to the number of domain managers you can add.
+
After adding a domain manager, an email invitation will be sent to that user with
+ instructions on how to set up an account.
+
To remove a domain manager, contact us for assistance.
+
{% if domain.permissions %}
diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py
index 3614db18e..14e2c9e3e 100644
--- a/src/registrar/templatetags/custom_filters.py
+++ b/src/registrar/templatetags/custom_filters.py
@@ -1,7 +1,10 @@
+import logging
from django import template
import re
+from registrar.models.domain_application import DomainApplication
register = template.Library()
+logger = logging.getLogger(__name__)
@register.filter(name="extract_value")
@@ -48,3 +51,16 @@ def contains_checkbox(html_list):
if re.search(r']*type="checkbox"', html_string):
return True
return False
+
+
+@register.filter
+def get_organization_long_name(organization_type):
+ organization_choices_dict = dict(
+ DomainApplication.OrganizationChoicesVerbose.choices
+ )
+ long_form_type = organization_choices_dict[organization_type]
+ if long_form_type is None:
+ logger.error("Organization type error, triggered by a template's custom filter")
+ return "Error"
+
+ return long_form_type
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index 239e50ae8..4e000e0c2 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -31,8 +31,11 @@ from epplibwrapper import (
info,
RegistryError,
ErrorCode,
+ responses,
)
+from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
+
logger = logging.getLogger(__name__)
@@ -667,6 +670,44 @@ class MockEppLib(TestCase):
registrant="regContact",
)
+ InfoDomainWithDefaultSecurityContact = fakedEppObject(
+ "fakepw",
+ cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
+ contacts=[
+ common.DomainContact(
+ contact="defaultSec",
+ type=PublicContact.ContactTypeChoices.SECURITY,
+ )
+ ],
+ hosts=["fake.host.com"],
+ statuses=[
+ common.Status(state="serverTransferProhibited", description="", lang="en"),
+ common.Status(state="inactive", description="", lang="en"),
+ ],
+ )
+
+ InfoDomainWithDefaultTechnicalContact = fakedEppObject(
+ "fakepw",
+ cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
+ contacts=[
+ common.DomainContact(
+ contact="defaultTech",
+ type=PublicContact.ContactTypeChoices.TECHNICAL,
+ )
+ ],
+ hosts=["fake.host.com"],
+ statuses=[
+ common.Status(state="serverTransferProhibited", description="", lang="en"),
+ common.Status(state="inactive", description="", lang="en"),
+ ],
+ )
+
+ mockDefaultTechnicalContact = InfoDomainWithContacts.dummyInfoContactResultData(
+ "defaultTech", "dotgov@cisa.dhs.gov"
+ )
+ mockDefaultSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData(
+ "defaultSec", "dotgov@cisa.dhs.gov"
+ )
mockSecurityContact = InfoDomainWithContacts.dummyInfoContactResultData(
"securityContact", "security@mail.gov"
)
@@ -771,51 +812,63 @@ class MockEppLib(TestCase):
],
)
+ def _mockDomainName(self, _name, _avail=False):
+ return MagicMock(
+ res_data=[
+ responses.check.CheckDomainResultData(
+ name=_name, avail=_avail, reason=None
+ ),
+ ]
+ )
+
+ def mockCheckDomainCommand(self, _request, cleaned):
+ if "gsa.gov" in getattr(_request, "names", None):
+ return self._mockDomainName("gsa.gov", True)
+ elif "GSA.gov" in getattr(_request, "names", None):
+ return self._mockDomainName("GSA.gov", True)
+ elif "igorvilleremixed.gov" in getattr(_request, "names", None):
+ return self._mockDomainName("igorvilleremixed.gov", False)
+ elif "errordomain.gov" in getattr(_request, "names", None):
+ raise RegistryError("Registry cannot find domain availability.")
+ else:
+ return self._mockDomainName("domainnotfound.gov", False)
+
def mockSend(self, _request, cleaned):
"""Mocks the registry.send function used inside of domain.py
registry is imported from epplibwrapper
returns objects that simulate what would be in a epp response
but only relevant pieces for tests"""
- if isinstance(_request, commands.InfoDomain):
- return self.mockInfoDomainCommands(_request, cleaned)
- elif isinstance(_request, commands.InfoContact):
- return self.mockInfoContactCommands(_request, cleaned)
- elif isinstance(_request, commands.UpdateDomain):
- return self.mockUpdateDomainCommands(_request, cleaned)
- elif (
- isinstance(_request, commands.CreateContact)
- and getattr(_request, "id", None) == "fail"
- and self.mockedSendFunction.call_count == 3
- ):
- # use this for when a contact is being updated
- # sets the second send() to fail
- raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
- elif isinstance(_request, commands.CreateHost):
- return MagicMock(
- res_data=[self.mockDataHostChange],
- code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
- )
- elif isinstance(_request, commands.UpdateHost):
- return MagicMock(
- res_data=[self.mockDataHostChange],
- code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
- )
- elif isinstance(_request, commands.DeleteHost):
- return MagicMock(
- res_data=[self.mockDataHostChange],
- code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
- )
- elif (
- isinstance(_request, commands.DeleteDomain)
- and getattr(_request, "name", None) == "failDelete.gov"
- ):
- name = getattr(_request, "name", None)
- fake_nameserver = "ns1.failDelete.gov"
- if name in fake_nameserver:
- raise RegistryError(
- code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
+
+ match type(_request):
+ case commands.InfoDomain:
+ return self.mockInfoDomainCommands(_request, cleaned)
+ case commands.InfoContact:
+ return self.mockInfoContactCommands(_request, cleaned)
+ case commands.CreateContact:
+ return self.mockCreateContactCommands(_request, cleaned)
+ case commands.UpdateDomain:
+ return self.mockUpdateDomainCommands(_request, cleaned)
+ case commands.CreateHost:
+ return MagicMock(
+ res_data=[self.mockDataHostChange],
+ code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
- return MagicMock(res_data=[self.mockDataInfoHosts])
+ case commands.UpdateHost:
+ return MagicMock(
+ res_data=[self.mockDataHostChange],
+ code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
+ )
+ case commands.DeleteHost:
+ return MagicMock(
+ res_data=[self.mockDataHostChange],
+ code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
+ )
+ case commands.CheckDomain:
+ return self.mockCheckDomainCommand(_request, cleaned)
+ case commands.DeleteDomain:
+ return self.mockDeleteDomainCommands(_request, cleaned)
+ case _:
+ return MagicMock(res_data=[self.mockDataInfoHosts])
def mockUpdateDomainCommands(self, _request, cleaned):
if getattr(_request, "name", None) == "dnssec-invalid.gov":
@@ -826,6 +879,16 @@ class MockEppLib(TestCase):
code=ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY,
)
+ def mockDeleteDomainCommands(self, _request, cleaned):
+ if getattr(_request, "name", None) == "failDelete.gov":
+ name = getattr(_request, "name", None)
+ fake_nameserver = "ns1.failDelete.gov"
+ if name in fake_nameserver:
+ raise RegistryError(
+ code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION
+ )
+ return None
+
def mockInfoDomainCommands(self, _request, cleaned):
request_name = getattr(_request, "name", None)
@@ -851,6 +914,8 @@ class MockEppLib(TestCase):
"namerserversubdomain.gov": (self.infoDomainCheckHostIPCombo, None),
"freeman.gov": (self.InfoDomainWithContacts, None),
"threenameserversDomain.gov": (self.infoDomainThreeHosts, None),
+ "defaultsecurity.gov": (self.InfoDomainWithDefaultSecurityContact, None),
+ "defaulttechnical.gov": (self.InfoDomainWithDefaultTechnicalContact, None),
}
# Retrieve the corresponding values from the dictionary
@@ -876,12 +941,34 @@ class MockEppLib(TestCase):
mocked_result = self.mockAdministrativeContact
case "regContact":
mocked_result = self.mockRegistrantContact
+ case "defaultSec":
+ mocked_result = self.mockDefaultSecurityContact
+ case "defaultTech":
+ mocked_result = self.mockDefaultTechnicalContact
case _:
# Default contact return
mocked_result = self.mockDataInfoContact
return MagicMock(res_data=[mocked_result])
+ def mockCreateContactCommands(self, _request, cleaned):
+ if (
+ getattr(_request, "id", None) == "fail"
+ and self.mockedSendFunction.call_count == 3
+ ):
+ # use this for when a contact is being updated
+ # sets the second send() to fail
+ raise RegistryError(code=ErrorCode.OBJECT_EXISTS)
+ elif getattr(_request, "email", None) == "test@failCreate.gov":
+ # use this for when a contact is being updated
+ # mocks a registry error on creation
+ raise RegistryError(code=None)
+ elif getattr(_request, "email", None) == "test@contactError.gov":
+ # use this for when a contact is being updated
+ # mocks a contact error on creation
+ raise ContactError(code=ContactErrorCodes.CONTACT_TYPE_NONE)
+ return MagicMock(res_data=[self.mockDataInfoHosts])
+
def setUp(self):
"""mock epp send function as this will fail locally"""
self.mockSendPatch = patch("registrar.models.domain.registry.send")
@@ -892,15 +979,11 @@ class MockEppLib(TestCase):
self, contact: PublicContact, disclose_email=False, createContact=True
):
DF = common.DiscloseField
- fields = {DF.FAX, DF.VOICE, DF.ADDR}
-
- if not disclose_email:
- fields.add(DF.EMAIL)
+ fields = {DF.EMAIL}
di = common.Disclose(
- flag=False,
+ flag=disclose_email,
fields=fields,
- types={DF.ADDR: "loc"},
)
# check docs here looks like we may have more than one address field but
diff --git a/src/registrar/tests/test_admin.py b/src/registrar/tests/test_admin.py
index 51ace34f7..f2db9d5ee 100644
--- a/src/registrar/tests/test_admin.py
+++ b/src/registrar/tests/test_admin.py
@@ -11,6 +11,7 @@ from registrar.admin import (
ListHeaderAdmin,
MyUserAdmin,
AuditedAdmin,
+ ContactAdmin,
)
from registrar.models import (
Domain,
@@ -52,6 +53,26 @@ class TestDomainAdmin(MockEppLib):
self.factory = RequestFactory()
super().setUp()
+ def test_short_org_name_in_domains_list(self):
+ """
+ Make sure the short name is displaying in admin on the list page
+ """
+ self.client.force_login(self.superuser)
+ application = completed_application(status=DomainApplication.IN_REVIEW)
+ application.approve()
+
+ response = self.client.get("/admin/registrar/domain/")
+
+ # There are 3 template references to Federal (3) plus one reference in the table
+ # for our actual application
+ self.assertContains(response, "Federal", count=4)
+ # This may be a bit more robust
+ self.assertContains(
+ response, '
Federal
', count=1
+ )
+ # Now let's make sure the long description does not exist
+ self.assertNotContains(response, "Federal: an agency of the U.S. government")
+
@skip("Why did this test stop working, and is is a good test")
def test_place_and_remove_hold(self):
domain = create_ready_domain()
@@ -243,8 +264,11 @@ class TestDomainAdmin(MockEppLib):
raise
def tearDown(self):
- User.objects.all().delete()
super().tearDown()
+ Domain.objects.all().delete()
+ DomainInformation.objects.all().delete()
+ DomainApplication.objects.all().delete()
+ User.objects.all().delete()
class TestDomainApplicationAdminForm(TestCase):
@@ -300,6 +324,23 @@ class TestDomainApplicationAdmin(MockEppLib):
self.superuser = create_superuser()
self.staffuser = create_user()
+ def test_short_org_name_in_applications_list(self):
+ """
+ Make sure the short name is displaying in admin on the list page
+ """
+ self.client.force_login(self.superuser)
+ completed_application()
+ response = self.client.get("/admin/registrar/domainapplication/")
+ # There are 3 template references to Federal (3) plus one reference in the table
+ # for our actual application
+ self.assertContains(response, "Federal", count=4)
+ # This may be a bit more robust
+ self.assertContains(
+ response, '
Federal
', count=1
+ )
+ # Now let's make sure the long description does not exist
+ self.assertNotContains(response, "Federal: an agency of the U.S. government")
+
@boto3_mocking.patching
def test_save_model_sends_submitted_email(self):
# make sure there is no user with this email
@@ -620,9 +661,6 @@ class TestDomainApplicationAdmin(MockEppLib):
expected_fields = [
"creator",
"about_your_organization",
- "address_line1",
- "address_line2",
- "zipcode",
"requested_domain",
"alternative_domains",
"purpose",
@@ -1313,3 +1351,38 @@ class DomainSessionVariableTest(TestCase):
{"_edit_domain": "true"},
follow=True,
)
+
+
+class ContactAdminTest(TestCase):
+ def setUp(self):
+ self.site = AdminSite()
+ self.factory = RequestFactory()
+ self.client = Client(HTTP_HOST="localhost:8080")
+ self.admin = ContactAdmin(model=get_user_model(), admin_site=None)
+ self.superuser = create_superuser()
+ self.staffuser = create_user()
+
+ def test_readonly_when_restricted_staffuser(self):
+ request = self.factory.get("/")
+ request.user = self.staffuser
+
+ readonly_fields = self.admin.get_readonly_fields(request)
+
+ expected_fields = [
+ "user",
+ ]
+
+ self.assertEqual(readonly_fields, expected_fields)
+
+ def test_readonly_when_restricted_superuser(self):
+ request = self.factory.get("/")
+ request.user = self.superuser
+
+ readonly_fields = self.admin.get_readonly_fields(request)
+
+ expected_fields = []
+
+ self.assertEqual(readonly_fields, expected_fields)
+
+ def tearDown(self):
+ User.objects.all().delete()
diff --git a/src/registrar/tests/test_forms.py b/src/registrar/tests/test_forms.py
index 95be195ba..4b1aeb12c 100644
--- a/src/registrar/tests/test_forms.py
+++ b/src/registrar/tests/test_forms.py
@@ -1,6 +1,6 @@
"""Test form validation requirements."""
-from django.test import TestCase
+from django.test import TestCase, RequestFactory
from registrar.forms.application_wizard import (
CurrentSitesForm,
@@ -16,9 +16,16 @@ from registrar.forms.application_wizard import (
AboutYourOrganizationForm,
)
from registrar.forms.domain import ContactForm
+from registrar.tests.common import MockEppLib
+from django.contrib.auth import get_user_model
-class TestFormValidation(TestCase):
+class TestFormValidation(MockEppLib):
+ def setUp(self):
+ super().setUp()
+ self.user = get_user_model().objects.create(username="username")
+ self.factory = RequestFactory()
+
def test_org_contact_zip_invalid(self):
form = OrganizationContactForm(data={"zipcode": "nah"})
self.assertEqual(
diff --git a/src/registrar/tests/test_migrations.py b/src/registrar/tests/test_migrations.py
index 95e5853ff..165ef6f71 100644
--- a/src/registrar/tests/test_migrations.py
+++ b/src/registrar/tests/test_migrations.py
@@ -36,7 +36,7 @@ class TestGroups(TestCase):
# Define the expected permission codenames
expected_permissions = [
"view_logentry",
- "view_contact",
+ "change_contact",
"view_domain",
"change_domainapplication",
"change_domaininformation",
@@ -45,6 +45,7 @@ class TestGroups(TestCase):
"change_draftdomain",
"analyst_access_permission",
"change_user",
+ "change_website",
]
# Get the codenames of actual permissions associated with the group
diff --git a/src/registrar/tests/test_models.py b/src/registrar/tests/test_models.py
index 2c6f78ef5..e76dea035 100644
--- a/src/registrar/tests/test_models.py
+++ b/src/registrar/tests/test_models.py
@@ -601,7 +601,7 @@ class TestInvitations(TestCase):
def test_retrieve_existing_role_no_error(self):
# make the overlapping role
UserDomainRole.objects.get_or_create(
- user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
+ user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
# this is not an error but does produce a console warning
with less_console_noise():
diff --git a/src/registrar/tests/test_models_domain.py b/src/registrar/tests/test_models_domain.py
index e612d7b22..1d87c6b1c 100644
--- a/src/registrar/tests/test_models_domain.py
+++ b/src/registrar/tests/test_models_domain.py
@@ -19,7 +19,7 @@ from registrar.utility.errors import ActionNotAllowed, NameserverError
from registrar.models.utility.contact_error import ContactError, ContactErrorCodes
-from .common import MockEppLib
+
from django_fsm import TransitionNotAllowed # type: ignore
from epplibwrapper import (
commands,
@@ -29,6 +29,7 @@ from epplibwrapper import (
RegistryError,
ErrorCode,
)
+from .common import MockEppLib
import logging
logger = logging.getLogger(__name__)
@@ -760,6 +761,198 @@ class TestRegistrantContacts(MockEppLib):
self.mockedSendFunction.assert_has_calls(expected_calls, any_order=True)
self.assertEqual(PublicContact.objects.filter(domain=self.domain).count(), 1)
+ def test_not_disclosed_on_other_contacts(self):
+ """
+ Scenario: Registrant creates a new domain with multiple contacts
+ When `domain` has registrant, admin, technical,
+ and security contacts
+ Then Domain sends `commands.CreateContact` to the registry
+ And the field `disclose` is set to false for DF.EMAIL
+ on all fields except security
+ """
+ # Generates a domain with four existing contacts
+ domain, _ = Domain.objects.get_or_create(name="freeman.gov")
+
+ # Contact setup
+ expected_admin = domain.get_default_administrative_contact()
+ expected_admin.email = self.mockAdministrativeContact.email
+
+ expected_registrant = domain.get_default_registrant_contact()
+ expected_registrant.email = self.mockRegistrantContact.email
+
+ expected_security = domain.get_default_security_contact()
+ expected_security.email = self.mockSecurityContact.email
+
+ expected_tech = domain.get_default_technical_contact()
+ expected_tech.email = self.mockTechnicalContact.email
+
+ domain.administrative_contact = expected_admin
+ domain.registrant_contact = expected_registrant
+ domain.security_contact = expected_security
+ domain.technical_contact = expected_tech
+
+ contacts = [
+ (expected_admin, domain.administrative_contact),
+ (expected_registrant, domain.registrant_contact),
+ (expected_security, domain.security_contact),
+ (expected_tech, domain.technical_contact),
+ ]
+
+ # Test for each contact
+ for contact in contacts:
+ expected_contact = contact[0]
+ actual_contact = contact[1]
+ is_security = expected_contact.contact_type == "security"
+
+ expectedCreateCommand = self._convertPublicContactToEpp(
+ expected_contact, disclose_email=is_security
+ )
+
+ # Should only be disclosed if the type is security, as the email is valid
+ self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
+
+ # The emails should match on both items
+ self.assertEqual(expected_contact.email, actual_contact.email)
+
+ def test_convert_public_contact_to_epp(self):
+ self.maxDiff = None
+ domain, _ = Domain.objects.get_or_create(name="freeman.gov")
+ dummy_contact = domain.get_default_security_contact()
+ test_disclose = self._convertPublicContactToEpp(
+ dummy_contact, disclose_email=True
+ ).__dict__
+ test_not_disclose = self._convertPublicContactToEpp(
+ dummy_contact, disclose_email=False
+ ).__dict__
+
+ # Separated for linter
+ disclose_email_field = {common.DiscloseField.EMAIL}
+ expected_disclose = {
+ "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
+ "disclose": common.Disclose(
+ flag=True, fields=disclose_email_field, types=None
+ ),
+ "email": "dotgov@cisa.dhs.gov",
+ "extensions": [],
+ "fax": None,
+ "id": "ThIq2NcRIDN7PauO",
+ "ident": None,
+ "notify_email": None,
+ "postal_info": common.PostalInfo(
+ name="Registry Customer Service",
+ addr=common.ContactAddr(
+ street=["4200 Wilson Blvd.", None, None],
+ city="Arlington",
+ pc="22201",
+ cc="US",
+ sp="VA",
+ ),
+ org="Cybersecurity and Infrastructure Security Agency",
+ type="loc",
+ ),
+ "vat": None,
+ "voice": "+1.8882820870",
+ }
+
+ # Separated for linter
+ expected_not_disclose = {
+ "auth_info": common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
+ "disclose": common.Disclose(
+ flag=False, fields=disclose_email_field, types=None
+ ),
+ "email": "dotgov@cisa.dhs.gov",
+ "extensions": [],
+ "fax": None,
+ "id": "ThrECENCHI76PGLh",
+ "ident": None,
+ "notify_email": None,
+ "postal_info": common.PostalInfo(
+ name="Registry Customer Service",
+ addr=common.ContactAddr(
+ street=["4200 Wilson Blvd.", None, None],
+ city="Arlington",
+ pc="22201",
+ cc="US",
+ sp="VA",
+ ),
+ org="Cybersecurity and Infrastructure Security Agency",
+ type="loc",
+ ),
+ "vat": None,
+ "voice": "+1.8882820870",
+ }
+
+ # Set the ids equal, since this value changes
+ test_disclose["id"] = expected_disclose["id"]
+ test_not_disclose["id"] = expected_not_disclose["id"]
+
+ self.assertEqual(test_disclose, expected_disclose)
+ self.assertEqual(test_not_disclose, expected_not_disclose)
+
+ def test_not_disclosed_on_default_security_contact(self):
+ """
+ Scenario: Registrant creates a new domain with no security email
+ When `domain.security_contact.email` is equal to the default
+ Then Domain sends `commands.CreateContact` to the registry
+ And the field `disclose` is set to false for DF.EMAIL
+ """
+ domain, _ = Domain.objects.get_or_create(name="defaultsecurity.gov")
+ expectedSecContact = PublicContact.get_default_security()
+ expectedSecContact.domain = domain
+ expectedSecContact.registry_id = "defaultSec"
+ domain.security_contact = expectedSecContact
+
+ expectedCreateCommand = self._convertPublicContactToEpp(
+ expectedSecContact, disclose_email=False
+ )
+
+ self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
+ # Confirm that we are getting a default email
+ self.assertEqual(domain.security_contact.email, expectedSecContact.email)
+
+ def test_not_disclosed_on_default_technical_contact(self):
+ """
+ Scenario: Registrant creates a new domain with no technical contact
+ When `domain.technical_contact.email` is equal to the default
+ Then Domain sends `commands.CreateContact` to the registry
+ And the field `disclose` is set to false for DF.EMAIL
+ """
+ domain, _ = Domain.objects.get_or_create(name="defaulttechnical.gov")
+ expectedTechContact = PublicContact.get_default_technical()
+ expectedTechContact.domain = domain
+ expectedTechContact.registry_id = "defaultTech"
+ domain.technical_contact = expectedTechContact
+
+ expectedCreateCommand = self._convertPublicContactToEpp(
+ expectedTechContact, disclose_email=False
+ )
+
+ self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
+ # Confirm that we are getting a default email
+ self.assertEqual(domain.technical_contact.email, expectedTechContact.email)
+
+ def test_is_disclosed_on_security_contact(self):
+ """
+ Scenario: Registrant creates a new domain with a security email
+ When `domain.security_contact.email` is set to a valid email
+ and is not the default
+ Then Domain sends `commands.CreateContact` to the registry
+ And the field `disclose` is set to true for DF.EMAIL
+ """
+ domain, _ = Domain.objects.get_or_create(name="igorville.gov")
+ expectedSecContact = PublicContact.get_default_security()
+ expectedSecContact.domain = domain
+ expectedSecContact.email = "123@mail.gov"
+ domain.security_contact = expectedSecContact
+
+ expectedCreateCommand = self._convertPublicContactToEpp(
+ expectedSecContact, disclose_email=True
+ )
+
+ self.mockedSendFunction.assert_any_call(expectedCreateCommand, cleaned=True)
+ # Confirm that we are getting the desired email
+ self.assertEqual(domain.security_contact.email, expectedSecContact.email)
+
@skip("not implemented yet")
def test_update_is_unsuccessful(self):
"""
diff --git a/src/registrar/tests/test_views.py b/src/registrar/tests/test_views.py
index 2a1cd8fed..0fba81fc1 100644
--- a/src/registrar/tests/test_views.py
+++ b/src/registrar/tests/test_views.py
@@ -89,7 +89,7 @@ class LoggedInTests(TestWithUser):
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
self.assertNotContains(response, "igorville.gov")
role, _ = UserDomainRole.objects.get_or_create(
- user=self.user, domain=domain, role=UserDomainRole.Roles.ADMIN
+ user=self.user, domain=domain, role=UserDomainRole.Roles.MANAGER
)
response = self.client.get("/")
# count = 2 because it is also in screenreader content
@@ -142,9 +142,12 @@ class DomainApplicationTests(TestWithUser, WebTest):
@boto3_mocking.patching
def test_application_form_submission(self):
- """Can fill out the entire form and submit.
+ """
+ Can fill out the entire form and submit.
As we add additional form pages, we need to include them here to make
this test work.
+
+ This test also looks for the long organization name on the summary page.
"""
num_pages_tested = 0
# elections, type_of_work, tribal_government, no_other_contacts
@@ -428,7 +431,8 @@ class DomainApplicationTests(TestWithUser, WebTest):
review_form = review_page.form
# Review page contains all the previously entered data
- self.assertContains(review_page, "Federal")
+ # Let's make sure the long org name is displayed
+ self.assertContains(review_page, "Federal: an agency of the U.S. government")
self.assertContains(review_page, "Executive")
self.assertContains(review_page, "Testorg")
self.assertContains(review_page, "address 1")
@@ -1066,6 +1070,26 @@ class DomainApplicationTests(TestWithUser, WebTest):
# page = self.app.get(url)
# self.assertNotContains(page, "VALUE")
+ def test_long_org_name_in_application(self):
+ """
+ Make sure the long name is displaying in the application form,
+ org step
+ """
+ request = self.app.get(reverse("application:")).follow()
+ self.assertContains(request, "Federal: an agency of the U.S. government")
+
+ def test_long_org_name_in_application_manage(self):
+ """
+ Make sure the long name is displaying in the application summary
+ page (manage your application)
+ """
+ completed_application(status=DomainApplication.SUBMITTED, user=self.user)
+ home_page = self.app.get("/")
+ self.assertContains(home_page, "city.gov")
+ # click the "Edit" link
+ detail_page = home_page.click("Manage")
+ self.assertContains(detail_page, "Federal: an agency of the U.S. government")
+
class TestWithDomainPermissions(TestWithUser):
def setUp(self):
@@ -1093,10 +1117,10 @@ class TestWithDomainPermissions(TestWithUser):
creator=self.user, domain=self.domain_dnssec_none
)
self.role, _ = UserDomainRole.objects.get_or_create(
- user=self.user, domain=self.domain, role=UserDomainRole.Roles.ADMIN
+ user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
)
UserDomainRole.objects.get_or_create(
- user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.ADMIN
+ user=self.user, domain=self.domain_dsdata, role=UserDomainRole.Roles.MANAGER
)
UserDomainRole.objects.get_or_create(
user=self.user,
@@ -1106,7 +1130,7 @@ class TestWithDomainPermissions(TestWithUser):
UserDomainRole.objects.get_or_create(
user=self.user,
domain=self.domain_dnssec_none,
- role=UserDomainRole.Roles.ADMIN,
+ role=UserDomainRole.Roles.MANAGER,
)
def tearDown(self):
@@ -1192,14 +1216,14 @@ class TestDomainOverview(TestWithDomainPermissions, WebTest):
self.assertEqual(response.status_code, 403)
-class TestDomainUserManagement(TestDomainOverview):
- def test_domain_user_management(self):
+class TestDomainManagers(TestDomainOverview):
+ def test_domain_managers(self):
response = self.client.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
)
- self.assertContains(response, "User management")
+ self.assertContains(response, "Domain managers")
- def test_domain_user_management_add_link(self):
+ def test_domain_managers_add_link(self):
"""Button to get to user add page works."""
management_page = self.app.get(
reverse("domain-users", kwargs={"pk": self.domain.id})
@@ -1547,6 +1571,78 @@ class TestDomainSecurityEmail(TestDomainOverview):
success_page, "The security email for this domain has been updated"
)
+ def test_security_email_form_messages(self):
+ """
+ Test against the success and error messages that are defined in the view
+ """
+ p = "adminpass"
+ self.client.login(username="superuser", password=p)
+
+ form_data_registry_error = {
+ "security_email": "test@failCreate.gov",
+ }
+
+ form_data_contact_error = {
+ "security_email": "test@contactError.gov",
+ }
+
+ form_data_success = {
+ "security_email": "test@something.gov",
+ }
+
+ test_cases = [
+ (
+ "RegistryError",
+ form_data_registry_error,
+ "Update failed. Cannot contact the registry.",
+ ),
+ ("ContactError", form_data_contact_error, "Value entered was wrong."),
+ (
+ "RegistrySuccess",
+ form_data_success,
+ "The security email for this domain has been updated.",
+ ),
+ # Add more test cases with different scenarios here
+ ]
+
+ for test_name, data, expected_message in test_cases:
+ response = self.client.post(
+ reverse("domain-security-email", kwargs={"pk": self.domain.id}),
+ data=data,
+ follow=True,
+ )
+
+ # Check the response status code, content, or any other relevant assertions
+ self.assertEqual(response.status_code, 200)
+
+ # Check if the expected message tag is set
+ if test_name == "RegistryError" or test_name == "ContactError":
+ message_tag = "error"
+ elif test_name == "RegistrySuccess":
+ message_tag = "success"
+ else:
+ # Handle other cases if needed
+ message_tag = "info" # Change to the appropriate default
+
+ # Check the message tag
+ messages = list(response.context["messages"])
+ self.assertEqual(len(messages), 1)
+ message = messages[0]
+ self.assertEqual(message.tags, message_tag)
+ self.assertEqual(message.message, expected_message)
+
+ def test_domain_overview_blocked_for_ineligible_user(self):
+ """We could easily duplicate this test for all domain management
+ views, but a single url test should be solid enough since all domain
+ management pages share the same permissions class"""
+ self.user.status = User.RESTRICTED
+ self.user.save()
+ home_page = self.app.get("/")
+ self.assertContains(home_page, "igorville.gov")
+ with less_console_noise():
+ response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
+ self.assertEqual(response.status_code, 403)
+
class TestDomainDNSSEC(TestDomainOverview):
diff --git a/src/registrar/views/domain.py b/src/registrar/views/domain.py
index 18a1a2921..1058c537a 100644
--- a/src/registrar/views/domain.py
+++ b/src/registrar/views/domain.py
@@ -25,6 +25,7 @@ from registrar.models import (
UserDomainRole,
)
from registrar.models.public_contact import PublicContact
+from registrar.models.utility.contact_error import ContactError
from ..forms import (
ContactForm,
@@ -41,6 +42,8 @@ from epplibwrapper import (
common,
extensions,
RegistryError,
+ CANNOT_CONTACT_REGISTRY,
+ GENERIC_ERROR,
)
from ..utility.email import send_templated_email, EmailSendingError
@@ -50,7 +53,81 @@ from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
logger = logging.getLogger(__name__)
-class DomainView(DomainPermissionView):
+class DomainBaseView(DomainPermissionView):
+ """
+ Base View for the Domain. Handles getting and setting the domain
+ in session cache on GETs. Also provides methods for getting
+ and setting the domain in cache
+ """
+
+ def get(self, request, *args, **kwargs):
+ self._get_domain(request)
+ context = self.get_context_data(object=self.object)
+ return self.render_to_response(context)
+
+ def _get_domain(self, request):
+ """
+ get domain from session cache or from db and set
+ to self.object
+ set session to self for downstream functions to
+ update session cache
+ """
+ self.session = request.session
+ # domain:private_key is the session key to use for
+ # caching the domain in the session
+ domain_pk = "domain:" + str(self.kwargs.get("pk"))
+ cached_domain = self.session.get(domain_pk)
+
+ if cached_domain:
+ self.object = cached_domain
+ else:
+ self.object = self.get_object()
+ self._update_session_with_domain()
+
+ def _update_session_with_domain(self):
+ """
+ update domain in the session cache
+ """
+ domain_pk = "domain:" + str(self.kwargs.get("pk"))
+ self.session[domain_pk] = self.object
+
+
+class DomainFormBaseView(DomainBaseView, FormMixin):
+ """
+ Form Base View for the Domain. Handles getting and setting
+ domain in cache when dealing with domain forms. Provides
+ implementations of post, form_valid and form_invalid.
+ """
+
+ def post(self, request, *args, **kwargs):
+ """Form submission posts to this view.
+
+ This post method harmonizes using DomainBaseView and FormMixin
+ """
+ self._get_domain(request)
+ form = self.get_form()
+ if form.is_valid():
+ return self.form_valid(form)
+ else:
+ return self.form_invalid(form)
+
+ def form_valid(self, form):
+ # updates session cache with domain
+ self._update_session_with_domain()
+
+ # superclass has the redirect
+ return super().form_valid(form)
+
+ def form_invalid(self, form):
+ # updates session cache with domain
+ self._update_session_with_domain()
+
+ # superclass has the redirect
+ return super().form_invalid(form)
+
+
+class DomainView(DomainBaseView):
+
"""Domain detail overview page."""
template_name = "domain_detail.html"
@@ -58,10 +135,10 @@ class DomainView(DomainPermissionView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- default_email = Domain().get_default_security_contact().email
+ default_email = self.object.get_default_security_contact().email
context["default_security_email"] = default_email
- security_email = self.get_object().get_security_email()
+ security_email = self.object.get_security_email()
if security_email is None or security_email == default_email:
context["security_email"] = None
return context
@@ -69,7 +146,7 @@ class DomainView(DomainPermissionView):
return context
-class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
+class DomainOrgNameAddressView(DomainFormBaseView):
"""Organization name and mailing address view"""
model = Domain
@@ -80,25 +157,13 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.organization_name instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
- form_kwargs["instance"] = self.get_object().domain_info
+ form_kwargs["instance"] = self.object.domain_info
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-org-name-address", kwargs={"pk": self.object.pk})
- def post(self, request, *args, **kwargs):
- """Form submission posts to this view.
-
- This post method harmonizes using DetailView and FormMixin together.
- """
- self.object = self.get_object()
- form = self.get_form()
- if form.is_valid():
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
def form_valid(self, form):
"""The form is valid, save the organization name and mailing address."""
form.save()
@@ -111,7 +176,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
return super().form_valid(form)
-class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
+class DomainAuthorizingOfficialView(DomainFormBaseView):
"""Domain authorizing official editing view."""
model = Domain
@@ -122,25 +187,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
def get_form_kwargs(self, *args, **kwargs):
"""Add domain_info.authorizing_official instance to make a bound form."""
form_kwargs = super().get_form_kwargs(*args, **kwargs)
- form_kwargs["instance"] = self.get_object().domain_info.authorizing_official
+ form_kwargs["instance"] = self.object.domain_info.authorizing_official
return form_kwargs
def get_success_url(self):
"""Redirect to the overview page for the domain."""
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
- def post(self, request, *args, **kwargs):
- """Form submission posts to this view.
-
- This post method harmonizes using DetailView and FormMixin together.
- """
- self.object = self.get_object()
- form = self.get_form()
- if form.is_valid():
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
def form_valid(self, form):
"""The form is valid, save the authorizing official."""
form.save()
@@ -153,13 +206,13 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
return super().form_valid(form)
-class DomainDNSView(DomainPermissionView):
+class DomainDNSView(DomainBaseView):
"""DNS Information View."""
template_name = "domain_dns.html"
-class DomainNameserversView(DomainPermissionView, FormMixin):
+class DomainNameserversView(DomainFormBaseView):
"""Domain nameserver editing view."""
template_name = "domain_nameservers.html"
@@ -167,8 +220,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
- domain = self.get_object()
- nameservers = domain.nameservers
+ nameservers = self.object.nameservers
initial_data = []
if nameservers is not None:
@@ -204,16 +256,6 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
form.fields["server"].required = False
return formset
- def post(self, request, *args, **kwargs):
- """Formset submission posts to this view."""
- self.object = self.get_object()
- formset = self.get_form()
-
- if formset.is_valid():
- return self.form_valid(formset)
- else:
- return self.form_invalid(formset)
-
def form_valid(self, formset):
"""The formset is valid, perform something with it."""
@@ -226,8 +268,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
except KeyError:
# no server information in this field, skip it
pass
- domain = self.get_object()
- domain.nameservers = nameservers
+ self.object.nameservers = nameservers
messages.success(
self.request, "The name servers for this domain have been updated."
@@ -237,7 +278,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
return super().form_valid(formset)
-class DomainDNSSECView(DomainPermissionView, FormMixin):
+class DomainDNSSECView(DomainFormBaseView):
"""Domain DNSSEC editing view."""
template_name = "domain_dnssec.html"
@@ -247,9 +288,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin):
"""The initial value for the form (which is a formset here)."""
context = super().get_context_data(**kwargs)
- self.domain = self.get_object()
-
- has_dnssec_records = self.domain.dnssecdata is not None
+ has_dnssec_records = self.object.dnssecdata is not None
# Create HTML for the modal button
modal_button = (
@@ -266,16 +305,16 @@ class DomainDNSSECView(DomainPermissionView, FormMixin):
def get_success_url(self):
"""Redirect to the DNSSEC page for the domain."""
- return reverse("domain-dns-dnssec", kwargs={"pk": self.domain.pk})
+ return reverse("domain-dns-dnssec", kwargs={"pk": self.object.pk})
def post(self, request, *args, **kwargs):
"""Form submission posts to this view."""
- self.domain = self.get_object()
+ self._get_domain(request)
form = self.get_form()
if form.is_valid():
if "disable_dnssec" in request.POST:
try:
- self.domain.dnssecdata = {}
+ self.object.dnssecdata = {}
except RegistryError as err:
errmsg = "Error removing existing DNSSEC record(s)."
logger.error(errmsg + ": " + err)
@@ -284,7 +323,7 @@ class DomainDNSSECView(DomainPermissionView, FormMixin):
return self.form_valid(form)
-class DomainDsDataView(DomainPermissionView, FormMixin):
+class DomainDsDataView(DomainFormBaseView):
"""Domain DNSSEC ds data editing view."""
template_name = "domain_dsdata.html"
@@ -293,8 +332,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin):
def get_initial(self):
"""The initial value for the form (which is a formset here)."""
- domain = self.get_object()
- dnssecdata: extensions.DNSSECExtension = domain.dnssecdata
+ dnssecdata: extensions.DNSSECExtension = self.object.dnssecdata
initial_data = []
if dnssecdata is not None and dnssecdata.dsData is not None:
@@ -329,7 +367,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin):
def post(self, request, *args, **kwargs):
"""Formset submission posts to this view."""
- self.object = self.get_object()
+ self._get_domain(request)
formset = self.get_form()
override = False
@@ -390,9 +428,8 @@ class DomainDsDataView(DomainPermissionView, FormMixin):
# as valid; this can happen if form has been added but
# not been interacted with; in that case, want to ignore
pass
- domain = self.get_object()
try:
- domain.dnssecdata = dnssecdata
+ self.object.dnssecdata = dnssecdata
except RegistryError as err:
errmsg = "Error updating DNSSEC data in the registry."
logger.error(errmsg)
@@ -407,7 +444,7 @@ class DomainDsDataView(DomainPermissionView, FormMixin):
return super().form_valid(formset)
-class DomainYourContactInformationView(DomainPermissionView, FormMixin):
+class DomainYourContactInformationView(DomainFormBaseView):
"""Domain your contact information editing view."""
template_name = "domain_your_contact_information.html"
@@ -423,16 +460,6 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
"""Redirect to the your contact information for the domain."""
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
- def post(self, request, *args, **kwargs):
- """Form submission posts to this view."""
- self.object = self.get_object()
- form = self.get_form()
- if form.is_valid():
- # there is a valid email address in the form
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
def form_valid(self, form):
"""The form is valid, call setter in model."""
@@ -447,7 +474,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
return super().form_valid(form)
-class DomainSecurityEmailView(DomainPermissionView, FormMixin):
+class DomainSecurityEmailView(DomainFormBaseView):
"""Domain security email editing view."""
template_name = "domain_security_email.html"
@@ -455,9 +482,8 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
def get_initial(self):
"""The initial value for the form."""
- domain = self.get_object()
initial = super().get_initial()
- security_contact = domain.security_contact
+ security_contact = self.object.security_contact
if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov":
initial["security_email"] = None
return initial
@@ -468,16 +494,6 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
"""Redirect to the security email page for the domain."""
return reverse("domain-security-email", kwargs={"pk": self.object.pk})
- def post(self, request, *args, **kwargs):
- """Form submission posts to this view."""
- self.object = self.get_object()
- form = self.get_form()
- if form.is_valid():
- # there is a valid email address in the form
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
def form_valid(self, form):
"""The form is valid, call setter in model."""
@@ -488,33 +504,44 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
if new_email is None or new_email.strip() == "":
new_email = PublicContact.get_default_security().email
- domain = self.get_object()
- contact = domain.security_contact
+ contact = self.object.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.")
+ messages.error(self.request, CANNOT_CONTACT_REGISTRY)
return redirect(self.get_success_url())
contact.email = new_email
- contact.save()
- messages.success(
- self.request, "The security email for this domain has been updated."
- )
+ try:
+ contact.save()
+ except RegistryError as Err:
+ if Err.is_connection_error():
+ messages.error(self.request, CANNOT_CONTACT_REGISTRY)
+ logger.error(f"Registry connection error: {Err}")
+ else:
+ messages.error(self.request, GENERIC_ERROR)
+ logger.error(f"Registry error: {Err}")
+ except ContactError as Err:
+ messages.error(self.request, GENERIC_ERROR)
+ logger.error(f"Generic registry error: {Err}")
+ else:
+ messages.success(
+ self.request, "The security email for this domain has been updated."
+ )
# superclass has the redirect
return redirect(self.get_success_url())
-class DomainUsersView(DomainPermissionView):
- """User management page in the domain details."""
+class DomainUsersView(DomainBaseView):
+ """Domain managers page in the domain details."""
template_name = "domain_users.html"
-class DomainAddUserView(DomainPermissionView, FormMixin):
+class DomainAddUserView(DomainFormBaseView):
"""Inside of a domain's user management, a form for adding users.
Multiple inheritance is used here for permissions, form handling, and
@@ -527,15 +554,6 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
def get_success_url(self):
return reverse("domain-users", kwargs={"pk": self.object.pk})
- def post(self, request, *args, **kwargs):
- self.object = self.get_object()
- form = self.get_form()
- if form.is_valid():
- # there is a valid email address in the form
- return self.form_valid(form)
- else:
- return self.form_invalid(form)
-
def _domain_abs_url(self):
"""Get an absolute URL for this domain."""
return self.request.build_absolute_uri(
@@ -598,7 +616,9 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
try:
UserDomainRole.objects.create(
- user=requested_user, domain=self.object, role=UserDomainRole.Roles.ADMIN
+ user=requested_user,
+ domain=self.object,
+ role=UserDomainRole.Roles.MANAGER,
)
except IntegrityError:
# User already has the desired role! Do nothing??