mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-05 10:43:23 +02:00
merge main
This commit is contained in:
commit
22ef6ddc5b
82 changed files with 8270 additions and 7043 deletions
|
@ -35,155 +35,155 @@ class ViewsTest(TestCase):
|
|||
pass
|
||||
|
||||
def test_openid_sets_next(self, mock_client):
|
||||
# setup
|
||||
callback_url = reverse("openid_login_callback")
|
||||
# mock
|
||||
mock_client.create_authn_request.side_effect = self.say_hi
|
||||
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||
# test
|
||||
response = self.client.get(reverse("login"), {"next": callback_url})
|
||||
# assert
|
||||
session = mock_client.create_authn_request.call_args[0][0]
|
||||
self.assertEqual(session["next"], callback_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Hi")
|
||||
with less_console_noise():
|
||||
# setup
|
||||
callback_url = reverse("openid_login_callback")
|
||||
# mock
|
||||
mock_client.create_authn_request.side_effect = self.say_hi
|
||||
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||
# test
|
||||
response = self.client.get(reverse("login"), {"next": callback_url})
|
||||
# assert
|
||||
session = mock_client.create_authn_request.call_args[0][0]
|
||||
self.assertEqual(session["next"], callback_url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Hi")
|
||||
|
||||
def test_openid_raises(self, mock_client):
|
||||
# mock
|
||||
mock_client.create_authn_request.side_effect = Exception("Test")
|
||||
# test
|
||||
with less_console_noise():
|
||||
# mock
|
||||
mock_client.create_authn_request.side_effect = Exception("Test")
|
||||
# test
|
||||
response = self.client.get(reverse("login"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertTemplateUsed(response, "500.html")
|
||||
self.assertIn("Server error", response.content.decode("utf-8"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertTemplateUsed(response, "500.html")
|
||||
self.assertIn("Server error", response.content.decode("utf-8"))
|
||||
|
||||
def test_callback_with_no_session_state(self, mock_client):
|
||||
"""If the local session is None (ie the server restarted while user was logged out),
|
||||
we do not throw an exception. Rather, we attempt to login again."""
|
||||
# mock
|
||||
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||
mock_client.callback.side_effect = NoStateDefined()
|
||||
# test
|
||||
with less_console_noise():
|
||||
# mock
|
||||
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||
mock_client.callback.side_effect = NoStateDefined()
|
||||
# test
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
def test_login_callback_reads_next(self, mock_client):
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["next"] = reverse("logout")
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("logout"))
|
||||
with less_console_noise():
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["next"] = reverse("logout")
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("logout"))
|
||||
|
||||
def test_login_callback_no_step_up_auth(self, mock_client):
|
||||
"""Walk through login_callback when requires_step_up_auth returns False
|
||||
and assert that we have a redirect to /"""
|
||||
# setup
|
||||
session = self.client.session
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
with less_console_noise():
|
||||
# setup
|
||||
session = self.client.session
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, "/")
|
||||
|
||||
def test_requires_step_up_auth(self, mock_client):
|
||||
"""Invoke login_callback passing it a request when requires_step_up_auth returns True
|
||||
and assert that session is updated and create_authn_request (mock) is called."""
|
||||
# Configure the mock to return an expected value for get_step_up_acr_value
|
||||
mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return True
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
|
||||
# Assert that acr_value is no longer empty string
|
||||
self.assertNotEqual(request.session["acr_value"], "")
|
||||
# And create_authn_request was called again
|
||||
mock_create_authn_request.assert_called_once()
|
||||
with less_console_noise():
|
||||
# Configure the mock to return an expected value for get_step_up_acr_value
|
||||
mock_client.return_value.get_step_up_acr_value.return_value = "step_up_acr_value"
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return True
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=True), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
# Assert that acr_value is no longer empty string
|
||||
self.assertNotEqual(request.session["acr_value"], "")
|
||||
# And create_authn_request was called again
|
||||
mock_create_authn_request.assert_called_once()
|
||||
|
||||
def test_does_not_requires_step_up_auth(self, mock_client):
|
||||
"""Invoke login_callback passing it a request when requires_step_up_auth returns False
|
||||
and assert that session is not updated and create_authn_request (mock) is not called.
|
||||
|
||||
Possibly redundant with test_login_callback_requires_step_up_auth"""
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return False
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
|
||||
# Assert that acr_value is NOT updated by testing that it is still an empty string
|
||||
self.assertEqual(request.session["acr_value"], "")
|
||||
# Assert create_authn_request was not called
|
||||
mock_create_authn_request.assert_not_called()
|
||||
with less_console_noise():
|
||||
# Create a mock request
|
||||
request = self.factory.get("/some-url")
|
||||
request.session = {"acr_value": ""}
|
||||
# Ensure that the CLIENT instance used in login_callback is the mock
|
||||
# patch requires_step_up_auth to return False
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), patch(
|
||||
"djangooidc.views.CLIENT.create_authn_request", return_value=MagicMock()
|
||||
) as mock_create_authn_request:
|
||||
login_callback(request)
|
||||
# create_authn_request only gets called when requires_step_up_auth is True
|
||||
# and it changes this acr_value in request.session
|
||||
# Assert that acr_value is NOT updated by testing that it is still an empty string
|
||||
self.assertEqual(request.session["acr_value"], "")
|
||||
# Assert create_authn_request was not called
|
||||
mock_create_authn_request.assert_not_called()
|
||||
|
||||
@patch("djangooidc.views.authenticate")
|
||||
def test_login_callback_raises(self, mock_auth, mock_client):
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
mock_auth.return_value = None
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertTemplateUsed(response, "401.html")
|
||||
self.assertIn("Unauthorized", response.content.decode("utf-8"))
|
||||
with less_console_noise():
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
mock_auth.return_value = None
|
||||
# test
|
||||
with patch("djangooidc.views.requires_step_up_auth", return_value=False), less_console_noise():
|
||||
response = self.client.get(reverse("openid_login_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 401)
|
||||
self.assertTemplateUsed(response, "401.html")
|
||||
self.assertIn("Unauthorized", response.content.decode("utf-8"))
|
||||
|
||||
def test_logout_redirect_url(self, mock_client):
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["state"] = "TEST" # nosec B105
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
|
||||
mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
|
||||
mock_client.client_id = "TEST"
|
||||
# test
|
||||
with less_console_noise():
|
||||
response = self.client.get(reverse("logout"))
|
||||
# assert
|
||||
expected = (
|
||||
"http://example.com/log_me_out?client_id=TEST&state"
|
||||
"=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback"
|
||||
)
|
||||
actual = response.url
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(actual, expected)
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["state"] = "TEST" # nosec B105
|
||||
session.save()
|
||||
# mock
|
||||
mock_client.callback.side_effect = self.user_info
|
||||
mock_client.registration_response = {"post_logout_redirect_uris": ["http://example.com/back"]}
|
||||
mock_client.provider_info = {"end_session_endpoint": "http://example.com/log_me_out"}
|
||||
mock_client.client_id = "TEST"
|
||||
# test
|
||||
with less_console_noise():
|
||||
response = self.client.get(reverse("logout"))
|
||||
# assert
|
||||
expected = (
|
||||
"http://example.com/log_me_out?client_id=TEST&state"
|
||||
"=TEST&post_logout_redirect_uri=http%3A%2F%2Fexample.com%2Fback"
|
||||
)
|
||||
actual = response.url
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
@patch("djangooidc.views.auth_logout")
|
||||
def test_logout_always_logs_out(self, mock_logout, _):
|
||||
|
@ -194,12 +194,13 @@ class ViewsTest(TestCase):
|
|||
self.assertTrue(mock_logout.called)
|
||||
|
||||
def test_logout_callback_redirects(self, _):
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["next"] = reverse("logout")
|
||||
session.save()
|
||||
# test
|
||||
response = self.client.get(reverse("openid_logout_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("logout"))
|
||||
with less_console_noise():
|
||||
# setup
|
||||
session = self.client.session
|
||||
session["next"] = reverse("logout")
|
||||
session.save()
|
||||
# test
|
||||
response = self.client.get(reverse("openid_logout_callback"))
|
||||
# assert
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertEqual(response.url, reverse("logout"))
|
||||
|
|
|
@ -25,6 +25,8 @@ services:
|
|||
- DJANGO_SECRET_KEY=really-long-random-string-BNPecI7+s8jMahQcGHZ3XQ5yUfRrSibdapVLIz0UemdktVPofDKcoy
|
||||
# Run Django in debug mode on local
|
||||
- DJANGO_DEBUG=True
|
||||
# Set DJANGO_LOG_LEVEL in env
|
||||
- DJANGO_LOG_LEVEL
|
||||
# Run Django without production flags
|
||||
- IS_PRODUCTION=False
|
||||
# Tell Django where it is being hosted
|
||||
|
|
51
src/epplibwrapper/tests/common.py
Normal file
51
src/epplibwrapper/tests/common.py
Normal file
|
@ -0,0 +1,51 @@
|
|||
import os
|
||||
import logging
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
|
||||
def get_handlers():
|
||||
"""Obtain pointers to all StreamHandlers."""
|
||||
handlers = {}
|
||||
|
||||
rootlogger = logging.getLogger()
|
||||
for h in rootlogger.handlers:
|
||||
if isinstance(h, logging.StreamHandler):
|
||||
handlers[h.name] = h
|
||||
|
||||
for logger in logging.Logger.manager.loggerDict.values():
|
||||
if not isinstance(logger, logging.PlaceHolder):
|
||||
for h in logger.handlers:
|
||||
if isinstance(h, logging.StreamHandler):
|
||||
handlers[h.name] = h
|
||||
|
||||
return handlers
|
||||
|
||||
|
||||
@contextmanager
|
||||
def less_console_noise():
|
||||
"""
|
||||
Context manager to use in tests to silence console logging.
|
||||
|
||||
This is helpful on tests which trigger console messages
|
||||
(such as errors) which are normal and expected.
|
||||
|
||||
It can easily be removed to debug a failing test.
|
||||
"""
|
||||
restore = {}
|
||||
handlers = get_handlers()
|
||||
devnull = open(os.devnull, "w")
|
||||
|
||||
# redirect all the streams
|
||||
for handler in handlers.values():
|
||||
prior = handler.setStream(devnull)
|
||||
restore[handler.name] = prior
|
||||
try:
|
||||
# run the test
|
||||
yield
|
||||
finally:
|
||||
# restore the streams
|
||||
for handler in handlers.values():
|
||||
handler.setStream(restore[handler.name])
|
||||
# close the file we opened
|
||||
devnull.close()
|
|
@ -9,7 +9,7 @@ from epplibwrapper.socket import Socket
|
|||
from epplibwrapper.utility.pool import EPPConnectionPool
|
||||
from registrar.models.domain import registry
|
||||
from contextlib import ExitStack
|
||||
|
||||
from .common import less_console_noise
|
||||
import logging
|
||||
|
||||
try:
|
||||
|
@ -135,23 +135,26 @@ class TestConnectionPool(TestCase):
|
|||
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
|
||||
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
|
||||
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
|
||||
# Restart the connection pool
|
||||
registry.start_connection_pool()
|
||||
# Pool should be running, and be the right size
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
with less_console_noise():
|
||||
# Restart the connection pool
|
||||
registry.start_connection_pool()
|
||||
# Pool should be running, and be the right size
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
|
||||
# Send a command
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
# Send a command
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
# Kill the connection pool
|
||||
registry.kill_pool()
|
||||
|
||||
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
|
||||
def test_pool_restarts_on_send(self):
|
||||
|
@ -198,35 +201,43 @@ class TestConnectionPool(TestCase):
|
|||
xml = (location).read_bytes()
|
||||
return xml
|
||||
|
||||
def do_nothing(command):
|
||||
pass
|
||||
|
||||
# Mock what happens inside the "with"
|
||||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
|
||||
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
|
||||
stack.enter_context(patch.object(EPPConnectionPool, "kill_all_connections", do_nothing))
|
||||
stack.enter_context(patch.object(SocketTransport, "send", self.fake_send))
|
||||
stack.enter_context(patch.object(SocketTransport, "receive", fake_receive))
|
||||
# Kill the connection pool
|
||||
registry.kill_pool()
|
||||
with less_console_noise():
|
||||
# Start the connection pool
|
||||
registry.start_connection_pool()
|
||||
# Kill the connection pool
|
||||
registry.kill_pool()
|
||||
|
||||
self.assertEqual(registry.pool_status.connection_success, False)
|
||||
self.assertEqual(registry.pool_status.pool_running, False)
|
||||
self.assertEqual(registry.pool_status.pool_running, False)
|
||||
|
||||
# An exception should be raised as end user will be informed
|
||||
# that they cannot connect to EPP
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
# An exception should be raised as end user will be informed
|
||||
# that they cannot connect to EPP
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
self.assertEqual(result, expected)
|
||||
|
||||
# A subsequent command should be successful, as the pool restarts
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
self.assertEqual(result, expected)
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
|
||||
# A subsequent command should be successful, as the pool restarts
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
# Should this ever fail, it either means that the schema has changed,
|
||||
# or the pool is broken.
|
||||
# If the schema has changed: Update the associated infoDomain.xml file
|
||||
self.assertEqual(result.__dict__, expected_result)
|
||||
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
# The number of open pools should match the number of requested ones.
|
||||
# If it is 0, then they failed to open
|
||||
self.assertEqual(len(registry._pool.conn), self.pool_options["size"])
|
||||
# Kill the connection pool
|
||||
registry.kill_pool()
|
||||
|
||||
@patch.object(EPPLibWrapper, "_test_registry_connection_success", patch_success)
|
||||
def test_raises_connection_error(self):
|
||||
|
@ -236,13 +247,16 @@ class TestConnectionPool(TestCase):
|
|||
with ExitStack() as stack:
|
||||
stack.enter_context(patch.object(EPPConnectionPool, "_create_socket", self.fake_socket))
|
||||
stack.enter_context(patch.object(Socket, "connect", self.fake_client))
|
||||
with less_console_noise():
|
||||
# Start the connection pool
|
||||
registry.start_connection_pool()
|
||||
|
||||
# Pool should be running
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
# Pool should be running
|
||||
self.assertEqual(registry.pool_status.connection_success, True)
|
||||
self.assertEqual(registry.pool_status.pool_running, True)
|
||||
|
||||
# Try to send a command out - should fail
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
self.assertEqual(result, expected)
|
||||
# Try to send a command out - should fail
|
||||
with self.assertRaises(RegistryError):
|
||||
expected = "InfoDomain failed to execute due to a connection error."
|
||||
result = registry.send(commands.InfoDomain(name="test.gov"), cleaned=True)
|
||||
self.assertEqual(result, expected)
|
||||
|
|
|
@ -85,6 +85,21 @@ class EPPConnectionPool(ConnectionPool):
|
|||
logger.error(message, exc_info=True)
|
||||
raise PoolError(code=PoolErrorCodes.KEEP_ALIVE_FAILED) from err
|
||||
|
||||
def _keepalive_periodic(self):
|
||||
"""Overriding _keepalive_periodic from geventconnpool so that PoolErrors
|
||||
are properly handled, as opposed to printing to stdout"""
|
||||
delay = float(self.keepalive) / self.size
|
||||
while 1:
|
||||
try:
|
||||
with self.get() as c:
|
||||
self._keepalive(c)
|
||||
except PoolError as err:
|
||||
logger.error(err.message, exc_info=True)
|
||||
except self.exc_classes:
|
||||
# Nothing to do, the pool will generate a new connection later
|
||||
pass
|
||||
gevent.sleep(delay)
|
||||
|
||||
def _create_socket(self, client, login) -> Socket:
|
||||
"""Creates and returns a socket instance"""
|
||||
socket = Socket(client, login)
|
||||
|
|
|
@ -20,6 +20,8 @@ from . import models
|
|||
from auditlog.models import LogEntry # type: ignore
|
||||
from auditlog.admin import LogEntryAdmin # type: ignore
|
||||
from django_fsm import TransitionNotAllowed # type: ignore
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.html import escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -452,6 +454,60 @@ class ContactAdmin(ListHeaderAdmin):
|
|||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Extend the change_view for Contact objects in django admin.
|
||||
Customize to display related objects to the Contact. These will be passed
|
||||
through the messages construct to the template for display to the user."""
|
||||
|
||||
# Fetch the Contact instance
|
||||
contact = models.Contact.objects.get(pk=object_id)
|
||||
|
||||
# initialize related_objects array
|
||||
related_objects = []
|
||||
# for all defined fields in the model
|
||||
for related_field in contact._meta.get_fields():
|
||||
# if the field is a relation to another object
|
||||
if related_field.is_relation:
|
||||
# Check if the related field is not None
|
||||
related_manager = getattr(contact, related_field.name)
|
||||
if related_manager is not None:
|
||||
# Check if it's a ManyToManyField/reverse ForeignKey or a OneToOneField
|
||||
# Do this by checking for get_queryset method on the related_manager
|
||||
if hasattr(related_manager, "get_queryset"):
|
||||
# Handles ManyToManyRel and ManyToOneRel
|
||||
queryset = related_manager.get_queryset()
|
||||
else:
|
||||
# Handles OneToOne rels, ie. User
|
||||
queryset = [related_manager]
|
||||
|
||||
for obj in queryset:
|
||||
# for each object, build the edit url in this view and add as tuple
|
||||
# to the related_objects array
|
||||
app_label = obj._meta.app_label
|
||||
model_name = obj._meta.model_name
|
||||
obj_id = obj.id
|
||||
change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id])
|
||||
related_objects.append((change_url, obj))
|
||||
|
||||
if related_objects:
|
||||
message = "<ul class='messagelist_content-list--unstyled'>"
|
||||
for i, (url, obj) in enumerate(related_objects):
|
||||
if i < 5:
|
||||
escaped_obj = escape(obj)
|
||||
message += f"<li>Joined to {obj.__class__.__name__}: <a href='{url}'>{escaped_obj}</a></li>"
|
||||
message += "</ul>"
|
||||
if len(related_objects) > 5:
|
||||
related_objects_over_five = len(related_objects) - 5
|
||||
message += f"<p class='font-sans-3xs'>And {related_objects_over_five} more...</p>"
|
||||
|
||||
message_html = mark_safe(message) # nosec
|
||||
messages.warning(
|
||||
request,
|
||||
message_html,
|
||||
)
|
||||
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
|
||||
class WebsiteAdmin(ListHeaderAdmin):
|
||||
"""Custom website admin class."""
|
||||
|
@ -570,7 +626,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["creator", "domain_application"]}),
|
||||
(None, {"fields": ["creator", "domain_application", "notes"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
|
@ -621,6 +677,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
|||
"type_of_work",
|
||||
"more_organization_information",
|
||||
"domain",
|
||||
"domain_application",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
|
@ -737,7 +794,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
# Detail view
|
||||
form = DomainApplicationAdminForm
|
||||
fieldsets = [
|
||||
(None, {"fields": ["status", "investigator", "creator", "approved_domain"]}),
|
||||
(None, {"fields": ["status", "investigator", "creator", "approved_domain", "notes"]}),
|
||||
(
|
||||
"Type of organization",
|
||||
{
|
||||
|
@ -789,6 +846,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"submitter",
|
||||
|
@ -826,14 +884,11 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
if (
|
||||
obj
|
||||
and original_obj.status == models.DomainApplication.ApplicationStatus.APPROVED
|
||||
and (
|
||||
obj.status == models.DomainApplication.ApplicationStatus.REJECTED
|
||||
or obj.status == models.DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
)
|
||||
and obj.status != models.DomainApplication.ApplicationStatus.APPROVED
|
||||
and not obj.domain_is_not_active()
|
||||
):
|
||||
# If an admin tried to set an approved application to
|
||||
# rejected or ineligible and the related domain is already
|
||||
# another status and the related domain is already
|
||||
# active, shortcut the action and throw a friendly
|
||||
# error message. This action would still not go through
|
||||
# shortcut or not as the rules are duplicated on the model,
|
||||
|
@ -991,6 +1046,13 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
"deleted",
|
||||
]
|
||||
|
||||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
|
||||
),
|
||||
)
|
||||
|
||||
# this ordering effects the ordering of results
|
||||
# in autocomplete_fields for domain
|
||||
ordering = ["name"]
|
||||
|
@ -1239,7 +1301,7 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by draft domain name."
|
||||
|
||||
|
||||
class VeryImportantPersonAdmin(ListHeaderAdmin):
|
||||
class VerifiedByStaffAdmin(ListHeaderAdmin):
|
||||
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||
search_fields = ["email"]
|
||||
search_help_text = "Search by email."
|
||||
|
@ -1282,4 +1344,4 @@ admin.site.register(models.Website, WebsiteAdmin)
|
|||
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)
|
||||
admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
||||
|
|
|
@ -283,19 +283,20 @@ function enableRelatedWidgetButtons(changeLink, deleteLink, viewLink, elementPk,
|
|||
(function (){
|
||||
|
||||
// Get the current date in the format YYYY-MM-DD
|
||||
var currentDate = new Date().toISOString().split('T')[0];
|
||||
let currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Default the value of the start date input field to the current date
|
||||
let startDateInput =document.getElementById('start');
|
||||
startDateInput.value = currentDate;
|
||||
|
||||
|
||||
// Default the value of the end date input field to the current date
|
||||
let endDateInput =document.getElementById('end');
|
||||
endDateInput.value = currentDate;
|
||||
|
||||
let exportGrowthReportButton = document.getElementById('exportLink');
|
||||
|
||||
if (exportGrowthReportButton) {
|
||||
startDateInput.value = currentDate;
|
||||
endDateInput.value = currentDate;
|
||||
|
||||
exportGrowthReportButton.addEventListener('click', function() {
|
||||
// Get the selected start and end dates
|
||||
let startDate = startDateInput.value;
|
||||
|
|
|
@ -130,7 +130,7 @@ function inlineToast(el, id, style, msg) {
|
|||
}
|
||||
}
|
||||
|
||||
function _checkDomainAvailability(el) {
|
||||
function checkDomainAvailability(el) {
|
||||
const callback = (response) => {
|
||||
toggleInputValidity(el, (response && response.available), msg=response.message);
|
||||
announce(el.id, response.message);
|
||||
|
@ -154,9 +154,6 @@ function _checkDomainAvailability(el) {
|
|||
fetchJSON(`available/?domain=${el.value}`, callback);
|
||||
}
|
||||
|
||||
/** Call the API to see if the domain is good. */
|
||||
const checkDomainAvailability = debounce(_checkDomainAvailability);
|
||||
|
||||
/** Hides the toast message and clears the aira live region. */
|
||||
function clearDomainAvailability(el) {
|
||||
el.classList.remove('usa-input--success');
|
||||
|
@ -206,13 +203,33 @@ function handleInputValidation(e) {
|
|||
}
|
||||
|
||||
/** On button click, handles running any associated validators. */
|
||||
function handleValidationClick(e) {
|
||||
function validateFieldInput(e) {
|
||||
const attribute = e.target.getAttribute("validate-for") || "";
|
||||
if (!attribute.length) return;
|
||||
const input = document.getElementById(attribute);
|
||||
removeFormErrors(input, true);
|
||||
runValidators(input);
|
||||
}
|
||||
|
||||
|
||||
function validateFormsetInputs(e, availabilityButton) {
|
||||
|
||||
// Collect input IDs from the repeatable forms
|
||||
let inputs = Array.from(document.querySelectorAll('.repeatable-form input'))
|
||||
|
||||
// Run validators for each input
|
||||
inputs.forEach(input => {
|
||||
removeFormErrors(input, true);
|
||||
runValidators(input);
|
||||
});
|
||||
|
||||
// Set the validate-for attribute on the button with the collected input IDs
|
||||
// Not needed for functionality but nice for accessibility
|
||||
inputs = inputs.map(input => input.id).join(', ');
|
||||
availabilityButton.setAttribute('validate-for', inputs);
|
||||
|
||||
}
|
||||
|
||||
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||
// Initialization code.
|
||||
|
||||
|
@ -232,14 +249,64 @@ function handleValidationClick(e) {
|
|||
for(const input of needsValidation) {
|
||||
input.addEventListener('input', handleInputValidation);
|
||||
}
|
||||
const alternativeDomainsAvailability = document.getElementById('validate-alt-domains-availability');
|
||||
const activatesValidation = document.querySelectorAll('[validate-for]');
|
||||
|
||||
for(const button of activatesValidation) {
|
||||
button.addEventListener('click', handleValidationClick);
|
||||
// Adds multi-field validation for alternative domains
|
||||
if (button === alternativeDomainsAvailability) {
|
||||
button.addEventListener('click', (e) => {
|
||||
validateFormsetInputs(e, alternativeDomainsAvailability)
|
||||
});
|
||||
} else {
|
||||
button.addEventListener('click', validateFieldInput);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Delete method for formsets that diff in the view and delete in the model (Nameservers, DS Data)
|
||||
* Removes form errors surrounding a form input
|
||||
*/
|
||||
function removeFormErrors(input, removeStaleAlerts=false){
|
||||
// Remove error message
|
||||
let errorMessage = document.getElementById(`${input.id}__error-message`);
|
||||
if (errorMessage) {
|
||||
errorMessage.remove();
|
||||
}else{
|
||||
return
|
||||
}
|
||||
|
||||
// Remove error classes
|
||||
if (input.classList.contains('usa-input--error')) {
|
||||
input.classList.remove('usa-input--error');
|
||||
}
|
||||
|
||||
// Get the form label
|
||||
let label = document.querySelector(`label[for="${input.id}"]`);
|
||||
if (label) {
|
||||
label.classList.remove('usa-label--error');
|
||||
|
||||
// Remove error classes from parent div
|
||||
let parentDiv = label.parentElement;
|
||||
if (parentDiv) {
|
||||
parentDiv.classList.remove('usa-form-group--error');
|
||||
}
|
||||
}
|
||||
|
||||
if (removeStaleAlerts){
|
||||
let staleAlerts = document.querySelectorAll(".usa-alert--error")
|
||||
for (let alert of staleAlerts){
|
||||
// Don't remove the error associated with the input
|
||||
if (alert.id !== `${input.id}--toast`) {
|
||||
alert.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the namerservers and DS data forms delete buttons
|
||||
* We will call this on the forms init, and also every time we add a form
|
||||
*
|
||||
*/
|
||||
function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||
|
@ -460,6 +527,7 @@ function hideDeletedForms() {
|
|||
let isNameserversForm = document.querySelector(".nameservers-form");
|
||||
let isOtherContactsForm = document.querySelector(".other-contacts-form");
|
||||
let isDsDataForm = document.querySelector(".ds-data-form");
|
||||
let isDotgovDomain = document.querySelector(".dotgov-domain-form");
|
||||
// The Nameservers formset features 2 required and 11 optionals
|
||||
if (isNameserversForm) {
|
||||
cloneIndex = 2;
|
||||
|
@ -472,6 +540,8 @@ function hideDeletedForms() {
|
|||
formLabel = "Organization contact";
|
||||
container = document.querySelector("#other-employees");
|
||||
formIdentifier = "other_contacts"
|
||||
} else if (isDotgovDomain) {
|
||||
formIdentifier = "dotgov_domain"
|
||||
}
|
||||
let totalForms = document.querySelector(`#id_${formIdentifier}-TOTAL_FORMS`);
|
||||
|
||||
|
@ -554,6 +624,7 @@ function hideDeletedForms() {
|
|||
// Reset the values of each input to blank
|
||||
inputs.forEach((input) => {
|
||||
input.classList.remove("usa-input--error");
|
||||
input.classList.remove("usa-input--success");
|
||||
if (input.type === "text" || input.type === "number" || input.type === "password" || input.type === "email" || input.type === "tel") {
|
||||
input.value = ""; // Set the value to an empty string
|
||||
|
||||
|
@ -566,22 +637,25 @@ function hideDeletedForms() {
|
|||
let selects = newForm.querySelectorAll("select");
|
||||
selects.forEach((select) => {
|
||||
select.classList.remove("usa-input--error");
|
||||
select.classList.remove("usa-input--success");
|
||||
select.selectedIndex = 0; // Set the value to an empty string
|
||||
});
|
||||
|
||||
let labels = newForm.querySelectorAll("label");
|
||||
labels.forEach((label) => {
|
||||
label.classList.remove("usa-label--error");
|
||||
label.classList.remove("usa-label--success");
|
||||
});
|
||||
|
||||
let usaFormGroups = newForm.querySelectorAll(".usa-form-group");
|
||||
usaFormGroups.forEach((usaFormGroup) => {
|
||||
usaFormGroup.classList.remove("usa-form-group--error");
|
||||
usaFormGroup.classList.remove("usa-form-group--success");
|
||||
});
|
||||
|
||||
// Remove any existing error messages
|
||||
let usaErrorMessages = newForm.querySelectorAll(".usa-error-message");
|
||||
usaErrorMessages.forEach((usaErrorMessage) => {
|
||||
// Remove any existing error and success messages
|
||||
let usaMessages = newForm.querySelectorAll(".usa-error-message, .usa-alert");
|
||||
usaMessages.forEach((usaErrorMessage) => {
|
||||
let parentDiv = usaErrorMessage.closest('div');
|
||||
if (parentDiv) {
|
||||
parentDiv.remove(); // Remove the parent div if it exists
|
||||
|
@ -592,7 +666,8 @@ function hideDeletedForms() {
|
|||
|
||||
// Attach click event listener on the delete buttons of the new form
|
||||
let newDeleteButton = newForm.querySelector(".delete-record");
|
||||
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||
if (newDeleteButton)
|
||||
prepareNewDeleteButton(newDeleteButton, formLabel);
|
||||
|
||||
// Disable the add more button if we have 13 forms
|
||||
if (isNameserversForm && formNum == 13) {
|
||||
|
|
|
@ -143,7 +143,7 @@ h1, h2, h3,
|
|||
|
||||
.module h3 {
|
||||
padding: 0;
|
||||
color: var(--primary);
|
||||
color: var(--link-fg);
|
||||
margin: units(2) 0 units(1) 0;
|
||||
}
|
||||
|
||||
|
@ -258,3 +258,15 @@ h1, h2, h3,
|
|||
#select2-id_user-results {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// Content list inside of a DjA alert, unstyled
|
||||
.messagelist_content-list--unstyled {
|
||||
padding-left: 0;
|
||||
li {
|
||||
font-family: "Source Sans Pro Web", "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
|
||||
font-size: 13.92px!important;
|
||||
background: none!important;
|
||||
padding: 0!important;
|
||||
margin: 0!important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,5 +17,8 @@
|
|||
.usa-alert__body::before {
|
||||
left: 1rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
.usa-alert__body.margin-left-1 {
|
||||
margin-left: 0.5rem!important;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,6 +44,22 @@ a.usa-button.disabled-link:focus {
|
|||
color: #454545 !important
|
||||
}
|
||||
|
||||
a.usa-button--unstyled.disabled-link,
|
||||
a.usa-button--unstyled.disabled-link:hover,
|
||||
a.usa-button--unstyled.disabled-link:focus {
|
||||
cursor: not-allowed !important;
|
||||
outline: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.usa-button--unstyled.disabled-button,
|
||||
.usa-button--unstyled.disabled-link:hover,
|
||||
.usa-button--unstyled.disabled-link:focus {
|
||||
cursor: not-allowed !important;
|
||||
outline: none !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
a.usa-button:not(.usa-button--unstyled, .usa-button--outline) {
|
||||
color: color('white');
|
||||
}
|
||||
|
|
14
src/registrar/assets/sass/_theme/_lists.scss
Normal file
14
src/registrar/assets/sass/_theme/_lists.scss
Normal file
|
@ -0,0 +1,14 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
dt {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
font-weight: font-weight('semibold');
|
||||
// The units mixin can only get us close, so it's between
|
||||
// hardcoding the value and using in markup
|
||||
font-size: 16.96px;
|
||||
}
|
||||
dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -6,12 +6,14 @@
|
|||
margin-top: units(-1);
|
||||
}
|
||||
|
||||
//Tighter spacing when H2 is immediatly after H1
|
||||
// Tighter spacing when h2 is immediatly after h1
|
||||
.register-form-step .usa-fieldset:first-of-type h2:first-of-type,
|
||||
.register-form-step h1 + h2 {
|
||||
margin-top: units(1);
|
||||
}
|
||||
|
||||
// register-form-review-header is used on the summary page and
|
||||
// should not be styled like the register form headers
|
||||
.register-form-step h3 {
|
||||
color: color('primary-dark');
|
||||
letter-spacing: $letter-space--xs;
|
||||
|
@ -23,6 +25,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
h3.register-form-review-header {
|
||||
color: color('primary-dark');
|
||||
margin-top: units(2);
|
||||
margin-bottom: 0;
|
||||
font-weight: font-weight('semibold');
|
||||
// The units mixin can only get us close, so it's between
|
||||
// hardcoding the value and using in markup
|
||||
font-size: 16.96px;
|
||||
}
|
||||
|
||||
.register-form-step h4 {
|
||||
margin-bottom: 0;
|
||||
|
||||
|
|
|
@ -116,6 +116,10 @@ in the form $setting: value,
|
|||
$theme-color-success-light: $dhs-green-30,
|
||||
$theme-color-success-lighter: $dhs-green-15,
|
||||
|
||||
/*---------------------------
|
||||
## Emergency state
|
||||
----------------------------*/
|
||||
$theme-color-emergency: #FFC3F9,
|
||||
|
||||
/*---------------------------
|
||||
# Input settings
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
--- Custom Styles ---------------------------------*/
|
||||
@forward "base";
|
||||
@forward "typography";
|
||||
@forward "lists";
|
||||
@forward "buttons";
|
||||
@forward "forms";
|
||||
@forward "fieldsets";
|
||||
|
|
|
@ -660,7 +660,6 @@ ALLOWED_HOSTS = [
|
|||
"getgov-rb.app.cloud.gov",
|
||||
"getgov-ko.app.cloud.gov",
|
||||
"getgov-ab.app.cloud.gov",
|
||||
"getgov-bl.app.cloud.gov",
|
||||
"getgov-rjm.app.cloud.gov",
|
||||
"getgov-dk.app.cloud.gov",
|
||||
"manage.get.gov",
|
||||
|
|
|
@ -142,6 +142,11 @@ urlpatterns = [
|
|||
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
|
||||
name="application-delete",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/<int:user_pk>/delete",
|
||||
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
||||
name="domain-user-delete",
|
||||
),
|
||||
]
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
|
|
|
@ -104,7 +104,7 @@ class DomainApplicationFixture:
|
|||
# Random choice of agency for selects, used as placeholders for testing.
|
||||
else random.choice(DomainApplication.AGENCIES) # nosec
|
||||
)
|
||||
|
||||
da.submission_date = fake.date()
|
||||
da.federal_type = (
|
||||
app["federal_type"]
|
||||
if "federal_type" in app
|
||||
|
|
|
@ -420,7 +420,7 @@ class AlternativeDomainForm(RegistrarForm):
|
|||
|
||||
alternative_domain = forms.CharField(
|
||||
required=False,
|
||||
label="",
|
||||
label="Alternative domain",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -182,8 +182,6 @@ class LoadExtraTransitionDomain:
|
|||
# STEP 5: Parse creation and expiration data
|
||||
updated_transition_domain = self.parse_creation_expiration_data(domain_name, transition_domain)
|
||||
|
||||
# Check if the instance has changed before saving
|
||||
updated_transition_domain.save()
|
||||
updated_transition_domains.append(updated_transition_domain)
|
||||
logger.info(f"{TerminalColors.OKCYAN}" f"Successfully updated {domain_name}" f"{TerminalColors.ENDC}")
|
||||
|
||||
|
@ -199,6 +197,28 @@ class LoadExtraTransitionDomain:
|
|||
)
|
||||
failed_transition_domains.append(domain_name)
|
||||
|
||||
updated_fields = [
|
||||
"organization_name",
|
||||
"organization_type",
|
||||
"federal_type",
|
||||
"federal_agency",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
"email",
|
||||
"phone",
|
||||
"epp_creation_date",
|
||||
"epp_expiration_date",
|
||||
]
|
||||
|
||||
batch_size = 1000
|
||||
# Create a Paginator object. Bulk_update on the full dataset
|
||||
# is too memory intensive for our current app config, so we can chunk this data instead.
|
||||
paginator = Paginator(updated_transition_domains, batch_size)
|
||||
for page_num in paginator.page_range:
|
||||
page = paginator.page(page_num)
|
||||
TransitionDomain.objects.bulk_update(page.object_list, updated_fields)
|
||||
|
||||
failed_count = len(failed_transition_domains)
|
||||
if failed_count == 0:
|
||||
if self.debug:
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-23 22:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0063_veryimportantperson"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="address_line1",
|
||||
field=models.TextField(blank=True, help_text="Street address", null=True, verbose_name="Address line 1"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainapplication",
|
||||
name="address_line2",
|
||||
field=models.TextField(
|
||||
blank=True, help_text="Street address line 2 (optional)", null=True, verbose_name="Address line 2"
|
||||
),
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0065_create_groups_v06.py
Normal file
37
src/registrar/migrations/0065_create_groups_v06.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0035 (which populates ContentType and Permissions)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0064_alter_domainapplication_address_line1_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,20 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-29 22:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0065_create_groups_v06"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name="VeryImportantPerson",
|
||||
new_name="VerifiedByStaff",
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="verifiedbystaff",
|
||||
options={"verbose_name_plural": "Verified by staff"},
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0067_create_groups_v07.py
Normal file
37
src/registrar/migrations/0067_create_groups_v07.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0035 (which populates ContentType and Permissions)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0066_rename_veryimportantperson_verifiedbystaff_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.7 on 2024-01-26 20:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0067_create_groups_v07"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainapplication",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, help_text="Notes about this request", null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domaininformation",
|
||||
name="notes",
|
||||
field=models.TextField(blank=True, help_text="Notes about the request", null=True),
|
||||
),
|
||||
]
|
|
@ -13,7 +13,7 @@ from .user import User
|
|||
from .user_group import UserGroup
|
||||
from .website import Website
|
||||
from .transition_domain import TransitionDomain
|
||||
from .very_important_person import VeryImportantPerson
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
|
||||
__all__ = [
|
||||
"Contact",
|
||||
|
@ -30,7 +30,7 @@ __all__ = [
|
|||
"UserGroup",
|
||||
"Website",
|
||||
"TransitionDomain",
|
||||
"VeryImportantPerson",
|
||||
"VerifiedByStaff",
|
||||
]
|
||||
|
||||
auditlog.register(Contact)
|
||||
|
@ -47,4 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"])
|
|||
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
||||
auditlog.register(Website)
|
||||
auditlog.register(TransitionDomain)
|
||||
auditlog.register(VeryImportantPerson)
|
||||
auditlog.register(VerifiedByStaff)
|
||||
|
|
|
@ -12,6 +12,7 @@ from django.utils import timezone
|
|||
from typing import Any
|
||||
from registrar.models.host import Host
|
||||
from registrar.models.host_ip import HostIP
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
from registrar.utility.errors import (
|
||||
ActionNotAllowed,
|
||||
|
@ -910,10 +911,15 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
raise NotImplementedError()
|
||||
|
||||
def get_security_email(self):
|
||||
logger.info("get_security_email-> getting the contact ")
|
||||
secContact = self.security_contact
|
||||
if secContact is not None:
|
||||
return secContact.email
|
||||
logger.info("get_security_email-> getting the contact")
|
||||
|
||||
security = PublicContact.ContactTypeChoices.SECURITY
|
||||
security_contact = self.generic_contact_getter(security)
|
||||
|
||||
# If we get a valid value for security_contact, pull its email
|
||||
# Otherwise, just return nothing
|
||||
if security_contact is not None and isinstance(security_contact, PublicContact):
|
||||
return security_contact.email
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -1121,7 +1127,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
If you wanted to setup getter logic for Security, you would call:
|
||||
cache_contact_helper(PublicContact.ContactTypeChoices.SECURITY),
|
||||
or cache_contact_helper("security").
|
||||
|
||||
"""
|
||||
# registrant_contact(s) are an edge case. They exist on
|
||||
# the "registrant" property as opposed to contacts.
|
||||
|
@ -1400,7 +1405,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
|
||||
DF = epp.DiscloseField
|
||||
fields = {DF.EMAIL}
|
||||
disclose = is_security and contact.email != PublicContact.get_default_security().email
|
||||
|
||||
hidden_security_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
|
||||
disclose = is_security and contact.email not in hidden_security_emails
|
||||
# Delete after testing on other devices
|
||||
logger.info("Updated domain contact %s to disclose: %s", contact.email, disclose)
|
||||
# Will only disclose DF.EMAIL if its not the default
|
||||
|
|
|
@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address",
|
||||
verbose_name="Address line 1",
|
||||
)
|
||||
address_line2 = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Street address line 2 (optional)",
|
||||
verbose_name="Address line 2",
|
||||
)
|
||||
city = models.TextField(
|
||||
null=True,
|
||||
|
@ -556,6 +558,12 @@ class DomainApplication(TimeStampedModel):
|
|||
help_text="Date submitted",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Notes about this request",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.requested_domain and self.requested_domain.name:
|
||||
|
@ -570,6 +578,19 @@ class DomainApplication(TimeStampedModel):
|
|||
return not self.approved_domain.is_active()
|
||||
return True
|
||||
|
||||
def delete_and_clean_up_domain(self, called_from):
|
||||
try:
|
||||
domain_state = self.approved_domain.state
|
||||
# Only reject if it exists on EPP
|
||||
if domain_state != Domain.State.UNKNOWN:
|
||||
self.approved_domain.deletedInEpp()
|
||||
self.approved_domain.save()
|
||||
self.approved_domain.delete()
|
||||
self.approved_domain = None
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error(f"Can't query an approved domain while attempting {called_from}")
|
||||
|
||||
def _send_status_update_email(self, new_status, email_template, email_template_subject, send_email=True):
|
||||
"""Send a status update email to the submitter.
|
||||
|
||||
|
@ -651,11 +672,19 @@ class DomainApplication(TimeStampedModel):
|
|||
ApplicationStatus.INELIGIBLE,
|
||||
],
|
||||
target=ApplicationStatus.IN_REVIEW,
|
||||
conditions=[domain_is_not_active],
|
||||
)
|
||||
def in_review(self):
|
||||
"""Investigate an application that has been submitted.
|
||||
|
||||
This action is logged."""
|
||||
This action is logged.
|
||||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("in_review")
|
||||
|
||||
literal = DomainApplication.ApplicationStatus.IN_REVIEW
|
||||
# Check if the tuple exists, then grab its value
|
||||
in_review = literal if literal is not None else "In Review"
|
||||
|
@ -670,11 +699,19 @@ class DomainApplication(TimeStampedModel):
|
|||
ApplicationStatus.INELIGIBLE,
|
||||
],
|
||||
target=ApplicationStatus.ACTION_NEEDED,
|
||||
conditions=[domain_is_not_active],
|
||||
)
|
||||
def action_needed(self):
|
||||
"""Send back an application that is under investigation or rejected.
|
||||
|
||||
This action is logged."""
|
||||
This action is logged.
|
||||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||
|
||||
literal = DomainApplication.ApplicationStatus.ACTION_NEEDED
|
||||
# Check if the tuple is setup correctly, then grab its value
|
||||
action_needed = literal if literal is not None else "Action Needed"
|
||||
|
@ -707,7 +744,7 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
# copy the information from domainapplication into domaininformation
|
||||
DomainInformation = apps.get_model("registrar.DomainInformation")
|
||||
DomainInformation.create_from_da(self, domain=created_domain)
|
||||
DomainInformation.create_from_da(domain_application=self, domain=created_domain)
|
||||
|
||||
# create the permission for the user
|
||||
UserDomainRole = apps.get_model("registrar.UserDomainRole")
|
||||
|
@ -747,18 +784,9 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
As side effects this will delete the domain and domain_information
|
||||
(will cascade), and send an email notification."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
try:
|
||||
domain_state = self.approved_domain.state
|
||||
# Only reject if it exists on EPP
|
||||
if domain_state != Domain.State.UNKNOWN:
|
||||
self.approved_domain.deletedInEpp()
|
||||
self.approved_domain.save()
|
||||
self.approved_domain.delete()
|
||||
self.approved_domain = None
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Can't query an approved domain while attempting a DA reject()")
|
||||
self.delete_and_clean_up_domain("reject")
|
||||
|
||||
self._send_status_update_email(
|
||||
"action needed",
|
||||
|
@ -787,17 +815,7 @@ class DomainApplication(TimeStampedModel):
|
|||
and domain_information (will cascade) when they exist."""
|
||||
|
||||
if self.status == self.ApplicationStatus.APPROVED:
|
||||
try:
|
||||
domain_state = self.approved_domain.state
|
||||
# Only reject if it exists on EPP
|
||||
if domain_state != Domain.State.UNKNOWN:
|
||||
self.approved_domain.deletedInEpp()
|
||||
self.approved_domain.save()
|
||||
self.approved_domain.delete()
|
||||
self.approved_domain = None
|
||||
except Exception as err:
|
||||
logger.error(err)
|
||||
logger.error("Can't query an approved domain while attempting a DA reject_with_prejudice()")
|
||||
self.delete_and_clean_up_domain("reject_with_prejudice")
|
||||
|
||||
self.creator.restrict_user()
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
from __future__ import annotations
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models.utility.domain_helper import DomainHelper
|
||||
from .domain_application import DomainApplication
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -202,6 +205,12 @@ class DomainInformation(TimeStampedModel):
|
|||
help_text="Acknowledged .gov acceptable use policy",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Notes about the request",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
if self.domain and self.domain.name:
|
||||
|
@ -212,37 +221,63 @@ class DomainInformation(TimeStampedModel):
|
|||
return ""
|
||||
|
||||
@classmethod
|
||||
def create_from_da(cls, domain_application, domain=None):
|
||||
"""Takes in a DomainApplication dict and converts it into DomainInformation"""
|
||||
da_dict = domain_application.to_dict()
|
||||
# remove the id so one can be assinged on creation
|
||||
da_id = da_dict.pop("id", None)
|
||||
def create_from_da(cls, domain_application: DomainApplication, domain=None):
|
||||
"""Takes in a DomainApplication and converts it into DomainInformation"""
|
||||
|
||||
# Throw an error if we get None - we can't create something from nothing
|
||||
if domain_application is None:
|
||||
raise ValueError("The provided DomainApplication is None")
|
||||
|
||||
# Throw an error if the da doesn't have an id
|
||||
if not hasattr(domain_application, "id"):
|
||||
raise ValueError("The provided DomainApplication has no id")
|
||||
|
||||
# check if we have a record that corresponds with the domain
|
||||
# application, if so short circuit the create
|
||||
domain_info = cls.objects.filter(domain_application__id=da_id).first()
|
||||
if domain_info:
|
||||
return domain_info
|
||||
# the following information below is not needed in the domain information:
|
||||
da_dict.pop("status", None)
|
||||
da_dict.pop("current_websites", None)
|
||||
da_dict.pop("investigator", None)
|
||||
da_dict.pop("alternative_domains", None)
|
||||
da_dict.pop("requested_domain", None)
|
||||
da_dict.pop("approved_domain", None)
|
||||
da_dict.pop("submission_date", None)
|
||||
other_contacts = da_dict.pop("other_contacts", [])
|
||||
domain_info = cls(**da_dict)
|
||||
domain_info.domain_application = domain_application
|
||||
# Save so the object now have PK
|
||||
# (needed to process the manytomany below before, first)
|
||||
domain_info.save()
|
||||
existing_domain_info = cls.objects.filter(domain_application__id=domain_application.id).first()
|
||||
if existing_domain_info:
|
||||
return existing_domain_info
|
||||
|
||||
# Process the remaining "many to many" stuff
|
||||
domain_info.other_contacts.add(*other_contacts)
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
common_fields = DomainHelper.get_common_fields(DomainApplication, DomainInformation)
|
||||
|
||||
# Get a list of all many_to_many relations on DomainInformation (needs to be saved differently)
|
||||
info_many_to_many_fields = DomainInformation._get_many_to_many_fields()
|
||||
|
||||
# Create a dictionary with only the common fields, and create a DomainInformation from it
|
||||
da_dict = {}
|
||||
da_many_to_many_dict = {}
|
||||
for field in common_fields:
|
||||
# If the field isn't many_to_many, populate the da_dict.
|
||||
# If it is, populate da_many_to_many_dict as we need to save this later.
|
||||
if hasattr(domain_application, field):
|
||||
if field not in info_many_to_many_fields:
|
||||
da_dict[field] = getattr(domain_application, field)
|
||||
else:
|
||||
da_many_to_many_dict[field] = getattr(domain_application, field).all()
|
||||
|
||||
# Create a placeholder DomainInformation object
|
||||
domain_info = DomainInformation(**da_dict)
|
||||
|
||||
# Add the domain_application and domain fields
|
||||
domain_info.domain_application = domain_application
|
||||
if domain:
|
||||
domain_info.domain = domain
|
||||
domain_info.save()
|
||||
|
||||
# Save the instance and set the many-to-many fields.
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
domain_info.save()
|
||||
for field, value in da_many_to_many_dict.items():
|
||||
getattr(domain_info, field).set(value)
|
||||
|
||||
return domain_info
|
||||
|
||||
@staticmethod
|
||||
def _get_many_to_many_fields():
|
||||
"""Returns a set of each field.name that has the many to many relation"""
|
||||
return {field.name for field in DomainInformation._meta.many_to_many} # type: ignore
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Domain information"
|
||||
|
|
|
@ -4,6 +4,8 @@ from string import ascii_uppercase, ascii_lowercase, digits
|
|||
|
||||
from django.db import models
|
||||
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
||||
|
@ -87,7 +89,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp="VA",
|
||||
pc="20598-0645",
|
||||
cc="US",
|
||||
email="dotgov@cisa.dhs.gov",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -104,7 +106,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email="dotgov@cisa.dhs.gov",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -121,7 +123,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email="dotgov@cisa.dhs.gov",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
@ -138,7 +140,7 @@ class PublicContact(TimeStampedModel):
|
|||
sp="VA",
|
||||
pc="22201",
|
||||
cc="US",
|
||||
email="dotgov@cisa.dhs.gov",
|
||||
email=DefaultEmail.PUBLIC_CONTACT_DEFAULT.value,
|
||||
voice="+1.8882820870",
|
||||
pw="thisisnotapassword",
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole
|
|||
|
||||
from .domain_invitation import DomainInvitation
|
||||
from .transition_domain import TransitionDomain
|
||||
from .very_important_person import VeryImportantPerson
|
||||
from .verified_by_staff import VerifiedByStaff
|
||||
from .domain import Domain
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||
|
@ -91,7 +91,7 @@ class User(AbstractUser):
|
|||
return False
|
||||
|
||||
# New users flagged by Staff to bypass ial2
|
||||
if VeryImportantPerson.objects.filter(email=email).exists():
|
||||
if VerifiedByStaff.objects.filter(email=email).exists():
|
||||
return False
|
||||
|
||||
# A new incoming user who is being invited to be a domain manager (that is,
|
||||
|
|
|
@ -66,6 +66,11 @@ class UserGroup(Group):
|
|||
"model": "userdomainrole",
|
||||
"permissions": ["view_userdomainrole", "delete_userdomainrole"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "verifiedbystaff",
|
||||
"permissions": ["add_verifiedbystaff", "change_verifiedbystaff", "delete_verifiedbystaff"],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import re
|
||||
|
||||
from typing import Type
|
||||
from django.db import models
|
||||
from django import forms
|
||||
from django.http import JsonResponse
|
||||
|
||||
|
@ -29,7 +30,6 @@ class DomainHelper:
|
|||
@classmethod
|
||||
def validate(cls, domain: str, blank_ok=False) -> str:
|
||||
"""Attempt to determine if a domain name could be requested."""
|
||||
|
||||
# Split into pieces for the linter
|
||||
domain = cls._validate_domain_string(domain, blank_ok)
|
||||
|
||||
|
@ -57,6 +57,9 @@ class DomainHelper:
|
|||
# If blank ok is true, just return the domain
|
||||
return domain
|
||||
|
||||
if domain.startswith("www."):
|
||||
domain = domain[4:]
|
||||
|
||||
if domain.endswith(".gov"):
|
||||
domain = domain[:-4]
|
||||
|
||||
|
@ -158,3 +161,29 @@ class DomainHelper:
|
|||
"""Get the top level domain. Example: `gsa.gov` -> `gov`."""
|
||||
parts = domain.rsplit(".")
|
||||
return parts[-1] if len(parts) > 1 else ""
|
||||
|
||||
@staticmethod
|
||||
def get_common_fields(model_1: Type[models.Model], model_2: Type[models.Model]):
|
||||
"""
|
||||
Returns a set of field names that two Django models have in common, excluding the 'id' field.
|
||||
|
||||
Args:
|
||||
model_1 (Type[models.Model]): The first Django model class.
|
||||
model_2 (Type[models.Model]): The second Django model class.
|
||||
|
||||
Returns:
|
||||
Set[str]: A set of field names that both models share.
|
||||
|
||||
Example:
|
||||
If model_1 has fields {"id", "name", "color"} and model_2 has fields {"id", "color"},
|
||||
the function will return {"color"}.
|
||||
"""
|
||||
|
||||
# Get a list of the existing fields on model_1 and model_2
|
||||
model_1_fields = set(field.name for field in model_1._meta.get_fields() if field != "id")
|
||||
model_2_fields = set(field.name for field in model_2._meta.get_fields() if field != "id")
|
||||
|
||||
# Get the fields that exist on both DomainApplication and DomainInformation
|
||||
common_fields = model_1_fields & model_2_fields
|
||||
|
||||
return common_fields
|
||||
|
|
|
@ -3,7 +3,7 @@ from django.db import models
|
|||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
||||
class VeryImportantPerson(TimeStampedModel):
|
||||
class VerifiedByStaff(TimeStampedModel):
|
||||
|
||||
"""emails that get added to this table will bypass ial2 on login."""
|
||||
|
||||
|
@ -28,5 +28,8 @@ class VeryImportantPerson(TimeStampedModel):
|
|||
help_text="Notes",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name_plural = "Verified by staff"
|
||||
|
||||
def __str__(self):
|
||||
return self.email
|
|
@ -24,34 +24,57 @@
|
|||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "css/styles.css" %}" />
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'css/styles.css' %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1>
|
||||
{% if user.is_anonymous %}
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endif %}
|
||||
{% block header %}
|
||||
{% if not IS_PRODUCTION %}
|
||||
{% with add_body_class="margin-left-1" %}
|
||||
{% include "includes/non-production-alert.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{# Djando update: this div will change to header #}
|
||||
<div id="header">
|
||||
<div id="branding">
|
||||
{% block branding %}
|
||||
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1>
|
||||
{% if user.is_anonymous %}
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% block usertools %}
|
||||
{% if has_permission %}
|
||||
<div id="user-tools">
|
||||
{% block welcome-msg %}
|
||||
{% translate 'Welcome,' %}
|
||||
<strong>{% firstof user.get_short_name user.get_username %}</strong>.
|
||||
{% endblock %}
|
||||
{% comment %}
|
||||
This was copied from the 'userlinks' template, with a few minor changes.
|
||||
You can find that here:
|
||||
https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59
|
||||
{% endcomment %}
|
||||
{% block userlinks %}
|
||||
{% if site_url %}
|
||||
<a href="{{ site_url }}">{% translate 'View site' %}</a> /
|
||||
{% endif %}
|
||||
{% if user.is_active and user.is_staff %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.has_usable_password %}
|
||||
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a>
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block nav-global %}{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% comment %}
|
||||
This was copied from the 'userlinks' template, with a few minor changes.
|
||||
You can find that here:
|
||||
https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59
|
||||
{% endcomment %}
|
||||
{% block userlinks %}
|
||||
{% if site_url %}
|
||||
<a href="{{ site_url }}">{% translate 'View site' %}</a> /
|
||||
{% endif %}
|
||||
{% if user.is_active and user.is_staff %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.has_usable_password %}
|
||||
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a>
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endblock %}
|
||||
{% block nav-global %}{% endblock %}
|
|
@ -11,7 +11,8 @@
|
|||
</ul>
|
||||
</p>
|
||||
|
||||
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations. In most instances, this requires including your state’s two-letter abbreviation.</p>
|
||||
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
|
||||
{% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}</p>
|
||||
|
||||
<p>Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
|
||||
|
@ -48,16 +49,15 @@
|
|||
{% endwith %}
|
||||
{% endwith %}
|
||||
<button
|
||||
id="check-availability-button"
|
||||
type="button"
|
||||
class="usa-button"
|
||||
class="usa-button usa-button--outline"
|
||||
validate-for="{{ forms.0.requested_domain.auto_id }}"
|
||||
>Check availability</button>
|
||||
</fieldset>
|
||||
|
||||
{{ forms.1.management_form }}
|
||||
|
||||
<fieldset class="usa-fieldset margin-top-1">
|
||||
<fieldset class="usa-fieldset margin-top-1 dotgov-domain-form" id="form-container">
|
||||
<legend>
|
||||
<h2>Alternative domains (optional)</h2>
|
||||
</legend>
|
||||
|
@ -66,23 +66,34 @@
|
|||
you your first choice?</p>
|
||||
|
||||
{% with attr_aria_describedby="alt_domain_instructions" %}
|
||||
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
||||
{# attr_auto_validate likewise triggers behavior in get-gov.js #}
|
||||
{% with append_gov=True attr_validate="domain" attr_auto_validate=True %}
|
||||
{% with add_class="blank-ok alternate-domain-input" %}
|
||||
{% for form in forms.1 %}
|
||||
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
||||
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
||||
{% for form in forms.1 %}
|
||||
<div class="repeatable-form">
|
||||
{% input_with_errors form.alternative_domain %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
||||
<button type="button" value="save" class="usa-button usa-button--unstyled" id="add-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another alternative</span>
|
||||
</button>
|
||||
|
||||
</fieldset>
|
||||
<div class="margin-bottom-3">
|
||||
<button
|
||||
id="validate-alt-domains-availability"
|
||||
type="button"
|
||||
class="usa-button usa-button--outline"
|
||||
validate-for="{{ forms.1.requested_domain.auto_id }}"
|
||||
>Check availability</button>
|
||||
</div>
|
||||
|
||||
|
||||
<p class="margin-top-05">If you’re not sure this is the domain you want, that’s ok. You can change the domain later. </p>
|
||||
|
||||
</fieldset>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static form_helpers url_helpers %}
|
||||
|
||||
{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
||||
{% block title %}{{form_titles|get_item:steps.current}} | Request a .gov | {% endblock %}
|
||||
{% block content %}
|
||||
<div class="grid-container">
|
||||
<div class="grid-row grid-gap">
|
||||
|
|
|
@ -20,108 +20,158 @@
|
|||
|
||||
{% block form_fields %}
|
||||
{% for step in steps.all|slice:":-1" %}
|
||||
<section class="review__step">
|
||||
<hr />
|
||||
<div class="review__step__title display-flex flex-justify">
|
||||
<div class="review__step__value">
|
||||
<div class="review__step__name">{{ form_titles|get_item:step }}</div>
|
||||
<div>
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% if application.organization_type is not None %}
|
||||
{% with long_org_type=application.organization_type|get_organization_long_name %}
|
||||
{{ long_org_type }}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
<section class="summary-item margin-top-3">
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.get_organization_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{{ application.tribe_name|default:"Incomplete" }}
|
||||
{% if application.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if application.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.tribe_name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% if application.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if application.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.get_federal_type_display|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=application %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='Incomplete' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{{ application.get_federal_type_display|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.about_your_organization|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.authorizing_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.authorizing_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=application.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% if application.organization_name %}
|
||||
{% include "includes/organization_address.html" with organization=application %}
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
<p>{{ application.about_your_organization|default:"Incomplete" }}</p>
|
||||
{% endif %}
|
||||
{% if step == Step.AUTHORIZING_OFFICIAL %}
|
||||
{% if application.authorizing_official %}
|
||||
<div class="margin-bottom-105">
|
||||
{% include "includes/contact.html" with contact=application.authorizing_official %}
|
||||
</div>
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
<ul class="add-list-reset">
|
||||
{% for site in application.current_websites.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% empty %}
|
||||
<li>None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
<ul class="add-list-reset margin-bottom-105">
|
||||
<li>{{ application.requested_domain.name|default:"Incomplete" }}</li>
|
||||
</ul>
|
||||
<ul class="add-list-reset">
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.requested_domain.name|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if application.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in application.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% if step == Step.PURPOSE %}
|
||||
{{ application.purpose|default:"Incomplete" }}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.purpose|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.submitter is not None %}
|
||||
{% with title=form_titles|get_item:step value=application.submitter %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% if application.submitter %}
|
||||
<div class="margin-bottom-105">
|
||||
{% include "includes/contact.html" with contact=application.submitter %}
|
||||
</div>
|
||||
{% else %}
|
||||
Incomplete
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% if application.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=application.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=application.no_other_contacts_rationale|default:"Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% for other in application.other_contacts.all %}
|
||||
<div class="margin-bottom-105">
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">Contact {{ forloop.counter }}</p>
|
||||
{% include "includes/contact.html" with contact=other %}
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="margin-bottom-105">
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">No other employees from your organization?</p>
|
||||
{{ application.no_other_contacts_rationale|default:"Incomplete" }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{{ application.anything_else|default:"No" }}
|
||||
{% endif %}
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{{ application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
aria-describedby="review_step_title__{{step}}"
|
||||
href="{% namespaced_url 'application' step %}"
|
||||
>Edit<span class="sr-only"> {{ form_titles|get_item:step }}</span></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ANYTHING_ELSE %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.anything_else|default:"No" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% namespaced_url 'application' step as application_url %}
|
||||
{% with title=form_titles|get_item:step value=application.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=application_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
<li class="usa-sidenav__item sidenav__step--locked">
|
||||
<span>
|
||||
{% if not this_step == steps.current %}
|
||||
<svg class="usa-icon text-green" aria-hidden="true" focsuable="false" role="img" width="24" height="24">
|
||||
<title id="checked-step__{{forloop.counter}}">Checked mark</title>
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
|
||||
</svg>
|
||||
{% if this_step != "review" %}
|
||||
<svg class="usa-icon text-green" aria-hidden="true" focsuable="false" role="img" width="24" height="24">
|
||||
<title id="checked-step__{{forloop.counter}}">Checked mark</title>
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
|
||||
</svg>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% namespaced_url 'application' this_step %}"
|
||||
{% if this_step == steps.current %}
|
||||
|
|
|
@ -52,8 +52,8 @@
|
|||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% with long_org_type=domainapplication.organization_type|get_organization_long_name %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=long_org_type heading_level=heading_level %}
|
||||
{% with org_type=domainapplication.get_organization_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domainapplication.tribe_name %}
|
||||
|
@ -74,7 +74,9 @@
|
|||
{% endif %}
|
||||
|
||||
{% if domainapplication.is_election_board %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=domainapplication.is_election_board heading_level=heading_level %}
|
||||
{% with value=domainapplication.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if domainapplication.organization_name %}
|
||||
|
@ -109,8 +111,12 @@
|
|||
{% include "includes/summary_item.html" with title='Your contact information' value=domainapplication.submitter contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
|
||||
{% if domainapplication.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=domainapplication.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/summary_item.html" with title='Anything else?' value=domainapplication.anything_else|default:"No" heading_level=heading_level %}
|
||||
|
||||
{% endwith %}
|
||||
|
|
|
@ -70,6 +70,10 @@
|
|||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
||||
|
||||
{% if not IS_PRODUCTION %}
|
||||
{% include "includes/non-production-alert.html" %}
|
||||
{% endif %}
|
||||
|
||||
<section class="usa-banner" aria-label="Official website of the United States government">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
{% block wrapper %}
|
||||
|
||||
<div id="wrapper" class="dashboard">
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>
|
||||
{{ message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block section_nav %}{% endblock %}
|
||||
|
||||
{% block hero %}{% endblock %}
|
||||
{% block content %}{% endblock %}
|
||||
{% block content %}
|
||||
{% block messages %}
|
||||
{% if messages %}
|
||||
<ul class="messages">
|
||||
{% for message in messages %}
|
||||
<li {% if message.tags %} class="{{ message.tags }}" {% endif %}>
|
||||
{{ message }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
<div role="complementary">{% block complementary %}{% endblock %}</div>
|
||||
|
||||
|
|
|
@ -56,7 +56,7 @@
|
|||
{% include "includes/summary_item.html" with title='Your contact information' value=request.user.contact contact='true' edit_link=url editable=domain.is_editable %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email != default_security_email%}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=domain.is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=domain.is_editable %}
|
||||
|
|
|
@ -16,10 +16,8 @@
|
|||
<li>There is no limit to the number of domain managers you can add.</li>
|
||||
<li>After adding a domain manager, an email invitation will be sent to that user with
|
||||
instructions on how to set up an account.</li>
|
||||
<li>To remove a domain manager, <a href="{% public_site_url 'contact/' %}"
|
||||
target="_blank" rel="noopener noreferrer" class="usa-link">contact us</a> for
|
||||
assistance.</li>
|
||||
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
|
||||
<li>Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain. Add another domain manager before you remove yourself from this domain.</li>
|
||||
</ul>
|
||||
|
||||
{% if domain.permissions %}
|
||||
|
@ -30,7 +28,8 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th data-sortable scope="col" role="columnheader">Email</th>
|
||||
<th data-sortable scope="col" role="columnheader">Role</th>
|
||||
<th class="grid-col-2" data-sortable scope="col" role="columnheader">Role</th>
|
||||
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -40,6 +39,61 @@
|
|||
{{ permission.user.email }}
|
||||
</th>
|
||||
<td data-label="Role">{{ permission.role|title }}</td>
|
||||
<td>
|
||||
{% if can_delete_users %}
|
||||
<a
|
||||
id="button-toggle-user-alert-{{ forloop.counter }}"
|
||||
href="#toggle-user-alert-{{ forloop.counter }}"
|
||||
class="usa-button--unstyled text-no-underline"
|
||||
aria-controls="toggle-user-alert-{{ forloop.counter }}"
|
||||
data-open-modal
|
||||
aria-disabled="false"
|
||||
>
|
||||
Remove
|
||||
</a>
|
||||
{# Display a custom message if the user is trying to delete themselves #}
|
||||
{% if permission.user.email == current_user_email %}
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-user-alert-{{ forloop.counter }}"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="You will be removed from this domain"
|
||||
data-force-action
|
||||
>
|
||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}">
|
||||
{% with domain_name=domain.name|force_escape %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %}
|
||||
{% endwith %}
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="toggle-user-alert-{{ forloop.counter }}"
|
||||
aria-labelledby="Are you sure you want to continue?"
|
||||
aria-describedby="{{ permission.user.email }} will be removed"
|
||||
data-force-action
|
||||
>
|
||||
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=permission.user.id %}">
|
||||
{% with email=permission.user.email|default:permission.user|force_escape domain_name=domain.name|force_escape %}
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %}
|
||||
{% endwith %}
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<input
|
||||
type="submit"
|
||||
class="usa-button--unstyled disabled-button usa-tooltip"
|
||||
value="Remove"
|
||||
data-position="bottom"
|
||||
title="Domains must have at least one domain manager"
|
||||
data-tooltip="true"
|
||||
aria-disabled="true"
|
||||
role="button"
|
||||
>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
@ -66,8 +120,8 @@
|
|||
<tr>
|
||||
<th data-sortable scope="col" role="columnheader">Email</th>
|
||||
<th data-sortable scope="col" role="columnheader">Date created</th>
|
||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="sr-only">Action</span></th>
|
||||
<th class="grid-col-2" data-sortable scope="col" role="columnheader">Status</th>
|
||||
<th class="grid-col-1" scope="col" role="columnheader"><span class="sr-only">Action</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -78,8 +132,9 @@
|
|||
</th>
|
||||
<td data-sort-value="{{ invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.created_at|date }} </td>
|
||||
<td data-label="Status">{{ invitation.status|title }}</td>
|
||||
<td><form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
|
||||
{% csrf_token %}<input type="submit" class="usa-button--unstyled" value="Cancel">
|
||||
<td>
|
||||
<form method="POST" action="{% url "invitation-delete" pk=invitation.id %}">
|
||||
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline" value="Cancel">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi.
|
||||
|
||||
{{ requester_email }} has added you as a manager on {{ domain.name }}.
|
||||
{{ requestor_email }} has added you as a manager on {{ domain.name }}.
|
||||
|
||||
You can manage this domain on the .gov registrar <https://manage.get.gov>.
|
||||
|
||||
|
|
|
@ -2,9 +2,22 @@ SUMMARY OF YOUR DOMAIN REQUEST
|
|||
|
||||
Type of organization:
|
||||
{{ application.get_organization_type_display }}
|
||||
|
||||
{% if application.show_organization_federal %}
|
||||
Federal government branch:
|
||||
{{ application.get_federal_type_display }}
|
||||
{% elif application.show_tribal_government %}
|
||||
Tribal government:
|
||||
{{ application.tribe_name|default:"Incomplete" }}{% if application.federally_recognized_tribe %}
|
||||
Federally-recognized tribe
|
||||
{% endif %}{% if application.state_recognized_tribe %}
|
||||
State-recognized tribe
|
||||
{% endif %}{% endif %}{% if application.show_organization_election %}
|
||||
Election office:
|
||||
{{ application.is_election_board|yesno:"Yes,No,Incomplete" }}
|
||||
{% endif %}
|
||||
Organization name and mailing address:
|
||||
{% spaceless %}{{ application.organization_name }}
|
||||
{% spaceless %}{{ application.federal_agency }}
|
||||
{{ application.organization_name }}
|
||||
{{ application.address_line1 }}{% if application.address_line2 %}
|
||||
{{ application.address_line2 }}{% endif %}
|
||||
{{ application.city }}, {{ application.state_territory }}
|
||||
|
@ -22,18 +35,21 @@ Current websites: {% for site in application.current_websites.all %}
|
|||
{% endfor %}{% endif %}
|
||||
.gov domain:
|
||||
{{ application.requested_domain.name }}
|
||||
{% if application.alternative_domains.all %}
|
||||
Alternative domains:
|
||||
{% for site in application.alternative_domains.all %}{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||
{% endfor %}
|
||||
{% endfor %}{% endif %}
|
||||
Purpose of your domain:
|
||||
{{ application.purpose }}
|
||||
|
||||
Your contact information:
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.submitter %}{% endspaceless %}
|
||||
{% if application.other_contacts.all %}
|
||||
Other employees from your organization:
|
||||
{% for other in application.other_contacts.all %}
|
||||
|
||||
Other employees from your organization:{% for other in application.other_contacts.all %}
|
||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=other %}{% endspaceless %}
|
||||
{% endfor %}{% endif %}{% if application.anything_else %}
|
||||
{% empty %}
|
||||
{{ application.no_other_contacts_rationale }}
|
||||
{% endfor %}{% if application.anything_else %}
|
||||
Anything else?
|
||||
{{ application.anything_else }}
|
||||
{% endif %}
|
|
@ -10,6 +10,9 @@
|
|||
{# the entire logged in page goes here #}
|
||||
|
||||
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
<h1>Manage your domains</h2>
|
||||
|
||||
<p class="margin-top-4">
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||
<div class="usa-alert__body">
|
||||
{{ message }}
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<div class="usa-alert usa-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||
<b>Attention:</b> You are on a test site.
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,7 @@
|
|||
<address>
|
||||
{% if organization.federal_agency %}
|
||||
{{ organization.federal_agency }}<br />
|
||||
{% endif %}
|
||||
{% if organization.organization_name %}
|
||||
{{ organization.organization_name }}
|
||||
{% endif %}
|
||||
|
|
|
@ -28,17 +28,22 @@
|
|||
{% if value|length == 1 %}
|
||||
{% include "includes/contact.html" with contact=value|first %}
|
||||
{% else %}
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
<li>
|
||||
<p class="text-semibold margin-top-1 margin-bottom-0">
|
||||
Contact {{forloop.counter}}
|
||||
</p>
|
||||
{% include "includes/contact.html" with contact=item %}</li>
|
||||
{% empty %}
|
||||
<li>None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if value %}
|
||||
<dl class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
<dt>
|
||||
Contact {{forloop.counter}}
|
||||
</dt>
|
||||
<dd>
|
||||
{% include "includes/contact.html" with contact=item %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>
|
||||
None
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% include "includes/contact.html" with contact=value %}
|
||||
|
@ -57,10 +62,10 @@
|
|||
{% endspaceless %})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p class="margin-top-0">{{ value | first }} </p>
|
||||
<p class="margin-top-0 margin-bottom-0">{{ value | first }} </p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<ul class="usa-list margin-top-0">
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
{% if users %}
|
||||
<li>{{ item.user.email }}</li>
|
||||
|
|
|
@ -12,6 +12,7 @@ from typing import List, Dict
|
|||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model, login
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
from registrar.models import (
|
||||
Contact,
|
||||
|
@ -526,6 +527,7 @@ def completed_application(
|
|||
has_anything_else=True,
|
||||
status=DomainApplication.ApplicationStatus.STARTED,
|
||||
user=False,
|
||||
submitter=False,
|
||||
name="city.gov",
|
||||
):
|
||||
"""A completed domain application."""
|
||||
|
@ -541,13 +543,14 @@ def completed_application(
|
|||
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
if not submitter:
|
||||
submitter, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
|
@ -567,7 +570,7 @@ def completed_application(
|
|||
zipcode="10002",
|
||||
authorizing_official=ao,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
submitter=submitter,
|
||||
creator=user,
|
||||
status=status,
|
||||
)
|
||||
|
@ -641,7 +644,7 @@ class MockEppLib(TestCase):
|
|||
self,
|
||||
id,
|
||||
email,
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
pw="thisisnotapassword",
|
||||
):
|
||||
fake = info.InfoContactResultData(
|
||||
|
@ -679,7 +682,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
mockDataInfoDomain = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.host.com"],
|
||||
statuses=[
|
||||
|
@ -690,7 +693,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
mockDataExtensionDomain = fakedEppObject(
|
||||
"fakePw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[common.DomainContact(contact="123", type=PublicContact.ContactTypeChoices.SECURITY)],
|
||||
hosts=["fake.host.com"],
|
||||
statuses=[
|
||||
|
@ -704,7 +707,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
InfoDomainWithContacts = fakedEppObject(
|
||||
"fakepw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="securityContact",
|
||||
|
@ -729,7 +732,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
InfoDomainWithDefaultSecurityContact = fakedEppObject(
|
||||
"fakepw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="defaultSec",
|
||||
|
@ -748,7 +751,7 @@ class MockEppLib(TestCase):
|
|||
)
|
||||
InfoDomainWithVerisignSecurityContact = fakedEppObject(
|
||||
"fakepw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="defaultVeri",
|
||||
|
@ -764,7 +767,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
InfoDomainWithDefaultTechnicalContact = fakedEppObject(
|
||||
"fakepw",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="defaultTech",
|
||||
|
@ -789,14 +792,14 @@ class MockEppLib(TestCase):
|
|||
|
||||
infoDomainNoContact = fakedEppObject(
|
||||
"security",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=["fake.host.com"],
|
||||
)
|
||||
|
||||
infoDomainThreeHosts = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.my-nameserver-1.com",
|
||||
|
@ -807,25 +810,25 @@ class MockEppLib(TestCase):
|
|||
|
||||
infoDomainNoHost = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[],
|
||||
)
|
||||
|
||||
infoDomainTwoHosts = fakedEppObject(
|
||||
"my-nameserver.gov",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=["ns1.my-nameserver-1.com", "ns1.my-nameserver-2.com"],
|
||||
)
|
||||
|
||||
mockDataInfoHosts = fakedEppObject(
|
||||
"lastPw",
|
||||
cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)),
|
||||
addrs=[common.Ip(addr="1.2.3.4"), common.Ip(addr="2.3.4.5")],
|
||||
)
|
||||
|
||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=datetime.datetime(2023, 8, 25, 19, 45, 35))
|
||||
mockDataHostChange = fakedEppObject("lastPw", cr_date=make_aware(datetime.datetime(2023, 8, 25, 19, 45, 35)))
|
||||
addDsData1 = {
|
||||
"keyTag": 1234,
|
||||
"alg": 3,
|
||||
|
@ -857,7 +860,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
infoDomainHasIP = fakedEppObject(
|
||||
"nameserverwithip.gov",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="securityContact",
|
||||
|
@ -882,7 +885,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
justNameserver = fakedEppObject(
|
||||
"justnameserver.com",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[
|
||||
common.DomainContact(
|
||||
contact="securityContact",
|
||||
|
@ -905,7 +908,7 @@ class MockEppLib(TestCase):
|
|||
|
||||
infoDomainCheckHostIPCombo = fakedEppObject(
|
||||
"nameserversubdomain.gov",
|
||||
cr_date=datetime.datetime(2023, 5, 25, 19, 45, 35),
|
||||
cr_date=make_aware(datetime.datetime(2023, 5, 25, 19, 45, 35)),
|
||||
contacts=[],
|
||||
hosts=[
|
||||
"ns1.nameserversubdomain.gov",
|
||||
|
|
|
@ -14,11 +14,11 @@ from registrar.admin import (
|
|||
ContactAdmin,
|
||||
DomainInformationAdmin,
|
||||
UserDomainRoleAdmin,
|
||||
VeryImportantPersonAdmin,
|
||||
VerifiedByStaffAdmin,
|
||||
)
|
||||
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.very_important_person import VeryImportantPerson
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
from .common import (
|
||||
MockSESClient,
|
||||
AuditedAdminMockData,
|
||||
|
@ -59,22 +59,22 @@ class TestDomainAdmin(MockEppLib):
|
|||
"""
|
||||
Make sure the short name is displaying in admin on the list page
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
with less_console_noise():
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
application.approve()
|
||||
|
||||
response = self.client.get("/admin/registrar/domain/")
|
||||
response = self.client.get("/admin/registrar/domain/")
|
||||
|
||||
# There are 3 template references to Federal (3) plus one reference in the table
|
||||
# for our actual application
|
||||
self.assertContains(response, "Federal", count=4)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
||||
# Now let's make sure the long description does not exist
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
# There are 3 template references to Federal (3) plus one reference in the table
|
||||
# for our actual application
|
||||
self.assertContains(response, "Federal", count=4)
|
||||
# This may be a bit more robust
|
||||
self.assertContains(response, '<td class="field-organization_type">Federal</td>', count=1)
|
||||
# Now let's make sure the long description does not exist
|
||||
self.assertNotContains(response, "Federal: an agency of the U.S. government")
|
||||
|
||||
@skip("Why did this test stop working, and is is a good test")
|
||||
def test_place_and_remove_hold(self):
|
||||
|
@ -120,40 +120,37 @@ class TestDomainAdmin(MockEppLib):
|
|||
Then a user-friendly success message is returned for displaying on the web
|
||||
And `state` is et to `DELETED`
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
with less_console_noise():
|
||||
domain = create_ready_domain()
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
def test_deletion_ready_fsm_failure(self):
|
||||
"""
|
||||
|
@ -162,38 +159,36 @@ class TestDomainAdmin(MockEppLib):
|
|||
Then a user-friendly error message is returned for displaying on the web
|
||||
And `state` is not set to `DELETED`
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
|
||||
# Test the error
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"Error deleting this Domain: "
|
||||
"Can't switch from state 'ready' to 'deleted'"
|
||||
", must be either 'dns_needed' or 'on_hold'",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
with less_console_noise():
|
||||
domain = create_ready_domain()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
# Test the error
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.ERROR,
|
||||
"Error deleting this Domain: "
|
||||
"Can't switch from state 'ready' to 'deleted'"
|
||||
", must be either 'dns_needed' or 'on_hold'",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.READY)
|
||||
|
||||
|
@ -205,62 +200,57 @@ class TestDomainAdmin(MockEppLib):
|
|||
Then `commands.DeleteDomain` is sent to the registry
|
||||
And Domain returns normally without an error dialog
|
||||
"""
|
||||
domain = create_ready_domain()
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
|
||||
# Delete it once
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
with less_console_noise():
|
||||
domain = create_ready_domain()
|
||||
# Put in client hold
|
||||
domain.place_client_hold()
|
||||
p = "userpass"
|
||||
self.client.login(username="staffuser", password=p)
|
||||
# Ensure everything is displaying correctly
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
# Try to delete it again
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"This domain is already deleted",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, domain.name)
|
||||
self.assertContains(response, "Remove from registry")
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
# Delete it once
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"Domain city.gov has been deleted. Thanks!",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
# Try to delete it again
|
||||
# Test the info dialog
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domain/{}/change/".format(domain.pk),
|
||||
{"_delete_domain": "Remove from registry", "name": domain.name},
|
||||
follow=True,
|
||||
)
|
||||
request.user = self.client
|
||||
with patch("django.contrib.messages.add_message") as mock_add_message:
|
||||
self.admin.do_delete_domain(request, domain)
|
||||
mock_add_message.assert_called_once_with(
|
||||
request,
|
||||
messages.INFO,
|
||||
"This domain is already deleted",
|
||||
extra_tags="",
|
||||
fail_silently=False,
|
||||
)
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
@skip("Waiting on epp lib to implement")
|
||||
def test_place_and_remove_hold_epp(self):
|
||||
|
@ -669,6 +659,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
"submission_date",
|
||||
"notes",
|
||||
"current_websites",
|
||||
"other_contacts",
|
||||
"alternative_domains",
|
||||
|
@ -686,6 +677,7 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
"creator",
|
||||
"about_your_organization",
|
||||
"requested_domain",
|
||||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"submitter",
|
||||
|
@ -759,33 +751,13 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
||||
# Create a request object with a superuser
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
request.user = self.superuser
|
||||
def trigger_saving_approved_to_another_state(self, domain_is_active, another_state):
|
||||
"""Helper method that triggers domain request state changes from approved to another state,
|
||||
with an associated domain that can be either active (READY) or not.
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return True # Override to return True
|
||||
Used to test errors when saving a change with an active domain, also used to test side effects
|
||||
when saving a change goes through."""
|
||||
|
||||
# Use ExitStack to combine patch contexts
|
||||
with ExitStack() as stack:
|
||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
||||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
messages.error.assert_called_once_with(
|
||||
request,
|
||||
"This action is not permitted. The domain " + "is already active.",
|
||||
)
|
||||
|
||||
def test_side_effects_when_saving_approved_to_rejected(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
|
@ -799,101 +771,60 @@ class TestDomainApplicationAdmin(MockEppLib):
|
|||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return False # Override to return False
|
||||
return domain_is_active # Override to return True
|
||||
|
||||
# Use ExitStack to combine patch contexts
|
||||
with ExitStack() as stack:
|
||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
||||
stack.enter_context(patch.object(messages, "error"))
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.ApplicationStatus.REJECTED
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was never called
|
||||
messages.error.assert_not_called()
|
||||
application.status = another_state
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
self.assertEqual(application.approved_domain, None)
|
||||
# Assert that the error message was called with the correct argument
|
||||
if domain_is_active:
|
||||
messages.error.assert_called_once_with(
|
||||
request,
|
||||
"This action is not permitted. The domain " + "is already active.",
|
||||
)
|
||||
else:
|
||||
# Assert that the error message was never called
|
||||
messages.error.assert_not_called()
|
||||
|
||||
# Assert that Domain got Deleted
|
||||
with self.assertRaises(Domain.DoesNotExist):
|
||||
domain.refresh_from_db()
|
||||
self.assertEqual(application.approved_domain, None)
|
||||
|
||||
# Assert that DomainInformation got Deleted
|
||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||
domain_information.refresh_from_db()
|
||||
# Assert that Domain got Deleted
|
||||
with self.assertRaises(Domain.DoesNotExist):
|
||||
domain.refresh_from_db()
|
||||
|
||||
# Assert that DomainInformation got Deleted
|
||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||
domain_information.refresh_from_db()
|
||||
|
||||
def test_error_when_saving_approved_to_in_review_and_domain_is_active(self):
|
||||
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
def test_error_when_saving_approved_to_action_needed_and_domain_is_active(self):
|
||||
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
def test_error_when_saving_approved_to_rejected_and_domain_is_active(self):
|
||||
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
def test_error_when_saving_approved_to_ineligible_and_domain_is_active(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
self.trigger_saving_approved_to_another_state(True, DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
# Create a request object with a superuser
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
request.user = self.superuser
|
||||
def test_side_effects_when_saving_approved_to_in_review(self):
|
||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.IN_REVIEW)
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return True # Override to return True
|
||||
def test_side_effects_when_saving_approved_to_action_needed(self):
|
||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.ACTION_NEEDED)
|
||||
|
||||
# Use ExitStack to combine patch contexts
|
||||
with ExitStack() as stack:
|
||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
||||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
messages.error.assert_called_once_with(
|
||||
request,
|
||||
"This action is not permitted. The domain " + "is already active.",
|
||||
)
|
||||
def test_side_effects_when_saving_approved_to_rejected(self):
|
||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.REJECTED)
|
||||
|
||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||
# Create an instance of the model
|
||||
application = completed_application(status=DomainApplication.ApplicationStatus.APPROVED)
|
||||
domain = Domain.objects.create(name=application.requested_domain.name)
|
||||
domain_information = DomainInformation.objects.create(creator=self.superuser, domain=domain)
|
||||
application.approved_domain = domain
|
||||
application.save()
|
||||
|
||||
# Create a request object with a superuser
|
||||
request = self.factory.post("/admin/registrar/domainapplication/{}/change/".format(application.pk))
|
||||
request.user = self.superuser
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return False # Override to return False
|
||||
|
||||
# Use ExitStack to combine patch contexts
|
||||
with ExitStack() as stack:
|
||||
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||
stack.enter_context(patch.object(Domain, "is_active", custom_is_active))
|
||||
stack.enter_context(patch.object(messages, "error"))
|
||||
|
||||
# Simulate saving the model
|
||||
application.status = DomainApplication.ApplicationStatus.INELIGIBLE
|
||||
self.admin.save_model(request, application, None, True)
|
||||
|
||||
# Assert that the error message was never called
|
||||
messages.error.assert_not_called()
|
||||
|
||||
self.assertEqual(application.approved_domain, None)
|
||||
|
||||
# Assert that Domain got Deleted
|
||||
with self.assertRaises(Domain.DoesNotExist):
|
||||
domain.refresh_from_db()
|
||||
|
||||
# Assert that DomainInformation got Deleted
|
||||
with self.assertRaises(DomainInformation.DoesNotExist):
|
||||
domain_information.refresh_from_db()
|
||||
self.trigger_saving_approved_to_another_state(False, DomainApplication.ApplicationStatus.INELIGIBLE)
|
||||
|
||||
def test_has_correct_filters(self):
|
||||
"""
|
||||
|
@ -1110,7 +1041,7 @@ class DomainInvitationAdminTest(TestCase):
|
|||
self.assertContains(response, retrieved_html, count=1)
|
||||
|
||||
|
||||
class DomainInformationAdminTest(TestCase):
|
||||
class TestDomainInformationAdmin(TestCase):
|
||||
def setUp(self):
|
||||
"""Setup environment for a mock admin user"""
|
||||
self.site = AdminSite()
|
||||
|
@ -1118,6 +1049,7 @@ class DomainInformationAdminTest(TestCase):
|
|||
self.admin = DomainInformationAdmin(model=DomainInformation, admin_site=self.site)
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.superuser = create_superuser()
|
||||
self.staffuser = create_user()
|
||||
self.mock_data_generator = AuditedAdminMockData()
|
||||
|
||||
self.test_helper = GenericTestHelper(
|
||||
|
@ -1161,6 +1093,27 @@ class DomainInformationAdminTest(TestCase):
|
|||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
def test_readonly_fields_for_analyst(self):
|
||||
"""Ensures that analysts have their permissions setup correctly"""
|
||||
request = self.factory.get("/")
|
||||
request.user = self.staffuser
|
||||
|
||||
readonly_fields = self.admin.get_readonly_fields(request)
|
||||
|
||||
expected_fields = [
|
||||
"creator",
|
||||
"type_of_work",
|
||||
"more_organization_information",
|
||||
"domain",
|
||||
"domain_application",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
]
|
||||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_domain_sortable(self):
|
||||
"""Tests if DomainInformation sorts by domain correctly"""
|
||||
p = "adminpass"
|
||||
|
@ -1325,64 +1278,62 @@ class ListHeaderAdminTest(TestCase):
|
|||
self.superuser = create_superuser()
|
||||
|
||||
def test_changelist_view(self):
|
||||
# Have to get creative to get past linter
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
||||
# Mock a user
|
||||
user = mock_user()
|
||||
|
||||
# Make the request using the Client class
|
||||
# which handles CSRF
|
||||
# Follow=True handles the redirect
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainapplication/",
|
||||
{
|
||||
"status__exact": "started",
|
||||
"investigator__id__exact": user.id,
|
||||
"q": "Hello",
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
|
||||
# Assert that the filters and search_query are added to the extra_context
|
||||
self.assertIn("filters", response.context)
|
||||
self.assertIn("search_query", response.context)
|
||||
# Assert the content of filters and search_query
|
||||
filters = response.context["filters"]
|
||||
search_query = response.context["search_query"]
|
||||
self.assertEqual(search_query, "Hello")
|
||||
self.assertEqual(
|
||||
filters,
|
||||
[
|
||||
{"parameter_name": "status", "parameter_value": "started"},
|
||||
with less_console_noise():
|
||||
# Have to get creative to get past linter
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
# Mock a user
|
||||
user = mock_user()
|
||||
# Make the request using the Client class
|
||||
# which handles CSRF
|
||||
# Follow=True handles the redirect
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainapplication/",
|
||||
{
|
||||
"parameter_name": "investigator",
|
||||
"parameter_value": user.first_name + " " + user.last_name,
|
||||
"status__exact": "started",
|
||||
"investigator__id__exact": user.id,
|
||||
"q": "Hello",
|
||||
},
|
||||
],
|
||||
)
|
||||
follow=True,
|
||||
)
|
||||
# Assert that the filters and search_query are added to the extra_context
|
||||
self.assertIn("filters", response.context)
|
||||
self.assertIn("search_query", response.context)
|
||||
# Assert the content of filters and search_query
|
||||
filters = response.context["filters"]
|
||||
search_query = response.context["search_query"]
|
||||
self.assertEqual(search_query, "Hello")
|
||||
self.assertEqual(
|
||||
filters,
|
||||
[
|
||||
{"parameter_name": "status", "parameter_value": "started"},
|
||||
{
|
||||
"parameter_name": "investigator",
|
||||
"parameter_value": user.first_name + " " + user.last_name,
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_get_filters(self):
|
||||
# Create a mock request object
|
||||
request = self.factory.get("/admin/yourmodel/")
|
||||
# Set the GET parameters for testing
|
||||
request.GET = {
|
||||
"status": "started",
|
||||
"investigator": "Jeff Lebowski",
|
||||
"q": "search_value",
|
||||
}
|
||||
# Call the get_filters method
|
||||
filters = self.admin.get_filters(request)
|
||||
|
||||
# Assert the filters extracted from the request GET
|
||||
self.assertEqual(
|
||||
filters,
|
||||
[
|
||||
{"parameter_name": "status", "parameter_value": "started"},
|
||||
{"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"},
|
||||
],
|
||||
)
|
||||
with less_console_noise():
|
||||
# Create a mock request object
|
||||
request = self.factory.get("/admin/yourmodel/")
|
||||
# Set the GET parameters for testing
|
||||
request.GET = {
|
||||
"status": "started",
|
||||
"investigator": "Jeff Lebowski",
|
||||
"q": "search_value",
|
||||
}
|
||||
# Call the get_filters method
|
||||
filters = self.admin.get_filters(request)
|
||||
# Assert the filters extracted from the request GET
|
||||
self.assertEqual(
|
||||
filters,
|
||||
[
|
||||
{"parameter_name": "status", "parameter_value": "started"},
|
||||
{"parameter_name": "investigator", "parameter_value": "Jeff Lebowski"},
|
||||
],
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
# delete any applications too
|
||||
|
@ -1781,11 +1732,86 @@ class ContactAdminTest(TestCase):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_less(self):
|
||||
"""Create a contact, join it to 4 domain requests. The 5th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins."""
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the model
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
|
||||
# join it to 4 domain requests. The 5th join will be a user.
|
||||
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 5th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to User: <a href='/admin/registrar/"
|
||||
f"user/{self.staffuser.pk}/change/'>staff@example.com</a></li>"
|
||||
"</ul>",
|
||||
)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_more(self):
|
||||
"""Create a contact, join it to 5 domain requests. The 6th join will be a user.
|
||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
# Create an instance of the model
|
||||
# join it to 5 domain requests. The 6th join will be a user.
|
||||
contact, _ = Contact.objects.get_or_create(user=self.staffuser)
|
||||
application1 = completed_application(submitter=contact, name="city1.gov")
|
||||
application2 = completed_application(submitter=contact, name="city2.gov")
|
||||
application3 = completed_application(submitter=contact, name="city3.gov")
|
||||
application4 = completed_application(submitter=contact, name="city4.gov")
|
||||
application5 = completed_application(submitter=contact, name="city5.gov")
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
logger.debug(mock_warning)
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 6th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to DomainApplication: <a href='/admin/registrar/"
|
||||
f"domainapplication/{application5.pk}/change/'>city5.gov</a></li>"
|
||||
"</ul>"
|
||||
"<p class='font-sans-3xs'>And 1 more...</p>",
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
DomainApplication.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
|
||||
class VeryImportantPersonAdminTestCase(TestCase):
|
||||
class VerifiedByStaffAdminTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.superuser = create_superuser()
|
||||
self.factory = RequestFactory()
|
||||
|
@ -1794,13 +1820,13 @@ class VeryImportantPersonAdminTestCase(TestCase):
|
|||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the admin class
|
||||
admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None)
|
||||
admin_instance = VerifiedByStaffAdmin(model=VerifiedByStaff, admin_site=None)
|
||||
|
||||
# Create a VeryImportantPerson instance
|
||||
vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes")
|
||||
# Create a VerifiedByStaff instance
|
||||
vip_instance = VerifiedByStaff(email="test@example.com", notes="Test Notes")
|
||||
|
||||
# Create a request object
|
||||
request = self.factory.post("/admin/yourapp/veryimportantperson/add/")
|
||||
request = self.factory.post("/admin/yourapp/VerifiedByStaff/add/")
|
||||
request.user = self.superuser
|
||||
|
||||
# Call the save_model method
|
||||
|
|
|
@ -102,9 +102,9 @@ class TestEmails(TestCase):
|
|||
application.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertNotIn("Other employees from your organization:", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5556\n\nAnything else")
|
||||
self.assertRegex(body, r"5556\n\nOther employees")
|
||||
self.assertRegex(body, r"None\n\nAnything else")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_alternative_govdomain_spacing(self):
|
||||
|
@ -117,7 +117,7 @@ class TestEmails(TestCase):
|
|||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("city1.gov", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"city.gov\ncity1.gov\n\nPurpose of your domain:")
|
||||
self.assertRegex(body, r"city.gov\n\nAlternative domains:\ncity1.gov\n\nPurpose of your domain:")
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation_no_alternative_govdomain_spacing(self):
|
||||
|
|
31
src/registrar/tests/test_environment_variables_effects.py
Normal file
31
src/registrar/tests/test_environment_variables_effects.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django.test import Client, TestCase, override_settings
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
|
||||
class MyTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.user.delete()
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_production_environment(self):
|
||||
"""No banner on prod."""
|
||||
home_page = self.client.get("/")
|
||||
self.assertNotContains(home_page, "You are on a test site.")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
def test_non_production_environment(self):
|
||||
"""Banner on non-prod."""
|
||||
home_page = self.client.get("/")
|
||||
self.assertContains(home_page, "You are on a test site.")
|
|
@ -64,6 +64,12 @@ class TestFormValidation(MockEppLib):
|
|||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
|
||||
def test_requested_domain_starting_www(self):
|
||||
"""Test a valid domain name with .www at the beginning."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "www.top-level-agency"})
|
||||
self.assertEqual(len(form.errors), 0)
|
||||
self.assertEqual(form.cleaned_data["requested_domain"], "top-level-agency")
|
||||
|
||||
def test_requested_domain_ending_dotgov(self):
|
||||
"""Just a valid domain name with .gov at the end."""
|
||||
form = DotGovDomainForm(data={"requested_domain": "top-level-agency.gov"})
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import copy
|
||||
import datetime
|
||||
from datetime import date, datetime, time
|
||||
from django.utils import timezone
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
|
@ -17,7 +18,7 @@ from django.core.management import call_command
|
|||
from unittest.mock import patch, call
|
||||
from epplibwrapper import commands, common
|
||||
|
||||
from .common import MockEppLib
|
||||
from .common import MockEppLib, less_console_noise
|
||||
|
||||
|
||||
class TestPopulateFirstReady(TestCase):
|
||||
|
@ -33,7 +34,9 @@ class TestPopulateFirstReady(TestCase):
|
|||
self.unknown_domain, _ = Domain.objects.get_or_create(name="fakeunknown.gov", state=Domain.State.UNKNOWN)
|
||||
|
||||
# Set a ready_at date for testing purposes
|
||||
self.ready_at_date = datetime.date(2022, 12, 31)
|
||||
self.ready_at_date = date(2022, 12, 31)
|
||||
_ready_at_datetime = datetime.combine(self.ready_at_date, time.min)
|
||||
self.ready_at_date_tz_aware = timezone.make_aware(_ready_at_datetime, timezone=timezone.utc)
|
||||
|
||||
def tearDown(self):
|
||||
"""Deletes all DB objects related to migrations"""
|
||||
|
@ -49,122 +52,103 @@ class TestPopulateFirstReady(TestCase):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the populate_first_ready command with the specified arguments.
|
||||
"""
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("populate_first_ready")
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("populate_first_ready")
|
||||
|
||||
def test_populate_first_ready_state_ready(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'ready'
|
||||
"""
|
||||
# Set the created at date
|
||||
self.ready_domain.created_at = self.ready_at_date
|
||||
self.ready_domain.save()
|
||||
|
||||
desired_domain = copy.deepcopy(self.ready_domain)
|
||||
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
|
||||
self.assertEqual(desired_domain, self.ready_domain)
|
||||
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.ready_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.ready_domain.save()
|
||||
desired_domain = copy.deepcopy(self.ready_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.ready_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeready.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
def test_populate_first_ready_state_deleted(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'deleted'
|
||||
"""
|
||||
# Set the created at date
|
||||
self.deleted_domain.created_at = self.ready_at_date
|
||||
self.deleted_domain.save()
|
||||
|
||||
desired_domain = copy.deepcopy(self.deleted_domain)
|
||||
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
|
||||
self.assertEqual(desired_domain, self.deleted_domain)
|
||||
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.deleted_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.deleted_domain.save()
|
||||
desired_domain = copy.deepcopy(self.deleted_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
self.assertEqual(desired_domain, self.deleted_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakedeleted.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
def test_populate_first_ready_state_dns_needed(self):
|
||||
"""
|
||||
Tests that the populate_first_ready doesn't make changes when a domain's state is 'dns_needed'
|
||||
"""
|
||||
# Set the created at date
|
||||
self.dns_needed_domain.created_at = self.ready_at_date
|
||||
self.dns_needed_domain.save()
|
||||
|
||||
desired_domain = copy.deepcopy(self.dns_needed_domain)
|
||||
|
||||
desired_domain.first_ready = None
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
|
||||
current_domain = self.dns_needed_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
|
||||
|
||||
# Explicitly test the first_ready date
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.dns_needed_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.dns_needed_domain.save()
|
||||
desired_domain = copy.deepcopy(self.dns_needed_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.dns_needed_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
first_ready = Domain.objects.filter(name="fakedns.gov").get().first_ready
|
||||
# Explicitly test the first_ready date
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
|
||||
def test_populate_first_ready_state_on_hold(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'on_hold'
|
||||
"""
|
||||
self.hold_domain.created_at = self.ready_at_date
|
||||
self.hold_domain.save()
|
||||
|
||||
desired_domain = copy.deepcopy(self.hold_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
|
||||
# Run the update first ready_at script
|
||||
self.run_populate_first_ready()
|
||||
|
||||
current_domain = self.hold_domain
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
with less_console_noise():
|
||||
self.hold_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.hold_domain.save()
|
||||
desired_domain = copy.deepcopy(self.hold_domain)
|
||||
desired_domain.first_ready = self.ready_at_date
|
||||
# Run the update first ready_at script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.hold_domain
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakehold.gov").get().first_ready
|
||||
self.assertEqual(first_ready, self.ready_at_date)
|
||||
|
||||
def test_populate_first_ready_state_unknown(self):
|
||||
"""
|
||||
Tests that the populate_first_ready works as expected for the state 'unknown'
|
||||
"""
|
||||
# Set the created at date
|
||||
self.unknown_domain.created_at = self.ready_at_date
|
||||
self.unknown_domain.save()
|
||||
|
||||
desired_domain = copy.deepcopy(self.unknown_domain)
|
||||
desired_domain.first_ready = None
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
|
||||
current_domain = self.unknown_domain
|
||||
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
with less_console_noise():
|
||||
# Set the created at date
|
||||
self.unknown_domain.created_at = self.ready_at_date_tz_aware
|
||||
self.unknown_domain.save()
|
||||
desired_domain = copy.deepcopy(self.unknown_domain)
|
||||
desired_domain.first_ready = None
|
||||
# Run the expiration date script
|
||||
self.run_populate_first_ready()
|
||||
current_domain = self.unknown_domain
|
||||
# The object should largely be unaltered (does not test first_ready)
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the first_ready date
|
||||
first_ready = Domain.objects.filter(name="fakeunknown.gov").get().first_ready
|
||||
self.assertNotEqual(first_ready, self.ready_at_date)
|
||||
self.assertEqual(first_ready, None)
|
||||
|
||||
|
||||
class TestPatchAgencyInfo(TestCase):
|
||||
|
@ -185,7 +169,8 @@ class TestPatchAgencyInfo(TestCase):
|
|||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", return_value=True)
|
||||
def call_patch_federal_agency_info(self, mock_prompt):
|
||||
"""Calls the patch_federal_agency_info command and mimics a keypress"""
|
||||
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
|
||||
with less_console_noise():
|
||||
call_command("patch_federal_agency_info", "registrar/tests/data/fake_current_full.csv", debug=True)
|
||||
|
||||
def test_patch_agency_info(self):
|
||||
"""
|
||||
|
@ -194,17 +179,14 @@ class TestPatchAgencyInfo(TestCase):
|
|||
of a `DomainInformation` object when the corresponding
|
||||
`TransitionDomain` object has a valid `federal_agency`.
|
||||
"""
|
||||
|
||||
# Ensure that the federal_agency is None
|
||||
self.assertEqual(self.domain_info.federal_agency, None)
|
||||
|
||||
self.call_patch_federal_agency_info()
|
||||
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
|
||||
# Check that the federal_agency field was updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "test agency")
|
||||
with less_console_noise():
|
||||
# Ensure that the federal_agency is None
|
||||
self.assertEqual(self.domain_info.federal_agency, None)
|
||||
self.call_patch_federal_agency_info()
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
# Check that the federal_agency field was updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "test agency")
|
||||
|
||||
def test_patch_agency_info_skip(self):
|
||||
"""
|
||||
|
@ -213,21 +195,18 @@ class TestPatchAgencyInfo(TestCase):
|
|||
of a `DomainInformation` object when the corresponding
|
||||
`TransitionDomain` object does not exist.
|
||||
"""
|
||||
# Set federal_agency to None to simulate a skip
|
||||
self.transition_domain.federal_agency = None
|
||||
self.transition_domain.save()
|
||||
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
|
||||
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertIsNone(self.domain_info.federal_agency)
|
||||
with less_console_noise():
|
||||
# Set federal_agency to None to simulate a skip
|
||||
self.transition_domain.federal_agency = None
|
||||
self.transition_domain.save()
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertIsNone(self.domain_info.federal_agency)
|
||||
|
||||
def test_patch_agency_info_skip_updates_data(self):
|
||||
"""
|
||||
|
@ -235,25 +214,21 @@ class TestPatchAgencyInfo(TestCase):
|
|||
updates the DomainInformation object, because a record exists in the
|
||||
provided current-full.csv file.
|
||||
"""
|
||||
# Set federal_agency to None to simulate a skip
|
||||
self.transition_domain.federal_agency = None
|
||||
self.transition_domain.save()
|
||||
|
||||
# Change the domain name to something parsable in the .csv
|
||||
self.domain.name = "cdomain1.gov"
|
||||
self.domain.save()
|
||||
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
|
||||
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
|
||||
with less_console_noise():
|
||||
# Set federal_agency to None to simulate a skip
|
||||
self.transition_domain.federal_agency = None
|
||||
self.transition_domain.save()
|
||||
# Change the domain name to something parsable in the .csv
|
||||
self.domain.name = "cdomain1.gov"
|
||||
self.domain.save()
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="WARNING") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("SOME AGENCY DATA WAS NONE", context.output[0])
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "World War I Centennial Commission")
|
||||
|
||||
def test_patch_agency_info_skips_valid_domains(self):
|
||||
"""
|
||||
|
@ -261,20 +236,17 @@ class TestPatchAgencyInfo(TestCase):
|
|||
does not update the `federal_agency` field
|
||||
of a `DomainInformation` object
|
||||
"""
|
||||
self.domain_info.federal_agency = "unchanged"
|
||||
self.domain_info.save()
|
||||
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("FINISHED", context.output[1])
|
||||
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "unchanged")
|
||||
with less_console_noise():
|
||||
self.domain_info.federal_agency = "unchanged"
|
||||
self.domain_info.save()
|
||||
with self.assertLogs("registrar.management.commands.patch_federal_agency_info", level="INFO") as context:
|
||||
self.call_patch_federal_agency_info()
|
||||
# Check that the correct log message was output
|
||||
self.assertIn("FINISHED", context.output[1])
|
||||
# Reload the domain_info object from the database
|
||||
self.domain_info.refresh_from_db()
|
||||
# Check that the federal_agency field was not updated
|
||||
self.assertEqual(self.domain_info.federal_agency, "unchanged")
|
||||
|
||||
|
||||
class TestExtendExpirationDates(MockEppLib):
|
||||
|
@ -283,39 +255,37 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
super().setUp()
|
||||
# Create a valid domain that is updatable
|
||||
Domain.objects.get_or_create(
|
||||
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=datetime.date(2023, 11, 15)
|
||||
name="waterbutpurple.gov", state=Domain.State.READY, expiration_date=date(2023, 11, 15)
|
||||
)
|
||||
TransitionDomain.objects.get_or_create(
|
||||
username="testytester@mail.com",
|
||||
domain_name="waterbutpurple.gov",
|
||||
epp_expiration_date=datetime.date(2023, 11, 15),
|
||||
epp_expiration_date=date(2023, 11, 15),
|
||||
)
|
||||
# Create a domain with an invalid expiration date
|
||||
Domain.objects.get_or_create(
|
||||
name="fake.gov", state=Domain.State.READY, expiration_date=datetime.date(2022, 5, 25)
|
||||
)
|
||||
Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY, expiration_date=date(2022, 5, 25))
|
||||
TransitionDomain.objects.get_or_create(
|
||||
username="themoonisactuallycheese@mail.com",
|
||||
domain_name="fake.gov",
|
||||
epp_expiration_date=datetime.date(2022, 5, 25),
|
||||
epp_expiration_date=date(2022, 5, 25),
|
||||
)
|
||||
# Create a domain with an invalid state
|
||||
Domain.objects.get_or_create(
|
||||
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=datetime.date(2023, 11, 15)
|
||||
name="fakeneeded.gov", state=Domain.State.DNS_NEEDED, expiration_date=date(2023, 11, 15)
|
||||
)
|
||||
TransitionDomain.objects.get_or_create(
|
||||
username="fakeneeded@mail.com",
|
||||
domain_name="fakeneeded.gov",
|
||||
epp_expiration_date=datetime.date(2023, 11, 15),
|
||||
epp_expiration_date=date(2023, 11, 15),
|
||||
)
|
||||
# Create a domain with a date greater than the maximum
|
||||
Domain.objects.get_or_create(
|
||||
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=datetime.date(2024, 12, 31)
|
||||
name="fakemaximum.gov", state=Domain.State.READY, expiration_date=date(2024, 12, 31)
|
||||
)
|
||||
TransitionDomain.objects.get_or_create(
|
||||
username="fakemaximum@mail.com",
|
||||
domain_name="fakemaximum.gov",
|
||||
epp_expiration_date=datetime.date(2024, 12, 31),
|
||||
epp_expiration_date=date(2024, 12, 31),
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -338,83 +308,82 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the extend_expiration_dates command with the specified arguments.
|
||||
"""
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("extend_expiration_dates")
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("extend_expiration_dates")
|
||||
|
||||
def test_extends_expiration_date_correctly(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method extends dates as expected
|
||||
"""
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = datetime.date(2024, 11, 15)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 11, 15))
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 11, 15))
|
||||
|
||||
def test_extends_expiration_date_skips_non_current(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains
|
||||
with an expiration date less than a certain threshold.
|
||||
"""
|
||||
desired_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
desired_domain.expiration_date = datetime.date(2022, 5, 25)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, datetime.date(2022, 5, 25))
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
desired_domain.expiration_date = date(2022, 5, 25)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="fake.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2022, 5, 25))
|
||||
|
||||
def test_extends_expiration_date_skips_maximum_date(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains
|
||||
with an expiration date more than a certain threshold.
|
||||
"""
|
||||
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
desired_domain.expiration_date = datetime.date(2024, 12, 31)
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 12, 31)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
current_domain = Domain.objects.filter(name="fakemaximum.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, datetime.date(2024, 12, 31))
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2024, 12, 31))
|
||||
|
||||
def test_extends_expiration_date_skips_non_ready(self):
|
||||
"""
|
||||
Tests that the extend_expiration_dates method correctly skips domains not in the state "ready"
|
||||
"""
|
||||
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
desired_domain.expiration_date = datetime.date(2023, 11, 15)
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
desired_domain.expiration_date = date(2023, 11, 15)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
current_domain = Domain.objects.filter(name="fakeneeded.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, datetime.date(2023, 11, 15))
|
||||
# Explicitly test the expiration date. The extend_expiration_dates script
|
||||
# will skip all dates less than date(2023, 11, 15), meaning that this domain
|
||||
# should not be affected by the change.
|
||||
self.assertEqual(current_domain.expiration_date, date(2023, 11, 15))
|
||||
|
||||
def test_extends_expiration_date_idempotent(self):
|
||||
"""
|
||||
|
@ -423,26 +392,21 @@ class TestExtendExpirationDates(MockEppLib):
|
|||
Verifies that running the method multiple times does not change the expiration date
|
||||
of a domain beyond the initial extension.
|
||||
"""
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = datetime.date(2024, 11, 15)
|
||||
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
|
||||
|
||||
# Run the expiration date script again
|
||||
self.run_extend_expiration_dates()
|
||||
|
||||
# The old domain shouldn't have changed
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
|
||||
# Explicitly test the expiration date - should be the same
|
||||
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
|
||||
with less_console_noise():
|
||||
desired_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
desired_domain.expiration_date = date(2024, 11, 15)
|
||||
# Run the expiration date script
|
||||
self.run_extend_expiration_dates()
|
||||
current_domain = Domain.objects.filter(name="waterbutpurple.gov").get()
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
# Run the expiration date script again
|
||||
self.run_extend_expiration_dates()
|
||||
# The old domain shouldn't have changed
|
||||
self.assertEqual(desired_domain, current_domain)
|
||||
# Explicitly test the expiration date - should be the same
|
||||
self.assertEqual(desired_domain.expiration_date, date(2024, 11, 15))
|
||||
|
||||
|
||||
class TestDiscloseEmails(MockEppLib):
|
||||
|
@ -461,39 +425,41 @@ class TestDiscloseEmails(MockEppLib):
|
|||
The 'call_command' function from Django's management framework is then used to
|
||||
execute the disclose_security_emails command.
|
||||
"""
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("disclose_security_emails")
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("disclose_security_emails")
|
||||
|
||||
def test_disclose_security_emails(self):
|
||||
"""
|
||||
Tests that command disclose_security_emails runs successfully with
|
||||
appropriate EPP calll to UpdateContact.
|
||||
"""
|
||||
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
|
||||
expectedSecContact = PublicContact.get_default_security()
|
||||
expectedSecContact.domain = domain
|
||||
expectedSecContact.email = "123@mail.gov"
|
||||
# set domain security email to 123@mail.gov instead of default email
|
||||
domain.security_contact = expectedSecContact
|
||||
self.run_disclose_security_emails()
|
||||
with less_console_noise():
|
||||
domain, _ = Domain.objects.get_or_create(name="testdisclose.gov", state=Domain.State.READY)
|
||||
expectedSecContact = PublicContact.get_default_security()
|
||||
expectedSecContact.domain = domain
|
||||
expectedSecContact.email = "123@mail.gov"
|
||||
# set domain security email to 123@mail.gov instead of default email
|
||||
domain.security_contact = expectedSecContact
|
||||
self.run_disclose_security_emails()
|
||||
|
||||
# running disclose_security_emails sends EPP call UpdateContact with disclose
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateContact(
|
||||
id=domain.security_contact.registry_id,
|
||||
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
|
||||
email=domain.security_contact.email,
|
||||
voice=domain.security_contact.voice,
|
||||
fax=domain.security_contact.fax,
|
||||
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
disclose=domain._disclose_fields(contact=domain.security_contact),
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
# running disclose_security_emails sends EPP call UpdateContact with disclose
|
||||
self.mockedSendFunction.assert_has_calls(
|
||||
[
|
||||
call(
|
||||
commands.UpdateContact(
|
||||
id=domain.security_contact.registry_id,
|
||||
postal_info=domain._make_epp_contact_postal_info(contact=domain.security_contact),
|
||||
email=domain.security_contact.email,
|
||||
voice=domain.security_contact.voice,
|
||||
fax=domain.security_contact.fax,
|
||||
auth_info=common.ContactAuthInfo(pw="2fooBAR123fooBaz"),
|
||||
disclose=domain._disclose_fields(contact=domain.security_contact),
|
||||
),
|
||||
cleaned=True,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
|
@ -43,6 +43,9 @@ class TestGroups(TestCase):
|
|||
"change_user",
|
||||
"delete_userdomainrole",
|
||||
"view_userdomainrole",
|
||||
"add_verifiedbystaff",
|
||||
"change_verifiedbystaff",
|
||||
"delete_verifiedbystaff",
|
||||
"change_website",
|
||||
]
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ from registrar.models import (
|
|||
|
||||
import boto3_mocking
|
||||
from registrar.models.transition_domain import TransitionDomain
|
||||
from registrar.models.very_important_person import VeryImportantPerson # type: ignore
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff # type: ignore
|
||||
from .common import MockSESClient, less_console_noise, completed_application
|
||||
from django_fsm import TransitionNotAllowed
|
||||
|
||||
|
@ -60,99 +60,106 @@ class TestDomainApplication(TestCase):
|
|||
|
||||
def assertNotRaises(self, exception_type):
|
||||
"""Helper method for testing allowed transitions."""
|
||||
return self.assertRaises(Exception, None, exception_type)
|
||||
with less_console_noise():
|
||||
return self.assertRaises(Exception, None, exception_type)
|
||||
|
||||
def test_empty_create_fails(self):
|
||||
"""Can't create a completely empty domain application.
|
||||
NOTE: something about theexception this test raises messes up with the
|
||||
atomic block in a custom tearDown method for the parent test class."""
|
||||
with self.assertRaisesRegex(IntegrityError, "creator"):
|
||||
DomainApplication.objects.create()
|
||||
with less_console_noise():
|
||||
with self.assertRaisesRegex(IntegrityError, "creator"):
|
||||
DomainApplication.objects.create()
|
||||
|
||||
def test_minimal_create(self):
|
||||
"""Can create with just a creator."""
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
|
||||
with less_console_noise():
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
self.assertEqual(application.status, DomainApplication.ApplicationStatus.STARTED)
|
||||
|
||||
def test_full_create(self):
|
||||
"""Can create with all fields."""
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
contact = Contact.objects.create()
|
||||
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
||||
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=user,
|
||||
investigator=user,
|
||||
organization_type=DomainApplication.OrganizationChoices.FEDERAL,
|
||||
federal_type=DomainApplication.BranchChoices.EXECUTIVE,
|
||||
is_election_board=False,
|
||||
organization_name="Test",
|
||||
address_line1="100 Main St.",
|
||||
address_line2="APT 1A",
|
||||
state_territory="CA",
|
||||
zipcode="12345-6789",
|
||||
authorizing_official=contact,
|
||||
requested_domain=domain,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
)
|
||||
application.current_websites.add(com_website)
|
||||
application.alternative_domains.add(gov_website)
|
||||
application.other_contacts.add(contact)
|
||||
application.save()
|
||||
with less_console_noise():
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
contact = Contact.objects.create()
|
||||
com_website, _ = Website.objects.get_or_create(website="igorville.com")
|
||||
gov_website, _ = Website.objects.get_or_create(website="igorville.gov")
|
||||
domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(
|
||||
creator=user,
|
||||
investigator=user,
|
||||
organization_type=DomainApplication.OrganizationChoices.FEDERAL,
|
||||
federal_type=DomainApplication.BranchChoices.EXECUTIVE,
|
||||
is_election_board=False,
|
||||
organization_name="Test",
|
||||
address_line1="100 Main St.",
|
||||
address_line2="APT 1A",
|
||||
state_territory="CA",
|
||||
zipcode="12345-6789",
|
||||
authorizing_official=contact,
|
||||
requested_domain=domain,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
)
|
||||
application.current_websites.add(com_website)
|
||||
application.alternative_domains.add(gov_website)
|
||||
application.other_contacts.add(contact)
|
||||
application.save()
|
||||
|
||||
def test_domain_info(self):
|
||||
"""Can create domain info with all fields."""
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
contact = Contact.objects.create()
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
information = DomainInformation.objects.create(
|
||||
creator=user,
|
||||
organization_type=DomainInformation.OrganizationChoices.FEDERAL,
|
||||
federal_type=DomainInformation.BranchChoices.EXECUTIVE,
|
||||
is_election_board=False,
|
||||
organization_name="Test",
|
||||
address_line1="100 Main St.",
|
||||
address_line2="APT 1A",
|
||||
state_territory="CA",
|
||||
zipcode="12345-6789",
|
||||
authorizing_official=contact,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
domain=domain,
|
||||
)
|
||||
information.other_contacts.add(contact)
|
||||
information.save()
|
||||
self.assertEqual(information.domain.id, domain.id)
|
||||
self.assertEqual(information.id, domain.domain_info.id)
|
||||
with less_console_noise():
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
contact = Contact.objects.create()
|
||||
domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
information = DomainInformation.objects.create(
|
||||
creator=user,
|
||||
organization_type=DomainInformation.OrganizationChoices.FEDERAL,
|
||||
federal_type=DomainInformation.BranchChoices.EXECUTIVE,
|
||||
is_election_board=False,
|
||||
organization_name="Test",
|
||||
address_line1="100 Main St.",
|
||||
address_line2="APT 1A",
|
||||
state_territory="CA",
|
||||
zipcode="12345-6789",
|
||||
authorizing_official=contact,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
domain=domain,
|
||||
)
|
||||
information.other_contacts.add(contact)
|
||||
information.save()
|
||||
self.assertEqual(information.domain.id, domain.id)
|
||||
self.assertEqual(information.id, domain.domain_info.id)
|
||||
|
||||
def test_status_fsm_submit_fail(self):
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
with less_console_noise():
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
application = DomainApplication.objects.create(creator=user)
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
with self.assertRaises(ValueError):
|
||||
# can't submit an application with a null domain name
|
||||
application.submit()
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
with self.assertRaises(ValueError):
|
||||
# can't submit an application with a null domain name
|
||||
application.submit()
|
||||
|
||||
def test_status_fsm_submit_succeed(self):
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=site)
|
||||
with less_console_noise():
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=site)
|
||||
|
||||
# no submitter email so this emits a log warning
|
||||
# no submitter email so this emits a log warning
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
application.submit()
|
||||
self.assertEqual(application.status, application.ApplicationStatus.SUBMITTED)
|
||||
|
||||
def test_submit_from_started_sends_email(self):
|
||||
"""Create an application and submit it and see if email was sent."""
|
||||
|
@ -160,8 +167,7 @@ class TestDomainApplication(TestCase):
|
|||
# submitter's email is mayor@igorville.gov
|
||||
application = completed_application()
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
application.submit()
|
||||
|
||||
# check to see if an email was sent
|
||||
|
@ -394,13 +400,13 @@ class TestDomainApplication(TestCase):
|
|||
(self.rejected_application, TransitionNotAllowed),
|
||||
(self.ineligible_application, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
try:
|
||||
application.action_needed()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
with less_console_noise():
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
try:
|
||||
application.action_needed()
|
||||
except TransitionNotAllowed:
|
||||
self.fail("TransitionNotAllowed was raised, but it was not expected.")
|
||||
|
||||
def test_action_needed_transition_not_allowed(self):
|
||||
"""
|
||||
|
@ -412,11 +418,11 @@ class TestDomainApplication(TestCase):
|
|||
(self.action_needed_application, TransitionNotAllowed),
|
||||
(self.withdrawn_application, TransitionNotAllowed),
|
||||
]
|
||||
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
application.action_needed()
|
||||
with less_console_noise():
|
||||
for application, exception_type in test_cases:
|
||||
with self.subTest(application=application, exception_type=exception_type):
|
||||
with self.assertRaises(exception_type):
|
||||
application.action_needed()
|
||||
|
||||
def test_approved_transition_allowed(self):
|
||||
"""
|
||||
|
@ -583,6 +589,46 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(exception_type):
|
||||
application.reject_with_prejudice()
|
||||
|
||||
def test_transition_not_allowed_approved_in_review_when_domain_is_active(self):
|
||||
"""Create an application with status approved, create a matching domain that
|
||||
is active, and call in_review against transition rules"""
|
||||
|
||||
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||
self.approved_application.approved_domain = domain
|
||||
self.approved_application.save()
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return True # Override to return True
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
# Use patch to temporarily replace is_active with the custom implementation
|
||||
with patch.object(Domain, "is_active", custom_is_active):
|
||||
# Now, when you call is_active on Domain, it will return True
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
self.approved_application.in_review()
|
||||
|
||||
def test_transition_not_allowed_approved_action_needed_when_domain_is_active(self):
|
||||
"""Create an application with status approved, create a matching domain that
|
||||
is active, and call action_needed against transition rules"""
|
||||
|
||||
domain = Domain.objects.create(name=self.approved_application.requested_domain.name)
|
||||
self.approved_application.approved_domain = domain
|
||||
self.approved_application.save()
|
||||
|
||||
# Define a custom implementation for is_active
|
||||
def custom_is_active(self):
|
||||
return True # Override to return True
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
# Use patch to temporarily replace is_active with the custom implementation
|
||||
with patch.object(Domain, "is_active", custom_is_active):
|
||||
# Now, when you call is_active on Domain, it will return True
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
self.approved_application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_approved_rejected_when_domain_is_active(self):
|
||||
"""Create an application with status approved, create a matching domain that
|
||||
is active, and call reject against transition rules"""
|
||||
|
@ -625,25 +671,29 @@ class TestDomainApplication(TestCase):
|
|||
|
||||
def test_has_rationale_returns_true(self):
|
||||
"""has_rationale() returns true when an application has no_other_contacts_rationale"""
|
||||
self.started_application.no_other_contacts_rationale = "You talkin' to me?"
|
||||
self.started_application.save()
|
||||
self.assertEquals(self.started_application.has_rationale(), True)
|
||||
with less_console_noise():
|
||||
self.started_application.no_other_contacts_rationale = "You talkin' to me?"
|
||||
self.started_application.save()
|
||||
self.assertEquals(self.started_application.has_rationale(), True)
|
||||
|
||||
def test_has_rationale_returns_false(self):
|
||||
"""has_rationale() returns false when an application has no no_other_contacts_rationale"""
|
||||
self.assertEquals(self.started_application.has_rationale(), False)
|
||||
with less_console_noise():
|
||||
self.assertEquals(self.started_application.has_rationale(), False)
|
||||
|
||||
def test_has_other_contacts_returns_true(self):
|
||||
"""has_other_contacts() returns true when an application has other_contacts"""
|
||||
# completed_application has other contacts by default
|
||||
self.assertEquals(self.started_application.has_other_contacts(), True)
|
||||
with less_console_noise():
|
||||
# completed_application has other contacts by default
|
||||
self.assertEquals(self.started_application.has_other_contacts(), True)
|
||||
|
||||
def test_has_other_contacts_returns_false(self):
|
||||
"""has_other_contacts() returns false when an application has no other_contacts"""
|
||||
application = completed_application(
|
||||
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
|
||||
)
|
||||
self.assertEquals(application.has_other_contacts(), False)
|
||||
with less_console_noise():
|
||||
application = completed_application(
|
||||
status=DomainApplication.ApplicationStatus.STARTED, name="no-others.gov", has_other_contacts=False
|
||||
)
|
||||
self.assertEquals(application.has_other_contacts(), False)
|
||||
|
||||
|
||||
class TestPermissions(TestCase):
|
||||
|
@ -674,9 +724,9 @@ class TestPermissions(TestCase):
|
|||
self.assertTrue(UserDomainRole.objects.get(user=user, domain=domain))
|
||||
|
||||
|
||||
class TestDomainInfo(TestCase):
|
||||
class TestDomainInformation(TestCase):
|
||||
|
||||
"""Test creation of Domain Information when approved."""
|
||||
"""Test the DomainInformation model, when approved or otherwise"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
@ -685,12 +735,18 @@ class TestDomainInfo(TestCase):
|
|||
def tearDown(self):
|
||||
super().tearDown()
|
||||
self.mock_client.EMAILS_SENT.clear()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
DraftDomain.objects.all().delete()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_approval_creates_info(self):
|
||||
self.maxDiff = None
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="igorville.gov")
|
||||
user, _ = User.objects.get_or_create()
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain)
|
||||
application = DomainApplication.objects.create(creator=user, requested_domain=draft_domain, notes="test notes")
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
|
@ -700,7 +756,25 @@ class TestDomainInfo(TestCase):
|
|||
|
||||
# should be an information present for this domain
|
||||
domain = Domain.objects.get(name="igorville.gov")
|
||||
self.assertTrue(DomainInformation.objects.get(domain=domain))
|
||||
domain_information = DomainInformation.objects.filter(domain=domain)
|
||||
self.assertTrue(domain_information.exists())
|
||||
|
||||
# Test that both objects are what we expect
|
||||
current_domain_information = domain_information.get().__dict__
|
||||
expected_domain_information = DomainInformation(
|
||||
creator=user,
|
||||
domain=domain,
|
||||
notes="test notes",
|
||||
domain_application=application,
|
||||
).__dict__
|
||||
|
||||
# Test the two records for consistency
|
||||
self.assertEqual(self.clean_dict(current_domain_information), self.clean_dict(expected_domain_information))
|
||||
|
||||
def clean_dict(self, dict_obj):
|
||||
"""Cleans dynamic fields in a dictionary"""
|
||||
bad_fields = ["_state", "created_at", "id", "updated_at"]
|
||||
return {k: v for k, v in dict_obj.items() if k not in bad_fields}
|
||||
|
||||
|
||||
class TestInvitations(TestCase):
|
||||
|
@ -782,7 +856,7 @@ class TestUser(TestCase):
|
|||
def test_identity_verification_with_very_important_person(self):
|
||||
"""A Very Important Person should return False
|
||||
when tested with class method needs_identity_verification"""
|
||||
VeryImportantPerson.objects.get_or_create(email=self.user.email)
|
||||
VerifiedByStaff.objects.get_or_create(email=self.user.email)
|
||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||
|
||||
def test_identity_verification_with_invited_user(self):
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,7 @@ import boto3_mocking
|
|||
from registrar.utility.s3_bucket import S3ClientError, S3ClientErrorCodes # type: ignore
|
||||
from datetime import date, datetime, timedelta
|
||||
from django.utils import timezone
|
||||
from .common import less_console_noise
|
||||
|
||||
|
||||
class CsvReportsTest(TestCase):
|
||||
|
@ -80,41 +81,43 @@ class CsvReportsTest(TestCase):
|
|||
@boto3_mocking.patching
|
||||
def test_generate_federal_report(self):
|
||||
"""Ensures that we correctly generate current-federal.csv"""
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_federal_report", checkpath=False)
|
||||
content = fake_open()
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_federal_report", checkpath=False)
|
||||
content = fake_open()
|
||||
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_generate_full_report(self):
|
||||
"""Ensures that we correctly generate current-full.csv"""
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_full_report", checkpath=False)
|
||||
content = fake_open()
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
fake_open = mock_open()
|
||||
expected_file_content = [
|
||||
call("Domain name,Domain type,Agency,Organization name,City,State,Security contact email\r\n"),
|
||||
call("cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,, \r\n"),
|
||||
call("ddomain3.gov,Federal,Armed Forces Retirement Home,,,, \r\n"),
|
||||
call("adomain2.gov,Interstate,,,,, \r\n"),
|
||||
]
|
||||
# We don't actually want to write anything for a test case,
|
||||
# we just want to verify what is being written.
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("builtins.open", fake_open):
|
||||
call_command("generate_current_full_report", checkpath=False)
|
||||
content = fake_open()
|
||||
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
content.write.assert_has_calls(expected_file_content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_not_found_full_report(self):
|
||||
|
@ -123,19 +126,20 @@ class CsvReportsTest(TestCase):
|
|||
def side_effect(Bucket, Key):
|
||||
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
|
||||
response = None
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-full")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
response = None
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-full")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_not_found_federal_report(self):
|
||||
|
@ -144,83 +148,86 @@ class CsvReportsTest(TestCase):
|
|||
def side_effect(Bucket, Key):
|
||||
raise ClientError({"Error": {"Code": "NoSuchKey", "Message": "No such key"}}, "get_object")
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
mock_client.get_object.side_effect = side_effect
|
||||
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-federal")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
with patch("boto3.client", return_value=mock_client):
|
||||
with self.assertRaises(S3ClientError) as context:
|
||||
response = self.client.get("/api/v1/get-report/current-federal")
|
||||
# Check that the response has status code 500
|
||||
self.assertEqual(response.status_code, 500)
|
||||
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
# Check that we get the right error back from the page
|
||||
self.assertEqual(context.exception.code, S3ClientErrorCodes.FILE_NOT_FOUND_ERROR)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_load_federal_report(self):
|
||||
"""Tests the get_current_federal api endpoint"""
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with open("registrar/tests/data/fake_current_federal.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
with open("registrar/tests/data/fake_current_federal.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_federal(request)
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_federal(request)
|
||||
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-federal.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,"
|
||||
).encode()
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,"
|
||||
).encode()
|
||||
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_load_full_report(self):
|
||||
"""Tests the current-federal api link"""
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
with less_console_noise():
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with open("registrar/tests/data/fake_current_full.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
with open("registrar/tests/data/fake_current_full.csv", "r") as file:
|
||||
file_content = file.read()
|
||||
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_full(request)
|
||||
# Mock a recieved file
|
||||
mock_client_instance.get_object.return_value = {"Body": io.BytesIO(file_content.encode())}
|
||||
with boto3_mocking.clients.handler_for("s3", mock_client):
|
||||
request = self.factory.get("/fake-path")
|
||||
response = get_current_full(request)
|
||||
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
# Check that we are sending the correct calls.
|
||||
# Ensures that we are decoding the file content recieved from AWS.
|
||||
expected_call = [call.get_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key="current-full.csv")]
|
||||
mock_client_instance.assert_has_calls(expected_call)
|
||||
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Check that the response has status code 200
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||
"adomain2.gov,Interstate,,,,,"
|
||||
).encode()
|
||||
# Check that the response contains what we expect
|
||||
expected_file_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,Security contact email\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,,,,\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,,,,\n"
|
||||
"adomain2.gov,Interstate,,,,,"
|
||||
).encode()
|
||||
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
self.assertEqual(expected_file_content, response.content)
|
||||
|
||||
|
||||
class ExportDataTest(MockEppLib):
|
||||
|
@ -339,192 +346,170 @@ class ExportDataTest(MockEppLib):
|
|||
def test_export_domains_to_writer_security_emails(self):
|
||||
"""Test that export_domains_to_writer returns the
|
||||
expected security email"""
|
||||
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"AO",
|
||||
"AO email",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
|
||||
self.maxDiff = None
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||
"AO email,Security contact email,Status,Expiration date\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
with less_console_noise():
|
||||
# Add security email information
|
||||
self.domain_1.name = "defaultsecurity.gov"
|
||||
self.domain_1.save()
|
||||
# Invoke setter
|
||||
self.domain_1.security_contact
|
||||
# Invoke setter
|
||||
self.domain_2.security_contact
|
||||
# Invoke setter
|
||||
self.domain_3.security_contact
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"AO",
|
||||
"AO email",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
self.maxDiff = None
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||
"AO email,Security contact email,Status,Expiration date\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\n"
|
||||
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_body(self):
|
||||
"""Test that write_body returns the
|
||||
existing domain, test that sort by domain name works,
|
||||
test that filter works"""
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"AO",
|
||||
"AO email",
|
||||
"Submitter",
|
||||
"Submitter title",
|
||||
"Submitter email",
|
||||
"Submitter phone",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||
"AO email,Submitter,Submitter title,Submitter email,Submitter phone,"
|
||||
"Security contact email,Status\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,Dns needed\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"AO",
|
||||
"AO email",
|
||||
"Submitter",
|
||||
"Submitter title",
|
||||
"Submitter email",
|
||||
"Submitter phone",
|
||||
"Security contact email",
|
||||
"Status",
|
||||
]
|
||||
sort_fields = ["domain__name"]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,State,AO,"
|
||||
"AO email,Submitter,Submitter title,Submitter email,Submitter phone,"
|
||||
"Security contact email,Status\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||
"adomain2.gov,Interstate,Dns needed\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission,Ready\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,On hold\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_body_additional(self):
|
||||
"""An additional test for filters and multi-column sort"""
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Security contact email",
|
||||
]
|
||||
sort_fields = ["domain__name", "federal_agency", "organization_type"]
|
||||
filter_condition = {
|
||||
"organization_type__icontains": "federal",
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# We expect READY domains,
|
||||
# federal only
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Security contact email\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home\n"
|
||||
)
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Security contact email",
|
||||
]
|
||||
sort_fields = ["domain__name", "federal_agency", "organization_type"]
|
||||
filter_condition = {
|
||||
"organization_type__icontains": "federal",
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
Domain.State.DNS_NEEDED,
|
||||
Domain.State.ON_HOLD,
|
||||
],
|
||||
}
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(writer, columns, sort_fields, filter_condition)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains,
|
||||
# federal only
|
||||
# sorted alphabetially by domain name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Security contact email\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home\n"
|
||||
"cdomain1.gov,Federal - Executive,World War I Centennial Commission\n"
|
||||
"ddomain3.gov,Federal,Armed Forces Retirement Home\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
def test_write_body_with_date_filter_pulls_domains_in_range(self):
|
||||
"""Test that domains that are
|
||||
|
@ -538,88 +523,88 @@ class ExportDataTest(MockEppLib):
|
|||
which are hard to mock.
|
||||
|
||||
TODO: Simplify is created_at is not needed for the report."""
|
||||
with less_console_noise():
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# We use timezone.make_aware to sync to server time a datetime object with the current date
|
||||
# (using date.today()) and a specific time (using datetime.min.time()).
|
||||
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time()))
|
||||
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
|
||||
|
||||
# Create a CSV file in memory
|
||||
csv_file = StringIO()
|
||||
writer = csv.writer(csv_file)
|
||||
# We use timezone.make_aware to sync to server time a datetime object with the current date (using date.today())
|
||||
# and a specific time (using datetime.min.time()).
|
||||
end_date = timezone.make_aware(datetime.combine(date.today() + timedelta(days=2), datetime.min.time()))
|
||||
start_date = timezone.make_aware(datetime.combine(date.today() - timedelta(days=2), datetime.min.time()))
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
]
|
||||
sort_fields = [
|
||||
"created_at",
|
||||
"domain__name",
|
||||
]
|
||||
sort_fields_for_deleted_domains = [
|
||||
"domain__deleted",
|
||||
"domain__name",
|
||||
]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
],
|
||||
"domain__first_ready__lte": end_date,
|
||||
"domain__first_ready__gte": start_date,
|
||||
}
|
||||
filter_conditions_for_deleted_domains = {
|
||||
"domain__state__in": [
|
||||
Domain.State.DELETED,
|
||||
],
|
||||
"domain__deleted__lte": end_date,
|
||||
"domain__deleted__gte": start_date,
|
||||
}
|
||||
|
||||
# Define columns, sort fields, and filter condition
|
||||
columns = [
|
||||
"Domain name",
|
||||
"Domain type",
|
||||
"Agency",
|
||||
"Organization name",
|
||||
"City",
|
||||
"State",
|
||||
"Status",
|
||||
"Expiration date",
|
||||
]
|
||||
sort_fields = [
|
||||
"created_at",
|
||||
"domain__name",
|
||||
]
|
||||
sort_fields_for_deleted_domains = [
|
||||
"domain__deleted",
|
||||
"domain__name",
|
||||
]
|
||||
filter_condition = {
|
||||
"domain__state__in": [
|
||||
Domain.State.READY,
|
||||
],
|
||||
"domain__first_ready__lte": end_date,
|
||||
"domain__first_ready__gte": start_date,
|
||||
}
|
||||
filter_conditions_for_deleted_domains = {
|
||||
"domain__state__in": [
|
||||
Domain.State.DELETED,
|
||||
],
|
||||
"domain__deleted__lte": end_date,
|
||||
"domain__deleted__gte": start_date,
|
||||
}
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
)
|
||||
write_body(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields_for_deleted_domains,
|
||||
filter_conditions_for_deleted_domains,
|
||||
)
|
||||
|
||||
# Call the export functions
|
||||
write_header(writer, columns)
|
||||
write_body(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields,
|
||||
filter_condition,
|
||||
)
|
||||
write_body(
|
||||
writer,
|
||||
columns,
|
||||
sort_fields_for_deleted_domains,
|
||||
filter_conditions_for_deleted_domains,
|
||||
)
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
|
||||
# Reset the CSV file's position to the beginning
|
||||
csv_file.seek(0)
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
|
||||
# Read the content into a variable
|
||||
csv_content = csv_file.read()
|
||||
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
|
||||
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Status,Expiration date\n"
|
||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
|
||||
"zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
"xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
)
|
||||
|
||||
# We expect READY domains first, created between today-2 and today+2, sorted by created_at then name
|
||||
# and DELETED domains deleted between today-2 and today+2, sorted by deleted then name
|
||||
expected_content = (
|
||||
"Domain name,Domain type,Agency,Organization name,City,"
|
||||
"State,Status,Expiration date\n"
|
||||
"cdomain1.gov,Federal-Executive,World War I Centennial Commission,,,,Ready,\n"
|
||||
"adomain10.gov,Federal,Armed Forces Retirement Home,,,,Ready,\n"
|
||||
"zdomain9.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
"sdomain8.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
"xdomain7.gov,Federal,Armed Forces Retirement Home,,,,Deleted,\n"
|
||||
)
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
# Normalize line endings and remove commas,
|
||||
# spaces and leading/trailing whitespace
|
||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
self.assertEqual(csv_content, expected_content)
|
||||
|
||||
|
||||
class HelperFunctions(TestCase):
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,7 @@ SAMPLE_KWARGS = {
|
|||
"content_type_id": "2",
|
||||
"object_id": "3",
|
||||
"domain": "whitehouse.gov",
|
||||
"user_pk": "1",
|
||||
}
|
||||
|
||||
# Our test suite will ignore some namespaces.
|
||||
|
|
File diff suppressed because it is too large
Load diff
2329
src/registrar/tests/test_views_application.py
Normal file
2329
src/registrar/tests/test_views_application.py
Normal file
File diff suppressed because it is too large
Load diff
1436
src/registrar/tests/test_views_domain.py
Normal file
1436
src/registrar/tests/test_views_domain.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -3,10 +3,13 @@ import logging
|
|||
from datetime import datetime
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from django.db.models import Value
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import F, Value, CharField
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,50 +23,83 @@ def write_header(writer, columns):
|
|||
|
||||
|
||||
def get_domain_infos(filter_condition, sort_fields):
|
||||
domain_infos = DomainInformation.objects.filter(**filter_condition).order_by(*sort_fields)
|
||||
return domain_infos
|
||||
domain_infos = (
|
||||
DomainInformation.objects.select_related("domain", "authorizing_official")
|
||||
.filter(**filter_condition)
|
||||
.order_by(*sort_fields)
|
||||
)
|
||||
|
||||
# Do a mass concat of the first and last name fields for authorizing_official.
|
||||
# The old operation was computationally heavy for some reason, so if we precompute
|
||||
# this here, it is vastly more efficient.
|
||||
domain_infos_cleaned = domain_infos.annotate(
|
||||
ao=Concat(
|
||||
Coalesce(F("authorizing_official__first_name"), Value("")),
|
||||
Value(" "),
|
||||
Coalesce(F("authorizing_official__last_name"), Value("")),
|
||||
output_field=CharField(),
|
||||
)
|
||||
)
|
||||
return domain_infos_cleaned
|
||||
|
||||
|
||||
def write_row(writer, columns, domain_info):
|
||||
security_contacts = domain_info.domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
|
||||
def parse_row(columns, domain_info: DomainInformation, security_emails_dict=None):
|
||||
"""Given a set of columns, generate a new row from cleaned column data"""
|
||||
|
||||
# For linter
|
||||
ao = " "
|
||||
if domain_info.authorizing_official:
|
||||
first_name = domain_info.authorizing_official.first_name or ""
|
||||
last_name = domain_info.authorizing_official.last_name or ""
|
||||
ao = first_name + " " + last_name
|
||||
# Domain should never be none when parsing this information
|
||||
if domain_info.domain is None:
|
||||
raise ValueError("Domain is none")
|
||||
|
||||
security_email = " "
|
||||
if security_contacts:
|
||||
security_email = security_contacts[0].email
|
||||
domain = domain_info.domain # type: ignore
|
||||
|
||||
# Grab the security email from a preset dictionary.
|
||||
# If nothing exists in the dictionary, grab from .contacts.
|
||||
if security_emails_dict is not None and domain.name in security_emails_dict:
|
||||
_email = security_emails_dict.get(domain.name)
|
||||
security_email = _email if _email is not None else " "
|
||||
else:
|
||||
# If the dictionary doesn't contain that data, lets filter for it manually.
|
||||
# This is a last resort as this is a more expensive operation.
|
||||
security_contacts = domain.contacts.filter(contact_type=PublicContact.ContactTypeChoices.SECURITY)
|
||||
_email = security_contacts[0].email if security_contacts else None
|
||||
security_email = _email if _email is not None else " "
|
||||
|
||||
invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"}
|
||||
# These are default emails that should not be displayed in the csv report
|
||||
if security_email is not None and security_email.lower() in invalid_emails:
|
||||
invalid_emails = {DefaultEmail.LEGACY_DEFAULT.value, DefaultEmail.PUBLIC_CONTACT_DEFAULT.value}
|
||||
if security_email.lower() in invalid_emails:
|
||||
security_email = "(blank)"
|
||||
|
||||
if domain_info.federal_type:
|
||||
domain_type = f"{domain_info.get_organization_type_display()} - {domain_info.get_federal_type_display()}"
|
||||
else:
|
||||
domain_type = domain_info.get_organization_type_display()
|
||||
|
||||
# create a dictionary of fields which can be included in output
|
||||
FIELDS = {
|
||||
"Domain name": domain_info.domain.name,
|
||||
"Domain type": domain_info.get_organization_type_display() + " - " + domain_info.get_federal_type_display()
|
||||
if domain_info.federal_type
|
||||
else domain_info.get_organization_type_display(),
|
||||
"Domain name": domain.name,
|
||||
"Domain type": domain_type,
|
||||
"Agency": domain_info.federal_agency,
|
||||
"Organization name": domain_info.organization_name,
|
||||
"City": domain_info.city,
|
||||
"State": domain_info.state_territory,
|
||||
"AO": ao,
|
||||
"AO": domain_info.ao, # type: ignore
|
||||
"AO email": domain_info.authorizing_official.email if domain_info.authorizing_official else " ",
|
||||
"Security contact email": security_email,
|
||||
"Status": domain_info.domain.get_state_display(),
|
||||
"Expiration date": domain_info.domain.expiration_date,
|
||||
"Created at": domain_info.domain.created_at,
|
||||
"First ready": domain_info.domain.first_ready,
|
||||
"Deleted": domain_info.domain.deleted,
|
||||
"Status": domain.get_state_display(),
|
||||
"Expiration date": domain.expiration_date,
|
||||
"Created at": domain.created_at,
|
||||
"First ready": domain.first_ready,
|
||||
"Deleted": domain.deleted,
|
||||
}
|
||||
|
||||
writer.writerow([FIELDS.get(column, "") for column in columns])
|
||||
# user_emails = [user.email for user in domain.permissions]
|
||||
|
||||
# Dynamically add user emails to the FIELDS dictionary
|
||||
# for i, user_email in enumerate(user_emails, start=1):
|
||||
# FIELDS[f"User{i} email"] = user_email
|
||||
|
||||
row = [FIELDS.get(column, "") for column in columns]
|
||||
return row
|
||||
|
||||
|
||||
def write_body(
|
||||
|
@ -78,13 +114,51 @@ def write_body(
|
|||
"""
|
||||
|
||||
# Get the domainInfos
|
||||
domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||
all_domain_infos = get_domain_infos(filter_condition, sort_fields)
|
||||
|
||||
all_domain_infos = list(domain_infos)
|
||||
# Store all security emails to avoid epp calls or excessive filters
|
||||
sec_contact_ids = all_domain_infos.values_list("domain__security_contact_registry_id", flat=True)
|
||||
security_emails_dict = {}
|
||||
public_contacts = (
|
||||
PublicContact.objects.only("email", "domain__name")
|
||||
.select_related("domain")
|
||||
.filter(registry_id__in=sec_contact_ids)
|
||||
)
|
||||
|
||||
# Write rows to CSV
|
||||
for domain_info in all_domain_infos:
|
||||
write_row(writer, columns, domain_info)
|
||||
# Populate a dictionary of domain names and their security contacts
|
||||
for contact in public_contacts:
|
||||
domain: Domain = contact.domain
|
||||
if domain is not None and domain.name not in security_emails_dict:
|
||||
security_emails_dict[domain.name] = contact.email
|
||||
else:
|
||||
logger.warning("csv_export -> Domain was none for PublicContact")
|
||||
|
||||
# all_user_nums = 0
|
||||
# for domain_info in all_domain_infos:
|
||||
# user_num = len(domain_info.domain.permissions)
|
||||
# all_user_nums.append(user_num)
|
||||
|
||||
# if user_num > highest_user_nums:
|
||||
# highest_user_nums = user_num
|
||||
|
||||
# Build the header here passing to it highest_user_nums
|
||||
|
||||
# Reduce the memory overhead when performing the write operation
|
||||
paginator = Paginator(all_domain_infos, 1000)
|
||||
for page_num in paginator.page_range:
|
||||
page = paginator.page(page_num)
|
||||
rows = []
|
||||
for domain_info in page.object_list:
|
||||
try:
|
||||
row = parse_row(columns, domain_info, security_emails_dict)
|
||||
rows.append(row)
|
||||
except ValueError:
|
||||
# This should not happen. If it does, just skip this row.
|
||||
# It indicates that DomainInformation.domain is None.
|
||||
logger.error("csv_export -> Error when parsing row, domain was None")
|
||||
continue
|
||||
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def export_data_type_to_csv(csv_file):
|
||||
|
|
|
@ -26,3 +26,15 @@ class LogCode(Enum):
|
|||
INFO = 3
|
||||
DEBUG = 4
|
||||
DEFAULT = 5
|
||||
|
||||
|
||||
class DefaultEmail(Enum):
|
||||
"""Stores the string values of default emails
|
||||
|
||||
Overview of emails:
|
||||
- PUBLIC_CONTACT_DEFAULT: "dotgov@cisa.dhs.gov"
|
||||
- LEGACY_DEFAULT: "registrar@dotgov.gov"
|
||||
"""
|
||||
|
||||
PUBLIC_CONTACT_DEFAULT = "dotgov@cisa.dhs.gov"
|
||||
LEGACY_DEFAULT = "registrar@dotgov.gov"
|
||||
|
|
|
@ -12,6 +12,7 @@ from .domain import (
|
|||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
DomainInvitationDeleteView,
|
||||
DomainDeleteUserView,
|
||||
)
|
||||
from .health import *
|
||||
from .index import *
|
||||
|
|
|
@ -159,7 +159,11 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
def storage(self):
|
||||
# marking session as modified on every access
|
||||
# so that updates to nested keys are always saved
|
||||
self.request.session.modified = True
|
||||
# Also - check that self.request.session has the attr
|
||||
# modified to account for test environments calling
|
||||
# view methods
|
||||
if hasattr(self.request.session, "modified"):
|
||||
self.request.session.modified = True
|
||||
return self.request.session.setdefault(self.prefix, {})
|
||||
|
||||
@storage.setter
|
||||
|
@ -211,6 +215,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
if current_url == self.EDIT_URL_NAME and "id" in kwargs:
|
||||
del self.storage
|
||||
self.storage["application_id"] = kwargs["id"]
|
||||
self.storage["step_history"] = self.db_check_for_unlocking_steps()
|
||||
|
||||
# if accessing this class directly, redirect to the first step
|
||||
# in other words, if `ApplicationWizard` is called as view
|
||||
|
@ -269,6 +274,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
and from the database if `use_db` is True (provided that record exists).
|
||||
An empty form will be provided if neither of those are true.
|
||||
"""
|
||||
|
||||
kwargs = {
|
||||
"files": files,
|
||||
"prefix": self.steps.current,
|
||||
|
@ -329,6 +335,43 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
]
|
||||
return DomainApplication.objects.filter(creator=self.request.user, status__in=check_statuses)
|
||||
|
||||
def db_check_for_unlocking_steps(self):
|
||||
"""Helper for get_context_data
|
||||
|
||||
Queries the DB for an application and returns a list of unlocked steps."""
|
||||
history_dict = {
|
||||
"organization_type": self.application.organization_type is not None,
|
||||
"tribal_government": self.application.tribe_name is not None,
|
||||
"organization_federal": self.application.federal_type is not None,
|
||||
"organization_election": self.application.is_election_board is not None,
|
||||
"organization_contact": (
|
||||
self.application.federal_agency is not None
|
||||
or self.application.organization_name is not None
|
||||
or self.application.address_line1 is not None
|
||||
or self.application.city is not None
|
||||
or self.application.state_territory is not None
|
||||
or self.application.zipcode is not None
|
||||
or self.application.urbanization is not None
|
||||
),
|
||||
"about_your_organization": self.application.about_your_organization is not None,
|
||||
"authorizing_official": self.application.authorizing_official is not None,
|
||||
"current_sites": (
|
||||
self.application.current_websites.exists() or self.application.requested_domain is not None
|
||||
),
|
||||
"dotgov_domain": self.application.requested_domain is not None,
|
||||
"purpose": self.application.purpose is not None,
|
||||
"your_contact": self.application.submitter is not None,
|
||||
"other_contacts": (
|
||||
self.application.other_contacts.exists() or self.application.no_other_contacts_rationale is not None
|
||||
),
|
||||
"anything_else": (
|
||||
self.application.anything_else is not None or self.application.is_policy_acknowledged is not None
|
||||
),
|
||||
"requirements": self.application.is_policy_acknowledged is not None,
|
||||
"review": self.application.is_policy_acknowledged is not None,
|
||||
}
|
||||
return [key for key, value in history_dict.items() if value]
|
||||
|
||||
def get_context_data(self):
|
||||
"""Define context for access on all wizard pages."""
|
||||
# Build the submit button that we'll pass to the modal.
|
||||
|
@ -338,6 +381,7 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
|||
modal_heading = "You are about to submit a domain request for " + str(self.application.requested_domain)
|
||||
else:
|
||||
modal_heading = "You are about to submit an incomplete request"
|
||||
|
||||
return {
|
||||
"form_titles": self.TITLES,
|
||||
"steps": self.steps,
|
||||
|
|
|
@ -22,6 +22,7 @@ from registrar.models import (
|
|||
UserDomainRole,
|
||||
)
|
||||
from registrar.models.public_contact import PublicContact
|
||||
from registrar.utility.enums import DefaultEmail
|
||||
from registrar.utility.errors import (
|
||||
GenericError,
|
||||
GenericErrorCodes,
|
||||
|
@ -33,6 +34,7 @@ from registrar.utility.errors import (
|
|||
SecurityEmailErrorCodes,
|
||||
)
|
||||
from registrar.models.utility.contact_error import ContactError
|
||||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||
|
||||
from ..forms import (
|
||||
ContactForm,
|
||||
|
@ -141,11 +143,12 @@ class DomainView(DomainBaseView):
|
|||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
default_email = self.object.get_default_security_contact().email
|
||||
context["default_security_email"] = default_email
|
||||
default_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
|
||||
|
||||
context["hidden_security_emails"] = default_emails
|
||||
|
||||
security_email = self.object.get_security_email()
|
||||
if security_email is None or security_email == default_email:
|
||||
if security_email is None or security_email in default_emails:
|
||||
context["security_email"] = None
|
||||
return context
|
||||
context["security_email"] = security_email
|
||||
|
@ -569,7 +572,7 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
|||
initial = super().get_initial()
|
||||
security_contact = self.object.security_contact
|
||||
|
||||
invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"]
|
||||
invalid_emails = [DefaultEmail.PUBLIC_CONTACT_DEFAULT.value, DefaultEmail.LEGACY_DEFAULT.value]
|
||||
if security_contact is None or security_contact.email in invalid_emails:
|
||||
initial["security_email"] = None
|
||||
return initial
|
||||
|
@ -630,6 +633,55 @@ class DomainUsersView(DomainBaseView):
|
|||
|
||||
template_name = "domain_users.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""The initial value for the form (which is a formset here)."""
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Add conditionals to the context (such as "can_delete_users")
|
||||
context = self._add_booleans_to_context(context)
|
||||
|
||||
# Add modal buttons to the context (such as for delete)
|
||||
context = self._add_modal_buttons_to_context(context)
|
||||
|
||||
# Get the email of the current user
|
||||
context["current_user_email"] = self.request.user.email
|
||||
|
||||
return context
|
||||
|
||||
def _add_booleans_to_context(self, context):
|
||||
# Determine if the current user can delete managers
|
||||
domain_pk = None
|
||||
can_delete_users = False
|
||||
|
||||
if self.kwargs is not None and "pk" in self.kwargs:
|
||||
domain_pk = self.kwargs["pk"]
|
||||
# Prevent the end user from deleting themselves as a manager if they are the
|
||||
# only manager that exists on a domain.
|
||||
can_delete_users = UserDomainRole.objects.filter(domain__id=domain_pk).count() > 1
|
||||
|
||||
context["can_delete_users"] = can_delete_users
|
||||
return context
|
||||
|
||||
def _add_modal_buttons_to_context(self, context):
|
||||
"""Adds modal buttons (and their HTML) to the context"""
|
||||
# Create HTML for the modal button
|
||||
modal_button = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="delete_domain_manager">Yes, remove domain manager</button>'
|
||||
)
|
||||
context["modal_button"] = modal_button
|
||||
|
||||
# Create HTML for the modal button when deleting yourself
|
||||
modal_button_self = (
|
||||
'<button type="submit" '
|
||||
'class="usa-button usa-button--secondary" '
|
||||
'name="delete_domain_manager_self">Yes, remove myself</button>'
|
||||
)
|
||||
context["modal_button_self"] = modal_button_self
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class DomainAddUserView(DomainFormBaseView):
|
||||
"""Inside of a domain's user management, a form for adding users.
|
||||
|
@ -648,7 +700,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
"""Get an absolute URL for this domain."""
|
||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
||||
|
||||
def _send_domain_invitation_email(self, email: str, requester: User, add_success=True):
|
||||
def _send_domain_invitation_email(self, email: str, requestor: User, add_success=True):
|
||||
"""Performs the sending of the domain invitation email,
|
||||
does not make a domain information object
|
||||
email: string- email to send to
|
||||
|
@ -656,16 +708,16 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
adding a success message to the view if the email sending succeeds"""
|
||||
|
||||
# Set a default email address to send to for staff
|
||||
requester_email = "help@get.gov"
|
||||
requestor_email = "help@get.gov"
|
||||
|
||||
# Check if the email requester has a valid email address
|
||||
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
|
||||
requester_email = requester.email
|
||||
elif not requester.is_staff:
|
||||
# Check if the email requestor has a valid email address
|
||||
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||
requestor_email = requestor.email
|
||||
elif not requestor.is_staff:
|
||||
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||
logger.error(
|
||||
f"Can't send email to '{email}' on domain '{self.object}'."
|
||||
f"No email exists for the requester '{requester.username}'.",
|
||||
f"No email exists for the requestor '{requestor.username}'.",
|
||||
exc_info=True,
|
||||
)
|
||||
return None
|
||||
|
@ -678,7 +730,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
context={
|
||||
"domain_url": self._domain_abs_url(),
|
||||
"domain": self.object,
|
||||
"requester_email": requester_email,
|
||||
"requestor_email": requestor_email,
|
||||
},
|
||||
)
|
||||
except EmailSendingError:
|
||||
|
@ -693,7 +745,7 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
if add_success:
|
||||
messages.success(self.request, f"{email} has been invited to this domain.")
|
||||
|
||||
def _make_invitation(self, email_address: str, requester: User):
|
||||
def _make_invitation(self, email_address: str, requestor: User):
|
||||
"""Make a Domain invitation for this email and redirect with a message."""
|
||||
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
if not created:
|
||||
|
@ -703,22 +755,22 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
f"{email_address} has already been invited to this domain.",
|
||||
)
|
||||
else:
|
||||
self._send_domain_invitation_email(email=email_address, requester=requester)
|
||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Add the specified user on this domain."""
|
||||
requested_email = form.cleaned_data["email"]
|
||||
requester = self.request.user
|
||||
requestor = self.request.user
|
||||
# look up a user with that email
|
||||
try:
|
||||
requested_user = User.objects.get(email=requested_email)
|
||||
except User.DoesNotExist:
|
||||
# no matching user, go make an invitation
|
||||
return self._make_invitation(requested_email, requester)
|
||||
return self._make_invitation(requested_email, requestor)
|
||||
else:
|
||||
# if user already exists then just send an email
|
||||
self._send_domain_invitation_email(requested_email, requester, add_success=False)
|
||||
self._send_domain_invitation_email(requested_email, requestor, add_success=False)
|
||||
|
||||
try:
|
||||
UserDomainRole.objects.create(
|
||||
|
@ -743,3 +795,60 @@ class DomainInvitationDeleteView(DomainInvitationPermissionDeleteView, SuccessMe
|
|||
|
||||
def get_success_message(self, cleaned_data):
|
||||
return f"Successfully canceled invitation for {self.object.email}."
|
||||
|
||||
|
||||
class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
||||
"""Inside of a domain's user management, a form for deleting users."""
|
||||
|
||||
object: UserDomainRole # workaround for type mismatch in DeleteView
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
"""Custom get_object definition to grab a UserDomainRole object from a domain_id and user_id"""
|
||||
domain_id = self.kwargs.get("pk")
|
||||
user_id = self.kwargs.get("user_pk")
|
||||
return UserDomainRole.objects.get(domain=domain_id, user=user_id)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Refreshes the page after a delete is successful"""
|
||||
return reverse("domain-users", kwargs={"pk": self.object.domain.id})
|
||||
|
||||
def get_success_message(self, delete_self=False):
|
||||
"""Returns confirmation content for the deletion event"""
|
||||
|
||||
# Grab the text representation of the user we want to delete
|
||||
email_or_name = self.object.user.email
|
||||
if email_or_name is None or email_or_name.strip() == "":
|
||||
email_or_name = self.object.user
|
||||
|
||||
# If the user is deleting themselves, return a specific message.
|
||||
# If not, return something more generic.
|
||||
if delete_self:
|
||||
message = f"You are no longer managing the domain {self.object.domain}."
|
||||
else:
|
||||
message = f"Removed {email_or_name} as a manager for this domain."
|
||||
|
||||
return message
|
||||
|
||||
def form_valid(self, form):
|
||||
"""Delete the specified user on this domain."""
|
||||
|
||||
# Delete the object
|
||||
super().form_valid(form)
|
||||
|
||||
# Is the user deleting themselves? If so, display a different message
|
||||
delete_self = self.request.user == self.object.user
|
||||
|
||||
# Add a success message
|
||||
messages.success(self.request, self.get_success_message(delete_self))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Custom post implementation to redirect to home in the event that the user deletes themselves"""
|
||||
response = super().post(request, *args, **kwargs)
|
||||
|
||||
# If the user is deleting themselves, redirect to home
|
||||
delete_self = self.request.user == self.object.user
|
||||
if delete_self:
|
||||
return redirect(reverse("home"))
|
||||
|
||||
return response
|
||||
|
|
|
@ -286,6 +286,43 @@ class DomainApplicationPermission(PermissionsLoginMixin):
|
|||
return True
|
||||
|
||||
|
||||
class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
||||
|
||||
"""Permission mixin for UserDomainRole if user
|
||||
has access, otherwise 403"""
|
||||
|
||||
def has_permission(self):
|
||||
"""Check if this user has access to this domain application.
|
||||
|
||||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
domain_pk = self.kwargs["pk"]
|
||||
user_pk = self.kwargs["user_pk"]
|
||||
|
||||
# Check if the user is authenticated
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
# Check if the UserDomainRole object exists, then check
|
||||
# if the user requesting the delete has permissions to do so
|
||||
has_delete_permission = UserDomainRole.objects.filter(
|
||||
user=user_pk,
|
||||
domain=domain_pk,
|
||||
domain__permissions__user=self.request.user,
|
||||
).exists()
|
||||
if not has_delete_permission:
|
||||
return False
|
||||
|
||||
# Check if more than one manager exists on the domain.
|
||||
# If only one exists, prevent this from happening
|
||||
has_multiple_managers = len(UserDomainRole.objects.filter(domain=domain_pk)) > 1
|
||||
if not has_multiple_managers:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class DomainApplicationPermissionWithdraw(PermissionsLoginMixin):
|
||||
|
||||
"""Permission mixin that redirects to withdraw action on domain application
|
||||
|
|
|
@ -4,6 +4,7 @@ import abc # abstract base class
|
|||
|
||||
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||
from registrar.models import Domain, DomainApplication, DomainInvitation
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
|
||||
from .mixins import (
|
||||
DomainPermission,
|
||||
|
@ -11,6 +12,7 @@ from .mixins import (
|
|||
DomainApplicationPermissionWithdraw,
|
||||
DomainInvitationPermission,
|
||||
ApplicationWizardPermission,
|
||||
UserDeleteDomainRolePermission,
|
||||
)
|
||||
import logging
|
||||
|
||||
|
@ -130,3 +132,20 @@ class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteV
|
|||
|
||||
model = DomainApplication
|
||||
object: DomainApplication
|
||||
|
||||
|
||||
class UserDomainRolePermissionDeleteView(UserDeleteDomainRolePermission, DeleteView, abc.ABC):
|
||||
|
||||
"""Abstract base view for deleting a UserDomainRole.
|
||||
|
||||
This abstract view cannot be instantiated. Actual views must specify
|
||||
`template_name`.
|
||||
"""
|
||||
|
||||
# DetailView property for what model this is viewing
|
||||
model = UserDomainRole
|
||||
# workaround for type mismatch in DeleteView
|
||||
object: UserDomainRole
|
||||
|
||||
# variable name in template context for the model object
|
||||
context_object_name = "userdomainrole"
|
||||
|
|
|
@ -6,4 +6,4 @@ set -o pipefail
|
|||
# Make sure that django's `collectstatic` has been run locally before pushing up to any environment,
|
||||
# so that the styles and static assets to show up correctly on any environment.
|
||||
|
||||
gunicorn registrar.config.wsgi -t 60
|
||||
gunicorn --worker-class=gevent registrar.config.wsgi -t 60
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue