mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-15 09:07:02 +02:00
Merge branch 'main' into dk/1623-notify-analysts-with-contact-related-objects
This commit is contained in:
commit
d8c8e1b688
66 changed files with 950 additions and 184 deletions
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -26,7 +26,6 @@ on:
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
|
||||||
- rjm
|
- rjm
|
||||||
- dk
|
- dk
|
||||||
|
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -26,7 +26,6 @@ on:
|
||||||
- rb
|
- rb
|
||||||
- ko
|
- ko
|
||||||
- ab
|
- ab
|
||||||
- bl
|
|
||||||
- rjm
|
- rjm
|
||||||
- dk
|
- dk
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -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://beta.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
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Which OIDC provider to use
|
# Which OIDC provider to use
|
||||||
OIDC_ACTIVE_PROVIDER: login.gov production
|
OIDC_ACTIVE_PROVIDER: login.gov production
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# use a non-default route to avoid conflicts
|
# use a non-default route to avoid conflicts
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov
|
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov
|
||||||
|
|
|
@ -22,7 +22,7 @@ applications:
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# default public site location
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
# Flag to disable/enable features in prod environments
|
# Flag to disable/enable features in prod environments
|
||||||
IS_PRODUCTION: False
|
IS_PRODUCTION: False
|
||||||
routes:
|
routes:
|
||||||
|
|
|
@ -33,6 +33,10 @@ class AuthenticationFailed(OIDCException):
|
||||||
friendly_message = "This login attempt didn't work."
|
friendly_message = "This login attempt didn't work."
|
||||||
|
|
||||||
|
|
||||||
|
class NoStateDefined(OIDCException):
|
||||||
|
friendly_message = "The session state is None."
|
||||||
|
|
||||||
|
|
||||||
class InternalError(OIDCException):
|
class InternalError(OIDCException):
|
||||||
status = status.INTERNAL_SERVER_ERROR
|
status = status.INTERNAL_SERVER_ERROR
|
||||||
friendly_message = "The system broke while trying to log you in."
|
friendly_message = "The system broke while trying to log you in."
|
||||||
|
|
|
@ -183,6 +183,8 @@ class Client(oic.Client):
|
||||||
if authn_response["state"] != session.get("state", None):
|
if authn_response["state"] != session.get("state", None):
|
||||||
# this most likely means the user's Django session vanished
|
# this most likely means the user's Django session vanished
|
||||||
logger.error("Received state not the same as expected for %s" % state)
|
logger.error("Received state not the same as expected for %s" % state)
|
||||||
|
if session.get("state", None) is None:
|
||||||
|
raise o_e.NoStateDefined()
|
||||||
raise o_e.AuthenticationFailed(locator=state)
|
raise o_e.AuthenticationFailed(locator=state)
|
||||||
|
|
||||||
if self.behaviour.get("response_type") == "code":
|
if self.behaviour.get("response_type") == "code":
|
||||||
|
@ -272,6 +274,11 @@ class Client(oic.Client):
|
||||||
|
|
||||||
super(Client, self).store_response(resp, info)
|
super(Client, self).store_response(resp, info)
|
||||||
|
|
||||||
|
def get_default_acr_value(self):
|
||||||
|
"""returns the acr_value from settings
|
||||||
|
this helper function is called from djangooidc views"""
|
||||||
|
return self.behaviour.get("acr_value")
|
||||||
|
|
||||||
def get_step_up_acr_value(self):
|
def get_step_up_acr_value(self):
|
||||||
"""returns the step_up_acr_value from settings
|
"""returns the step_up_acr_value from settings
|
||||||
this helper function is called from djangooidc views"""
|
this helper function is called from djangooidc views"""
|
||||||
|
|
|
@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.test import Client, TestCase, RequestFactory
|
from django.test import Client, TestCase, RequestFactory
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from djangooidc.exceptions import NoStateDefined
|
||||||
from ..views import login_callback
|
from ..views import login_callback
|
||||||
|
|
||||||
from .common import less_console_noise
|
from .common import less_console_noise
|
||||||
|
@ -17,6 +19,9 @@ class ViewsTest(TestCase):
|
||||||
def say_hi(*args):
|
def say_hi(*args):
|
||||||
return HttpResponse("Hi")
|
return HttpResponse("Hi")
|
||||||
|
|
||||||
|
def create_acr(*args):
|
||||||
|
return "any string"
|
||||||
|
|
||||||
def user_info(*args):
|
def user_info(*args):
|
||||||
return {
|
return {
|
||||||
"sub": "TEST",
|
"sub": "TEST",
|
||||||
|
@ -34,6 +39,7 @@ class ViewsTest(TestCase):
|
||||||
callback_url = reverse("openid_login_callback")
|
callback_url = reverse("openid_login_callback")
|
||||||
# mock
|
# mock
|
||||||
mock_client.create_authn_request.side_effect = self.say_hi
|
mock_client.create_authn_request.side_effect = self.say_hi
|
||||||
|
mock_client.get_default_acr_value.side_effect = self.create_acr
|
||||||
# test
|
# test
|
||||||
response = self.client.get(reverse("login"), {"next": callback_url})
|
response = self.client.get(reverse("login"), {"next": callback_url})
|
||||||
# assert
|
# assert
|
||||||
|
@ -53,6 +59,19 @@ class ViewsTest(TestCase):
|
||||||
self.assertTemplateUsed(response, "500.html")
|
self.assertTemplateUsed(response, "500.html")
|
||||||
self.assertIn("Server error", response.content.decode("utf-8"))
|
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():
|
||||||
|
response = self.client.get(reverse("openid_login_callback"))
|
||||||
|
# assert
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(response.url, "/")
|
||||||
|
|
||||||
def test_login_callback_reads_next(self, mock_client):
|
def test_login_callback_reads_next(self, mock_client):
|
||||||
# setup
|
# setup
|
||||||
session = self.client.session
|
session = self.client.session
|
||||||
|
|
|
@ -55,6 +55,10 @@ def error_page(request, error):
|
||||||
|
|
||||||
def openid(request):
|
def openid(request):
|
||||||
"""Redirect the user to an authentication provider (OP)."""
|
"""Redirect the user to an authentication provider (OP)."""
|
||||||
|
|
||||||
|
# If the session reset because of a server restart, attempt to login again
|
||||||
|
request.session["acr_value"] = CLIENT.get_default_acr_value()
|
||||||
|
|
||||||
request.session["next"] = request.GET.get("next", "/")
|
request.session["next"] = request.GET.get("next", "/")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -78,9 +82,13 @@ def login_callback(request):
|
||||||
if user:
|
if user:
|
||||||
login(request, user)
|
login(request, user)
|
||||||
logger.info("Successfully logged in user %s" % user)
|
logger.info("Successfully logged in user %s" % user)
|
||||||
|
# Double login bug (1507)?
|
||||||
return redirect(request.session.get("next", "/"))
|
return redirect(request.session.get("next", "/"))
|
||||||
else:
|
else:
|
||||||
raise o_e.BannedUser()
|
raise o_e.BannedUser()
|
||||||
|
except o_e.NoStateDefined as nsd_err:
|
||||||
|
logger.warning(f"No State Defined: {nsd_err}")
|
||||||
|
return redirect(request.session.get("next", "/"))
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
return error_page(request, err)
|
return error_page(request, err)
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ services:
|
||||||
# Is this a production environment
|
# Is this a production environment
|
||||||
- IS_PRODUCTION
|
- IS_PRODUCTION
|
||||||
# Public site URL link
|
# Public site URL link
|
||||||
- GETGOV_PUBLIC_SITE_URL=https://beta.get.gov
|
- GETGOV_PUBLIC_SITE_URL=https://get.gov
|
||||||
# Set a username for accessing the registry
|
# Set a username for accessing the registry
|
||||||
- REGISTRY_CL_ID=nothing
|
- REGISTRY_CL_ID=nothing
|
||||||
# Set a password for accessing the registry
|
# Set a password for accessing the registry
|
||||||
|
|
|
@ -665,7 +665,7 @@ class DomainInformationAdmin(ListHeaderAdmin):
|
||||||
),
|
),
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
("Anything else?", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Requirements for operating .gov domains",
|
"Requirements for operating a .gov domain",
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
{"fields": ["is_policy_acknowledged"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -834,7 +834,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
),
|
),
|
||||||
("Anything else?", {"fields": ["anything_else"]}),
|
("Anything else?", {"fields": ["anything_else"]}),
|
||||||
(
|
(
|
||||||
"Requirements for operating .gov domains",
|
"Requirements for operating a .gov domain",
|
||||||
{"fields": ["is_policy_acknowledged"]},
|
{"fields": ["is_policy_acknowledged"]},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -1294,6 +1294,29 @@ class DraftDomainAdmin(ListHeaderAdmin):
|
||||||
search_help_text = "Search by draft domain name."
|
search_help_text = "Search by draft domain name."
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPersonAdmin(ListHeaderAdmin):
|
||||||
|
list_display = ("email", "requestor", "truncated_notes", "created_at")
|
||||||
|
search_fields = ["email"]
|
||||||
|
search_help_text = "Search by email."
|
||||||
|
list_filter = [
|
||||||
|
"requestor",
|
||||||
|
]
|
||||||
|
readonly_fields = [
|
||||||
|
"requestor",
|
||||||
|
]
|
||||||
|
|
||||||
|
def truncated_notes(self, obj):
|
||||||
|
# Truncate the 'notes' field to 50 characters
|
||||||
|
return str(obj.notes)[:50]
|
||||||
|
|
||||||
|
truncated_notes.short_description = "Notes (Truncated)" # type: ignore
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
# Set the user field to the current admin user
|
||||||
|
obj.requestor = request.user if request.user.is_authenticated else None
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
|
||||||
admin.site.unregister(LogEntry) # Unregister the default registration
|
admin.site.unregister(LogEntry) # Unregister the default registration
|
||||||
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
admin.site.register(LogEntry, CustomLogEntryAdmin)
|
||||||
admin.site.register(models.User, MyUserAdmin)
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
|
@ -1314,3 +1337,4 @@ admin.site.register(models.Website, WebsiteAdmin)
|
||||||
admin.site.register(models.PublicContact, AuditedAdmin)
|
admin.site.register(models.PublicContact, AuditedAdmin)
|
||||||
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
admin.site.register(models.DomainApplication, DomainApplicationAdmin)
|
||||||
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
admin.site.register(models.TransitionDomain, TransitionDomainAdmin)
|
||||||
|
admin.site.register(models.VeryImportantPerson, VeryImportantPersonAdmin)
|
||||||
|
|
|
@ -270,9 +270,21 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||||
// h2 and legend for DS form, label for nameservers
|
// h2 and legend for DS form, label for nameservers
|
||||||
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
Array.from(form.querySelectorAll('h2, legend, label, p')).forEach((node) => {
|
||||||
|
|
||||||
|
let innerSpan = node.querySelector('span')
|
||||||
|
if (innerSpan) {
|
||||||
|
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||||
|
} else {
|
||||||
|
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
||||||
|
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
||||||
|
}
|
||||||
|
|
||||||
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
// If the node is a nameserver label, one of the first 2 which was previously 3 and up (not required)
|
||||||
// inject the USWDS required markup and make sure the INPUT is required
|
// inject the USWDS required markup and make sure the INPUT is required
|
||||||
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
if (isNameserversForm && index <= 1 && node.innerHTML.includes('server') && !node.innerHTML.includes('*')) {
|
||||||
|
|
||||||
|
// Remove the word optional
|
||||||
|
innerSpan.textContent = innerSpan.textContent.replace(/\s*\(\s*optional\s*\)\s*/, '');
|
||||||
|
|
||||||
// Create a new element
|
// Create a new element
|
||||||
const newElement = document.createElement('abbr');
|
const newElement = document.createElement('abbr');
|
||||||
newElement.textContent = '*';
|
newElement.textContent = '*';
|
||||||
|
@ -295,13 +307,8 @@ function removeForm(e, formLabel, isNameserversForm, addButton, formIdentifier){
|
||||||
nextInputElement.required = true;
|
nextInputElement.required = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let innerSpan = node.querySelector('span')
|
|
||||||
if (innerSpan) {
|
|
||||||
innerSpan.textContent = innerSpan.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
|
||||||
} else {
|
|
||||||
node.textContent = node.textContent.replace(formLabelRegex, `${formLabel} ${index + 1}`);
|
|
||||||
node.textContent = node.textContent.replace(formExampleRegex, `ns${index + 1}`);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display the add more button if we have less than 13 forms
|
// Display the add more button if we have less than 13 forms
|
||||||
|
@ -521,16 +528,24 @@ function hideDeletedForms() {
|
||||||
formNum++;
|
formNum++;
|
||||||
|
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
newForm.innerHTML = newForm.innerHTML.replace(formNumberRegex, `${formIdentifier}-${formNum-1}-`);
|
||||||
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
|
||||||
// since the form on the backend employs Django's DELETE widget. For the other formsets, we delete the form
|
|
||||||
// in JS (completely remove from teh DOM) so we update the headers/labels based on total number of forms.
|
|
||||||
if (isOtherContactsForm) {
|
if (isOtherContactsForm) {
|
||||||
|
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
|
||||||
|
// since the form on the backend employs Django's DELETE widget.
|
||||||
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`);
|
||||||
|
} else {
|
||||||
|
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
|
||||||
|
// if indices 0 or 1 have been deleted
|
||||||
|
let containsOptional = newForm.innerHTML.includes('(optional)');
|
||||||
|
if (isNameserversForm && !containsOptional) {
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum} (optional)`);
|
||||||
} else {
|
} else {
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${formNum}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
newForm.innerHTML = newForm.innerHTML.replace(formExampleRegex, `ns${formNum}`);
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(/\n/g, ''); // Remove newline characters
|
||||||
|
newForm.innerHTML = newForm.innerHTML.replace(/>\s*</g, '><'); // Remove spaces between tags
|
||||||
container.insertBefore(newForm, addButton);
|
container.insertBefore(newForm, addButton);
|
||||||
|
|
||||||
newForm.style.display = 'block';
|
newForm.style.display = 'block';
|
||||||
|
|
|
@ -25,6 +25,22 @@
|
||||||
color: color('primary-darker');
|
color: color('primary-darker');
|
||||||
padding-bottom: units(2px);
|
padding-bottom: units(2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ticket #1510
|
||||||
|
// @include at-media('desktop') {
|
||||||
|
// th:first-child {
|
||||||
|
// width: 220px;
|
||||||
|
// }
|
||||||
|
// th:nth-child(2) {
|
||||||
|
// width: 175px;
|
||||||
|
// }
|
||||||
|
// th:nth-child(3) {
|
||||||
|
// width: 130px;
|
||||||
|
// }
|
||||||
|
// th:nth-child(5) {
|
||||||
|
// width: 130px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotgov-table {
|
.dotgov-table {
|
||||||
|
|
|
@ -335,7 +335,7 @@ CSP_INCLUDE_NONCE_IN = ["script-src-elem"]
|
||||||
# Cross-Origin Resource Sharing (CORS) configuration
|
# Cross-Origin Resource Sharing (CORS) configuration
|
||||||
# Sets clients that allow access control to manage.get.gov
|
# Sets clients that allow access control to manage.get.gov
|
||||||
# TODO: remove :8080 to see if we can have all localhost access
|
# TODO: remove :8080 to see if we can have all localhost access
|
||||||
CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov"]
|
CORS_ALLOWED_ORIGINS = ["http://localhost:8080", "https://beta.get.gov", "https://get.gov"]
|
||||||
CORS_ALLOWED_ORIGIN_REGEXES = [r"https://[\w-]+\.sites\.pages\.cloud\.gov"]
|
CORS_ALLOWED_ORIGIN_REGEXES = [r"https://[\w-]+\.sites\.pages\.cloud\.gov"]
|
||||||
|
|
||||||
# Content-Length header is set by django.middleware.common.CommonMiddleware
|
# Content-Length header is set by django.middleware.common.CommonMiddleware
|
||||||
|
@ -660,7 +660,6 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-rb.app.cloud.gov",
|
"getgov-rb.app.cloud.gov",
|
||||||
"getgov-ko.app.cloud.gov",
|
"getgov-ko.app.cloud.gov",
|
||||||
"getgov-ab.app.cloud.gov",
|
"getgov-ab.app.cloud.gov",
|
||||||
"getgov-bl.app.cloud.gov",
|
|
||||||
"getgov-rjm.app.cloud.gov",
|
"getgov-rjm.app.cloud.gov",
|
||||||
"getgov-dk.app.cloud.gov",
|
"getgov-dk.app.cloud.gov",
|
||||||
"manage.get.gov",
|
"manage.get.gov",
|
||||||
|
|
|
@ -137,6 +137,11 @@ urlpatterns = [
|
||||||
views.DomainInvitationDeleteView.as_view(http_method_names=["post"]),
|
views.DomainInvitationDeleteView.as_view(http_method_names=["post"]),
|
||||||
name="invitation-delete",
|
name="invitation-delete",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"application/<int:pk>/delete",
|
||||||
|
views.DomainApplicationDeleteView.as_view(http_method_names=["post"]),
|
||||||
|
name="application-delete",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||||
|
|
|
@ -487,7 +487,8 @@ class DotGovDomainForm(RegistrarForm):
|
||||||
values = {}
|
values = {}
|
||||||
requested_domain = getattr(obj, "requested_domain", None)
|
requested_domain = getattr(obj, "requested_domain", None)
|
||||||
if requested_domain is not None:
|
if requested_domain is not None:
|
||||||
values["requested_domain"] = Domain.sld(requested_domain.name)
|
domain_name = requested_domain.name
|
||||||
|
values["requested_domain"] = Domain.sld(domain_name)
|
||||||
return values
|
return values
|
||||||
|
|
||||||
def clean_requested_domain(self):
|
def clean_requested_domain(self):
|
||||||
|
@ -837,8 +838,8 @@ class AnythingElseForm(RegistrarForm):
|
||||||
|
|
||||||
class RequirementsForm(RegistrarForm):
|
class RequirementsForm(RegistrarForm):
|
||||||
is_policy_acknowledged = forms.BooleanField(
|
is_policy_acknowledged = forms.BooleanField(
|
||||||
label="I read and agree to the requirements for operating .gov domains.",
|
label="I read and agree to the requirements for operating a .gov domain.",
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": ("Check the box if you read and agree to the requirements for operating .gov domains.")
|
"required": ("Check the box if you read and agree to the requirements for operating a .gov domain.")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
""""
|
||||||
|
Converts all ready and DNS needed domains with a non-default public contact
|
||||||
|
to disclose their public contact. Created for Issue#1535 to resolve
|
||||||
|
disclose issue of domains with missing security emails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.models import Domain
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Disclose all nondefault domain security emails."
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Sets global variables for code tidiness"""
|
||||||
|
super().__init__()
|
||||||
|
# domains with errors, which are not successfully updated to disclose
|
||||||
|
self.domains_with_errors: list[str] = []
|
||||||
|
# domains that are successfully disclosed
|
||||||
|
self.disclosed_domain_contacts_count = 0
|
||||||
|
# domains that skip disclose due to having contact registrar@dotgov.gov
|
||||||
|
self.skipped_domain_contacts_count = 0
|
||||||
|
|
||||||
|
def handle(self, **options):
|
||||||
|
"""
|
||||||
|
Converts all ready and DNS needed domains with a non-default public contact
|
||||||
|
to disclose their public contact.
|
||||||
|
"""
|
||||||
|
logger.info("Updating security emails to public")
|
||||||
|
|
||||||
|
# Initializes domains that need to be disclosed
|
||||||
|
|
||||||
|
statuses = [Domain.State.READY, Domain.State.DNS_NEEDED]
|
||||||
|
domains = Domain.objects.filter(state__in=statuses)
|
||||||
|
|
||||||
|
logger.info(f"Found {len(domains)} domains with status Ready or DNS Needed.")
|
||||||
|
|
||||||
|
# Update EPP contact for domains with a security contact
|
||||||
|
for domain in domains:
|
||||||
|
try:
|
||||||
|
contact = domain.security_contact # noqa on these items as we only want to call security_contact
|
||||||
|
logger.info(f"Domain {domain.name} security contact: {domain.security_contact.email}")
|
||||||
|
if domain.security_contact.email != "registrar@dotgov.gov":
|
||||||
|
domain._update_epp_contact(contact=domain.security_contact)
|
||||||
|
self.disclosed_domain_contacts_count += 1
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"Skipping disclose for {domain.name} security contact {domain.security_contact.email}."
|
||||||
|
)
|
||||||
|
self.skipped_domain_contacts_count += 1
|
||||||
|
except Exception as err:
|
||||||
|
# error condition if domain not in database
|
||||||
|
self.domains_with_errors.append(copy.deepcopy(domain.name))
|
||||||
|
logger.error(f"error retrieving domain {domain.name} contact {domain.security_contact}: {err}")
|
||||||
|
|
||||||
|
# Inform user how many contacts were disclosed, skipped, and errored
|
||||||
|
logger.info(f"Updated {self.disclosed_domain_contacts_count} contacts to disclosed.")
|
||||||
|
logger.info(
|
||||||
|
f"Skipped disclosing {self.skipped_domain_contacts_count} contacts with security email "
|
||||||
|
f"registrar@dotgov.gov."
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"Error disclosing the following {len(self.domains_with_errors)} contacts: {self.domains_with_errors}"
|
||||||
|
)
|
37
src/registrar/migrations/0063_veryimportantperson.py
Normal file
37
src/registrar/migrations/0063_veryimportantperson.py
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
# Generated by Django 4.2.7 on 2024-01-23 23:51
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0062_alter_host_name"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="VeryImportantPerson",
|
||||||
|
fields=[
|
||||||
|
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("updated_at", models.DateTimeField(auto_now=True)),
|
||||||
|
("email", models.EmailField(db_index=True, help_text="Email", max_length=254)),
|
||||||
|
("notes", models.TextField(help_text="Notes")),
|
||||||
|
(
|
||||||
|
"requestor",
|
||||||
|
models.ForeignKey(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="verifiedby_user",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -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"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -13,6 +13,7 @@ from .user import User
|
||||||
from .user_group import UserGroup
|
from .user_group import UserGroup
|
||||||
from .website import Website
|
from .website import Website
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
|
from .very_important_person import VeryImportantPerson
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Contact",
|
"Contact",
|
||||||
|
@ -29,6 +30,7 @@ __all__ = [
|
||||||
"UserGroup",
|
"UserGroup",
|
||||||
"Website",
|
"Website",
|
||||||
"TransitionDomain",
|
"TransitionDomain",
|
||||||
|
"VeryImportantPerson",
|
||||||
]
|
]
|
||||||
|
|
||||||
auditlog.register(Contact)
|
auditlog.register(Contact)
|
||||||
|
@ -45,3 +47,4 @@ auditlog.register(User, m2m_fields=["user_permissions", "groups"])
|
||||||
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
auditlog.register(UserGroup, m2m_fields=["permissions"])
|
||||||
auditlog.register(Website)
|
auditlog.register(Website)
|
||||||
auditlog.register(TransitionDomain)
|
auditlog.register(TransitionDomain)
|
||||||
|
auditlog.register(VeryImportantPerson)
|
||||||
|
|
|
@ -1396,11 +1396,13 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
def _disclose_fields(self, contact: PublicContact):
|
def _disclose_fields(self, contact: PublicContact):
|
||||||
"""creates a disclose object that can be added to a contact Create using
|
"""creates a disclose object that can be added to a contact Create using
|
||||||
.disclose= <this function> on the command before sending.
|
.disclose= <this function> on the command before sending.
|
||||||
if item is security email then make sure email is visable"""
|
if item is security email then make sure email is visible"""
|
||||||
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
|
is_security = contact.contact_type == contact.ContactTypeChoices.SECURITY
|
||||||
DF = epp.DiscloseField
|
DF = epp.DiscloseField
|
||||||
fields = {DF.EMAIL}
|
fields = {DF.EMAIL}
|
||||||
disclose = is_security and contact.email != PublicContact.get_default_security().email
|
disclose = is_security and contact.email != PublicContact.get_default_security().email
|
||||||
|
# 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
|
# Will only disclose DF.EMAIL if its not the default
|
||||||
return epp.Disclose(
|
return epp.Disclose(
|
||||||
flag=disclose,
|
flag=disclose,
|
||||||
|
|
|
@ -431,11 +431,13 @@ class DomainApplication(TimeStampedModel):
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address",
|
help_text="Street address",
|
||||||
|
verbose_name="Address line 1",
|
||||||
)
|
)
|
||||||
address_line2 = models.TextField(
|
address_line2 = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Street address line 2 (optional)",
|
help_text="Street address line 2 (optional)",
|
||||||
|
verbose_name="Address line 2",
|
||||||
)
|
)
|
||||||
city = models.TextField(
|
city = models.TextField(
|
||||||
null=True,
|
null=True,
|
||||||
|
|
|
@ -7,6 +7,7 @@ from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
|
||||||
from .domain_invitation import DomainInvitation
|
from .domain_invitation import DomainInvitation
|
||||||
from .transition_domain import TransitionDomain
|
from .transition_domain import TransitionDomain
|
||||||
|
from .very_important_person import VeryImportantPerson
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
|
|
||||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||||
|
@ -89,6 +90,10 @@ class User(AbstractUser):
|
||||||
if TransitionDomain.objects.filter(username=email).exists():
|
if TransitionDomain.objects.filter(username=email).exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# New users flagged by Staff to bypass ial2
|
||||||
|
if VeryImportantPerson.objects.filter(email=email).exists():
|
||||||
|
return False
|
||||||
|
|
||||||
# A new incoming user who is being invited to be a domain manager (that is,
|
# A new incoming user who is being invited to be a domain manager (that is,
|
||||||
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
# their email address is in DomainInvitation for an invitation that is not yet "retrieved").
|
||||||
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
invited = DomainInvitation.DomainInvitationStatus.INVITED
|
||||||
|
|
32
src/registrar/models/very_important_person.py
Normal file
32
src/registrar/models/very_important_person.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPerson(TimeStampedModel):
|
||||||
|
|
||||||
|
"""emails that get added to this table will bypass ial2 on login."""
|
||||||
|
|
||||||
|
email = models.EmailField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text="Email",
|
||||||
|
db_index=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
requestor = models.ForeignKey(
|
||||||
|
"registrar.User",
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
related_name="verifiedby_user",
|
||||||
|
)
|
||||||
|
|
||||||
|
notes = models.TextField(
|
||||||
|
null=False,
|
||||||
|
blank=False,
|
||||||
|
help_text="Notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static form_helpers url_helpers %}
|
{% load static form_helpers url_helpers %}
|
||||||
|
|
||||||
{% block title %}Apply for a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
{% block title %}Request a .gov domain | {{form_titles|get_item:steps.current}} | {% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
<h2>Time to complete the form</h2>
|
<h2>Time to complete the form</h2>
|
||||||
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
|
||||||
completing your domain request might take around 15 minutes.</p>
|
completing your domain request might take around 15 minutes.</p>
|
||||||
<p><a href="{% public_site_url 'contact/' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Contact us if you need help with your request</a>.</p>
|
|
||||||
|
|
||||||
{% block form_buttons %}
|
{% block form_buttons %}
|
||||||
<div class="stepnav">
|
<div class="stepnav">
|
||||||
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="caption margin-top-5"><a href="{% public_site_url 'privacy-policy#pra' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Paperwork Reduction Act statement</a> (OMB control number: 1670-0049; expiration date: 10/31/2026)</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{% extends 'application_form.html' %}
|
{% extends 'application_form.html' %}
|
||||||
{% load field_helpers %}
|
{% load field_helpers url_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="https://beta.get.gov/about/data/" target="_blank">.gov’s public data.</a></p>
|
<p>If your domain request is approved, the name of your organization and your city/state will be listed in <a href="{% public_site_url 'about/data/' %}" target="_blank">.gov’s public data.</a></p>
|
||||||
|
|
||||||
<h2>What is the name and mailing address of the organization you represent?</h2>
|
<h2>What is the name and mailing address of the organization you represent?</h2>
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
{% load static field_helpers %}
|
{% load static field_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization.
|
<p>To help us determine your organization’s eligibility for a .gov domain, it’s helpful to have contact information for other employees from your organization.
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request. </li>
|
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request.</li>
|
||||||
<li>They don't need to be involved with the technical management of your domain (although they can be). </li>
|
<li>They don’t need to be involved with the technical management of your domain (although they can be).</li>
|
||||||
<li>We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate first with you, the requestor. </li>
|
<li><strong>We typically don’t reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -88,9 +88,7 @@
|
||||||
<legend>
|
<legend>
|
||||||
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
|
<h2 class="margin-bottom-0">No other employees from your organization?</h2>
|
||||||
</legend>
|
</legend>
|
||||||
<p>You don't need to provide names of other employees now, but it may
|
<p>You don’t need to provide names of other employees now, but it may slow down our assessment of your eligibility. Describe why there are no other employees who can help verify your request.</p>
|
||||||
slow down our assessment of your eligibility. Describe why there are
|
|
||||||
no other employees who can help verify your request.</p>
|
|
||||||
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
{% with attr_maxlength=1000 add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.2.no_other_contacts_rationale %}
|
{% input_with_errors forms.2.no_other_contacts_rationale %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{% load field_helpers %}
|
{% load field_helpers %}
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating .gov domains.</p>
|
<p>Please read this page. Check the box at the bottom to show that you agree to the requirements for operating a .gov domain.</p>
|
||||||
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
<p>The .gov domain space exists to support a broad diversity of government missions. Generally, we don’t review or audit how government organizations use their registered domains. However, misuse of a .gov domain can reflect upon the integrity of the entire .gov space. There are categories of misuse that are statutorily prohibited or abusive in nature.</p>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -90,7 +90,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if domainapplication.current_websites.all %}
|
{% if domainapplication.current_websites.all %}
|
||||||
{% include "includes/summary_item.html" with title='Current website for your organization' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Current websites' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if domainapplication.requested_domain %}
|
{% if domainapplication.requested_domain %}
|
||||||
|
|
|
@ -70,19 +70,6 @@
|
||||||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||||
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
<a class="usa-skipnav" href="#main-content">Skip to main content</a>
|
||||||
|
|
||||||
{% if IS_DEMO_SITE %}
|
|
||||||
<section aria-label="Alert" >
|
|
||||||
<div class="usa-alert usa-alert--info">
|
|
||||||
<div class="usa-alert__body">
|
|
||||||
<h4 class="usa-alert__heading">New domain requests are paused</h4>
|
|
||||||
<p class="usa-alert__text measure-none">
|
|
||||||
This is the new registrar for managing .gov domains. Note that we’re not accepting requests for new .gov domains until January 2024. Follow .gov updates at <a href="https://get.gov/updates/" class="usa-link">get.gov/updates/</a>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<section class="usa-banner" aria-label="Official website of the United States government">
|
<section class="usa-banner" aria-label="Official website of the United States government">
|
||||||
<div class="usa-accordion">
|
<div class="usa-accordion">
|
||||||
<header class="usa-banner__header">
|
<header class="usa-banner__header">
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
{% if widget.attrs.required %}
|
{% if widget.attrs.required %}
|
||||||
<!--Don't add asterisk to one-field forms -->
|
<!--Don't add asterisk to one-field forms -->
|
||||||
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating .gov domains." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% extends "domain_base.html" %}
|
{% extends "domain_base.html" %}
|
||||||
{% load static field_helpers %}
|
{% load static field_helpers %}
|
||||||
|
|
||||||
{% block title %}Add another user | {% endblock %}
|
{% block title %}Add a domain manager | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
<h1>Add a domain manager</h1>
|
<h1>Add a domain manager</h1>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="usa-button"
|
class="usa-button"
|
||||||
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov"%}Add security email{% else %}Save{% endif %}</button>
|
>{% if form.security_email.value is None or form.security_email.value == "dotgov@cisa.dhs.gov" or form.security_email.value == "registrar@dotgov.gov"%}Add security email{% else %}Save{% endif %}</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% endblock %} {# domain_content #}
|
{% endblock %} {# domain_content #}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
Hi.
|
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>.
|
You can manage this domain on the .gov registrar <https://manage.get.gov>.
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ About your organization:
|
||||||
Authorizing official:
|
Authorizing official:
|
||||||
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
{% spaceless %}{% include "emails/includes/contact.txt" with contact=application.authorizing_official %}{% endspaceless %}
|
||||||
{% if application.current_websites.exists %}{# if block makes a newline #}
|
{% if application.current_websites.exists %}{# if block makes a newline #}
|
||||||
Current website for your organization: {% for site in application.current_websites.all %}
|
Current websites: {% for site in application.current_websites.all %}
|
||||||
{% spaceless %}{{ site.website }}{% endspaceless %}
|
{% spaceless %}{{ site.website }}{% endspaceless %}
|
||||||
{% endfor %}{% endif %}
|
{% endfor %}{% endif %}
|
||||||
.gov domain:
|
.gov domain:
|
||||||
|
|
|
@ -32,6 +32,14 @@ Learn more about:
|
||||||
- Domain security best practices <https://get.gov/domains/security/>
|
- Domain security best practices <https://get.gov/domains/security/>
|
||||||
|
|
||||||
|
|
||||||
|
WE’LL PRELOAD THIS DOMAIN
|
||||||
|
We add new .gov domains to the HSTS preload list each month. This requires browsers to use a secure HTTPS connection to any website at this domain and ensures the content you publish is exactly what your visitors get. It also means you’ll need to support HTTPS anywhere the domain is used for websites – on the internet or internally. We’ll add your domain to the preload list soon.
|
||||||
|
|
||||||
|
Learn more about:
|
||||||
|
- What preloading is <https://get.gov/domains/security/#preload-your-domain>
|
||||||
|
- Why we preload new domains <https://get.gov/posts/2021-06-21-an-intent-to-preload/>
|
||||||
|
|
||||||
|
|
||||||
THANK YOU
|
THANK YOU
|
||||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
|
||||||
|
|
||||||
|
|
|
@ -9,29 +9,17 @@
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
{# the entire logged in page goes here #}
|
{# the entire logged in page goes here #}
|
||||||
|
|
||||||
<div class="tablet:grid-offset-1 desktop:grid-offset-2">
|
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||||
<h1>Manage your domains</h2>
|
<h1>Manage your domains</h2>
|
||||||
|
|
||||||
<p class="margin-top-4">
|
<p class="margin-top-4">
|
||||||
{% if IS_PRODUCTION %}
|
|
||||||
<a href="javascript:void(0)"
|
|
||||||
class="usa-button usa-tooltip disabled-link"
|
|
||||||
data-position="right"
|
|
||||||
title="Coming in 2024"
|
|
||||||
aria-disabled="true"
|
|
||||||
data-tooltip="true"
|
|
||||||
>
|
|
||||||
Start a new domain request
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<a href="{% url 'application:' %}" class="usa-button"
|
<a href="{% url 'application:' %}" class="usa-button"
|
||||||
>
|
>
|
||||||
Start a new domain request
|
Start a new domain request
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined">
|
||||||
<h2>Domains</h2>
|
<h2>Domains</h2>
|
||||||
{% if domains %}
|
{% if domains %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
|
@ -41,7 +29,12 @@
|
||||||
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
<th data-sortable scope="col" role="columnheader">Domain name</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Expires</th>
|
<th data-sortable scope="col" role="columnheader">Expires</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th
|
||||||
|
scope="col"
|
||||||
|
role="columnheader"
|
||||||
|
>
|
||||||
|
<span class="usa-sr-only">Action</span>
|
||||||
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -58,7 +51,7 @@
|
||||||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
||||||
DNS needed
|
DNS needed
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|capfirst }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
@ -94,7 +87,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined">
|
||||||
<h2>Domain requests</h2>
|
<h2>Domain requests</h2>
|
||||||
{% if domain_applications %}
|
{% if domain_applications %}
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||||
|
@ -105,13 +98,23 @@
|
||||||
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
<th data-sortable scope="col" role="columnheader">Date submitted</th>
|
||||||
<th data-sortable scope="col" role="columnheader">Status</th>
|
<th data-sortable scope="col" role="columnheader">Status</th>
|
||||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||||
|
{% if has_deletable_applications %}
|
||||||
|
<th scope="col" role="columnheader"><span class="usa-sr-only">Delete Action</span></th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for application in domain_applications %}
|
{% for application in domain_applications %}
|
||||||
<tr>
|
<tr>
|
||||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||||
{{ application.requested_domain.name|default:"New domain request" }}
|
{% if application.requested_domain is None %}
|
||||||
|
New domain request
|
||||||
|
{# Add a breakpoint #}
|
||||||
|
<div aria-hidden="true"></div>
|
||||||
|
<span class="text-base font-body-xs">({{ application.created_at }} UTC)</span>
|
||||||
|
{% else %}
|
||||||
|
{{ application.requested_domain.name }}
|
||||||
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
|
<td data-sort-value="{{ application.submission_date|date:"U" }}" data-label="Date submitted">
|
||||||
{% if application.submission_date %}
|
{% if application.submission_date %}
|
||||||
|
@ -122,22 +125,88 @@
|
||||||
</td>
|
</td>
|
||||||
<td data-label="Status">{{ application.get_status_display }}</td>
|
<td data-label="Status">{{ application.get_status_display }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
{% with prefix="New domain request ("%}
|
||||||
|
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||||
|
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||||
|
{% if application.status == application.ApplicationStatus.STARTED or application.status == application.ApplicationStatus.ACTION_NEEDED or application.status == application.ApplicationStatus.WITHDRAWN %}
|
||||||
<a href="{% url 'edit-application' application.pk %}">
|
<a href="{% url 'edit-application' application.pk %}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Edit <span class="usa-sr-only">{{ application.requested_domain.name|default:"New domain request" }} </span>
|
{% if application.requested_domain is not None%}
|
||||||
|
Edit <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||||
|
{% else %}
|
||||||
|
Edit <span class="usa-sr-only">{{ name_default }}</span>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'application-status' application.pk %}">
|
<a href="{% url 'application-status' application.pk %}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#settings"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Manage <span class="usa-sr-only">{{application.requested_domain.name}} </span>
|
Manage <span class="usa-sr-only">{{ application.requested_domain.name|default:name_default }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
{% if has_deletable_applications %}
|
||||||
|
<td>
|
||||||
|
{% if application.status == "started" or application.status == "withdrawn" %}
|
||||||
|
<a
|
||||||
|
role="button"
|
||||||
|
id="button-toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||||
|
href="#toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||||
|
class="usa-button--unstyled text-no-underline"
|
||||||
|
aria-controls="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||||
|
data-open-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
|
</svg>
|
||||||
|
{% with prefix="New domain request ("%}
|
||||||
|
{% with date=application.created_at|date:"DATETIME_FORMAT"%}
|
||||||
|
{% with name_default=prefix|add:date|add:" UTC)"%}
|
||||||
|
{% if application.requested_domain is not None %}
|
||||||
|
Delete <span class="usa-sr-only">{{ application.requested_domain.name }}</span>
|
||||||
|
{% else %}
|
||||||
|
Delete <span class="usa-sr-only">{{ name_default }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="toggle-delete-domain-alert-{{ forloop.counter }}"
|
||||||
|
aria-labelledby="Are you sure you want to continue?"
|
||||||
|
aria-describedby="Domain will be removed"
|
||||||
|
data-force-action
|
||||||
|
>
|
||||||
|
<form method="POST" action="{% url "application-delete" pk=application.id %}">
|
||||||
|
{% if application.requested_domain is None %}
|
||||||
|
{% if application.created_at %}
|
||||||
|
{% with prefix="(created " %}
|
||||||
|
{% with formatted_date=application.created_at|date:"DATETIME_FORMAT" %}
|
||||||
|
{% with modal_content=prefix|add:formatted_date|add:" UTC)" %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete this domain request?" modal_description="This will remove the domain request "|add:modal_content|add:" from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endwith %}
|
||||||
|
{% else %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete New domain request?" modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% with modal_heading_value=application.requested_domain.name|add:"?" %}
|
||||||
|
{% include 'includes/modal.html' with modal_heading="Are you sure you want to delete" heading_value=modal_heading_value modal_description="This will remove the domain request from the .gov registrar. This action cannot be undone." modal_button=modal_button|safe %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
|
@ -4,6 +4,11 @@
|
||||||
<div class="usa-modal__main">
|
<div class="usa-modal__main">
|
||||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||||
{{ modal_heading }}
|
{{ modal_heading }}
|
||||||
|
{% if heading_value is not None %}
|
||||||
|
{# Add a breakpoint #}
|
||||||
|
<div aria-hidden="true"></div>
|
||||||
|
{{ heading_value }}
|
||||||
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="usa-prose">
|
<div class="usa-prose">
|
||||||
<p id="modal-1-description">
|
<p id="modal-1-description">
|
||||||
|
|
|
@ -14,9 +14,11 @@ from registrar.admin import (
|
||||||
ContactAdmin,
|
ContactAdmin,
|
||||||
DomainInformationAdmin,
|
DomainInformationAdmin,
|
||||||
UserDomainRoleAdmin,
|
UserDomainRoleAdmin,
|
||||||
|
VeryImportantPersonAdmin,
|
||||||
)
|
)
|
||||||
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
from registrar.models import Domain, DomainApplication, DomainInformation, User, DomainInvitation, Contact, Website
|
||||||
from registrar.models.user_domain_role import UserDomainRole
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
from registrar.models.very_important_person import VeryImportantPerson
|
||||||
from .common import (
|
from .common import (
|
||||||
MockSESClient,
|
MockSESClient,
|
||||||
AuditedAdminMockData,
|
AuditedAdminMockData,
|
||||||
|
@ -1804,3 +1806,28 @@ class ContactAdminTest(TestCase):
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
Contact.objects.all().delete()
|
Contact.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class VeryImportantPersonAdminTestCase(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_save_model_sets_user_field(self):
|
||||||
|
self.client.force_login(self.superuser)
|
||||||
|
|
||||||
|
# Create an instance of the admin class
|
||||||
|
admin_instance = VeryImportantPersonAdmin(model=VeryImportantPerson, admin_site=None)
|
||||||
|
|
||||||
|
# Create a VeryImportantPerson instance
|
||||||
|
vip_instance = VeryImportantPerson(email="test@example.com", notes="Test Notes")
|
||||||
|
|
||||||
|
# Create a request object
|
||||||
|
request = self.factory.post("/admin/yourapp/veryimportantperson/add/")
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
# Call the save_model method
|
||||||
|
admin_instance.save_model(request, vip_instance, None, None)
|
||||||
|
|
||||||
|
# Check that the user field is set to the request.user
|
||||||
|
self.assertEqual(vip_instance.requestor, self.superuser)
|
||||||
|
|
|
@ -47,7 +47,7 @@ class TestEmails(TestCase):
|
||||||
# check for optional things
|
# check for optional things
|
||||||
self.assertIn("Other employees from your organization:", body)
|
self.assertIn("Other employees from your organization:", body)
|
||||||
self.assertIn("Testy2 Tester2", body)
|
self.assertIn("Testy2 Tester2", body)
|
||||||
self.assertIn("Current website for your organization:", body)
|
self.assertIn("Current websites:", body)
|
||||||
self.assertIn("city.com", body)
|
self.assertIn("city.com", body)
|
||||||
self.assertIn("About your organization:", body)
|
self.assertIn("About your organization:", body)
|
||||||
self.assertIn("Anything else", body)
|
self.assertIn("Anything else", body)
|
||||||
|
@ -61,7 +61,7 @@ class TestEmails(TestCase):
|
||||||
application.submit()
|
application.submit()
|
||||||
_, kwargs = self.mock_client.send_email.call_args
|
_, kwargs = self.mock_client.send_email.call_args
|
||||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertNotIn("Current website for your organization:", body)
|
self.assertNotIn("Current websites:", body)
|
||||||
# spacing should be right between adjacent elements
|
# spacing should be right between adjacent elements
|
||||||
self.assertRegex(body, r"5555\n\n.gov domain:")
|
self.assertRegex(body, r"5555\n\n.gov domain:")
|
||||||
|
|
||||||
|
@ -74,9 +74,9 @@ class TestEmails(TestCase):
|
||||||
application.submit()
|
application.submit()
|
||||||
_, kwargs = self.mock_client.send_email.call_args
|
_, kwargs = self.mock_client.send_email.call_args
|
||||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("Current website for your organization:", body)
|
self.assertIn("Current websites:", body)
|
||||||
# spacing should be right between adjacent elements
|
# spacing should be right between adjacent elements
|
||||||
self.assertRegex(body, r"5555\n\nCurrent website for")
|
self.assertRegex(body, r"5555\n\nCurrent websites:")
|
||||||
self.assertRegex(body, r"city.com\n\n.gov domain:")
|
self.assertRegex(body, r"city.com\n\n.gov domain:")
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
|
|
|
@ -101,7 +101,7 @@ class TestFormValidation(MockEppLib):
|
||||||
(
|
(
|
||||||
"whitehouse.gov",
|
"whitehouse.gov",
|
||||||
"That domain isn’t available. <a class='usa-link' "
|
"That domain isn’t available. <a class='usa-link' "
|
||||||
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
|
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
|
||||||
"choosing your .gov domain</a>.",
|
"choosing your .gov domain</a>.",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -151,7 +151,7 @@ class TestFormValidation(MockEppLib):
|
||||||
(
|
(
|
||||||
"whitehouse.gov",
|
"whitehouse.gov",
|
||||||
"That domain isn’t available. <a class='usa-link' "
|
"That domain isn’t available. <a class='usa-link' "
|
||||||
"href='https://beta.get.gov/domains/choosing' target='_blank'>Read more about "
|
"href='https://get.gov/domains/choosing' target='_blank'>Read more about "
|
||||||
"choosing your .gov domain</a>.",
|
"choosing your .gov domain</a>.",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -338,7 +338,7 @@ class TestFormValidation(MockEppLib):
|
||||||
form = RequirementsForm(data={})
|
form = RequirementsForm(data={})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["is_policy_acknowledged"],
|
form.errors["is_policy_acknowledged"],
|
||||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_requirements_form_unchecked(self):
|
def test_requirements_form_unchecked(self):
|
||||||
|
@ -346,7 +346,7 @@ class TestFormValidation(MockEppLib):
|
||||||
form = RequirementsForm(data={"is_policy_acknowledged": False})
|
form = RequirementsForm(data={"is_policy_acknowledged": False})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
form.errors["is_policy_acknowledged"],
|
form.errors["is_policy_acknowledged"],
|
||||||
["Check the box if you read and agree to the requirements for operating .gov domains."],
|
["Check the box if you read and agree to the requirements for operating a .gov domain."],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_tribal_government_unrecognized(self):
|
def test_tribal_government_unrecognized(self):
|
||||||
|
|
|
@ -11,9 +11,11 @@ from registrar.models import (
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
)
|
)
|
||||||
|
from registrar.models.public_contact import PublicContact
|
||||||
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch, call
|
||||||
|
from epplibwrapper import commands, common
|
||||||
|
|
||||||
from .common import MockEppLib
|
from .common import MockEppLib
|
||||||
|
|
||||||
|
@ -441,3 +443,57 @@ class TestExtendExpirationDates(MockEppLib):
|
||||||
|
|
||||||
# Explicitly test the expiration date - should be the same
|
# Explicitly test the expiration date - should be the same
|
||||||
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
|
self.assertEqual(desired_domain.expiration_date, datetime.date(2024, 11, 15))
|
||||||
|
|
||||||
|
|
||||||
|
class TestDiscloseEmails(MockEppLib):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
PublicContact.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
|
||||||
|
def run_disclose_security_emails(self):
|
||||||
|
"""
|
||||||
|
This method executes the disclose_security_emails command.
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
|
@ -15,7 +15,8 @@ from registrar.models import (
|
||||||
)
|
)
|
||||||
|
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.models.transition_domain import TransitionDomain # type: ignore
|
from registrar.models.transition_domain import TransitionDomain
|
||||||
|
from registrar.models.very_important_person import VeryImportantPerson # type: ignore
|
||||||
from .common import MockSESClient, less_console_noise, completed_application
|
from .common import MockSESClient, less_console_noise, completed_application
|
||||||
from django_fsm import TransitionNotAllowed
|
from django_fsm import TransitionNotAllowed
|
||||||
|
|
||||||
|
@ -652,6 +653,12 @@ class TestUser(TestCase):
|
||||||
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
|
TransitionDomain.objects.get_or_create(username=self.user.email, domain_name=self.domain_name)
|
||||||
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||||
|
|
||||||
|
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)
|
||||||
|
self.assertFalse(User.needs_identity_verification(self.user.email, self.user.username))
|
||||||
|
|
||||||
def test_identity_verification_with_invited_user(self):
|
def test_identity_verification_with_invited_user(self):
|
||||||
"""An invited user should return False when tested with class
|
"""An invited user should return False when tested with class
|
||||||
method needs_identity_verification"""
|
method needs_identity_verification"""
|
||||||
|
|
|
@ -399,7 +399,7 @@ class ExportDataTest(MockEppLib):
|
||||||
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
"adomain10.gov,Federal,Armed Forces Retirement Home,Ready\n"
|
||||||
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
"adomain2.gov,Interstate,(blank),Dns needed\n"
|
||||||
"ddomain3.gov,Federal,Armed Forces Retirement Home,123@mail.gov,On hold,2023-05-25\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,dotgov@cisa.dhs.gov,Ready"
|
"defaultsecurity.gov,Federal - Executive,World War I Centennial Commission,(blank),Ready"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
|
|
@ -89,17 +89,230 @@ class LoggedInTests(TestWithUser):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.client.force_login(self.user)
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
super().tearDown()
|
||||||
|
Contact.objects.all().delete()
|
||||||
|
|
||||||
def test_home_lists_domain_applications(self):
|
def test_home_lists_domain_applications(self):
|
||||||
response = self.client.get("/")
|
response = self.client.get("/")
|
||||||
self.assertNotContains(response, "igorville.gov")
|
self.assertNotContains(response, "igorville.gov")
|
||||||
site = DraftDomain.objects.create(name="igorville.gov")
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
|
application = DomainApplication.objects.create(creator=self.user, requested_domain=site)
|
||||||
response = self.client.get("/")
|
response = self.client.get("/")
|
||||||
# count = 2 because it is also in screenreader content
|
|
||||||
self.assertContains(response, "igorville.gov", count=2)
|
# count = 7 because of screenreader content
|
||||||
|
self.assertContains(response, "igorville.gov", count=7)
|
||||||
|
|
||||||
# clean up
|
# clean up
|
||||||
application.delete()
|
application.delete()
|
||||||
|
|
||||||
|
def test_home_deletes_withdrawn_domain_application(self):
|
||||||
|
"""Tests if the user can delete a DomainApplication in the 'withdrawn' status"""
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.WITHDRAWN
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||||
|
self.assertContains(home_page, "Delete")
|
||||||
|
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
application.delete()
|
||||||
|
|
||||||
|
def test_home_deletes_started_domain_application(self):
|
||||||
|
"""Tests if the user can delete a DomainApplication in the 'started' status"""
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user, requested_domain=site, status=DomainApplication.ApplicationStatus.STARTED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the delete button exists. We can do this by checking for its id and text content.
|
||||||
|
self.assertContains(home_page, "Delete")
|
||||||
|
self.assertContains(home_page, "button-toggle-delete-domain-alert-1")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
application.delete()
|
||||||
|
|
||||||
|
def test_home_doesnt_delete_other_domain_applications(self):
|
||||||
|
"""Tests to ensure the user can't delete Applications not in the status of STARTED or WITHDRAWN"""
|
||||||
|
|
||||||
|
# Given that we are including a subset of items that can be deleted while excluding the rest,
|
||||||
|
# subTest is appropriate here as otherwise we would need many duplicate tests for the same reason.
|
||||||
|
draft_domain = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
for status in DomainApplication.ApplicationStatus:
|
||||||
|
if status not in [
|
||||||
|
DomainApplication.ApplicationStatus.STARTED,
|
||||||
|
DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||||
|
]:
|
||||||
|
with self.subTest(status=status):
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user, requested_domain=draft_domain, status=status
|
||||||
|
)
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("application-delete", kwargs={"pk": application.pk}), follow=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for a 403 error - the end user should not be allowed to do this
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
desired_application = DomainApplication.objects.filter(requested_domain=draft_domain)
|
||||||
|
|
||||||
|
# Make sure the DomainApplication wasn't deleted
|
||||||
|
self.assertEqual(desired_application.count(), 1)
|
||||||
|
|
||||||
|
# clean up
|
||||||
|
application.delete()
|
||||||
|
|
||||||
|
def test_home_deletes_domain_application_and_orphans(self):
|
||||||
|
"""Tests if delete for DomainApplication deletes orphaned Contact objects"""
|
||||||
|
|
||||||
|
# Create the site and contacts to delete (orphaned)
|
||||||
|
contact = Contact.objects.create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="Mcfakerson",
|
||||||
|
)
|
||||||
|
contact_shared = Contact.objects.create(
|
||||||
|
first_name="Relative",
|
||||||
|
last_name="Aether",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two non-orphaned contacts
|
||||||
|
contact_2 = Contact.objects.create(
|
||||||
|
first_name="Saturn",
|
||||||
|
last_name="Mars",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach a user object to a contact (should not be deleted)
|
||||||
|
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site,
|
||||||
|
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||||
|
authorizing_official=contact,
|
||||||
|
submitter=contact_user,
|
||||||
|
)
|
||||||
|
application.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
# Create a second application to attach contacts to
|
||||||
|
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||||
|
application_2 = DomainApplication.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site_2,
|
||||||
|
status=DomainApplication.ApplicationStatus.STARTED,
|
||||||
|
authorizing_official=contact_2,
|
||||||
|
submitter=contact_shared,
|
||||||
|
)
|
||||||
|
application_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
# Ensure that igorville.gov exists on the page
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("application-delete", kwargs={"pk": application.pk}), follow=True)
|
||||||
|
|
||||||
|
# igorville is now deleted
|
||||||
|
self.assertNotContains(response, "igorville.gov")
|
||||||
|
|
||||||
|
# Check if the orphaned contact was deleted
|
||||||
|
orphan = Contact.objects.filter(id=contact.id)
|
||||||
|
self.assertFalse(orphan.exists())
|
||||||
|
|
||||||
|
# All non-orphan contacts should still exist and are unaltered
|
||||||
|
try:
|
||||||
|
current_user = Contact.objects.filter(id=contact_user.id).get()
|
||||||
|
except Contact.DoesNotExist:
|
||||||
|
self.fail("contact_user (a non-orphaned contact) was deleted")
|
||||||
|
|
||||||
|
self.assertEqual(current_user, contact_user)
|
||||||
|
try:
|
||||||
|
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||||
|
except Contact.DoesNotExist:
|
||||||
|
self.fail("contact_2 (a non-orphaned contact) was deleted")
|
||||||
|
|
||||||
|
self.assertEqual(edge_case, contact_2)
|
||||||
|
|
||||||
|
def test_home_deletes_domain_application_and_shared_orphans(self):
|
||||||
|
"""Test the edge case for an object that will become orphaned after a delete
|
||||||
|
(but is not an orphan at the time of deletion)"""
|
||||||
|
|
||||||
|
# Create the site and contacts to delete (orphaned)
|
||||||
|
contact = Contact.objects.create(
|
||||||
|
first_name="Henry",
|
||||||
|
last_name="Mcfakerson",
|
||||||
|
)
|
||||||
|
contact_shared = Contact.objects.create(
|
||||||
|
first_name="Relative",
|
||||||
|
last_name="Aether",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create two non-orphaned contacts
|
||||||
|
contact_2 = Contact.objects.create(
|
||||||
|
first_name="Saturn",
|
||||||
|
last_name="Mars",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach a user object to a contact (should not be deleted)
|
||||||
|
contact_user, _ = Contact.objects.get_or_create(user=self.user)
|
||||||
|
|
||||||
|
site = DraftDomain.objects.create(name="igorville.gov")
|
||||||
|
application = DomainApplication.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site,
|
||||||
|
status=DomainApplication.ApplicationStatus.WITHDRAWN,
|
||||||
|
authorizing_official=contact,
|
||||||
|
submitter=contact_user,
|
||||||
|
)
|
||||||
|
application.other_contacts.set([contact_2])
|
||||||
|
|
||||||
|
# Create a second application to attach contacts to
|
||||||
|
site_2 = DraftDomain.objects.create(name="teaville.gov")
|
||||||
|
application_2 = DomainApplication.objects.create(
|
||||||
|
creator=self.user,
|
||||||
|
requested_domain=site_2,
|
||||||
|
status=DomainApplication.ApplicationStatus.STARTED,
|
||||||
|
authorizing_official=contact_2,
|
||||||
|
submitter=contact_shared,
|
||||||
|
)
|
||||||
|
application_2.other_contacts.set([contact_shared])
|
||||||
|
|
||||||
|
home_page = self.client.get("/")
|
||||||
|
self.assertContains(home_page, "teaville.gov")
|
||||||
|
|
||||||
|
# Trigger the delete logic
|
||||||
|
response = self.client.post(reverse("application-delete", kwargs={"pk": application_2.pk}), follow=True)
|
||||||
|
|
||||||
|
self.assertNotContains(response, "teaville.gov")
|
||||||
|
|
||||||
|
# Check if the orphaned contact was deleted
|
||||||
|
orphan = Contact.objects.filter(id=contact_shared.id)
|
||||||
|
self.assertFalse(orphan.exists())
|
||||||
|
|
||||||
def test_application_form_view(self):
|
def test_application_form_view(self):
|
||||||
response = self.client.get("/request/", follow=True)
|
response = self.client.get("/request/", follow=True)
|
||||||
self.assertContains(
|
self.assertContains(
|
||||||
|
@ -2590,7 +2803,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
)
|
)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester_non_existent(self):
|
def test_domain_invitation_email_has_email_as_requestor_non_existent(self):
|
||||||
"""Inviting a non existent user sends them an email, with email as the name."""
|
"""Inviting a non existent user sends them an email, with email as the name."""
|
||||||
# make sure there is no user with this email
|
# make sure there is no user with this email
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2623,13 +2836,13 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("info@example.com", email_content)
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester(self):
|
def test_domain_invitation_email_has_email_as_requestor(self):
|
||||||
"""Inviting a user sends them an email, with email as the name."""
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
# Create a fake user object
|
# Create a fake user object
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2662,13 +2875,13 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("info@example.com", email_content)
|
self.assertIn("info@example.com", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_domain_invitation_email_has_email_as_requester_staff(self):
|
def test_domain_invitation_email_has_email_as_requestor_staff(self):
|
||||||
"""Inviting a user sends them an email, with email as the name."""
|
"""Inviting a user sends them an email, with email as the name."""
|
||||||
# Create a fake user object
|
# Create a fake user object
|
||||||
email_address = "mayor@igorville.gov"
|
email_address = "mayor@igorville.gov"
|
||||||
|
@ -2705,7 +2918,7 @@ class TestDomainManagers(TestDomainOverview):
|
||||||
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn("help@get.gov", email_content)
|
self.assertIn("help@get.gov", email_content)
|
||||||
|
|
||||||
# Check that the requesters first/last name do not exist
|
# Check that the requestors first/last name do not exist
|
||||||
self.assertNotIn("First", email_content)
|
self.assertNotIn("First", email_content)
|
||||||
self.assertNotIn("Last", email_content)
|
self.assertNotIn("Last", email_content)
|
||||||
self.assertNotIn("First Last", email_content)
|
self.assertNotIn("First Last", email_content)
|
||||||
|
|
|
@ -38,7 +38,7 @@ def write_row(writer, columns, domain_info):
|
||||||
if security_contacts:
|
if security_contacts:
|
||||||
security_email = security_contacts[0].email
|
security_email = security_contacts[0].email
|
||||||
|
|
||||||
invalid_emails = {"registrar@dotgov.gov"}
|
invalid_emails = {"registrar@dotgov.gov", "dotgov@cisa.dhs.gov"}
|
||||||
# These are default emails that should not be displayed in the csv report
|
# 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:
|
if security_email is not None and security_email.lower() in invalid_emails:
|
||||||
security_email = "(blank)"
|
security_email = "(blank)"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||||
from django.shortcuts import redirect, render
|
from django.shortcuts import redirect, render
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
|
@ -10,8 +10,11 @@ from django.contrib import messages
|
||||||
|
|
||||||
from registrar.forms import application_wizard as forms
|
from registrar.forms import application_wizard as forms
|
||||||
from registrar.models import DomainApplication
|
from registrar.models import DomainApplication
|
||||||
|
from registrar.models.contact import Contact
|
||||||
|
from registrar.models.user import User
|
||||||
from registrar.utility import StrEnum
|
from registrar.utility import StrEnum
|
||||||
from registrar.views.utility import StepsHelper
|
from registrar.views.utility import StepsHelper
|
||||||
|
from registrar.views.utility.permission_views import DomainApplicationPermissionDeleteView
|
||||||
|
|
||||||
from .utility import (
|
from .utility import (
|
||||||
DomainApplicationPermissionView,
|
DomainApplicationPermissionView,
|
||||||
|
@ -83,13 +86,13 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
|
Step.ORGANIZATION_CONTACT: _("Organization name and mailing address"),
|
||||||
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
Step.ABOUT_YOUR_ORGANIZATION: _("About your organization"),
|
||||||
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"),
|
Step.AUTHORIZING_OFFICIAL: _("Authorizing official"),
|
||||||
Step.CURRENT_SITES: _("Current website for your organization"),
|
Step.CURRENT_SITES: _("Current websites"),
|
||||||
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
||||||
Step.PURPOSE: _("Purpose of your domain"),
|
Step.PURPOSE: _("Purpose of your domain"),
|
||||||
Step.YOUR_CONTACT: _("Your contact information"),
|
Step.YOUR_CONTACT: _("Your contact information"),
|
||||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||||
Step.ANYTHING_ELSE: _("Anything else?"),
|
Step.ANYTHING_ELSE: _("Anything else?"),
|
||||||
Step.REQUIREMENTS: _("Requirements for operating .gov domains"),
|
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||||
Step.REVIEW: _("Review and submit your domain request"),
|
Step.REVIEW: _("Review and submit your domain request"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,20 +131,26 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
if self._application:
|
if self._application:
|
||||||
return self._application
|
return self._application
|
||||||
|
|
||||||
|
# For linter. The else block should never be hit, but if it does,
|
||||||
|
# there may be a UI consideration. That will need to be handled in another ticket.
|
||||||
|
creator = None
|
||||||
|
if self.request.user is not None and isinstance(self.request.user, User):
|
||||||
|
creator = self.request.user
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid value for User")
|
||||||
|
|
||||||
if self.has_pk():
|
if self.has_pk():
|
||||||
id = self.storage["application_id"]
|
id = self.storage["application_id"]
|
||||||
try:
|
try:
|
||||||
self._application = DomainApplication.objects.get(
|
self._application = DomainApplication.objects.get(
|
||||||
creator=self.request.user, # type: ignore
|
creator=creator,
|
||||||
pk=id,
|
pk=id,
|
||||||
)
|
)
|
||||||
return self._application
|
return self._application
|
||||||
except DomainApplication.DoesNotExist:
|
except DomainApplication.DoesNotExist:
|
||||||
logger.debug("Application id %s did not have a DomainApplication" % id)
|
logger.debug("Application id %s did not have a DomainApplication" % id)
|
||||||
|
|
||||||
self._application = DomainApplication.objects.create(
|
self._application = DomainApplication.objects.create(creator=self.request.user)
|
||||||
creator=self.request.user, # type: ignore
|
|
||||||
)
|
|
||||||
|
|
||||||
self.storage["application_id"] = self._application.id
|
self.storage["application_id"] = self._application.id
|
||||||
return self._application
|
return self._application
|
||||||
|
@ -150,7 +159,6 @@ class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
def storage(self):
|
def storage(self):
|
||||||
# marking session as modified on every access
|
# marking session as modified on every access
|
||||||
# so that updates to nested keys are always saved
|
# so that updates to nested keys are always saved
|
||||||
# push to sandbox will remove
|
|
||||||
self.request.session.modified = True
|
self.request.session.modified = True
|
||||||
return self.request.session.setdefault(self.prefix, {})
|
return self.request.session.setdefault(self.prefix, {})
|
||||||
|
|
||||||
|
@ -611,3 +619,102 @@ class ApplicationWithdrawn(DomainApplicationPermissionWithdrawView):
|
||||||
application.withdraw()
|
application.withdraw()
|
||||||
application.save()
|
application.save()
|
||||||
return HttpResponseRedirect(reverse("home"))
|
return HttpResponseRedirect(reverse("home"))
|
||||||
|
|
||||||
|
|
||||||
|
class DomainApplicationDeleteView(DomainApplicationPermissionDeleteView):
|
||||||
|
"""Delete view for home that allows the end user to delete DomainApplications"""
|
||||||
|
|
||||||
|
object: DomainApplication # workaround for type mismatch in DeleteView
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Custom override for has_permission to exclude all statuses, except WITHDRAWN and STARTED"""
|
||||||
|
has_perm = super().has_permission()
|
||||||
|
if not has_perm:
|
||||||
|
return False
|
||||||
|
|
||||||
|
status = self.get_object().status
|
||||||
|
valid_statuses = [DomainApplication.ApplicationStatus.WITHDRAWN, DomainApplication.ApplicationStatus.STARTED]
|
||||||
|
if status not in valid_statuses:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_success_url(self):
|
||||||
|
"""After a delete is successful, redirect to home"""
|
||||||
|
return reverse("home")
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
# Grab all orphaned contacts
|
||||||
|
application: DomainApplication = self.get_object()
|
||||||
|
contacts_to_delete, duplicates = self._get_orphaned_contacts(application)
|
||||||
|
|
||||||
|
# Delete the DomainApplication
|
||||||
|
response = super().post(request, *args, **kwargs)
|
||||||
|
|
||||||
|
# Delete orphaned contacts - but only for if they are not associated with a user
|
||||||
|
Contact.objects.filter(id__in=contacts_to_delete, user=None).delete()
|
||||||
|
|
||||||
|
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||||
|
# This determines if any of these three fields share a contact, which is used for
|
||||||
|
# the edge case where the same user may be an AO, and a submitter, for example.
|
||||||
|
if len(duplicates) > 0:
|
||||||
|
duplicates_to_delete, _ = self._get_orphaned_contacts(application, check_db=True)
|
||||||
|
Contact.objects.filter(id__in=duplicates_to_delete, user=None).delete()
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def _get_orphaned_contacts(self, application: DomainApplication, check_db=False):
|
||||||
|
"""
|
||||||
|
Collects all orphaned contacts associated with a given DomainApplication object.
|
||||||
|
|
||||||
|
An orphaned contact is defined as a contact that is associated with the application,
|
||||||
|
but not with any other application. This includes the authorizing official, the submitter,
|
||||||
|
and any other contacts linked to the application.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
application (DomainApplication): The DomainApplication object for which to find orphaned contacts.
|
||||||
|
check_db (bool, optional): A flag indicating whether to check the database for the existence of the contacts.
|
||||||
|
Defaults to False.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: A tuple containing two lists. The first list contains the IDs of the orphaned contacts.
|
||||||
|
The second list contains any duplicate contacts found. ([Contacts], [Contacts])
|
||||||
|
"""
|
||||||
|
contacts_to_delete = []
|
||||||
|
|
||||||
|
# Get each contact object on the DomainApplication object
|
||||||
|
ao = application.authorizing_official
|
||||||
|
submitter = application.submitter
|
||||||
|
other_contacts = list(application.other_contacts.all())
|
||||||
|
other_contact_ids = application.other_contacts.all().values_list("id", flat=True)
|
||||||
|
|
||||||
|
# Check if the desired item still exists in the DB
|
||||||
|
if check_db:
|
||||||
|
ao = self._get_contacts_by_id([ao.id]).first() if ao is not None else None
|
||||||
|
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
|
||||||
|
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
||||||
|
|
||||||
|
# Pair each contact with its db related name for use in checking if it has joins
|
||||||
|
checked_contacts = [(ao, "authorizing_official"), (submitter, "submitted_applications")]
|
||||||
|
checked_contacts.extend((contact, "contact_applications") for contact in other_contacts)
|
||||||
|
|
||||||
|
for contact, related_name in checked_contacts:
|
||||||
|
if contact is not None and not contact.has_more_than_one_join(related_name):
|
||||||
|
contacts_to_delete.append(contact.id)
|
||||||
|
|
||||||
|
return (contacts_to_delete, self._get_duplicates(checked_contacts))
|
||||||
|
|
||||||
|
def _get_contacts_by_id(self, contact_ids):
|
||||||
|
"""Given a list of ids, grab contacts if it exists"""
|
||||||
|
contacts = Contact.objects.filter(id__in=contact_ids)
|
||||||
|
return contacts
|
||||||
|
|
||||||
|
def _get_duplicates(self, objects):
|
||||||
|
"""Given a list of objects, return a list of which items were duplicates"""
|
||||||
|
# Gets the occurence count
|
||||||
|
object_dict = defaultdict(int)
|
||||||
|
for contact, _related in objects:
|
||||||
|
object_dict[contact] += 1
|
||||||
|
|
||||||
|
duplicates = [item for item, count in object_dict.items() if count > 1]
|
||||||
|
return duplicates
|
||||||
|
|
|
@ -568,7 +568,9 @@ class DomainSecurityEmailView(DomainFormBaseView):
|
||||||
"""The initial value for the form."""
|
"""The initial value for the form."""
|
||||||
initial = super().get_initial()
|
initial = super().get_initial()
|
||||||
security_contact = self.object.security_contact
|
security_contact = self.object.security_contact
|
||||||
if security_contact is None or security_contact.email == "dotgov@cisa.dhs.gov":
|
|
||||||
|
invalid_emails = ["dotgov@cisa.dhs.gov", "registrar@dotgov.gov"]
|
||||||
|
if security_contact is None or security_contact.email in invalid_emails:
|
||||||
initial["security_email"] = None
|
initial["security_email"] = None
|
||||||
return initial
|
return initial
|
||||||
initial["security_email"] = security_contact.email
|
initial["security_email"] = security_contact.email
|
||||||
|
@ -646,7 +648,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
"""Get an absolute URL for this domain."""
|
"""Get an absolute URL for this domain."""
|
||||||
return self.request.build_absolute_uri(reverse("domain", kwargs={"pk": self.object.id}))
|
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,
|
"""Performs the sending of the domain invitation email,
|
||||||
does not make a domain information object
|
does not make a domain information object
|
||||||
email: string- email to send to
|
email: string- email to send to
|
||||||
|
@ -654,16 +656,16 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
adding a success message to the view if the email sending succeeds"""
|
adding a success message to the view if the email sending succeeds"""
|
||||||
|
|
||||||
# Set a default email address to send to for staff
|
# 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
|
# Check if the email requestor has a valid email address
|
||||||
if not requester.is_staff and requester.email is not None and requester.email.strip() != "":
|
if not requestor.is_staff and requestor.email is not None and requestor.email.strip() != "":
|
||||||
requester_email = requester.email
|
requestor_email = requestor.email
|
||||||
elif not requester.is_staff:
|
elif not requestor.is_staff:
|
||||||
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
messages.error(self.request, "Can't send invitation email. No email is associated with your account.")
|
||||||
logger.error(
|
logger.error(
|
||||||
f"Can't send email to '{email}' on domain '{self.object}'."
|
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,
|
exc_info=True,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
@ -676,7 +678,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
context={
|
context={
|
||||||
"domain_url": self._domain_abs_url(),
|
"domain_url": self._domain_abs_url(),
|
||||||
"domain": self.object,
|
"domain": self.object,
|
||||||
"requester_email": requester_email,
|
"requestor_email": requestor_email,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
except EmailSendingError:
|
except EmailSendingError:
|
||||||
|
@ -691,7 +693,7 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
if add_success:
|
if add_success:
|
||||||
messages.success(self.request, f"{email} has been invited to this domain.")
|
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."""
|
"""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)
|
invitation, created = DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||||
if not created:
|
if not created:
|
||||||
|
@ -701,22 +703,22 @@ class DomainAddUserView(DomainFormBaseView):
|
||||||
f"{email_address} has already been invited to this domain.",
|
f"{email_address} has already been invited to this domain.",
|
||||||
)
|
)
|
||||||
else:
|
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())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
"""Add the specified user on this domain."""
|
"""Add the specified user on this domain."""
|
||||||
requested_email = form.cleaned_data["email"]
|
requested_email = form.cleaned_data["email"]
|
||||||
requester = self.request.user
|
requestor = self.request.user
|
||||||
# look up a user with that email
|
# look up a user with that email
|
||||||
try:
|
try:
|
||||||
requested_user = User.objects.get(email=requested_email)
|
requested_user = User.objects.get(email=requested_email)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
# no matching user, go make an invitation
|
# no matching user, go make an invitation
|
||||||
return self._make_invitation(requested_email, requester)
|
return self._make_invitation(requested_email, requestor)
|
||||||
else:
|
else:
|
||||||
# if user already exists then just send an email
|
# 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:
|
try:
|
||||||
UserDomainRole.objects.create(
|
UserDomainRole.objects.create(
|
||||||
|
|
|
@ -7,15 +7,55 @@ def index(request):
|
||||||
"""This page is available to anyone without logging in."""
|
"""This page is available to anyone without logging in."""
|
||||||
context = {}
|
context = {}
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
applications = DomainApplication.objects.filter(creator=request.user)
|
# Get all domain applications the user has access to
|
||||||
|
applications, deletable_applications = _get_applications(request)
|
||||||
|
|
||||||
|
context["domain_applications"] = applications
|
||||||
|
|
||||||
|
# Get all domains the user has access to
|
||||||
|
domains = _get_domains(request)
|
||||||
|
context["domains"] = domains
|
||||||
|
|
||||||
|
# Determine if the user will see applications that they can delete
|
||||||
|
has_deletable_applications = deletable_applications.exists()
|
||||||
|
context["has_deletable_applications"] = has_deletable_applications
|
||||||
|
|
||||||
|
# If they can delete applications, add the delete button to the context
|
||||||
|
if has_deletable_applications:
|
||||||
|
# Add the delete modal button to the context
|
||||||
|
modal_button = (
|
||||||
|
'<button type="submit" '
|
||||||
|
'class="usa-button usa-button--secondary" '
|
||||||
|
'name="delete-application">Yes, delete request</button>'
|
||||||
|
)
|
||||||
|
context["modal_button"] = modal_button
|
||||||
|
|
||||||
|
return render(request, "home.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_applications(request):
|
||||||
|
"""Given the current request,
|
||||||
|
get all DomainApplications that are associated with the UserDomainRole object.
|
||||||
|
|
||||||
|
Returns a tuple of all applications, and those that are deletable by the user.
|
||||||
|
"""
|
||||||
# Let's exclude the approved applications since our
|
# Let's exclude the approved applications since our
|
||||||
# domain_applications context will be used to populate
|
# domain_applications context will be used to populate
|
||||||
# the active applications table
|
# the active applications table
|
||||||
context["domain_applications"] = applications.exclude(status="approved")
|
applications = DomainApplication.objects.filter(creator=request.user).exclude(
|
||||||
|
status=DomainApplication.ApplicationStatus.APPROVED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a placeholder DraftDomain for each incomplete draft
|
||||||
|
valid_statuses = [DomainApplication.ApplicationStatus.STARTED, DomainApplication.ApplicationStatus.WITHDRAWN]
|
||||||
|
deletable_applications = applications.filter(status__in=valid_statuses)
|
||||||
|
|
||||||
|
return (applications, deletable_applications)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_domains(request):
|
||||||
|
"""Given the current request,
|
||||||
|
get all domains that are associated with the UserDomainRole object"""
|
||||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
domains = Domain.objects.filter(id__in=domain_ids)
|
return Domain.objects.filter(id__in=domain_ids)
|
||||||
|
|
||||||
context["domains"] = domains
|
|
||||||
return render(request, "home.html", context)
|
|
||||||
|
|
|
@ -122,3 +122,11 @@ class DomainInvitationPermissionDeleteView(DomainInvitationPermission, DeleteVie
|
||||||
|
|
||||||
model = DomainInvitation
|
model = DomainInvitation
|
||||||
object: DomainInvitation # workaround for type mismatch in DeleteView
|
object: DomainInvitation # workaround for type mismatch in DeleteView
|
||||||
|
|
||||||
|
|
||||||
|
class DomainApplicationPermissionDeleteView(DomainApplicationPermission, DeleteView, abc.ABC):
|
||||||
|
|
||||||
|
"""Abstract view for deleting a DomainApplication."""
|
||||||
|
|
||||||
|
model = DomainApplication
|
||||||
|
object: DomainApplication
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue