merge main

This commit is contained in:
Rachid Mrad 2024-02-07 16:24:42 -05:00
commit 22ef6ddc5b
No known key found for this signature in database
82 changed files with 8270 additions and 7043 deletions

View file

@ -48,7 +48,7 @@ All other changes require just a single approving review.-->
- [ ] Added at least 2 developers as PR reviewers (only 1 will need to approve)
- [ ] Messaged on Slack or in standup to notify the team that a PR is ready for review
- [ ] Changes to “how we do things” are documented in READMEs and or onboarding guide
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited.
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.
#### Ensured code standards are met (Original Developer)
@ -72,7 +72,7 @@ All other changes require just a single approving review.-->
- [ ] Reviewed this code and left comments
- [ ] Checked that all code is adequately covered by tests
- [ ] Made it clear which comments need to be addressed before this work is merged
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the assoicated migrations file has been commited.
- [ ] If any model was updated to modify/add/delete columns, makemigrations was ran and the associated migrations file has been commited.
#### Ensured code standards are met (Code reviewer)

View file

@ -38,3 +38,11 @@ jobs:
cf_org: cisa-dotgov
cf_space: development
push_arguments: "-f ops/manifests/manifest-development.yaml"
- name: Run Django migrations
uses: cloud-gov/cg-cli-tools@main
with:
cf_username: ${{ secrets.CF_DEVELOPMENT_USERNAME }}
cf_password: ${{ secrets.CF_DEVELOPMENT_PASSWORD }}
cf_org: cisa-dotgov
cf_space: development
cf_command: "run-task getgov-development --command 'python manage.py migrate' --name migrate"

View file

@ -26,7 +26,6 @@ on:
- rb
- ko
- ab
- bl
- rjm
- dk

View file

@ -26,7 +26,6 @@ on:
- rb
- ko
- ab
- bl
- rjm
- dk

View file

@ -36,7 +36,7 @@ jobs:
- name: Unit tests
working-directory: ./src
run: docker compose run app python manage.py test
run: docker compose run app python manage.py test --parallel
django-migrations-complete:
runs-on: ubuntu-latest

View file

@ -145,7 +145,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
## Mock data
There is a `post_migrate` signal in [signals.py](../../src/registrar/signals.py) that will load the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
[load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_applications.py](../../src/registrar/fixtures_applications.py), giving you some test data to play with while developing.
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.

View file

@ -237,3 +237,7 @@ Bugs on production software need to be documented quickly and triaged to determi
3. In the case where the engineering lead is is unresponsive or unavailable to assign the ticket immediately, the product team will make sure an engineer volunteers or is assigned to the ticket/PR review ASAP.
4. Once done, the developer must make a PR and should tag the assigned PR reviewers in our Slack dev channel stating that the PR is now waiting on their review. These reviewers should drop other tasks in order to review this promptly.
5. See the the section above on [Making bug fixes on stable](#making-bug-fixes-on-stable-during-production) for how to push changes to stable once the PR is approved
# Investigating and monitoring the health of the Registrar
Sometimes, we may want individuals to routinely monitor the Registrar's health, such as after big feature launches. The cadence of such monitoring and what we look for is subject to change and is instead documented in [Checklist for production verification document](https://docs.google.com/document/d/15b_qwEZMiL76BHeRHnznV1HxDQcxNRt--vPSEfixBOI). All project team members should feel free to suggest edits to this document and should refer to it if production-level monitoring is underway.

View file

@ -1,32 +0,0 @@
---
applications:
- name: getgov-bl
buildpacks:
- python_buildpack
path: ../../src
instances: 1
memory: 512M
stack: cflinuxfs4
timeout: 180
command: ./run.sh
health-check-type: http
health-check-http-endpoint: /health
health-check-invocation-timeout: 40
env:
# Send stdout and stderr straight to the terminal without buffering
PYTHONUNBUFFERED: yup
# Tell Django where to find its configuration
DJANGO_SETTINGS_MODULE: registrar.config.settings
# Tell Django where it is being hosted
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
# Tell Django how much stuff to log
DJANGO_LOG_LEVEL: INFO
# default public site location
GETGOV_PUBLIC_SITE_URL: https://get.gov
# Flag to disable/enable features in prod environments
IS_PRODUCTION: False
routes:
- route: getgov-bl.app.cloud.gov
services:
- getgov-credentials
- getgov-bl-database

View file

@ -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"))

View file

@ -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

View 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()

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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;

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -17,5 +17,8 @@
.usa-alert__body::before {
left: 1rem !important;
}
}
}
.usa-alert__body.margin-left-1 {
margin-left: 0.5rem!important;
}
}

View file

@ -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');
}

View 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;
}

View file

@ -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;

View file

@ -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

View file

@ -10,6 +10,7 @@
--- Custom Styles ---------------------------------*/
@forward "base";
@forward "typography";
@forward "lists";
@forward "buttons";
@forward "forms";
@forward "fieldsets";

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -420,7 +420,7 @@ class AlternativeDomainForm(RegistrarForm):
alternative_domain = forms.CharField(
required=False,
label="",
label="Alternative domain",
)

View file

@ -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:

View file

@ -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"
),
),
]

View 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,
),
]

View file

@ -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"},
),
]

View 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,
),
]

View file

@ -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),
),
]

View file

@ -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)

View file

@ -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

View file

@ -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()

View file

@ -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"

View file

@ -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",
)

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 %}

View file

@ -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 states 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 states two-letter abbreviation.{% endif %}</p>
<p>Requests for your organizations 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 youre not sure this is the domain you want, thats ok. You can change the domain later. </p>
</fieldset>
{% endblock %}

View file

@ -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">

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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">

View file

@ -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>

View file

@ -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 %}

View file

@ -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 cant remove yourself as a domain manager if youre 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>

View file

@ -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>.

View file

@ -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 %}

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -1,4 +1,7 @@
<address>
{% if organization.federal_agency %}
{{ organization.federal_agency }}<br />
{% endif %}
{% if organization.organization_name %}
{{ organization.organization_name }}
{% endif %}

View file

@ -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>

View file

@ -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",

View file

@ -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

View file

@ -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):

View 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.")

View file

@ -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"})

View file

@ -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,
)
]
)

View file

@ -43,6 +43,9 @@ class TestGroups(TestCase):
"change_user",
"delete_userdomainrole",
"view_userdomainrole",
"add_verifiedbystaff",
"change_verifiedbystaff",
"delete_verifiedbystaff",
"change_website",
]

View file

@ -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

View file

@ -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

View file

@ -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

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -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):

View 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"

View file

@ -12,6 +12,7 @@ from .domain import (
DomainUsersView,
DomainAddUserView,
DomainInvitationDeleteView,
DomainDeleteUserView,
)
from .health import *
from .index import *

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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