mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-05 01:11:55 +02:00
Merge branch 'main' into za/1848-copy-contact-email-to-clipboard
This commit is contained in:
commit
a5d8a3d3bf
53 changed files with 1841 additions and 189 deletions
|
@ -29,6 +29,7 @@ django-login-required-middleware = "*"
|
|||
greenlet = "*"
|
||||
gevent = "*"
|
||||
fred-epplib = {git = "https://github.com/cisagov/epplib.git", ref = "master"}
|
||||
pyzipper="*"
|
||||
tblib = "*"
|
||||
|
||||
[dev-packages]
|
||||
|
@ -44,4 +45,4 @@ django-webtest = "*"
|
|||
types-cachetools = "*"
|
||||
boto3-mocking = "*"
|
||||
boto3-stubs = "*"
|
||||
django-model2puml = "*"
|
||||
django-model2puml = "*"
|
65
src/Pipfile.lock
generated
65
src/Pipfile.lock
generated
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "b5d93b1b9ccafc37019276a222957544bab3f1f46b5dab8a0f2ffc2e5c9e1678"
|
||||
"sha256": "082a951f15bb26a28f2dca7e0840fdf61518b3d90c42d77a310f982344cbd1dc"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
|
@ -32,20 +32,20 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
||||
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
||||
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
|
@ -376,20 +376,20 @@
|
|||
"django"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:cc421ddb143fa30183568164755aa113a160e555cd19e97e664c478662032c24",
|
||||
"sha256:feeaf28f17fd0499f9cd7c0fcf408c6d82c308e69e335eb92d09322fc9ed8138"
|
||||
"sha256:069727a8f73d8ba8d033d3cd95c0da231d44f38f1da773bf076cef168d312ee8",
|
||||
"sha256:e0bcfd41c718c07a7db422f9109e490746450da38793fe4ee197f397b9343435"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.3.0"
|
||||
"version": "==11.0.0"
|
||||
},
|
||||
"faker": {
|
||||
"hashes": [
|
||||
"sha256:117ce1a2805c1bc5ca753b3dc6f9d567732893b2294b827d3164261ee8f20267",
|
||||
"sha256:458d93580de34403a8dec1e8d5e6be2fee96c4deca63b95d71df7a6a80a690de"
|
||||
"sha256:2456d674f40bd51eb3acbf85221277027822e529a90cc826453d9a25dff932b1",
|
||||
"sha256:ea6f784c40730de0f77067e49e78cdd590efb00bec3d33f577492262206c17fc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==23.3.0"
|
||||
"version": "==24.0.0"
|
||||
},
|
||||
"fred-epplib": {
|
||||
"git": "https://github.com/cisagov/epplib.git",
|
||||
|
@ -708,11 +708,11 @@
|
|||
},
|
||||
"marshmallow": {
|
||||
"hashes": [
|
||||
"sha256:20f53be28c6e374a711a16165fb22a8dc6003e3f7cda1285e3ca777b9193885b",
|
||||
"sha256:e7997f83571c7fd476042c2c188e4ee8a78900ca5e74bd9c8097afa56624e9bd"
|
||||
"sha256:4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3",
|
||||
"sha256:f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.21.0"
|
||||
"version": "==3.21.1"
|
||||
},
|
||||
"oic": {
|
||||
"hashes": [
|
||||
|
@ -994,6 +994,15 @@
|
|||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.0.1"
|
||||
},
|
||||
"pyzipper": {
|
||||
"hashes": [
|
||||
"sha256:0adca90a00c36a93fbe49bfa8c5add452bfe4ef85a1b8e3638739dd1c7b26bfc",
|
||||
"sha256:6d097f465bfa47796b1494e12ea65d1478107d38e13bc56f6e58eedc4f6c1a87"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.4'",
|
||||
"version": "==0.3.6"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
|
||||
|
@ -1186,12 +1195,12 @@
|
|||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:8b3f5cc7fbedcbb22271c328039df8a6ab343001e746e0cdb24774c426cadcf8",
|
||||
"sha256:f201b6a416f809283d554c652211eecec9fe3a52ed4063dab3f3e7aea7571d9c"
|
||||
"sha256:300888f0c1b6f32f27f85a9aa876f50f46514ec619647af7e4d20db74d339714",
|
||||
"sha256:b26928f9a21cf3649cea20a59061340f3294c6e7785ceb6e1a953eb8010dc3ba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"boto3-mocking": {
|
||||
"hashes": [
|
||||
|
@ -1204,28 +1213,28 @@
|
|||
},
|
||||
"boto3-stubs": {
|
||||
"hashes": [
|
||||
"sha256:7db5194e47f76e0010cd00b6ad9725db114d6a3fd04e52ceed3ef1181fe326bc",
|
||||
"sha256:c7b2e8b99f4896cf1226df47d4badaaa8df7426008c96a428bf00205695669e9"
|
||||
"sha256:627f8eca69d832581ee1676d39df099a2a2e3a86d6b3ebd21c81c5f11ed6a6fa",
|
||||
"sha256:a87e7ecbab6235ec371b4363027e57483bca349a9cd5c891f40db81dadfa273e"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:4061ff4be3efcf53547ebadf2c94d419dfc8be7beec24e9fa1819599ffd936fa",
|
||||
"sha256:bf215d93e9d5544c593962780d194e74c6ee40b883d0b885e62ef35fc0ec01e5"
|
||||
"sha256:bffeb71ab21d47d4ecf947d9bdb2fbd1b0bbd0c27742cea7cf0b77b701c41d9f",
|
||||
"sha256:fff66e22a5589c2d58fba57d1d95c334ce771895e831f80365f6cff6453285ec"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"botocore-stubs": {
|
||||
"hashes": [
|
||||
"sha256:958f0084322dc9e549f73151b686fa51b15858fb2b3a573b9f4367f073fff463",
|
||||
"sha256:bcc35bfbd14d1261813681c40108f2ce85fdf082c15b0a04016d3c22dd93b73f"
|
||||
"sha256:018e001e3add5eb1828ef444b45fb8c9faf695e08334031bf2d96853cd9af703",
|
||||
"sha256:25468ba6983987b704b1856bb155f297f576e6d4a690b021ab0c7122889ba907"
|
||||
],
|
||||
"markers": "python_version >= '3.8' and python_version < '4.0'",
|
||||
"version": "==1.34.54"
|
||||
"version": "==1.34.56"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
|
|
|
@ -49,3 +49,17 @@ def less_console_noise():
|
|||
handler.setStream(restore[handler.name])
|
||||
# close the file we opened
|
||||
devnull.close()
|
||||
|
||||
|
||||
def less_console_noise_decorator(func):
|
||||
"""
|
||||
Decorator to silence console logging using the less_console_noise() function.
|
||||
"""
|
||||
|
||||
# "Wrap" the original function in the less_console_noise with clause,
|
||||
# then just return this wrapper.
|
||||
def wrapper(*args, **kwargs):
|
||||
with less_console_noise():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
|
|
@ -6,12 +6,13 @@ from django.conf import settings
|
|||
from django.contrib.auth import logout as auth_logout
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.shortcuts import redirect
|
||||
from urllib.parse import parse_qs, urlencode
|
||||
|
||||
from djangooidc.oidc import Client
|
||||
from djangooidc import exceptions as o_e
|
||||
from registrar.models import User
|
||||
from registrar.views.utility.error_views import custom_500_error_view, custom_401_error_view
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -49,27 +50,19 @@ def error_page(request, error):
|
|||
"""Display a sensible message and log the error."""
|
||||
logger.error(error)
|
||||
if isinstance(error, o_e.AuthenticationFailed):
|
||||
return render(
|
||||
request,
|
||||
"401.html",
|
||||
context={
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator,
|
||||
},
|
||||
status=401,
|
||||
)
|
||||
context = {
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator,
|
||||
}
|
||||
return custom_401_error_view(request, context)
|
||||
if isinstance(error, o_e.InternalError):
|
||||
return render(
|
||||
request,
|
||||
"500.html",
|
||||
context={
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator,
|
||||
},
|
||||
status=500,
|
||||
)
|
||||
context = {
|
||||
"friendly_message": error.friendly_message,
|
||||
"log_identifier": error.locator,
|
||||
}
|
||||
return custom_500_error_view(request, context)
|
||||
if isinstance(error, Exception):
|
||||
return render(request, "500.html", status=500)
|
||||
return custom_500_error_view(request)
|
||||
|
||||
|
||||
def openid(request):
|
||||
|
|
|
@ -58,6 +58,8 @@ services:
|
|||
- AWS_S3_SECRET_ACCESS_KEY
|
||||
- AWS_S3_REGION
|
||||
- AWS_S3_BUCKET_NAME
|
||||
# File encryption credentials
|
||||
- SECRET_ENCRYPT_METADATA
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ports:
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
"""Provide a wrapper around epplib to handle authentication and errors."""
|
||||
|
||||
import logging
|
||||
from gevent.lock import BoundedSemaphore
|
||||
|
||||
try:
|
||||
from epplib.client import Client
|
||||
|
@ -52,10 +53,16 @@ class EPPLibWrapper:
|
|||
"urn:ietf:params:xml:ns:contact-1.0",
|
||||
],
|
||||
)
|
||||
# We should only ever have one active connection at a time
|
||||
self.connection_lock = BoundedSemaphore(1)
|
||||
|
||||
self.connection_lock.acquire()
|
||||
try:
|
||||
self._initialize_client()
|
||||
except Exception:
|
||||
logger.warning("Unable to configure epplib. Registrar cannot contact registry.")
|
||||
logger.warning("Unable to configure the connection to the registry.")
|
||||
finally:
|
||||
self.connection_lock.release()
|
||||
|
||||
def _initialize_client(self) -> None:
|
||||
"""Initialize a client, assuming _login defined. Sets _client to initialized
|
||||
|
@ -74,11 +81,7 @@ class EPPLibWrapper:
|
|||
)
|
||||
try:
|
||||
# use the _client object to connect
|
||||
self._client.connect() # type: ignore
|
||||
response = self._client.send(self._login) # type: ignore
|
||||
if response.code >= 2000: # type: ignore
|
||||
self._client.close() # type: ignore
|
||||
raise LoginError(response.msg) # type: ignore
|
||||
self._connect()
|
||||
except TransportError as err:
|
||||
message = "_initialize_client failed to execute due to a connection error."
|
||||
logger.error(f"{message} Error: {err}")
|
||||
|
@ -90,13 +93,33 @@ class EPPLibWrapper:
|
|||
logger.error(f"{message} Error: {err}")
|
||||
raise RegistryError(message) from err
|
||||
|
||||
def _connect(self) -> None:
|
||||
"""Connects to EPP. Sends a login command. If an invalid response is returned,
|
||||
the client will be closed and a LoginError raised."""
|
||||
self._client.connect() # type: ignore
|
||||
response = self._client.send(self._login) # type: ignore
|
||||
if response.code >= 2000: # type: ignore
|
||||
self._client.close() # type: ignore
|
||||
raise LoginError(response.msg) # type: ignore
|
||||
|
||||
def _disconnect(self) -> None:
|
||||
"""Close the connection."""
|
||||
"""Close the connection. Sends a logout command and closes the connection."""
|
||||
self._send_logout_command()
|
||||
self._close_client()
|
||||
|
||||
def _send_logout_command(self):
|
||||
"""Sends a logout command to epp"""
|
||||
try:
|
||||
self._client.send(commands.Logout()) # type: ignore
|
||||
self._client.close() # type: ignore
|
||||
except Exception:
|
||||
logger.warning("Connection to registry was not cleanly closed.")
|
||||
except Exception as err:
|
||||
logger.warning(f"Logout command not sent successfully: {err}")
|
||||
|
||||
def _close_client(self):
|
||||
"""Closes an active client connection"""
|
||||
try:
|
||||
self._client.close()
|
||||
except Exception as err:
|
||||
logger.warning(f"Connection to registry was not cleanly closed: {err}")
|
||||
|
||||
def _send(self, command):
|
||||
"""Helper function used by `send`."""
|
||||
|
@ -146,6 +169,8 @@ class EPPLibWrapper:
|
|||
cmd_type = command.__class__.__name__
|
||||
if not cleaned:
|
||||
raise ValueError("Please sanitize user input before sending it.")
|
||||
|
||||
self.connection_lock.acquire()
|
||||
try:
|
||||
return self._send(command)
|
||||
except RegistryError as err:
|
||||
|
@ -161,6 +186,8 @@ class EPPLibWrapper:
|
|||
return self._retry(command)
|
||||
else:
|
||||
raise err
|
||||
finally:
|
||||
self.connection_lock.release()
|
||||
|
||||
|
||||
try:
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import datetime
|
||||
from dateutil.tz import tzlocal # type: ignore
|
||||
from unittest.mock import MagicMock, patch
|
||||
from pathlib import Path
|
||||
from django.test import TestCase
|
||||
from gevent.exceptions import ConcurrentObjectUseError
|
||||
from epplibwrapper.client import EPPLibWrapper
|
||||
from epplibwrapper.errors import RegistryError, LoginError
|
||||
from .common import less_console_noise
|
||||
|
@ -8,6 +12,9 @@ import logging
|
|||
try:
|
||||
from epplib.exceptions import TransportError
|
||||
from epplib.responses import Result
|
||||
from epplib.transport import SocketTransport
|
||||
from epplib import commands
|
||||
from epplib.models import common, info
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
@ -255,3 +262,116 @@ class TestClient(TestCase):
|
|||
mock_close.assert_called_once()
|
||||
# send() is called 5 times: send(login), send(command) fail, send(logout), send(login), send(command)
|
||||
self.assertEquals(mock_send.call_count, 5)
|
||||
|
||||
def fake_failure_send_concurrent_threads(self, command=None, cleaned=None):
|
||||
"""
|
||||
Raises a ConcurrentObjectUseError, which gevent throws when accessing
|
||||
the same thread from two different locations.
|
||||
"""
|
||||
# This error is thrown when two threads are being used concurrently
|
||||
raise ConcurrentObjectUseError("This socket is already used by another greenlet")
|
||||
|
||||
def do_nothing(self, command=None):
|
||||
"""
|
||||
A placeholder method that performs no action.
|
||||
"""
|
||||
pass # noqa
|
||||
|
||||
def fake_success_send(self, command=None, cleaned=None):
|
||||
"""
|
||||
Simulates receiving a success response from EPP.
|
||||
"""
|
||||
mock = MagicMock(
|
||||
code=1000,
|
||||
msg="Command completed successfully",
|
||||
res_data=None,
|
||||
cl_tr_id="xkw1uo#2023-10-17T15:29:09.559376",
|
||||
sv_tr_id="5CcH4gxISuGkq8eqvr1UyQ==-35a",
|
||||
extensions=[],
|
||||
msg_q=None,
|
||||
)
|
||||
return mock
|
||||
|
||||
def fake_info_domain_received(self, command=None, cleaned=None):
|
||||
"""
|
||||
Simulates receiving a response by reading from a predefined XML file.
|
||||
"""
|
||||
location = Path(__file__).parent / "utility" / "infoDomain.xml"
|
||||
xml = (location).read_bytes()
|
||||
return xml
|
||||
|
||||
def get_fake_epp_result(self):
|
||||
"""Mimics a return from EPP by returning a dictionary in the same format"""
|
||||
result = {
|
||||
"cl_tr_id": None,
|
||||
"code": 1000,
|
||||
"extensions": [],
|
||||
"msg": "Command completed successfully",
|
||||
"msg_q": None,
|
||||
"res_data": [
|
||||
info.InfoDomainResultData(
|
||||
roid="DF1340360-GOV",
|
||||
statuses=[
|
||||
common.Status(
|
||||
state="serverTransferProhibited",
|
||||
description=None,
|
||||
lang="en",
|
||||
),
|
||||
common.Status(state="inactive", description=None, lang="en"),
|
||||
],
|
||||
cl_id="gov2023-ote",
|
||||
cr_id="gov2023-ote",
|
||||
cr_date=datetime.datetime(2023, 8, 15, 23, 56, 36, tzinfo=tzlocal()),
|
||||
up_id="gov2023-ote",
|
||||
up_date=datetime.datetime(2023, 8, 17, 2, 3, 19, tzinfo=tzlocal()),
|
||||
tr_date=None,
|
||||
name="test3.gov",
|
||||
registrant="TuaWnx9hnm84GCSU",
|
||||
admins=[],
|
||||
nsset=None,
|
||||
keyset=None,
|
||||
ex_date=datetime.date(2024, 8, 15),
|
||||
auth_info=info.DomainAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
)
|
||||
],
|
||||
"sv_tr_id": "wRRNVhKhQW2m6wsUHbo/lA==-29a",
|
||||
}
|
||||
return result
|
||||
|
||||
def test_send_command_close_failure_recovers(self):
|
||||
"""
|
||||
Validates the resilience of the connection handling mechanism
|
||||
during command execution on retry.
|
||||
|
||||
Scenario:
|
||||
- Initialization of the connection is successful.
|
||||
- An attempt to send a command fails with a specific error code (ConcurrentObjectUseError)
|
||||
- The client attempts to retry.
|
||||
- Subsequently, the client re-initializes the connection.
|
||||
- A retry of the command execution post-reinitialization succeeds.
|
||||
"""
|
||||
|
||||
expected_result = self.get_fake_epp_result()
|
||||
wrapper = None
|
||||
# Trigger a retry
|
||||
# Do nothing on connect, as we aren't testing it and want to connect while
|
||||
# mimicking the rest of the client as closely as possible (which is not entirely possible with MagicMock)
|
||||
with patch.object(EPPLibWrapper, "_connect", self.do_nothing):
|
||||
with patch.object(SocketTransport, "send", self.fake_failure_send_concurrent_threads):
|
||||
wrapper = EPPLibWrapper()
|
||||
tested_command = commands.InfoDomain(name="test.gov")
|
||||
try:
|
||||
wrapper.send(tested_command, cleaned=True)
|
||||
except RegistryError as err:
|
||||
expected_error = "InfoDomain failed to execute due to an unknown error."
|
||||
self.assertEqual(err.args[0], expected_error)
|
||||
else:
|
||||
self.fail("Registry error was not thrown")
|
||||
|
||||
# After a retry, try sending again to see if the connection recovers
|
||||
with patch.object(EPPLibWrapper, "_connect", self.do_nothing):
|
||||
with patch.object(SocketTransport, "send", self.fake_success_send), patch.object(
|
||||
SocketTransport, "receive", self.fake_info_domain_received
|
||||
):
|
||||
result = wrapper.send(tested_command, cleaned=True)
|
||||
self.assertEqual(expected_result, result.__dict__)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from datetime import date
|
||||
import logging
|
||||
import copy
|
||||
|
||||
from django import forms
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
@ -865,18 +866,21 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["creator", "domain_request", "notes"]}),
|
||||
(None, {"fields": ["creator", "submitter", "domain_request", "notes"]}),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
"fields": [
|
||||
"organization_type",
|
||||
"is_election_board",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"about_your_organization",
|
||||
]
|
||||
},
|
||||
|
@ -886,28 +890,15 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state_territory",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Your contact information", {"fields": ["submitter"]}),
|
||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
||||
(
|
||||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
|
@ -1019,6 +1010,8 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
if self.value() == "0":
|
||||
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
|
||||
|
||||
change_form_template = "django/admin/domain_application_change_form.html"
|
||||
|
||||
# Columns
|
||||
list_display = [
|
||||
"requested_domain",
|
||||
|
@ -1067,18 +1060,34 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain or submitter."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["status", "rejection_reason", "investigator", "creator", "approved_domain", "notes"]}),
|
||||
(
|
||||
None,
|
||||
{
|
||||
"fields": [
|
||||
"status",
|
||||
"rejection_reason",
|
||||
"investigator",
|
||||
"creator",
|
||||
"submitter",
|
||||
"approved_domain",
|
||||
"notes",
|
||||
]
|
||||
},
|
||||
),
|
||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||
("Contacts", {"fields": ["authorizing_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["purpose", "anything_else", "current_websites"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
"fields": [
|
||||
"organization_type",
|
||||
"is_election_board",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"tribe_name",
|
||||
"federally_recognized_tribe",
|
||||
"state_recognized_tribe",
|
||||
"tribe_name",
|
||||
"federal_agency",
|
||||
"federal_type",
|
||||
"is_election_board",
|
||||
"about_your_organization",
|
||||
]
|
||||
},
|
||||
|
@ -1088,30 +1097,15 @@ class DomainRequestAdmin(ListHeaderAdmin):
|
|||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"state_territory",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
},
|
||||
),
|
||||
("Authorizing official", {"fields": ["authorizing_official"]}),
|
||||
("Current websites", {"fields": ["current_websites"]}),
|
||||
(".gov domain", {"fields": ["requested_domain", "alternative_domains"]}),
|
||||
("Purpose of your domain", {"fields": ["purpose"]}),
|
||||
("Your contact information", {"fields": ["submitter"]}),
|
||||
("Other employees from your organization?", {"fields": ["other_contacts"]}),
|
||||
(
|
||||
"No other employees from your organization?",
|
||||
{"fields": ["no_other_contacts_rationale"]},
|
||||
),
|
||||
("Anything else?", {"fields": ["anything_else"]}),
|
||||
(
|
||||
"Requirements for operating a .gov domain",
|
||||
{"fields": ["is_policy_acknowledged"]},
|
||||
),
|
||||
]
|
||||
|
||||
# Read only that we'll leverage for CISA Analysts
|
||||
|
@ -1345,7 +1339,13 @@ class DomainInformationInline(admin.StackedInline):
|
|||
|
||||
model = models.DomainInformation
|
||||
|
||||
fieldsets = DomainInformationAdmin.fieldsets
|
||||
fieldsets = copy.deepcopy(DomainInformationAdmin.fieldsets)
|
||||
# remove .gov domain from fieldset
|
||||
for index, (title, f) in enumerate(fieldsets):
|
||||
if title == ".gov domain":
|
||||
del fieldsets[index]
|
||||
break
|
||||
|
||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||
# For each filter_horizontal, init in admin js extendFilterHorizontalWidgets
|
||||
# to activate the edit/delete/view buttons
|
||||
|
@ -1488,6 +1488,20 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
# Table ordering
|
||||
ordering = ["name"]
|
||||
|
||||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/domain_delete_selected_confirmation.html"
|
||||
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
"""
|
||||
Custom delete_view to perform additional actions or customize the template.
|
||||
"""
|
||||
|
||||
# Set the delete template to a custom one
|
||||
self.delete_confirmation_template = "django/admin/domain_delete_confirmation.html"
|
||||
response = super().delete_view(request, object_id, extra_context=extra_context)
|
||||
|
||||
return response
|
||||
|
||||
def changeform_view(self, request, object_id=None, form_url="", extra_context=None):
|
||||
"""Custom changeform implementation to pass in context information"""
|
||||
if extra_context is None:
|
||||
|
@ -1833,9 +1847,6 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
|
|||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||
search_fields = ["email"]
|
||||
search_help_text = "Search by email."
|
||||
list_filter = [
|
||||
"requestor",
|
||||
]
|
||||
readonly_fields = [
|
||||
"requestor",
|
||||
]
|
||||
|
|
|
@ -29,20 +29,26 @@ function openInNewTab(el, removeAttribute = false){
|
|||
*/
|
||||
(function (){
|
||||
function createPhantomModalFormButtons(){
|
||||
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"]');
|
||||
let submitButtons = document.querySelectorAll('.usa-modal button[type="submit"].dja-form-placeholder');
|
||||
form = document.querySelector("form")
|
||||
submitButtons.forEach((button) => {
|
||||
|
||||
let input = document.createElement("input");
|
||||
input.type = "submit";
|
||||
input.name = button.name;
|
||||
input.value = button.value;
|
||||
|
||||
if(button.name){
|
||||
input.name = button.name;
|
||||
}
|
||||
|
||||
if(button.value){
|
||||
input.value = button.value;
|
||||
}
|
||||
|
||||
input.style.display = "none"
|
||||
|
||||
// Add the hidden input to the form
|
||||
form.appendChild(input);
|
||||
button.addEventListener("click", () => {
|
||||
console.log("clicking")
|
||||
input.click();
|
||||
})
|
||||
})
|
||||
|
@ -50,6 +56,61 @@ function openInNewTab(el, removeAttribute = false){
|
|||
|
||||
createPhantomModalFormButtons();
|
||||
})();
|
||||
|
||||
/** An IIFE for DomainRequest to hook a modal to a dropdown option.
|
||||
* This intentionally does not interact with createPhantomModalFormButtons()
|
||||
*/
|
||||
(function (){
|
||||
function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
|
||||
|
||||
// If these exist all at the same time, we're on the right page
|
||||
if (linkClickedDisplaysModal && statusDropdown && statusDropdown.value){
|
||||
|
||||
// Set the previous value in the event the user cancels.
|
||||
let previousValue = statusDropdown.value;
|
||||
if (actionButton){
|
||||
|
||||
// Otherwise, if the confirmation buttion is pressed, set it to that
|
||||
actionButton.addEventListener('click', function() {
|
||||
// Revert the dropdown to its previous value
|
||||
statusDropdown.value = valueToCheck;
|
||||
});
|
||||
}else {
|
||||
console.log("displayModalOnDropdownClick() -> Cancel button was null")
|
||||
}
|
||||
|
||||
// Add a change event listener to the dropdown.
|
||||
statusDropdown.addEventListener('change', function() {
|
||||
// Check if "Ineligible" is selected
|
||||
if (this.value && this.value.toLowerCase() === valueToCheck) {
|
||||
// Set the old value in the event the user cancels,
|
||||
// or otherwise exists the dropdown.
|
||||
statusDropdown.value = previousValue
|
||||
|
||||
// Display the modal.
|
||||
linkClickedDisplaysModal.click()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// When the status dropdown is clicked and is set to "ineligible", toggle a confirmation dropdown.
|
||||
function hookModalToIneligibleStatus(){
|
||||
// Grab the invisible element that will hook to the modal.
|
||||
// This doesn't technically need to be done with one, but this is simpler to manage.
|
||||
let modalButton = document.getElementById("invisible-ineligible-modal-toggler")
|
||||
let statusDropdown = document.getElementById("id_status")
|
||||
|
||||
// Because the modal button does not have the class "dja-form-placeholder",
|
||||
// it will not be affected by the createPhantomModalFormButtons() function.
|
||||
let actionButton = document.querySelector('button[name="_set_domain_request_ineligible"]');
|
||||
let valueToCheck = "ineligible"
|
||||
displayModalOnDropdownClick(modalButton, statusDropdown, actionButton, valueToCheck);
|
||||
}
|
||||
|
||||
hookModalToIneligibleStatus()
|
||||
})();
|
||||
|
||||
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||
* Currently only appends target="_blank" to the domain_form object,
|
||||
* but this can be expanded.
|
||||
|
|
|
@ -143,6 +143,10 @@ h1, h2, h3,
|
|||
font-weight: font-weight('bold');
|
||||
}
|
||||
|
||||
div#content > h2 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.module h3 {
|
||||
padding: 0;
|
||||
color: var(--link-fg);
|
||||
|
@ -300,6 +304,19 @@ input.admin-confirm-button {
|
|||
}
|
||||
}
|
||||
|
||||
.django-admin-modal .usa-prose ul > li {
|
||||
list-style-type: inherit;
|
||||
// Styling based off of the <p> styling in django admin
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0;
|
||||
margin-top: 0;
|
||||
max-width: 68ex;
|
||||
}
|
||||
|
||||
.usa-summary-box__dhs-color {
|
||||
color: $dhs-blue-70;
|
||||
}
|
||||
|
||||
.admin-icon-group {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
@ -38,3 +38,18 @@ legend.float-left-tablet + button.float-right-tablet {
|
|||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Custom style for disabled inputs
|
||||
@media (prefers-color-scheme: light) {
|
||||
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||
background-color: #eeeeee;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||
background-color: var(--body-fg);
|
||||
color: var(--close-button-hover-bg);
|
||||
}
|
||||
}
|
|
@ -126,7 +126,6 @@ in the form $setting: value,
|
|||
----------------------------*/
|
||||
$theme-input-line-height: 5,
|
||||
|
||||
|
||||
/*---------------------------
|
||||
# Component settings
|
||||
-----------------------------
|
||||
|
|
|
@ -74,6 +74,9 @@ secret_aws_s3_key_id = secret("access_key_id", None) or secret("AWS_S3_ACCESS_KE
|
|||
secret_aws_s3_key = secret("secret_access_key", None) or secret("AWS_S3_SECRET_ACCESS_KEY", None)
|
||||
secret_aws_s3_bucket_name = secret("bucket", None) or secret("AWS_S3_BUCKET_NAME", None)
|
||||
|
||||
# Passphrase for the encrypted metadata email
|
||||
secret_encrypt_metadata = secret("SECRET_ENCRYPT_METADATA", None)
|
||||
|
||||
secret_registry_cl_id = secret("REGISTRY_CL_ID")
|
||||
secret_registry_password = secret("REGISTRY_PASSWORD")
|
||||
secret_registry_cert = b64decode(secret("REGISTRY_CERT", ""))
|
||||
|
@ -94,6 +97,7 @@ DEBUG = env_debug
|
|||
|
||||
# Controls production specific feature toggles
|
||||
IS_PRODUCTION = env_is_production
|
||||
SECRET_ENCRYPT_METADATA = secret_encrypt_metadata
|
||||
|
||||
# Applications are modular pieces of code.
|
||||
# They are provided by Django, by third-parties, or by yourself.
|
||||
|
@ -635,6 +639,8 @@ ALLOWED_HOSTS = [
|
|||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-development.app.cloud.gov",
|
||||
"getgov-bob.app.cloud.gov",
|
||||
"getgov-meoward.app.cloud.gov",
|
||||
"getgov-backup.app.cloud.gov",
|
||||
"getgov-ky.app.cloud.gov",
|
||||
"getgov-es.app.cloud.gov",
|
||||
|
|
|
@ -149,6 +149,18 @@ urlpatterns = [
|
|||
),
|
||||
]
|
||||
|
||||
# Djangooidc strips out context data from that context, so we define a custom error
|
||||
# view through this method.
|
||||
# If Djangooidc is left to its own devices and uses reverse directly,
|
||||
# then both context and session information will be obliterated due to:
|
||||
|
||||
# a) Djangooidc being out of scope for context_processors
|
||||
# b) Potential cyclical import errors restricting what kind of data is passable.
|
||||
|
||||
# Rather than dealing with that, we keep everything centralized in one location.
|
||||
# This way, we can share a view for djangooidc, and other pages as we see fit.
|
||||
handler500 = "registrar.views.utility.error_views.custom_500_error_view"
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
# DEBUG = False even when these apps have been loaded because settings.DEBUG
|
||||
# was actually True. Instead, let's add these URLs any time we are able to
|
||||
|
|
|
@ -163,6 +163,12 @@ class UserFixture:
|
|||
"last_name": "Chin-Analyst",
|
||||
"email": "szu.chin@ecstech.com",
|
||||
},
|
||||
{
|
||||
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||
"first_name": "Alex-Analyst",
|
||||
"last_name": "Mcelya-Analyst",
|
||||
"email": "ALEXANDER.MCELYA@cisa.dhs.gov",
|
||||
},
|
||||
]
|
||||
|
||||
def load_users(cls, users, group_name):
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
"""Forms for domain management."""
|
||||
|
||||
import logging
|
||||
from django import forms
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator
|
||||
from django.forms import formset_factory
|
||||
|
||||
from registrar.models import DomainRequest
|
||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from registrar.utility.errors import (
|
||||
NameserverError,
|
||||
NameserverErrorCodes as nsErrorCodes,
|
||||
|
@ -23,6 +25,9 @@ from .common import (
|
|||
import re
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainAddUserForm(forms.Form):
|
||||
"""Form for adding a user to a domain."""
|
||||
|
||||
|
@ -205,6 +210,13 @@ class ContactForm(forms.ModelForm):
|
|||
"required": "Enter your email address in the required format, like name@example.com."
|
||||
}
|
||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||
self.domainInfo = None
|
||||
|
||||
def set_domain_info(self, domainInfo):
|
||||
"""Set the domain information for the form.
|
||||
The form instance is associated with the contact itself. In order to access the associated
|
||||
domain information object, this needs to be set in the form by the view."""
|
||||
self.domainInfo = domainInfo
|
||||
|
||||
|
||||
class AuthorizingOfficialContactForm(ContactForm):
|
||||
|
@ -212,7 +224,7 @@ class AuthorizingOfficialContactForm(ContactForm):
|
|||
|
||||
JOIN = "authorizing_official"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
def __init__(self, disable_fields=False, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Overriding bc phone not required in this form
|
||||
|
@ -232,20 +244,36 @@ class AuthorizingOfficialContactForm(ContactForm):
|
|||
self.fields["email"].error_messages = {
|
||||
"required": "Enter an email address in the required format, like name@example.com."
|
||||
}
|
||||
self.domainInfo = None
|
||||
|
||||
def set_domain_info(self, domainInfo):
|
||||
"""Set the domain information for the form.
|
||||
The form instance is associated with the contact itself. In order to access the associated
|
||||
domain information object, this needs to be set in the form by the view."""
|
||||
self.domainInfo = domainInfo
|
||||
# All fields should be disabled if the domain is federal or tribal
|
||||
if disable_fields:
|
||||
DomainHelper.mass_disable_fields(fields=self.fields, disable_required=True, disable_maxlength=True)
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Override the save() method of the BaseModelForm."""
|
||||
"""
|
||||
Override the save() method of the BaseModelForm.
|
||||
Used to perform checks on the underlying domain_information object.
|
||||
If this doesn't exist, we just save as normal.
|
||||
"""
|
||||
|
||||
# If the underlying Domain doesn't have a domainInfo object,
|
||||
# just let the default super handle it.
|
||||
if not self.domainInfo:
|
||||
return super().save()
|
||||
|
||||
# Determine if the domain is federal or tribal
|
||||
is_federal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
is_tribal = self.domainInfo.organization_type == DomainRequest.OrganizationChoices.TRIBAL
|
||||
|
||||
# Get the Contact object from the db for the Authorizing Official
|
||||
db_ao = Contact.objects.get(id=self.instance.id)
|
||||
if self.domainInfo and db_ao.has_more_than_one_join("information_authorizing_official"):
|
||||
|
||||
if (is_federal or is_tribal) and self.has_changed():
|
||||
# This action should be blocked by the UI, as the text fields are readonly.
|
||||
# If they get past this point, we forbid it this way.
|
||||
# This could be malicious, so lets reserve information for the backend only.
|
||||
raise ValueError("Authorizing Official cannot be modified for federal or tribal domains.")
|
||||
elif db_ao.has_more_than_one_join("information_authorizing_official"):
|
||||
# Handle the case where the domain information object is available and the AO Contact
|
||||
# has more than one joined object.
|
||||
# In this case, create a new Contact, and update the new Contact with form data.
|
||||
|
@ -254,6 +282,7 @@ class AuthorizingOfficialContactForm(ContactForm):
|
|||
self.domainInfo.authorizing_official = Contact.objects.create(**data)
|
||||
self.domainInfo.save()
|
||||
else:
|
||||
# If all checks pass, just save normally
|
||||
super().save()
|
||||
|
||||
|
||||
|
@ -304,11 +333,11 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
|||
},
|
||||
}
|
||||
widgets = {
|
||||
# We need to set the required attributed for federal_agency and
|
||||
# state/territory because for these fields we are creating an individual
|
||||
# We need to set the required attributed for State/territory
|
||||
# because for this fields we are creating an individual
|
||||
# instance of the Select. For the other fields we use the for loop to set
|
||||
# the class's required attribute to true.
|
||||
"federal_agency": forms.Select(attrs={"required": True}, choices=DomainInformation.AGENCY_CHOICES),
|
||||
"federal_agency": forms.TextInput,
|
||||
"organization_name": forms.TextInput,
|
||||
"address_line1": forms.TextInput,
|
||||
"address_line2": forms.TextInput,
|
||||
|
@ -334,6 +363,46 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
|||
self.fields["state_territory"].widget.attrs.pop("maxlength", None)
|
||||
self.fields["zipcode"].widget.attrs.pop("maxlength", None)
|
||||
|
||||
self.is_federal = self.instance.organization_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
self.is_tribal = self.instance.organization_type == DomainRequest.OrganizationChoices.TRIBAL
|
||||
|
||||
field_to_disable = None
|
||||
if self.is_federal:
|
||||
field_to_disable = "federal_agency"
|
||||
elif self.is_tribal:
|
||||
field_to_disable = "organization_name"
|
||||
|
||||
# Disable any field that should be disabled, if applicable
|
||||
if field_to_disable is not None:
|
||||
DomainHelper.disable_field(self.fields[field_to_disable], disable_required=True)
|
||||
|
||||
def save(self, commit=True):
|
||||
"""Override the save() method of the BaseModelForm."""
|
||||
if self.has_changed():
|
||||
|
||||
# This action should be blocked by the UI, as the text fields are readonly.
|
||||
# If they get past this point, we forbid it this way.
|
||||
# This could be malicious, so lets reserve information for the backend only.
|
||||
if self.is_federal and not self._field_unchanged("federal_agency"):
|
||||
raise ValueError("federal_agency cannot be modified when the organization_type is federal")
|
||||
elif self.is_tribal and not self._field_unchanged("organization_name"):
|
||||
raise ValueError("organization_name cannot be modified when the organization_type is tribal")
|
||||
|
||||
else:
|
||||
super().save()
|
||||
|
||||
def _field_unchanged(self, field_name) -> bool:
|
||||
"""
|
||||
Checks if a specified field has not changed between the old value
|
||||
and the new value.
|
||||
|
||||
The old value is grabbed from self.initial.
|
||||
The new value is grabbed from self.cleaned_data.
|
||||
"""
|
||||
old_value = self.initial.get(field_name, None)
|
||||
new_value = self.cleaned_data.get(field_name, None)
|
||||
return old_value == new_value
|
||||
|
||||
|
||||
class DomainDnssecForm(forms.Form):
|
||||
"""Form for enabling and disabling dnssec"""
|
||||
|
|
|
@ -319,8 +319,8 @@ class AboutYourOrganizationForm(RegistrarForm):
|
|||
widget=forms.Textarea(),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
1000,
|
||||
message="Response must be less than 1000 characters.",
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": ("Enter more information about your organization.")},
|
||||
|
@ -515,8 +515,8 @@ class PurposeForm(RegistrarForm):
|
|||
widget=forms.Textarea(),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
1000,
|
||||
message="Response must be less than 1000 characters.",
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Describe how you’ll use the .gov domain you’re requesting."},
|
||||
|
@ -830,8 +830,8 @@ class AnythingElseForm(RegistrarForm):
|
|||
widget=forms.Textarea(),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
1000,
|
||||
message="Response must be less than 1000 characters.",
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
"""Generates current-metadata.csv then uploads to S3 + sends email"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import pyzipper
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.conf import settings
|
||||
from registrar.utility import csv_export
|
||||
from registrar.utility.s3_bucket import S3ClientHelper
|
||||
from ...utility.email import send_templated_email
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = (
|
||||
"Generates and uploads a domain-metadata.csv file to our S3 bucket "
|
||||
"which is based off of all existing Domains."
|
||||
)
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add our two filename arguments."""
|
||||
parser.add_argument("--directory", default="migrationdata", help="Desired directory")
|
||||
parser.add_argument(
|
||||
"--checkpath",
|
||||
default=True,
|
||||
help="Flag that determines if we do a check for os.path.exists. Used for test cases",
|
||||
)
|
||||
|
||||
def handle(self, **options):
|
||||
"""Grabs the directory then creates domain-metadata.csv in that directory"""
|
||||
file_name = "domain-metadata.csv"
|
||||
# Ensures a slash is added
|
||||
directory = os.path.join(options.get("directory"), "")
|
||||
check_path = options.get("checkpath")
|
||||
|
||||
logger.info("Generating report...")
|
||||
try:
|
||||
self.email_current_metadata_report(directory, file_name, check_path)
|
||||
except Exception as err:
|
||||
# TODO - #1317: Notify operations when auto report generation fails
|
||||
raise err
|
||||
else:
|
||||
logger.info(f"Success! Created {file_name} and successfully sent out an email!")
|
||||
|
||||
def email_current_metadata_report(self, directory, file_name, check_path):
|
||||
"""Creates a current-metadata.csv file under the specified directory,
|
||||
then uploads it to a AWS S3 bucket. This is done for resiliency
|
||||
reasons in the event our application goes down and/or the email
|
||||
cannot send -- we'll still be able to grab info from the S3
|
||||
instance"""
|
||||
s3_client = S3ClientHelper()
|
||||
file_path = os.path.join(directory, file_name)
|
||||
|
||||
# Generate a file locally for upload
|
||||
with open(file_path, "w") as file:
|
||||
csv_export.export_data_type_to_csv(file)
|
||||
|
||||
if check_path and not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Could not find newly created file at '{file_path}'")
|
||||
|
||||
s3_client.upload_file(file_path, file_name)
|
||||
|
||||
# Set zip file name
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
|
||||
# Pre-set zip file name
|
||||
encrypted_metadata_output = current_filename
|
||||
|
||||
# Set context for the subject
|
||||
current_date_str = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
# Encrypt the metadata
|
||||
encrypted_metadata_in_bytes = self._encrypt_metadata(
|
||||
s3_client.get_file(file_name), encrypted_metadata_output, str.encode(settings.SECRET_ENCRYPT_METADATA)
|
||||
)
|
||||
|
||||
# Send the metadata file that is zipped
|
||||
send_templated_email(
|
||||
template_name="emails/metadata_body.txt",
|
||||
subject_template_name="emails/metadata_subject.txt",
|
||||
to_address=settings.DEFAULT_FROM_EMAIL,
|
||||
context={"current_date_str": current_date_str},
|
||||
attachment_file=encrypted_metadata_in_bytes,
|
||||
)
|
||||
|
||||
def _encrypt_metadata(self, input_file, output_file, password):
|
||||
"""Helper function for encrypting the attachment file"""
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.csv"
|
||||
# Using ZIP_DEFLATED bc it's a more common compression method supported by most zip utilities and faster
|
||||
# We could also use compression=pyzipper.ZIP_LZMA if we are looking for smaller file size
|
||||
with pyzipper.AESZipFile(
|
||||
output_file, "w", compression=pyzipper.ZIP_DEFLATED, encryption=pyzipper.WZ_AES
|
||||
) as f_out:
|
||||
f_out.setpassword(password)
|
||||
f_out.writestr(current_filename, input_file)
|
||||
with open(output_file, "rb") as file_data:
|
||||
attachment_in_bytes = file_data.read()
|
||||
return attachment_in_bytes
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-13 21:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0075_create_groups_v08"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="current_websites",
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name="current+", to="registrar.website", verbose_name="Current websites"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests",
|
||||
to="registrar.contact",
|
||||
verbose_name="Other employees",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="other_contacts",
|
||||
field=models.ManyToManyField(
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
verbose_name="Other employees",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -198,6 +198,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
|
|
@ -183,7 +183,7 @@ class DomainInformation(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_domain_requests_information",
|
||||
verbose_name="contacts",
|
||||
verbose_name="Other employees",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
|
|
@ -505,7 +505,7 @@ class DomainRequest(TimeStampedModel):
|
|||
"registrar.Website",
|
||||
blank=True,
|
||||
related_name="current+",
|
||||
verbose_name="websites",
|
||||
verbose_name="Current websites",
|
||||
)
|
||||
|
||||
approved_domain = models.OneToOneField(
|
||||
|
@ -551,7 +551,7 @@ class DomainRequest(TimeStampedModel):
|
|||
"registrar.Contact",
|
||||
blank=True,
|
||||
related_name="contact_domain_requests",
|
||||
verbose_name="contacts",
|
||||
verbose_name="Other employees",
|
||||
)
|
||||
|
||||
no_other_contacts_rationale = models.TextField(
|
||||
|
|
|
@ -188,3 +188,33 @@ class DomainHelper:
|
|||
common_fields = model_1_fields & model_2_fields
|
||||
|
||||
return common_fields
|
||||
|
||||
@staticmethod
|
||||
def mass_disable_fields(fields, disable_required=False, disable_maxlength=False):
|
||||
"""
|
||||
Given some fields, invoke .disabled = True on them.
|
||||
disable_required: bool -> invokes .required = False on each field.
|
||||
disable_maxlength: bool -> pops "maxlength" from each field.
|
||||
"""
|
||||
for field in fields.values():
|
||||
field = DomainHelper.disable_field(field, disable_required, disable_maxlength)
|
||||
return fields
|
||||
|
||||
@staticmethod
|
||||
def disable_field(field, disable_required=False, disable_maxlength=False):
|
||||
"""
|
||||
Given a fields, invoke .disabled = True on it.
|
||||
disable_required: bool -> invokes .required = False for the field.
|
||||
disable_maxlength: bool -> pops "maxlength" for the field.
|
||||
"""
|
||||
field.disabled = True
|
||||
|
||||
if disable_required:
|
||||
# if a field is disabled, it can't be required
|
||||
field.required = False
|
||||
|
||||
if disable_maxlength:
|
||||
# Remove the maxlength dialog
|
||||
if "maxlength" in field.widget.attrs:
|
||||
field.widget.attrs.pop("maxlength", None)
|
||||
return field
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
{% extends 'admin/change_form.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block field_sets %}
|
||||
{# Create an invisible <a> tag so that we can use a click event to toggle the modal. #}
|
||||
<a id="invisible-ineligible-modal-toggler" class="display-none" href="#toggle-set-ineligible" aria-controls="toggle-set-ineligible" data-open-modal></a>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
|
||||
{% block submit_buttons_bottom %}
|
||||
{% comment %}
|
||||
Modals behave very weirdly in django admin.
|
||||
They tend to "strip out" any injected form elements, leaving only the main form.
|
||||
In addition, USWDS handles modals by first destroying the element, then repopulating it toward the end of the page.
|
||||
In effect, this means that the modal is not, and cannot, be surrounded by any form element at compile time.
|
||||
|
||||
The current workaround for this is to use javascript to inject a hidden input, and bind submit of that
|
||||
element to the click of the confirmation button within this modal.
|
||||
|
||||
This is controlled by the class `dja-form-placeholder` on the button.
|
||||
|
||||
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||
{% endcomment %}
|
||||
{# Create a modal for when a domain is marked as ineligible #}
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-set-ineligible"
|
||||
aria-labelledby="Are you sure you want to select ineligible status?"
|
||||
aria-describedby="This request will be marked as ineligible."
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
Are you sure you want to select ineligible status?
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>
|
||||
When a domain request is in ineligible status, the registrant's permissions within the registrar are restricted as follows:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="font-body-sm">They cannot edit the ineligible request or any other pending requests.</li>
|
||||
<li class="font-body-sm">They cannot manage any of their approved domains.</li>
|
||||
<li class="font-body-sm">They cannot initiate a new domain request.</li>
|
||||
</ul>
|
||||
<p>
|
||||
The restrictions will not take effect until you “save” the changes for this domain request.
|
||||
This action can be reversed, if needed.
|
||||
</p>
|
||||
<p>
|
||||
Domain: <b>{{ original.requested_domain.name }}</b>
|
||||
{# Acts as a <br> #}
|
||||
<div class="display-inline"></div>
|
||||
New status: <b>{{ original.DomainRequestStatus.INELIGIBLE|capfirst }}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
name="_set_domain_request_ineligible"
|
||||
data-close-modal
|
||||
>
|
||||
Yes, select ineligible status
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
name="_cancel_domain_request_ineligible"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -11,18 +11,15 @@
|
|||
</div>
|
||||
<div class="desktop:flex-align-self-end">
|
||||
{% if original.state != original.State.DELETED %}
|
||||
<a
|
||||
class="text-middle"
|
||||
href="#toggle-extend-expiration-alert"
|
||||
aria-controls="toggle-extend-expiration-alert"
|
||||
data-open-modal
|
||||
>
|
||||
<a class="text-middle" href="#toggle-extend-expiration-alert" aria-controls="toggle-extend-expiration-alert" data-open-modal>
|
||||
Extend expiration date
|
||||
</a>
|
||||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||
{% endif %}
|
||||
{% if original.state == original.State.READY %}
|
||||
<input type="submit" value="Place hold" name="_place_client_hold" class="custom-link-button">
|
||||
<a class="text-middle" href="#toggle-place-on-hold" aria-controls="toggle-place-on-hold" data-open-modal>
|
||||
Place hold
|
||||
</a>
|
||||
{% elif original.state == original.State.ON_HOLD %}
|
||||
<input type="submit" value="Remove hold" name="_remove_client_hold" class="custom-link-button">
|
||||
{% endif %}
|
||||
|
@ -30,7 +27,9 @@
|
|||
<span class="margin-left-05 margin-right-05 text-middle"> | </span>
|
||||
{% endif %}
|
||||
{% if original.state != original.State.DELETED %}
|
||||
<input type="submit" value="Remove from registry" name="_delete_domain" class="custom-link-button">
|
||||
<a class="text-middle" href="#toggle-remove-from-registry" aria-controls="toggle-remove-from-registry" data-open-modal>
|
||||
Remove from registry
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -52,8 +51,10 @@
|
|||
In addition, the modal element MUST be placed low in the DOM. The script loads slower on DJA than on other portions
|
||||
of the application, so this means that it will briefly "populate", causing unintended visual effects.
|
||||
{% endcomment %}
|
||||
|
||||
{# Create a modal for the _extend_expiration_date button #}
|
||||
<div
|
||||
class="usa-modal"
|
||||
class="usa-modal django-admin-modal"
|
||||
id="toggle-extend-expiration-alert"
|
||||
aria-labelledby="Are you sure you want to extend the expiration date?"
|
||||
aria-describedby="This expiration date will be extended."
|
||||
|
@ -78,7 +79,7 @@
|
|||
{{test}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
|
@ -114,5 +115,140 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Create a modal for the _on_hold button #}
|
||||
<div
|
||||
class="usa-modal django-admin-modal"
|
||||
id="toggle-place-on-hold"
|
||||
aria-labelledby="Are you sure you want to place this domain on hold?"
|
||||
aria-describedby="This domain will be put on hold"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
Are you sure you want to place this domain on hold?
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>
|
||||
When a domain is on hold:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="font-body-sm">The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.</li>
|
||||
<li class="font-body-sm">The domain will still appear in the registrar / admin.</li>
|
||||
<li class="font-body-sm">Domain managers won’t be able to edit the domain.</li>
|
||||
</ul>
|
||||
<p>
|
||||
This action can be reversed, if needed.
|
||||
</p>
|
||||
<p>
|
||||
Domain: <b>{{ original.name }}</b>
|
||||
{# Acts as a <br> #}
|
||||
<div class="display-inline"></div>
|
||||
New status: <b>{{ original.State.ON_HOLD|capfirst }}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button dja-form-placeholder"
|
||||
name="_place_client_hold"
|
||||
>
|
||||
Yes, place hold
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{# Create a modal for the _remove_domain button #}
|
||||
<div
|
||||
class="usa-modal django-admin-modal"
|
||||
id="toggle-remove-from-registry"
|
||||
aria-labelledby="Are you sure you want to remove this domain from the registry?"
|
||||
aria-describedby="This domain will be removed."
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
Are you sure you want to remove this domain from the registry?
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>
|
||||
When a domain is removed from the registry:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="font-body-sm">The domain and its subdomains won’t resolve in DNS. Any infrastructure (like websites) will go offline.</li>
|
||||
<li class="font-body-sm">The domain will still appear in the registrar / admin.</li>
|
||||
<li class="font-body-sm">Domain managers won’t be able to edit the domain.</li>
|
||||
</ul>
|
||||
<p>
|
||||
This action cannot be undone.
|
||||
</p>
|
||||
<p>
|
||||
Domain: <b>{{ original.name }}</b>
|
||||
{# Acts as a <br> #}
|
||||
<div class="display-inline"></div>
|
||||
New status: <b>{{ original.State.DELETED|capfirst }}</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button dja-form-placeholder"
|
||||
name="_delete_domain"
|
||||
>
|
||||
Yes, remove from registry
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-modal__close"
|
||||
aria-label="Close this window"
|
||||
data-close-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
{% extends 'admin/delete_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="usa-summary-box width-tablet"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-description"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
||||
When a domain is deleted:
|
||||
</h3>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul class="usa-list">
|
||||
<li>The domain will no longer appear in the registrar / admin.</li>
|
||||
<li>It will be removed from the registry. </li>
|
||||
<li>The domain and its subdomains won’t resolve in DNS.</li>
|
||||
<li>Any infrastructure (like websites) will go offline.</li>
|
||||
</ul>
|
||||
<p>You should probably remove this domain from the registry instead of deleting it.</p>
|
||||
<p><strong>This action cannot be undone.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,28 @@
|
|||
{% extends 'admin/delete_selected_confirmation.html' %}
|
||||
{% load i18n static %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div
|
||||
class="usa-summary-box width-tablet"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-description"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
||||
When a domain is deleted:
|
||||
</h3>
|
||||
<div class="usa-summary-box__text">
|
||||
<ul class="usa-list">
|
||||
<li>The domain will no longer appear in the registrar / admin.</li>
|
||||
<li>It will be removed from the registry. </li>
|
||||
<li>The domain and its subdomains won’t resolve in DNS.</li>
|
||||
<li>Any infrastructure (like websites) will go offline.</li>
|
||||
</ul>
|
||||
<p>You should probably remove these domains from the registry instead.</p>
|
||||
<p><strong>This action cannot be undone.</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -11,12 +11,28 @@
|
|||
|
||||
<p>Your authorizing official is a person within your organization who can
|
||||
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
|
||||
|
||||
|
||||
{% if organization_type == "federal" or organization_type == "tribal" %}
|
||||
<p>
|
||||
The authorizing official for your organization can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% else %}
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if organization_type == "federal" or organization_type == "tribal" %}
|
||||
{# If all fields are disabled, add SR content #}
|
||||
<div class="usa-sr-only" aria-labelledby="id_first_name" id="sr-ao-first-name">{{ form.first_name.value }}</div>
|
||||
<div class="usa-sr-only" aria-labelledby="id_last_name" id="sr-ao-last-name">{{ form.last_name.value }}</div>
|
||||
<div class="usa-sr-only" aria-labelledby="id_title" id="sr-ao-title">{{ form.title.value }}</div>
|
||||
<div class="usa-sr-only" aria-labelledby="id_email" id="sr-ao-email">{{ form.email.value }}</div>
|
||||
{% endif %}
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
||||
{% input_with_errors form.last_name %}
|
||||
|
@ -24,11 +40,9 @@
|
|||
{% input_with_errors form.title %}
|
||||
|
||||
{% input_with_errors form.email %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save</button>
|
||||
</form>
|
||||
|
||||
|
||||
{% if organization_type != "federal" and organization_type != "tribal" %}
|
||||
<button type="submit" class="usa-button">Save</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -11,6 +11,18 @@
|
|||
|
||||
<p>The name of your organization will be publicly listed as the domain registrant.</p>
|
||||
|
||||
{% if domain.domain_info.organization_type == "federal" %}
|
||||
<p>
|
||||
The federal agency for your organization can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% elif domain.domain_info.organization_type == "tribal" %}
|
||||
<p>
|
||||
Your organization name can’t be updated here.
|
||||
To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.about_your_organization %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
|
||||
|
||||
{% block form_fields %}
|
||||
{% with add_label_class="usa-sr-only" attr_maxlength=1000 %}
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.anything_else %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.purpose %}
|
||||
{% endwith %}
|
||||
{% endblock %}
|
||||
|
|
1
src/registrar/templates/emails/metadata_body.txt
Normal file
1
src/registrar/templates/emails/metadata_body.txt
Normal file
|
@ -0,0 +1 @@
|
|||
An export of all .gov metadata.
|
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
2
src/registrar/templates/emails/metadata_subject.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Domain metadata - {{current_date_str}}
|
||||
|
|
@ -97,7 +97,7 @@ def less_console_noise(output_stream=None):
|
|||
class GenericTestHelper(TestCase):
|
||||
"""A helper class that contains various helper functions for TestCases"""
|
||||
|
||||
def __init__(self, admin, model=None, url=None, user=None, factory=None, **kwargs):
|
||||
def __init__(self, admin, model=None, url=None, user=None, factory=None, client=None, **kwargs):
|
||||
"""
|
||||
Parameters:
|
||||
admin (ModelAdmin): The Django ModelAdmin instance associated with the model.
|
||||
|
@ -112,6 +112,7 @@ class GenericTestHelper(TestCase):
|
|||
self.admin = admin
|
||||
self.model = model
|
||||
self.url = url
|
||||
self.client = client
|
||||
|
||||
def assert_table_sorted(self, o_index, sort_fields):
|
||||
"""
|
||||
|
@ -147,9 +148,7 @@ class GenericTestHelper(TestCase):
|
|||
dummy_request.user = self.user
|
||||
|
||||
# Mock a user request
|
||||
middleware = SessionMiddleware(lambda req: req)
|
||||
middleware.process_request(dummy_request)
|
||||
dummy_request.session.save()
|
||||
dummy_request = self._mock_user_request_for_factory(dummy_request)
|
||||
|
||||
expected_sort_order = list(self.model.objects.order_by(*sort_fields))
|
||||
|
||||
|
@ -160,6 +159,27 @@ class GenericTestHelper(TestCase):
|
|||
|
||||
self.assertEqual(expected_sort_order, returned_sort_order)
|
||||
|
||||
def _mock_user_request_for_factory(self, request):
|
||||
"""Adds sessionmiddleware when using factory to associate session information"""
|
||||
middleware = SessionMiddleware(lambda req: req)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
||||
|
||||
def get_table_delete_confirmation_page(self, selected_across: str, index: str):
|
||||
"""
|
||||
Grabs the response for the delete confirmation page (generated from the actions toolbar).
|
||||
selected_across and index must both be numbers encoded as str, e.g. "0" rather than 0
|
||||
"""
|
||||
|
||||
response = self.client.post(
|
||||
self.url,
|
||||
{"action": "delete_selected", "select_across": selected_across, "index": index, "_selected_action": "23"},
|
||||
follow=True,
|
||||
)
|
||||
print(f"what is the response? {response}")
|
||||
return response
|
||||
|
||||
|
||||
class MockUserLogin:
|
||||
def __init__(self, get_response):
|
||||
|
|
|
@ -61,6 +61,16 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
self.factory = RequestFactory()
|
||||
self.app.set_user(self.superuser.username)
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Contains some test tools
|
||||
self.test_helper = GenericTestHelper(
|
||||
factory=self.factory,
|
||||
user=self.superuser,
|
||||
admin=self.admin,
|
||||
url=reverse("admin:registrar_domain_changelist"),
|
||||
model=Domain,
|
||||
client=self.client,
|
||||
)
|
||||
super().setUp()
|
||||
|
||||
@skip("TODO for another ticket. This test case is grabbing old db data.")
|
||||
|
@ -230,6 +240,35 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
)
|
||||
mock_add_message.assert_has_calls([expected_call], 1)
|
||||
|
||||
def test_custom_delete_confirmation_page(self):
|
||||
"""Tests if we override the delete confirmation page for custom content"""
|
||||
# Create a ready domain with a preset expiration date
|
||||
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||
|
||||
domain_change_page = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||
|
||||
self.assertContains(domain_change_page, "fake.gov")
|
||||
# click the "Manage" link
|
||||
confirmation_page = domain_change_page.click("Delete", index=0)
|
||||
|
||||
content_slice = "When a domain is deleted:"
|
||||
self.assertContains(confirmation_page, content_slice)
|
||||
|
||||
def test_custom_delete_confirmation_page_table(self):
|
||||
"""Tests if we override the delete confirmation page for custom content on the table"""
|
||||
# Create a ready domain
|
||||
domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||
|
||||
# Get the index. The post expects the index to be encoded as a string
|
||||
index = f"{domain.id}"
|
||||
|
||||
# Simulate selecting a single record, then clicking "Delete selected domains"
|
||||
response = self.test_helper.get_table_delete_confirmation_page("0", index)
|
||||
|
||||
# Check that our content exists
|
||||
content_slice = "When a domain is deleted:"
|
||||
self.assertContains(response, content_slice)
|
||||
|
||||
def test_short_org_name_in_domains_list(self):
|
||||
"""
|
||||
Make sure the short name is displaying in admin on the list page
|
||||
|
@ -309,6 +348,17 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
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),
|
||||
|
@ -325,8 +375,60 @@ class TestDomainAdmin(MockEppLib, WebTest):
|
|||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
# The modal should still exist
|
||||
self.assertContains(response, "Are you sure you want to remove this domain from the registry?")
|
||||
self.assertContains(response, "When a domain is removed from the registry:")
|
||||
self.assertContains(response, "Yes, remove from registry")
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
def test_on_hold_is_successful_web_test(self):
|
||||
"""
|
||||
Scenario: Domain on_hold is successful through webtest
|
||||
"""
|
||||
with less_console_noise():
|
||||
domain = create_ready_domain()
|
||||
|
||||
response = self.app.get(reverse("admin:registrar_domain_change", args=[domain.pk]))
|
||||
|
||||
# Check the contents of the modal
|
||||
# Check for the header
|
||||
self.assertContains(response, "Are you sure you want to place this domain on hold?")
|
||||
|
||||
# Check for some of its body
|
||||
self.assertContains(response, "When a domain is on hold:")
|
||||
|
||||
# Check for some of the button content
|
||||
self.assertContains(response, "Yes, place hold")
|
||||
|
||||
# Grab the form to submit
|
||||
form = response.forms["domain_form"]
|
||||
|
||||
# Submit the form
|
||||
response = form.submit("_place_client_hold")
|
||||
|
||||
# Follow the response
|
||||
response = response.follow()
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove hold")
|
||||
|
||||
# The modal should still exist
|
||||
# Check for the header
|
||||
self.assertContains(response, "Are you sure you want to place this domain on hold?")
|
||||
|
||||
# Check for some of its body
|
||||
self.assertContains(response, "When a domain is on hold:")
|
||||
|
||||
# Check for some of the button content
|
||||
self.assertContains(response, "Yes, place hold")
|
||||
|
||||
# Web test has issues grabbing up to date data from the db, so we can test
|
||||
# the returned view instead
|
||||
self.assertContains(response, '<div class="readonly">On hold</div>')
|
||||
|
||||
def test_deletion_ready_fsm_failure(self):
|
||||
"""
|
||||
Scenario: Domain deletion is unsuccessful
|
||||
|
@ -1101,7 +1203,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk), follow=True
|
||||
)
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
# Modify the domain request's property
|
||||
|
@ -1113,6 +1217,64 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# Test that approved domain exists and equals requested domain
|
||||
self.assertEqual(domain_request.creator.status, "restricted")
|
||||
|
||||
def test_user_sets_restricted_status_modal(self):
|
||||
"""Tests the modal for when a user sets the status to restricted"""
|
||||
with less_console_noise():
|
||||
# make sure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain_request.requested_domain.name)
|
||||
|
||||
# Check that the modal has the right content
|
||||
# Check for the header
|
||||
self.assertContains(response, "Are you sure you want to select ineligible status?")
|
||||
|
||||
# Check for some of its body
|
||||
self.assertContains(response, "When a domain request is in ineligible status")
|
||||
|
||||
# Check for some of the button content
|
||||
self.assertContains(response, "Yes, select ineligible status")
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domainrequest{}/change/".format(domain_request.pk), follow=True
|
||||
)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
# Modify the domain request's property
|
||||
domain_request.status = DomainRequest.DomainRequestStatus.INELIGIBLE
|
||||
|
||||
# Use the model admin's save_model method
|
||||
self.admin.save_model(request, domain_request, form=None, change=True)
|
||||
|
||||
# Test that approved domain exists and equals requested domain
|
||||
self.assertEqual(domain_request.creator.status, "restricted")
|
||||
|
||||
# 'Get' to the domain request again
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainrequest/{}/change/".format(domain_request.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain_request.requested_domain.name)
|
||||
|
||||
# The modal should be unchanged
|
||||
self.assertContains(response, "Are you sure you want to select ineligible status?")
|
||||
self.assertContains(response, "When a domain request is in ineligible status")
|
||||
self.assertContains(response, "Yes, select ineligible status")
|
||||
|
||||
def test_readonly_when_restricted_creator(self):
|
||||
with less_console_noise():
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
|
|
|
@ -5,7 +5,8 @@ from unittest.mock import MagicMock
|
|||
from django.test import TestCase
|
||||
from .common import completed_domain_request, less_console_noise
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
from registrar.utility import email
|
||||
import boto3_mocking # type: ignore
|
||||
|
||||
|
||||
|
@ -182,3 +183,32 @@ class TestEmails(TestCase):
|
|||
self.assertNotIn("Anything else", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5557\n\n----")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_send_email_with_attachment(self):
|
||||
with boto3_mocking.clients.handler_for("ses", self.mock_client_class):
|
||||
sender_email = "sender@example.com"
|
||||
recipient_email = "recipient@example.com"
|
||||
subject = "Test Subject"
|
||||
body = "Test Body"
|
||||
attachment_file = b"Attachment file content"
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
|
||||
email.send_email_with_attachment(
|
||||
sender_email, recipient_email, subject, body, attachment_file, self.mock_client
|
||||
)
|
||||
# Assert that the `send_raw_email` method of the mocked SES client was called with the expected params
|
||||
self.mock_client.send_raw_email.assert_called_once()
|
||||
|
||||
# Get the args passed to the `send_raw_email` method
|
||||
call_args = self.mock_client.send_raw_email.call_args[1]
|
||||
|
||||
# Assert that the attachment filename is correct
|
||||
self.assertEqual(call_args["RawMessage"]["Data"].count(f'filename="{current_filename}"'), 1)
|
||||
|
||||
# Assert that the attachment content is encrypted
|
||||
self.assertIn("Content-Type: application/octet-stream", call_args["RawMessage"]["Data"])
|
||||
self.assertIn("Content-Transfer-Encoding: base64", call_args["RawMessage"]["Data"])
|
||||
self.assertIn("Content-Disposition: attachment;", call_args["RawMessage"]["Data"])
|
||||
self.assertNotIn("Attachment file content", call_args["RawMessage"]["Data"])
|
||||
|
|
|
@ -226,7 +226,7 @@ class TestFormValidation(MockEppLib):
|
|||
)
|
||||
|
||||
def test_purpose_form_character_count_invalid(self):
|
||||
"""Response must be less than 1000 characters."""
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = PurposeForm(
|
||||
data={
|
||||
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
|
@ -247,15 +247,33 @@ class TestFormValidation(MockEppLib):
|
|||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
"Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||
"ground round fatback turducken chislic shoulder turducken"
|
||||
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
form.errors["purpose"],
|
||||
["Response must be less than 1000 characters."],
|
||||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
def test_anything_else_form_about_your_organization_character_count_invalid(self):
|
||||
"""Response must be less than 1000 characters."""
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AnythingElseForm(
|
||||
data={
|
||||
"anything_else": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
|
@ -276,15 +294,32 @@ class TestFormValidation(MockEppLib):
|
|||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||
"ground round fatback turducken chislic shoulder turducken"
|
||||
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
form.errors["anything_else"],
|
||||
["Response must be less than 1000 characters."],
|
||||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
def test_anything_else_form_character_count_invalid(self):
|
||||
"""Response must be less than 1000 characters."""
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = AboutYourOrganizationForm(
|
||||
data={
|
||||
"about_your_organization": "Bacon ipsum dolor amet fatback"
|
||||
|
@ -306,11 +341,29 @@ class TestFormValidation(MockEppLib):
|
|||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
"strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
"Buffalo biltong chuck pork chop tongue bresaola turkey. Doner"
|
||||
"ground round strip steak, jowl tail chuck ribeye bacon"
|
||||
"beef ribs swine filet ball tip pancetta strip steak sirloin"
|
||||
"mignon ham spare ribs rump. Tail shank biltong beef ribs doner"
|
||||
"buffalo swine bacon. Tongue cow picanha brisket bacon chuck"
|
||||
"leberkas pork loin pork, drumstick capicola. Doner short loin"
|
||||
"ground round fatback turducken chislic shoulder turducken"
|
||||
"spare ribs, burgdoggen kielbasa kevin frankfurter ball tip"
|
||||
"pancetta cupim. Turkey meatball andouille porchetta hamburger"
|
||||
"pork chop corned beef. Brisket short ribs turducken, pork chop"
|
||||
"chislic turkey ball pork chop leberkas rump, rump bacon, jowl"
|
||||
"tip ham. Shankle salami tongue venison short ribs kielbasa"
|
||||
"tri-tip ham hock swine hamburger. Flank meatball corned beef"
|
||||
"cow sausage ball tip kielbasa ham hock. Ball tip cupim meatloaf"
|
||||
"beef ribs rump jowl tenderloin swine sausage biltong"
|
||||
"bacon rump tail boudin meatball boudin meatball boudin."
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
form.errors["about_your_organization"],
|
||||
["Response must be less than 1000 characters."],
|
||||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
def test_your_contact_email_invalid(self):
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
from django.test import Client, TestCase, override_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .common import MockEppLib # type: ignore
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.views.domain import DomainNameserversView
|
||||
|
||||
from .common import MockEppLib # type: ignore
|
||||
from unittest.mock import patch
|
||||
from django.urls import reverse
|
||||
|
||||
from registrar.models import (
|
||||
DomainRequest,
|
||||
|
@ -66,6 +72,7 @@ class TestEnvironmentVariablesEffects(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Domain.objects.all().delete()
|
||||
self.user.delete()
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
|
@ -79,3 +86,52 @@ class TestEnvironmentVariablesEffects(TestCase):
|
|||
"""Banner on non-prod."""
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "You are on a test site.")
|
||||
|
||||
def side_effect_raise_value_error(self):
|
||||
"""Side effect that raises a 500 error"""
|
||||
raise ValueError("Some error")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
def test_non_production_environment_raises_500_and_shows_banner(self):
|
||||
"""Tests if the non-prod banner is still shown on a 500"""
|
||||
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
|
||||
# Add a role
|
||||
fake_role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||
with self.assertRaises(ValueError):
|
||||
contact_page_500 = self.client.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||
)
|
||||
|
||||
# Check that a 500 response is returned
|
||||
self.assertEqual(contact_page_500.status_code, 500)
|
||||
|
||||
self.assertContains(contact_page_500, "You are on a test site.")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_production_environment_raises_500_and_doesnt_show_banner(self):
|
||||
"""Test if the non-prod banner is not shown on production when a 500 is raised"""
|
||||
|
||||
fake_domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
|
||||
# Add a role
|
||||
fake_role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=fake_domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
with patch.object(DomainNameserversView, "get_initial", side_effect=self.side_effect_raise_value_error):
|
||||
with self.assertRaises(ValueError):
|
||||
contact_page_500 = self.client.get(
|
||||
reverse("domain-dns-nameservers", kwargs={"pk": fake_domain.id}),
|
||||
)
|
||||
|
||||
# Check that a 500 response is returned
|
||||
self.assertEqual(contact_page_500.status_code, 500)
|
||||
|
||||
self.assertNotContains(contact_page_500, "You are on a test site.")
|
||||
|
|
|
@ -1021,6 +1021,144 @@ class TestDomainAuthorizingOfficial(TestDomainOverview):
|
|||
self.assertEqual("Testy2", self.domain_information.authorizing_official.first_name)
|
||||
self.assertEqual(ao_pk, self.domain_information.authorizing_official.id)
|
||||
|
||||
def assert_all_form_fields_have_expected_values(self, form, test_cases, test_for_disabled=False):
|
||||
"""
|
||||
Asserts that each specified form field has the expected value and, optionally, checks if the field is disabled.
|
||||
|
||||
This method iterates over a list of tuples, where each
|
||||
tuple contains a field name and the expected value for that field.
|
||||
It uses subtests to isolate each assertion, allowing multiple field
|
||||
checks within a single test method without stopping at the first failure.
|
||||
|
||||
Example usage:
|
||||
test_cases = [
|
||||
("first_name", "John"),
|
||||
("last_name", "Doe"),
|
||||
("email", "john.doe@example.com"),
|
||||
]
|
||||
self.assert_all_form_fields_have_expected_values(my_form, test_cases, test_for_disabled=True)
|
||||
"""
|
||||
for field_name, expected_value in test_cases:
|
||||
with self.subTest(field_name=field_name, expected_value=expected_value):
|
||||
# Test that each field has the value we expect
|
||||
self.assertEqual(expected_value, form[field_name].value)
|
||||
|
||||
if test_for_disabled:
|
||||
# Test for disabled on each field
|
||||
self.assertTrue("disabled" in form[field_name].attrs)
|
||||
|
||||
def test_domain_edit_authorizing_official_federal(self):
|
||||
"""Tests that no edit can occur when the underlying domain is federal"""
|
||||
|
||||
# Set the org type to federal
|
||||
self.domain_information.organization_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||
self.domain_information.save()
|
||||
|
||||
# Add an AO. We can do this at the model level, just not the form level.
|
||||
self.domain_information.authorizing_official = Contact(
|
||||
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
|
||||
)
|
||||
self.domain_information.authorizing_official.save()
|
||||
self.domain_information.save()
|
||||
|
||||
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Test if the form is populating data correctly
|
||||
ao_form = ao_page.forms[0]
|
||||
|
||||
test_cases = [
|
||||
("first_name", "Apple"),
|
||||
("last_name", "Tester"),
|
||||
("title", "CIO"),
|
||||
("email", "nobody@igorville.gov"),
|
||||
]
|
||||
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True)
|
||||
|
||||
# Attempt to change data on each field. Because this domain is federal,
|
||||
# this should not succeed.
|
||||
ao_form["first_name"] = "Orange"
|
||||
ao_form["last_name"] = "Smoothie"
|
||||
ao_form["title"] = "Cat"
|
||||
ao_form["email"] = "somebody@igorville.gov"
|
||||
|
||||
submission = ao_form.submit()
|
||||
|
||||
# A 302 indicates this page underwent a redirect.
|
||||
self.assertEqual(submission.status_code, 302)
|
||||
|
||||
followed_submission = submission.follow()
|
||||
|
||||
# Test the returned form for data accuracy. These values should be unchanged.
|
||||
new_form = followed_submission.forms[0]
|
||||
self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
|
||||
|
||||
# refresh domain information. Test these values in the DB.
|
||||
self.domain_information.refresh_from_db()
|
||||
|
||||
# All values should be unchanged. These are defined manually for code clarity.
|
||||
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name)
|
||||
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name)
|
||||
self.assertEqual("CIO", self.domain_information.authorizing_official.title)
|
||||
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email)
|
||||
|
||||
def test_domain_edit_authorizing_official_tribal(self):
|
||||
"""Tests that no edit can occur when the underlying domain is tribal"""
|
||||
|
||||
# Set the org type to federal
|
||||
self.domain_information.organization_type = DomainInformation.OrganizationChoices.TRIBAL
|
||||
self.domain_information.save()
|
||||
|
||||
# Add an AO. We can do this at the model level, just not the form level.
|
||||
self.domain_information.authorizing_official = Contact(
|
||||
first_name="Apple", last_name="Tester", title="CIO", email="nobody@igorville.gov"
|
||||
)
|
||||
self.domain_information.authorizing_official.save()
|
||||
self.domain_information.save()
|
||||
|
||||
ao_page = self.app.get(reverse("domain-authorizing-official", kwargs={"pk": self.domain.id}))
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Test if the form is populating data correctly
|
||||
ao_form = ao_page.forms[0]
|
||||
|
||||
test_cases = [
|
||||
("first_name", "Apple"),
|
||||
("last_name", "Tester"),
|
||||
("title", "CIO"),
|
||||
("email", "nobody@igorville.gov"),
|
||||
]
|
||||
self.assert_all_form_fields_have_expected_values(ao_form, test_cases, test_for_disabled=True)
|
||||
|
||||
# Attempt to change data on each field. Because this domain is federal,
|
||||
# this should not succeed.
|
||||
ao_form["first_name"] = "Orange"
|
||||
ao_form["last_name"] = "Smoothie"
|
||||
ao_form["title"] = "Cat"
|
||||
ao_form["email"] = "somebody@igorville.gov"
|
||||
|
||||
submission = ao_form.submit()
|
||||
|
||||
# A 302 indicates this page underwent a redirect.
|
||||
self.assertEqual(submission.status_code, 302)
|
||||
|
||||
followed_submission = submission.follow()
|
||||
|
||||
# Test the returned form for data accuracy. These values should be unchanged.
|
||||
new_form = followed_submission.forms[0]
|
||||
self.assert_all_form_fields_have_expected_values(new_form, test_cases, test_for_disabled=True)
|
||||
|
||||
# refresh domain information. Test these values in the DB.
|
||||
self.domain_information.refresh_from_db()
|
||||
|
||||
# All values should be unchanged. These are defined manually for code clarity.
|
||||
self.assertEqual("Apple", self.domain_information.authorizing_official.first_name)
|
||||
self.assertEqual("Tester", self.domain_information.authorizing_official.last_name)
|
||||
self.assertEqual("CIO", self.domain_information.authorizing_official.title)
|
||||
self.assertEqual("nobody@igorville.gov", self.domain_information.authorizing_official.email)
|
||||
|
||||
def test_domain_edit_authorizing_official_creates_new(self):
|
||||
"""When editing an authorizing official for domain information and AO IS
|
||||
joined to another object"""
|
||||
|
@ -1088,6 +1226,149 @@ class TestDomainOrganization(TestDomainOverview):
|
|||
self.assertContains(success_result_page, "Not igorville")
|
||||
self.assertContains(success_result_page, "Faketown")
|
||||
|
||||
def test_domain_org_name_address_form_tribal(self):
|
||||
"""
|
||||
Submitting a change to organization_name is blocked for tribal domains
|
||||
"""
|
||||
# Set the current domain to a tribal organization with a preset value.
|
||||
# Save first, so we can test if saving is unaffected (it should be).
|
||||
tribal_org_type = DomainInformation.OrganizationChoices.TRIBAL
|
||||
self.domain_information.organization_type = tribal_org_type
|
||||
self.domain_information.save()
|
||||
try:
|
||||
# Add an org name
|
||||
self.domain_information.organization_name = "Town of Igorville"
|
||||
self.domain_information.save()
|
||||
except ValueError as err:
|
||||
self.fail(f"A ValueError was caught during the test: {err}")
|
||||
|
||||
self.assertEqual(self.domain_information.organization_type, tribal_org_type)
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
|
||||
form = org_name_page.forms[0]
|
||||
# Check the value of the input field
|
||||
organization_name_input = form.fields["organization_name"][0]
|
||||
self.assertEqual(organization_name_input.value, "Town of Igorville")
|
||||
|
||||
# Check if the input field is disabled
|
||||
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
org_name_page.form["organization_name"] = "Not igorville"
|
||||
org_name_page.form["city"] = "Faketown"
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Make the change. The org name should be unchanged, but city should be modifiable.
|
||||
success_result_page = org_name_page.form.submit()
|
||||
self.assertEqual(success_result_page.status_code, 200)
|
||||
|
||||
# Check for the old and new value
|
||||
self.assertContains(success_result_page, "Town of Igorville")
|
||||
self.assertNotContains(success_result_page, "Not igorville")
|
||||
|
||||
# Do another check on the form itself
|
||||
form = success_result_page.forms[0]
|
||||
# Check the value of the input field
|
||||
organization_name_input = form.fields["organization_name"][0]
|
||||
self.assertEqual(organization_name_input.value, "Town of Igorville")
|
||||
|
||||
# Check if the input field is disabled
|
||||
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||
|
||||
# Check for the value we want to update
|
||||
self.assertContains(success_result_page, "Faketown")
|
||||
|
||||
def test_domain_org_name_address_form_federal(self):
|
||||
"""
|
||||
Submitting a change to federal_agency is blocked for federal domains
|
||||
"""
|
||||
# Set the current domain to a tribal organization with a preset value.
|
||||
# Save first, so we can test if saving is unaffected (it should be).
|
||||
fed_org_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||
self.domain_information.organization_type = fed_org_type
|
||||
self.domain_information.save()
|
||||
try:
|
||||
self.domain_information.federal_agency = "AMTRAK"
|
||||
self.domain_information.save()
|
||||
except ValueError as err:
|
||||
self.fail(f"A ValueError was caught during the test: {err}")
|
||||
|
||||
self.assertEqual(self.domain_information.organization_type, fed_org_type)
|
||||
|
||||
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"pk": self.domain.id}))
|
||||
|
||||
form = org_name_page.forms[0]
|
||||
# Check the value of the input field
|
||||
agency_input = form.fields["federal_agency"][0]
|
||||
self.assertEqual(agency_input.value, "AMTRAK")
|
||||
|
||||
# Check if the input field is disabled
|
||||
self.assertTrue("disabled" in agency_input.attrs)
|
||||
self.assertEqual(agency_input.attrs.get("disabled"), "")
|
||||
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
org_name_page.form["federal_agency"] = "Department of State"
|
||||
org_name_page.form["city"] = "Faketown"
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
# Make the change. The agency should be unchanged, but city should be modifiable.
|
||||
success_result_page = org_name_page.form.submit()
|
||||
self.assertEqual(success_result_page.status_code, 200)
|
||||
|
||||
# Check for the old and new value
|
||||
self.assertContains(success_result_page, "AMTRAK")
|
||||
self.assertNotContains(success_result_page, "Department of State")
|
||||
|
||||
# Do another check on the form itself
|
||||
form = success_result_page.forms[0]
|
||||
# Check the value of the input field
|
||||
organization_name_input = form.fields["federal_agency"][0]
|
||||
self.assertEqual(organization_name_input.value, "AMTRAK")
|
||||
|
||||
# Check if the input field is disabled
|
||||
self.assertTrue("disabled" in organization_name_input.attrs)
|
||||
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
|
||||
|
||||
# Check for the value we want to update
|
||||
self.assertContains(success_result_page, "Faketown")
|
||||
|
||||
def test_federal_agency_submit_blocked(self):
|
||||
"""
|
||||
Submitting a change to federal_agency is blocked for federal domains
|
||||
"""
|
||||
# Set the current domain to a tribal organization with a preset value.
|
||||
# Save first, so we can test if saving is unaffected (it should be).
|
||||
federal_org_type = DomainInformation.OrganizationChoices.FEDERAL
|
||||
self.domain_information.organization_type = federal_org_type
|
||||
self.domain_information.save()
|
||||
|
||||
old_federal_agency_value = ("AMTRAK", "AMTRAK")
|
||||
try:
|
||||
# Add a federal agency. Defined as a tuple since this list may change order.
|
||||
self.domain_information.federal_agency = old_federal_agency_value
|
||||
self.domain_information.save()
|
||||
except ValueError as err:
|
||||
self.fail(f"A ValueError was caught during the test: {err}")
|
||||
|
||||
self.assertEqual(self.domain_information.organization_type, federal_org_type)
|
||||
|
||||
new_value = ("Department of State", "Department of State")
|
||||
self.client.post(
|
||||
reverse("domain-org-name-address", kwargs={"pk": self.domain.id}),
|
||||
{
|
||||
"federal_agency": new_value,
|
||||
},
|
||||
)
|
||||
self.assertEqual(self.domain_information.federal_agency, old_federal_agency_value)
|
||||
self.assertNotEqual(self.domain_information.federal_agency, new_value)
|
||||
|
||||
|
||||
class TestDomainContactInformation(TestDomainOverview):
|
||||
def test_domain_your_contact_information(self):
|
||||
|
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
import boto3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -15,7 +19,14 @@ class EmailSendingError(RuntimeError):
|
|||
pass
|
||||
|
||||
|
||||
def send_templated_email(template_name: str, subject_template_name: str, to_address: str, bcc_address="", context={}):
|
||||
def send_templated_email(
|
||||
template_name: str,
|
||||
subject_template_name: str,
|
||||
to_address: str,
|
||||
bcc_address="",
|
||||
context={},
|
||||
attachment_file: str = None,
|
||||
):
|
||||
"""Send an email built from a template to one email address.
|
||||
|
||||
template_name and subject_template_name are relative to the same template
|
||||
|
@ -45,15 +56,50 @@ def send_templated_email(template_name: str, subject_template_name: str, to_addr
|
|||
destination["BccAddresses"] = [bcc_address]
|
||||
|
||||
try:
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination=destination,
|
||||
Content={
|
||||
"Simple": {
|
||||
"Subject": {"Data": subject},
|
||||
"Body": {"Text": {"Data": email_body}},
|
||||
if attachment_file is None:
|
||||
ses_client.send_email(
|
||||
FromEmailAddress=settings.DEFAULT_FROM_EMAIL,
|
||||
Destination=destination,
|
||||
Content={
|
||||
"Simple": {
|
||||
"Subject": {"Data": subject},
|
||||
"Body": {"Text": {"Data": email_body}},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
)
|
||||
else:
|
||||
ses_client = boto3.client(
|
||||
"ses",
|
||||
region_name=settings.AWS_REGION,
|
||||
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
|
||||
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
|
||||
config=settings.BOTO_CONFIG,
|
||||
)
|
||||
send_email_with_attachment(
|
||||
settings.DEFAULT_FROM_EMAIL, to_address, subject, email_body, attachment_file, ses_client
|
||||
)
|
||||
except Exception as exc:
|
||||
raise EmailSendingError("Could not send SES email.") from exc
|
||||
|
||||
|
||||
def send_email_with_attachment(sender, recipient, subject, body, attachment_file, ses_client):
|
||||
# Create a multipart/mixed parent container
|
||||
msg = MIMEMultipart("mixed")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = sender
|
||||
msg["To"] = recipient
|
||||
|
||||
# Add the text part
|
||||
text_part = MIMEText(body, "plain")
|
||||
msg.attach(text_part)
|
||||
|
||||
# Add the attachment part
|
||||
attachment_part = MIMEApplication(attachment_file)
|
||||
# Adding attachment header + filename that the attachment will be called
|
||||
current_date = datetime.now().strftime("%m%d%Y")
|
||||
current_filename = f"domain-metadata-{current_date}.zip"
|
||||
attachment_part.add_header("Content-Disposition", f'attachment; filename="{current_filename}"')
|
||||
msg.attach(attachment_part)
|
||||
|
||||
response = ses_client.send_raw_email(Source=sender, Destinations=[recipient], RawMessage={"Data": msg.as_string()})
|
||||
return response
|
||||
|
|
|
@ -18,6 +18,8 @@ from django.conf import settings
|
|||
|
||||
from registrar.models import (
|
||||
Domain,
|
||||
DomainRequest,
|
||||
DomainInformation,
|
||||
DomainInvitation,
|
||||
User,
|
||||
UserDomainRole,
|
||||
|
@ -134,6 +136,20 @@ class DomainFormBaseView(DomainBaseView, FormMixin):
|
|||
# superclass has the redirect
|
||||
return super().form_invalid(form)
|
||||
|
||||
def get_domain_info_from_domain(self) -> DomainInformation | None:
|
||||
"""
|
||||
Grabs the underlying domain_info object based off of self.object.name.
|
||||
Returns None if nothing is found.
|
||||
"""
|
||||
_domain_info = DomainInformation.objects.filter(domain__name=self.object.name)
|
||||
current_domain_info = None
|
||||
if _domain_info.exists() and _domain_info.count() == 1:
|
||||
current_domain_info = _domain_info.get()
|
||||
else:
|
||||
logger.error("Could get domain_info. No domain info exists, or duplicates exist.")
|
||||
|
||||
return current_domain_info
|
||||
|
||||
|
||||
class DomainView(DomainBaseView):
|
||||
"""Domain detail overview page."""
|
||||
|
@ -217,16 +233,29 @@ class DomainAuthorizingOfficialView(DomainFormBaseView):
|
|||
"""Add domain_info.authorizing_official instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.object.domain_info.authorizing_official
|
||||
|
||||
domain_info = self.get_domain_info_from_domain()
|
||||
invalid_fields = [DomainRequest.OrganizationChoices.FEDERAL, DomainRequest.OrganizationChoices.TRIBAL]
|
||||
is_federal_or_tribal = domain_info and (domain_info.organization_type in invalid_fields)
|
||||
|
||||
form_kwargs["disable_fields"] = is_federal_or_tribal
|
||||
return form_kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Adds custom context."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["organization_type"] = self.object.domain_info.organization_type
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the overview page for the domain."""
|
||||
return reverse("domain-authorizing-official", kwargs={"pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, save the authorizing official."""
|
||||
|
||||
# Set the domain information in the form so that it can be accessible
|
||||
# to associate a new Contact as authorizing official, if new Contact is needed
|
||||
# to associate a new Contact, if a new Contact is needed
|
||||
# in the save() method
|
||||
form.set_domain_info(self.object.domain_info)
|
||||
form.save()
|
||||
|
|
32
src/registrar/views/utility/error_views.py
Normal file
32
src/registrar/views/utility/error_views.py
Normal file
|
@ -0,0 +1,32 @@
|
|||
"""
|
||||
Custom views that allow for error view customization.
|
||||
|
||||
Used as a general handler for 500 errors both coming from the registrar app, but
|
||||
also the djangooidc app.
|
||||
|
||||
If Djangooidc is left to its own devices and uses reverse directly,
|
||||
then both context and session information will be obliterated due to:
|
||||
|
||||
a) Djangooidc being out of scope for context_processors
|
||||
b) Potential cyclical import errors restricting what kind of data is passable.
|
||||
|
||||
Rather than dealing with that, we keep everything centralized in one location.
|
||||
"""
|
||||
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
def custom_500_error_view(request, context=None):
|
||||
"""Used to redirect 500 errors to a custom view"""
|
||||
if context is None:
|
||||
return render(request, "500.html", status=500)
|
||||
else:
|
||||
return render(request, "500.html", context=context, status=500)
|
||||
|
||||
|
||||
def custom_401_error_view(request, context=None):
|
||||
"""Used to redirect 401 errors to a custom view"""
|
||||
if context is None:
|
||||
return render(request, "401.html", status=401)
|
||||
else:
|
||||
return render(request, "401.html", context=context, status=401)
|
|
@ -1,8 +1,8 @@
|
|||
-i https://pypi.python.org/simple
|
||||
annotated-types==0.6.0; python_version >= '3.8'
|
||||
asgiref==3.7.2; python_version >= '3.7'
|
||||
boto3==1.34.54; python_version >= '3.8'
|
||||
botocore==1.34.54; python_version >= '3.8'
|
||||
boto3==1.34.56; python_version >= '3.8'
|
||||
botocore==1.34.56; python_version >= '3.8'
|
||||
cachetools==5.3.3; python_version >= '3.7'
|
||||
certifi==2024.2.2; python_version >= '3.6'
|
||||
cfenv==0.5.3
|
||||
|
@ -22,8 +22,8 @@ django-fsm==2.8.1
|
|||
django-login-required-middleware==0.9.0
|
||||
django-phonenumber-field[phonenumberslite]==7.3.0; python_version >= '3.8'
|
||||
django-widget-tweaks==1.5.0; python_version >= '3.8'
|
||||
environs[django]==10.3.0; python_version >= '3.8'
|
||||
faker==23.3.0; python_version >= '3.8'
|
||||
environs[django]==11.0.0; python_version >= '3.8'
|
||||
faker==24.0.0; python_version >= '3.8'
|
||||
fred-epplib@ git+https://github.com/cisagov/epplib.git@d56d183f1664f34c40ca9716a3a9a345f0ef561c
|
||||
furl==2.1.3
|
||||
future==1.0.0; python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
|
@ -35,7 +35,7 @@ jmespath==1.0.1; python_version >= '3.7'
|
|||
lxml==5.1.0; python_version >= '3.6'
|
||||
mako==1.3.2; python_version >= '3.8'
|
||||
markupsafe==2.1.5; python_version >= '3.7'
|
||||
marshmallow==3.21.0; python_version >= '3.8'
|
||||
marshmallow==3.21.1; python_version >= '3.8'
|
||||
oic==1.6.1; python_version ~= '3.7'
|
||||
orderedmultidict==1.0.1
|
||||
packaging==23.2; python_version >= '3.7'
|
||||
|
@ -49,6 +49,7 @@ pydantic-settings==2.2.1; python_version >= '3.8'
|
|||
pyjwkest==1.4.2
|
||||
python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
|
||||
python-dotenv==1.0.1; python_version >= '3.8'
|
||||
pyzipper==0.3.6; python_version >= '3.4'
|
||||
requests==2.31.0; python_version >= '3.7'
|
||||
s3transfer==0.10.0; python_version >= '3.8'
|
||||
setuptools==69.1.1; python_version >= '3.8'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue