mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-02 01:03:28 +02:00
merged with main and resolved conflict
This commit is contained in:
commit
1a680f6470
52 changed files with 1360 additions and 160 deletions
|
@ -1,6 +1,6 @@
|
||||||
# Get (your very own) .gov
|
# Infrastructure as a (public) service
|
||||||
|
|
||||||
Welcome to the repo for a WIP brand new registrar for .gov domains. Get.gov intends to serve all government entities in the United States looking for a .gov domain to use publicly (for a website, for an email address, etc.). Here you can find the code for the registrar and other artifacts about our product strategy and research.
|
The .gov domain helps U.S.-based government organizations gain public trust by being easily recognized online. This repo contains the code for the new .gov registrar – where governments request and manage domains – and other artifacts about our product strategy and research.
|
||||||
|
|
||||||
## Onboarding
|
## Onboarding
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,8 @@ applications:
|
||||||
DJANGO_BASE_URL: https://getgov-ab.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-ab.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# Public site base URL
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-ab.app.cloud.gov
|
- route: getgov-ab.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -20,8 +20,8 @@ applications:
|
||||||
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-bl.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# Public site base URL
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-bl.app.cloud.gov
|
- route: getgov-bl.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -20,8 +20,8 @@ applications:
|
||||||
DJANGO_BASE_URL: https://getgov-dk.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-dk.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# Public site base URL
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-dk.app.cloud.gov
|
- route: getgov-dk.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -20,8 +20,8 @@ applications:
|
||||||
DJANGO_BASE_URL: https://getgov-rjm.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-rjm.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# Public site base URL
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-rjm.app.cloud.gov
|
- route: getgov-rjm.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
|
@ -20,8 +20,8 @@ applications:
|
||||||
DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov
|
DJANGO_BASE_URL: https://getgov-stable.app.cloud.gov
|
||||||
# Tell Django how much stuff to log
|
# Tell Django how much stuff to log
|
||||||
DJANGO_LOG_LEVEL: INFO
|
DJANGO_LOG_LEVEL: INFO
|
||||||
# Public site base URL
|
# default public site location
|
||||||
GETGOV_PUBLIC_SITE_URL: https://federalist-877ab29f-16f6-4f12-961c-96cf064cf070.sites.pages.cloud.gov/site/cisagov/getgov-home/
|
GETGOV_PUBLIC_SITE_URL: https://beta.get.gov
|
||||||
routes:
|
routes:
|
||||||
- route: getgov-stable.app.cloud.gov
|
- route: getgov-stable.app.cloud.gov
|
||||||
services:
|
services:
|
||||||
|
|
128
ops/scripts/create_environment_migrate.sh
Executable file
128
ops/scripts/create_environment_migrate.sh
Executable file
|
@ -0,0 +1,128 @@
|
||||||
|
# This script sets up a completely new Cloud.gov CF Space with all the corresponding
|
||||||
|
# infrastructure needed to run get.gov. It can serve for documentation for running
|
||||||
|
# NOTE: This script was written for MacOS and to be run at the root directory
|
||||||
|
# of the repository. It uses `docker compose` to compile assets, so it is
|
||||||
|
# safest to `docker compose down` before using this script.
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo 'Please specify a name on the command line for the new space (i.e. lmm)' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! $(command -v gh) ] || [ ! $(command -v jq) ] || [ ! $(command -v cf) ]; then
|
||||||
|
echo "jq, cf, and gh packages must be installed. Please install via your preferred manager."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
upcase_name=$(printf "%s" "$1" | tr '[:lower:]' '[:upper:]')
|
||||||
|
|
||||||
|
read -p "Are you on a new branch? We will have to commit this work. (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
git checkout -b new-environment-$1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf target -o cisa-dotgov
|
||||||
|
|
||||||
|
read -p "Are you logged in to the cisa-dotgov CF org above? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
cf login -a https://api.fr.cloud.gov --sso
|
||||||
|
fi
|
||||||
|
|
||||||
|
gh auth status
|
||||||
|
read -p "Are you logged into a Github account with access to cisagov/getgov? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
gh auth login
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating manifest for $1..."
|
||||||
|
cp ops/scripts/manifest-sandbox-template-migrate.yaml ops/manifests/manifest-$1.yaml
|
||||||
|
sed -i '' "s/ENVIRONMENT/$1/" "ops/manifests/manifest-$1.yaml"
|
||||||
|
|
||||||
|
echo "Creating new cloud.gov space for $1..."
|
||||||
|
cf create-space $1
|
||||||
|
cf target -o "cisa-dotgov" -s $1
|
||||||
|
cf bind-security-group public_networks_egress cisa-dotgov --space $1
|
||||||
|
cf bind-security-group trusted_local_networks_egress cisa-dotgov --space $1
|
||||||
|
|
||||||
|
echo "Creating new cloud.gov DB for $1. This usually takes about 5 minutes..."
|
||||||
|
cf create-service aws-rds micro-psql getgov-$1-database
|
||||||
|
|
||||||
|
until cf service getgov-$1-database | grep -q 'The service instance status is succeeded'
|
||||||
|
do
|
||||||
|
echo "Database not up yet, waiting..."
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Creating new cloud.gov credentials for $1..."
|
||||||
|
django_key=$(python3 -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())')
|
||||||
|
openssl req -nodes -x509 -days 365 -newkey rsa:2048 -keyout private-$1.pem -out public-$1.crt
|
||||||
|
login_key=$(base64 -i private-$1.pem)
|
||||||
|
jq -n --arg django_key "$django_key" --arg login_key "$login_key" '{"DJANGO_SECRET_KEY":$django_key,"DJANGO_SECRET_LOGIN_KEY":$login_key}' > credentials-$1.json
|
||||||
|
cf cups getgov-credentials -p credentials-$1.json
|
||||||
|
|
||||||
|
echo "Now you will need to update some things for Login. Please sign-in to https://dashboard.int.identitysandbox.gov/."
|
||||||
|
echo "Navigate to our application config: https://dashboard.int.identitysandbox.gov/service_providers/2640/edit?"
|
||||||
|
echo "There are two things to update."
|
||||||
|
echo "1. You need to upload the public-$1.crt file generated as part of the previous command."
|
||||||
|
echo "2. You need to add two redirect URIs: https://getgov-$1.app.cloud.gov/openid/callback/login/ and
|
||||||
|
https://getgov-$1.app.cloud.gov/openid/callback/logout/ to the list of URIs."
|
||||||
|
read -p "Please confirm when this is done (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Database create succeeded and credentials created. Deploying the get.gov application to the new space $1..."
|
||||||
|
echo "Building assets..."
|
||||||
|
open -a Docker
|
||||||
|
cd src/
|
||||||
|
./build.sh
|
||||||
|
cd ..
|
||||||
|
cf push getgov-$1 -f ops/manifests/manifest-$1.yaml
|
||||||
|
|
||||||
|
read -p "Please provide the email of the space developer: " -r
|
||||||
|
cf set-space-role $REPLY cisa-dotgov $1 SpaceDeveloper
|
||||||
|
|
||||||
|
read -p "Should we run migrations? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
cf run-task getgov-$1 --command 'python manage.py migrate' --name migrate
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Alright, your app is up and running at https://getgov-$1.app.cloud.gov!"
|
||||||
|
echo
|
||||||
|
echo "Moving on to setup Github automation..."
|
||||||
|
|
||||||
|
echo "Creating space deployer for Github deploys..."
|
||||||
|
cf create-service cloud-gov-service-account space-deployer github-cd-account
|
||||||
|
cf create-service-key github-cd-account github-cd-key
|
||||||
|
cf service-key github-cd-account github-cd-key
|
||||||
|
read -p "Please confirm we should set the above username and key to Github secrets. (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ ! $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cf service-key github-cd-account github-cd-key | sed 1,2d | jq -r '[.credentials.username, .credentials.password]|@tsv' |
|
||||||
|
while read -r username password; do
|
||||||
|
gh secret --repo cisagov/getgov set CF_${upcase_name}_USERNAME --body $username
|
||||||
|
gh secret --repo cisagov/getgov set CF_${upcase_name}_PASSWORD --body $password
|
||||||
|
done
|
||||||
|
|
||||||
|
read -p "All done! Should we open a PR with these changes? (y/n) " -n 1 -r
|
||||||
|
echo
|
||||||
|
if [[ $REPLY =~ ^[Yy]$ ]]
|
||||||
|
then
|
||||||
|
git add ops/manifests/manifest-$1.yaml .github/workflows/ src/registrar/config/settings.py
|
||||||
|
git commit -m "Add new developer sandbox '"$1"' infrastructure"
|
||||||
|
gh pr create
|
||||||
|
fi
|
30
ops/scripts/manifest-sandbox-template-migrate.yaml
Normal file
30
ops/scripts/manifest-sandbox-template-migrate.yaml
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-ENVIRONMENT
|
||||||
|
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
|
||||||
|
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-ENVIRONMENT.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
|
||||||
|
# use a non-default route to avoid conflicts
|
||||||
|
routes:
|
||||||
|
- route: getgov-ENVIRONMENT-migrate.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-ENVIRONMENT-database
|
36
ops/scripts/migrate_new_old.sh
Executable file
36
ops/scripts/migrate_new_old.sh
Executable file
|
@ -0,0 +1,36 @@
|
||||||
|
# This script sets up a completely new Cloud.gov CF Space with all the corresponding
|
||||||
|
# infrastructure needed to run get.gov. It can serve for documentation for running
|
||||||
|
# NOTE: This script was written for MacOS and to be run at the root directory
|
||||||
|
# of the repository.
|
||||||
|
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo 'Please specify a name on the command line for the new space (i.e. lmm)' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The user running this script has to be a SpaceDeveloper on both the
|
||||||
|
# new and old org/spaces.
|
||||||
|
|
||||||
|
OLD_ORG="cisa-getgov-prototyping"
|
||||||
|
NEW_ORG="cisa-dotgov"
|
||||||
|
|
||||||
|
#
|
||||||
|
# delete old route
|
||||||
|
cf target -o $OLD_ORG -s $1
|
||||||
|
cf delete-route app.cloud.gov -n getgov-$1
|
||||||
|
|
||||||
|
# re-claim the route on new orf
|
||||||
|
cf target -o $NEW_ORG -s $1
|
||||||
|
cf map-route getgov-$1 app.cloud.gov -n getgov-$1
|
||||||
|
cf delete-route app.cloud.gov -n getgov-$1-migrate
|
||||||
|
|
||||||
|
# delete old app and services
|
||||||
|
cf target -o $OLD_ORG -s $1
|
||||||
|
cf delete getgov-$1
|
||||||
|
cf delete-service getgov-$1-database
|
||||||
|
cf delete-service getgov-credentials
|
||||||
|
cf delete-service getgov-cd-account
|
||||||
|
cf delete-space $1
|
||||||
|
|
||||||
|
|
||||||
|
printf "Remove -migrate from ops/manifests/manifest-$1.yaml"
|
|
@ -238,6 +238,9 @@ class Client(oic.Client):
|
||||||
"client_secret": self.client_secret,
|
"client_secret": self.client_secret,
|
||||||
},
|
},
|
||||||
authn_method=self.registration_response["token_endpoint_auth_method"],
|
authn_method=self.registration_response["token_endpoint_auth_method"],
|
||||||
|
# There is a time desync issue between login.gov and cloud
|
||||||
|
# this addresses that by adding a clock skew.
|
||||||
|
skew=10,
|
||||||
)
|
)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
logger.error(err)
|
logger.error(err)
|
||||||
|
|
|
@ -54,6 +54,7 @@ services:
|
||||||
# command: "python"
|
# command: "python"
|
||||||
command: >
|
command: >
|
||||||
bash -c " python manage.py migrate &&
|
bash -c " python manage.py migrate &&
|
||||||
|
python manage.py load &&
|
||||||
python manage.py runserver 0.0.0.0:8080"
|
python manage.py runserver 0.0.0.0:8080"
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
|
|
@ -104,6 +104,36 @@ class MyUserAdmin(BaseUserAdmin):
|
||||||
|
|
||||||
inlines = [UserContactInline]
|
inlines = [UserContactInline]
|
||||||
|
|
||||||
|
list_display = (
|
||||||
|
"email",
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"status",
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{"fields": ("username", "password", "status")},
|
||||||
|
),
|
||||||
|
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
|
||||||
|
(
|
||||||
|
"Permissions",
|
||||||
|
{
|
||||||
|
"fields": (
|
||||||
|
"is_active",
|
||||||
|
"is_staff",
|
||||||
|
"is_superuser",
|
||||||
|
"groups",
|
||||||
|
"user_permissions",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
|
)
|
||||||
|
|
||||||
def get_list_display(self, request):
|
def get_list_display(self, request):
|
||||||
if not request.user.is_superuser:
|
if not request.user.is_superuser:
|
||||||
# Customize the list display for staff users
|
# Customize the list display for staff users
|
||||||
|
@ -146,7 +176,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
readonly_fields = ["state"]
|
readonly_fields = ["state"]
|
||||||
|
|
||||||
def response_change(self, request, obj):
|
def response_change(self, request, obj):
|
||||||
ACTION_BUTTON = "_place_client_hold"
|
|
||||||
GET_SECURITY_EMAIL = "_get_security_email"
|
GET_SECURITY_EMAIL = "_get_security_email"
|
||||||
SET_SECURITY_CONTACT = "_set_security_contact"
|
SET_SECURITY_CONTACT = "_set_security_contact"
|
||||||
MAKE_DOMAIN = "_make_domain_in_registry"
|
MAKE_DOMAIN = "_make_domain_in_registry"
|
||||||
|
@ -157,7 +187,10 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
REMOVE_CLIENT_HOLD = "_rem_client_hold"
|
REMOVE_CLIENT_HOLD = "_rem_client_hold"
|
||||||
DELETE_DOMAIN = "_delete_domain"
|
DELETE_DOMAIN = "_delete_domain"
|
||||||
|
|
||||||
if ACTION_BUTTON in request.POST:
|
|
||||||
|
PLACE_HOLD = "_place_client_hold"
|
||||||
|
EDIT_DOMAIN = "_edit_domain"
|
||||||
|
if PLACE_HOLD in request.POST:
|
||||||
try:
|
try:
|
||||||
obj.place_client_hold()
|
obj.place_client_hold()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -195,7 +228,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
if SET_SECURITY_CONTACT in request.POST:
|
elif SET_SECURITY_CONTACT in request.POST:
|
||||||
try:
|
try:
|
||||||
fake_email = "manuallyEnteredEmail@test.gov"
|
fake_email = "manuallyEnteredEmail@test.gov"
|
||||||
if PublicContact.objects.filter(
|
if PublicContact.objects.filter(
|
||||||
|
@ -218,7 +251,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
("The security email is %" ". Thanks!") % fake_email,
|
("The security email is %" ". Thanks!") % fake_email,
|
||||||
)
|
)
|
||||||
|
|
||||||
if MAKE_DOMAIN in request.POST:
|
elif MAKE_DOMAIN in request.POST:
|
||||||
try:
|
try:
|
||||||
obj._get_or_create_domain()
|
obj._get_or_create_domain()
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -230,9 +263,8 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
# make nameservers here
|
|
||||||
|
|
||||||
if MAKE_NAMESERVERS in request.POST:
|
elif MAKE_NAMESERVERS in request.POST:
|
||||||
try:
|
try:
|
||||||
hosts = [("ns1.example.com", None), ("ns2.example.com", None)]
|
hosts = [("ns1.example.com", None), ("ns2.example.com", None)]
|
||||||
obj.nameservers = hosts
|
obj.nameservers = hosts
|
||||||
|
@ -244,7 +276,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
("Hosts set to be %s" ". Thanks!") % hosts,
|
("Hosts set to be %s" ". Thanks!") % hosts,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
if GET_NAMESERVERS in request.POST:
|
elif GET_NAMESERVERS in request.POST:
|
||||||
try:
|
try:
|
||||||
nameservers = obj.nameservers
|
nameservers = obj.nameservers
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -256,7 +288,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
if GET_STATUS in request.POST:
|
elif GET_STATUS in request.POST:
|
||||||
try:
|
try:
|
||||||
statuses = obj.statuses
|
statuses = obj.statuses
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
|
@ -268,7 +300,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
if SET_CLIENT_HOLD in request.POST:
|
elif SET_CLIENT_HOLD in request.POST:
|
||||||
try:
|
try:
|
||||||
obj.clientHold()
|
obj.clientHold()
|
||||||
obj.save()
|
obj.save()
|
||||||
|
@ -281,7 +313,7 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
|
||||||
if REMOVE_CLIENT_HOLD in request.POST:
|
elif REMOVE_CLIENT_HOLD in request.POST:
|
||||||
try:
|
try:
|
||||||
obj.revertClientHold()
|
obj.revertClientHold()
|
||||||
obj.save()
|
obj.save()
|
||||||
|
@ -293,7 +325,8 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
("Domain %s will now have client hold removed") % obj.name,
|
("Domain %s will now have client hold removed") % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
if DELETE_DOMAIN in request.POST:
|
|
||||||
|
elif DELETE_DOMAIN in request.POST:
|
||||||
try:
|
try:
|
||||||
obj.deleted()
|
obj.deleted()
|
||||||
obj.save()
|
obj.save()
|
||||||
|
@ -305,8 +338,30 @@ class DomainAdmin(ListHeaderAdmin):
|
||||||
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
|
("Domain %s Should now be deleted " ". Thanks!") % obj.name,
|
||||||
)
|
)
|
||||||
return HttpResponseRedirect(".")
|
return HttpResponseRedirect(".")
|
||||||
|
elif EDIT_DOMAIN in request.POST:
|
||||||
|
# We want to know, globally, when an edit action occurs
|
||||||
|
request.session["analyst_action"] = "edit"
|
||||||
|
# Restricts this action to this domain (pk) only
|
||||||
|
request.session["analyst_action_location"] = obj.id
|
||||||
|
return HttpResponseRedirect(reverse("domain", args=(obj.id,)))
|
||||||
return super().response_change(request, obj)
|
return super().response_change(request, obj)
|
||||||
|
|
||||||
|
def change_view(self, request, object_id):
|
||||||
|
# If the analyst was recently editing a domain page,
|
||||||
|
# delete any associated session values
|
||||||
|
if "analyst_action" in request.session:
|
||||||
|
del request.session["analyst_action"]
|
||||||
|
del request.session["analyst_action_location"]
|
||||||
|
return super().change_view(request, object_id)
|
||||||
|
|
||||||
|
def has_change_permission(self, request, obj=None):
|
||||||
|
# Fixes a bug wherein users which are only is_staff
|
||||||
|
# can access 'change' when GET,
|
||||||
|
# but cannot access this page when it is a request of type POST.
|
||||||
|
if request.user.is_staff:
|
||||||
|
return True
|
||||||
|
return super().has_change_permission(request, obj)
|
||||||
|
|
||||||
|
|
||||||
class ContactAdmin(ListHeaderAdmin):
|
class ContactAdmin(ListHeaderAdmin):
|
||||||
"""Custom contact admin class to add search."""
|
"""Custom contact admin class to add search."""
|
||||||
|
@ -319,6 +374,10 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
"""Customize the applications listing view."""
|
"""Customize the applications listing view."""
|
||||||
|
|
||||||
|
# Set multi-selects 'read-only' (hide selects and show data)
|
||||||
|
# based on user perms and application creator's status
|
||||||
|
# form = DomainApplicationForm
|
||||||
|
|
||||||
# Columns
|
# Columns
|
||||||
list_display = [
|
list_display = [
|
||||||
"requested_domain",
|
"requested_domain",
|
||||||
|
@ -392,7 +451,7 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
]
|
]
|
||||||
|
|
||||||
# Read only that we'll leverage for CISA Analysts
|
# Read only that we'll leverage for CISA Analysts
|
||||||
readonly_fields = [
|
analyst_readonly_fields = [
|
||||||
"creator",
|
"creator",
|
||||||
"type_of_work",
|
"type_of_work",
|
||||||
"more_organization_information",
|
"more_organization_information",
|
||||||
|
@ -410,49 +469,81 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
# Trigger action when a fieldset is changed
|
# Trigger action when a fieldset is changed
|
||||||
def save_model(self, request, obj, form, change):
|
def save_model(self, request, obj, form, change):
|
||||||
if change: # Check if the application is being edited
|
if obj and obj.creator.status != models.User.RESTRICTED:
|
||||||
# Get the original application from the database
|
if change: # Check if the application is being edited
|
||||||
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
# Get the original application from the database
|
||||||
|
original_obj = models.DomainApplication.objects.get(pk=obj.pk)
|
||||||
|
|
||||||
if obj.status != original_obj.status:
|
if obj.status != original_obj.status:
|
||||||
if obj.status == models.DomainApplication.STARTED:
|
status_method_mapping = {
|
||||||
# No conditions
|
models.DomainApplication.STARTED: None,
|
||||||
pass
|
models.DomainApplication.SUBMITTED: obj.submit,
|
||||||
elif obj.status == models.DomainApplication.SUBMITTED:
|
models.DomainApplication.IN_REVIEW: obj.in_review,
|
||||||
# This is an fsm in model which will throw an error if the
|
models.DomainApplication.ACTION_NEEDED: obj.action_needed,
|
||||||
# transition condition is violated, so we roll back the
|
models.DomainApplication.APPROVED: obj.approve,
|
||||||
# status to what it was before the admin user changed it and
|
models.DomainApplication.WITHDRAWN: obj.withdraw,
|
||||||
# let the fsm method set it. Same comment applies to
|
models.DomainApplication.REJECTED: obj.reject,
|
||||||
# transition method calls below.
|
models.DomainApplication.INELIGIBLE: obj.reject_with_prejudice,
|
||||||
obj.status = original_obj.status
|
}
|
||||||
obj.submit()
|
selected_method = status_method_mapping.get(obj.status)
|
||||||
elif obj.status == models.DomainApplication.IN_REVIEW:
|
if selected_method is None:
|
||||||
obj.status = original_obj.status
|
logger.warning("Unknown status selected in django admin")
|
||||||
obj.in_review()
|
else:
|
||||||
elif obj.status == models.DomainApplication.ACTION_NEEDED:
|
# This is an fsm in model which will throw an error if the
|
||||||
obj.status = original_obj.status
|
# transition condition is violated, so we roll back the
|
||||||
obj.action_needed()
|
# status to what it was before the admin user changed it and
|
||||||
elif obj.status == models.DomainApplication.APPROVED:
|
# let the fsm method set it.
|
||||||
obj.status = original_obj.status
|
obj.status = original_obj.status
|
||||||
obj.approve()
|
selected_method()
|
||||||
elif obj.status == models.DomainApplication.WITHDRAWN:
|
|
||||||
obj.status = original_obj.status
|
|
||||||
obj.withdraw()
|
|
||||||
elif obj.status == models.DomainApplication.REJECTED:
|
|
||||||
obj.status = original_obj.status
|
|
||||||
obj.reject()
|
|
||||||
else:
|
|
||||||
logger.warning("Unknown status selected in django admin")
|
|
||||||
|
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
else:
|
||||||
|
# Clear the success message
|
||||||
|
messages.set_level(request, messages.ERROR)
|
||||||
|
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
"This action is not permitted for applications "
|
||||||
|
+ "with a restricted creator.",
|
||||||
|
)
|
||||||
|
|
||||||
def get_readonly_fields(self, request, obj=None):
|
def get_readonly_fields(self, request, obj=None):
|
||||||
|
"""Set the read-only state on form elements.
|
||||||
|
We have 2 conditions that determine which fields are read-only:
|
||||||
|
admin user permissions and the application creator's status, so
|
||||||
|
we'll use the baseline readonly_fields and extend it as needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
readonly_fields = list(self.readonly_fields)
|
||||||
|
|
||||||
|
# Check if the creator is restricted
|
||||||
|
if obj and obj.creator.status == models.User.RESTRICTED:
|
||||||
|
# For fields like CharField, IntegerField, etc., the widget used is
|
||||||
|
# straightforward and the readonly_fields list can control their behavior
|
||||||
|
readonly_fields.extend([field.name for field in self.model._meta.fields])
|
||||||
|
# Add the multi-select fields to readonly_fields:
|
||||||
|
# Complex fields like ManyToManyField require special handling
|
||||||
|
readonly_fields.extend(
|
||||||
|
["current_websites", "other_contacts", "alternative_domains"]
|
||||||
|
)
|
||||||
|
|
||||||
if request.user.is_superuser:
|
if request.user.is_superuser:
|
||||||
# Superusers have full access, no fields are read-only
|
return readonly_fields
|
||||||
return []
|
|
||||||
else:
|
else:
|
||||||
# Regular users can only view the specified fields
|
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||||
return self.readonly_fields
|
return readonly_fields
|
||||||
|
|
||||||
|
def display_restricted_warning(self, request, obj):
|
||||||
|
if obj and obj.creator.status == models.User.RESTRICTED:
|
||||||
|
messages.warning(
|
||||||
|
request,
|
||||||
|
"Cannot edit an application with a restricted creator.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
self.display_restricted_warning(request, obj)
|
||||||
|
return super().change_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(models.User, MyUserAdmin)
|
admin.site.register(models.User, MyUserAdmin)
|
||||||
|
|
50
src/registrar/assets/js/get-gov-admin.js
Normal file
50
src/registrar/assets/js/get-gov-admin.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* @file get-gov-admin.js includes custom code for the .gov registrar admin portal.
|
||||||
|
*
|
||||||
|
* Constants and helper functions are at the top.
|
||||||
|
* Event handlers are in the middle.
|
||||||
|
* Initialization (run-on-load) stuff goes at the bottom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Helper functions.
|
||||||
|
/** Either sets attribute target="_blank" to a given element, or removes it */
|
||||||
|
function openInNewTab(el, removeAttribute = false){
|
||||||
|
if(removeAttribute){
|
||||||
|
el.setAttribute("target", "_blank");
|
||||||
|
}else{
|
||||||
|
el.removeAttribute("target", "_blank");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Event handlers.
|
||||||
|
|
||||||
|
// <<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>><<>>
|
||||||
|
// Initialization code.
|
||||||
|
|
||||||
|
/** An IIFE for pages in DjangoAdmin which may need custom JS implementation.
|
||||||
|
* Currently only appends target="_blank" to the domain_form object,
|
||||||
|
* but this can be expanded.
|
||||||
|
*/
|
||||||
|
(function (){
|
||||||
|
/*
|
||||||
|
On mouseover, appends target="_blank" on domain_form under the Domain page.
|
||||||
|
The reason for this is that the template has a form that contains multiple buttons.
|
||||||
|
The structure of that template complicates seperating those buttons
|
||||||
|
out of the form (while maintaining the same position on the page).
|
||||||
|
However, if we want to open one of those submit actions to a new tab -
|
||||||
|
such as the manage domain button - we need to dynamically append target.
|
||||||
|
As there is no built-in django method which handles this, we do it here.
|
||||||
|
*/
|
||||||
|
function prepareDjangoAdmin() {
|
||||||
|
let domainFormElement = document.getElementById("domain_form");
|
||||||
|
let domainSubmitButton = document.getElementById("manageDomainSubmitButton");
|
||||||
|
if(domainSubmitButton && domainFormElement){
|
||||||
|
domainSubmitButton.addEventListener("mouseover", () => openInNewTab(domainFormElement, true));
|
||||||
|
domainSubmitButton.addEventListener("mouseout", () => openInNewTab(domainFormElement, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareDjangoAdmin();
|
||||||
|
})();
|
|
@ -432,3 +432,21 @@ abbr[title] {
|
||||||
height: units('mobile');
|
height: units('mobile');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fixes some font size disparities with the Figma
|
||||||
|
// for usa-alert alert elements
|
||||||
|
.usa-alert {
|
||||||
|
.usa-alert__heading.larger-font-sizing {
|
||||||
|
font-size: units(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The icon was off center for some reason
|
||||||
|
// Fixes that issue
|
||||||
|
@media (min-width: 64em){
|
||||||
|
.usa-alert--warning{
|
||||||
|
.usa-alert__body::before {
|
||||||
|
left: 1rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -251,8 +251,7 @@ AWS_MAX_ATTEMPTS = 3
|
||||||
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
|
BOTO_CONFIG = Config(retries={"mode": AWS_RETRY_MODE, "max_attempts": AWS_MAX_ATTEMPTS})
|
||||||
|
|
||||||
# email address to use for various automated correspondence
|
# email address to use for various automated correspondence
|
||||||
# TODO: pick something sensible here
|
DEFAULT_FROM_EMAIL = "help@get.gov <help@get.gov>"
|
||||||
DEFAULT_FROM_EMAIL = "registrar@get.gov"
|
|
||||||
|
|
||||||
# connect to an (external) SMTP server for sending email
|
# connect to an (external) SMTP server for sending email
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
|
||||||
|
|
|
@ -63,7 +63,7 @@ class UserFixture:
|
||||||
"last_name": "Adkinson",
|
"last_name": "Adkinson",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "bb21f687-c773-4df3-9243-111cfd4c0be4",
|
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
|
||||||
"first_name": "Paul",
|
"first_name": "Paul",
|
||||||
"last_name": "Kuykendall",
|
"last_name": "Kuykendall",
|
||||||
},
|
},
|
||||||
|
@ -72,6 +72,16 @@ class UserFixture:
|
||||||
"first_name": "Rebecca",
|
"first_name": "Rebecca",
|
||||||
"last_name": "Hsieh",
|
"last_name": "Hsieh",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
|
||||||
|
"first_name": "David",
|
||||||
|
"last_name": "Kennedy",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
|
||||||
|
"first_name": "Nicolle",
|
||||||
|
"last_name": "LeClair",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF = [
|
STAFF = [
|
||||||
|
@ -86,6 +96,12 @@ class UserFixture:
|
||||||
"first_name": "Alysia-Analyst",
|
"first_name": "Alysia-Analyst",
|
||||||
"last_name": "Alysia-Analyst",
|
"last_name": "Alysia-Analyst",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
||||||
|
"first_name": "Katherine-Analyst",
|
||||||
|
"last_name": "Osos-Analyst",
|
||||||
|
"email": "kosos@truss.works",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
||||||
"first_name": "Zander-Analyst",
|
"first_name": "Zander-Analyst",
|
||||||
|
@ -101,6 +117,23 @@ class UserFixture:
|
||||||
"first_name": "Rebecca-Analyst",
|
"first_name": "Rebecca-Analyst",
|
||||||
"last_name": "Hsieh-Analyst",
|
"last_name": "Hsieh-Analyst",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"username": "5dc6c9a6-61d9-42b4-ba54-4beff28bac3c",
|
||||||
|
"first_name": "David-Analyst",
|
||||||
|
"last_name": "Kennedy-Analyst",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "0eb6f326-a3d4-410f-a521-aa4c1fad4e47",
|
||||||
|
"first_name": "Gaby-Analyst",
|
||||||
|
"last_name": "DiSarli-Analyst",
|
||||||
|
"email": "gaby@truss.works",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"username": "cfe7c2fc-e24a-480e-8b78-28645a1459b3",
|
||||||
|
"first_name": "Nicolle-Analyst",
|
||||||
|
"last_name": "LeClair",
|
||||||
|
"email": "nicolle.leclair@ecstech.com",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
STAFF_PERMISSIONS = [
|
STAFF_PERMISSIONS = [
|
||||||
|
@ -248,6 +281,14 @@ class DomainApplicationFixture:
|
||||||
"status": "withdrawn",
|
"status": "withdrawn",
|
||||||
"organization_name": "Example - Withdrawn",
|
"organization_name": "Example - Withdrawn",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"status": "action needed",
|
||||||
|
"organization_name": "Example - Action Needed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"status": "rejected",
|
||||||
|
"organization_name": "Example - Rejected",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from auditlog.context import disable_auditlog # type: ignore
|
from auditlog.context import disable_auditlog # type: ignore
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
|
from registrar.fixtures import UserFixture, DomainApplicationFixture, DomainFixture
|
||||||
|
|
||||||
|
@ -12,8 +13,11 @@ class Command(BaseCommand):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
# django-auditlog has some bugs with fixtures
|
# django-auditlog has some bugs with fixtures
|
||||||
# https://github.com/jazzband/django-auditlog/issues/17
|
# https://github.com/jazzband/django-auditlog/issues/17
|
||||||
with disable_auditlog():
|
if settings.DEBUG:
|
||||||
UserFixture.load()
|
with disable_auditlog():
|
||||||
DomainApplicationFixture.load()
|
UserFixture.load()
|
||||||
DomainFixture.load()
|
DomainApplicationFixture.load()
|
||||||
logger.info("All fixtures loaded.")
|
DomainFixture.load()
|
||||||
|
logger.info("All fixtures loaded.")
|
||||||
|
else:
|
||||||
|
logger.warn("Refusing to load fixture data in a non DEBUG env")
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-08-18 16:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django_fsm
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0028_alter_domainapplication_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="user",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("ineligible", "ineligible")],
|
||||||
|
default=None,
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="domainapplication",
|
||||||
|
name="status",
|
||||||
|
field=django_fsm.FSMField(
|
||||||
|
choices=[
|
||||||
|
("started", "started"),
|
||||||
|
("submitted", "submitted"),
|
||||||
|
("in review", "in review"),
|
||||||
|
("action needed", "action needed"),
|
||||||
|
("approved", "approved"),
|
||||||
|
("withdrawn", "withdrawn"),
|
||||||
|
("rejected", "rejected"),
|
||||||
|
("ineligible", "ineligible"),
|
||||||
|
],
|
||||||
|
default="started",
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
23
src/registrar/migrations/0030_alter_user_status.py
Normal file
23
src/registrar/migrations/0030_alter_user_status.py
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
# Generated by Django 4.2.1 on 2023-08-29 17:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0029_user_status_alter_domainapplication_status"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="user",
|
||||||
|
name="status",
|
||||||
|
field=models.CharField(
|
||||||
|
blank=True,
|
||||||
|
choices=[("restricted", "restricted")],
|
||||||
|
default=None,
|
||||||
|
max_length=10,
|
||||||
|
null=True,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -721,6 +721,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
help_text="Very basic info about the lifecycle of this domain object",
|
help_text="Very basic info about the lifecycle of this domain object",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def isActive(self):
|
||||||
|
return self.state == Domain.State.CREATED
|
||||||
|
|
||||||
# ForeignKey on UserDomainRole creates a "permissions" member for
|
# ForeignKey on UserDomainRole creates a "permissions" member for
|
||||||
# all of the user-roles that are in place for this domain
|
# all of the user-roles that are in place for this domain
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
APPROVED = "approved"
|
APPROVED = "approved"
|
||||||
WITHDRAWN = "withdrawn"
|
WITHDRAWN = "withdrawn"
|
||||||
REJECTED = "rejected"
|
REJECTED = "rejected"
|
||||||
|
INELIGIBLE = "ineligible"
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
(STARTED, STARTED),
|
(STARTED, STARTED),
|
||||||
(SUBMITTED, SUBMITTED),
|
(SUBMITTED, SUBMITTED),
|
||||||
|
@ -34,6 +35,7 @@ class DomainApplication(TimeStampedModel):
|
||||||
(APPROVED, APPROVED),
|
(APPROVED, APPROVED),
|
||||||
(WITHDRAWN, WITHDRAWN),
|
(WITHDRAWN, WITHDRAWN),
|
||||||
(REJECTED, REJECTED),
|
(REJECTED, REJECTED),
|
||||||
|
(INELIGIBLE, INELIGIBLE),
|
||||||
]
|
]
|
||||||
|
|
||||||
class StateTerritoryChoices(models.TextChoices):
|
class StateTerritoryChoices(models.TextChoices):
|
||||||
|
@ -554,7 +556,9 @@ class DomainApplication(TimeStampedModel):
|
||||||
)
|
)
|
||||||
|
|
||||||
@transition(
|
@transition(
|
||||||
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED
|
field="status",
|
||||||
|
source=[SUBMITTED, IN_REVIEW, REJECTED, INELIGIBLE],
|
||||||
|
target=APPROVED,
|
||||||
)
|
)
|
||||||
def approve(self):
|
def approve(self):
|
||||||
"""Approve an application that has been submitted.
|
"""Approve an application that has been submitted.
|
||||||
|
@ -590,6 +594,11 @@ class DomainApplication(TimeStampedModel):
|
||||||
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
|
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
|
||||||
def withdraw(self):
|
def withdraw(self):
|
||||||
"""Withdraw an application that has been submitted."""
|
"""Withdraw an application that has been submitted."""
|
||||||
|
self._send_status_update_email(
|
||||||
|
"withdraw",
|
||||||
|
"emails/domain_request_withdrawn.txt",
|
||||||
|
"emails/domain_request_withdrawn_subject.txt",
|
||||||
|
)
|
||||||
|
|
||||||
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED)
|
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED)
|
||||||
def reject(self):
|
def reject(self):
|
||||||
|
@ -603,6 +612,17 @@ class DomainApplication(TimeStampedModel):
|
||||||
"emails/status_change_rejected_subject.txt",
|
"emails/status_change_rejected_subject.txt",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@transition(field="status", source=[IN_REVIEW, APPROVED], target=INELIGIBLE)
|
||||||
|
def reject_with_prejudice(self):
|
||||||
|
"""The applicant is a bad actor, reject with prejudice.
|
||||||
|
|
||||||
|
No email As a side effect, but we block the applicant from editing
|
||||||
|
any existing domains/applications and from submitting new aplications.
|
||||||
|
We do this by setting an ineligible status on the user, which the
|
||||||
|
permissions classes test against"""
|
||||||
|
|
||||||
|
self.creator.restrict_user()
|
||||||
|
|
||||||
# ## Form policies ###
|
# ## Form policies ###
|
||||||
#
|
#
|
||||||
# These methods control what questions need to be answered by applicants
|
# These methods control what questions need to be answered by applicants
|
||||||
|
|
|
@ -17,6 +17,18 @@ class User(AbstractUser):
|
||||||
but can be customized later.
|
but can be customized later.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# #### Constants for choice fields ####
|
||||||
|
RESTRICTED = "restricted"
|
||||||
|
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
||||||
|
|
||||||
|
status = models.CharField(
|
||||||
|
max_length=10,
|
||||||
|
choices=STATUS_CHOICES,
|
||||||
|
default=None, # Set the default value to None
|
||||||
|
null=True, # Allow the field to be null
|
||||||
|
blank=True, # Allow the field to be blank
|
||||||
|
)
|
||||||
|
|
||||||
domains = models.ManyToManyField(
|
domains = models.ManyToManyField(
|
||||||
"registrar.Domain",
|
"registrar.Domain",
|
||||||
through="registrar.UserDomainRole",
|
through="registrar.UserDomainRole",
|
||||||
|
@ -39,6 +51,17 @@ class User(AbstractUser):
|
||||||
else:
|
else:
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
|
def restrict_user(self):
|
||||||
|
self.status = self.RESTRICTED
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def unrestrict_user(self):
|
||||||
|
self.status = None
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
def is_restricted(self):
|
||||||
|
return self.status == self.RESTRICTED
|
||||||
|
|
||||||
def first_login(self):
|
def first_login(self):
|
||||||
"""Callback when the user is authenticated for the very first time.
|
"""Callback when the user is authenticated for the very first time.
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from django.conf import settings
|
from django.db.models.signals import post_save
|
||||||
from django.core.management import call_command
|
|
||||||
from django.db.models.signals import post_save, post_migrate
|
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
|
|
||||||
from .models import User, Contact
|
from .models import User, Contact
|
||||||
|
@ -55,13 +53,3 @@ def handle_profile(sender, instance, **kwargs):
|
||||||
"There are multiple Contacts with the same email address."
|
"There are multiple Contacts with the same email address."
|
||||||
f" Picking #{contacts[0].id} for User #{instance.id}."
|
f" Picking #{contacts[0].id} for User #{instance.id}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_migrate)
|
|
||||||
def handle_loaddata(**kwargs):
|
|
||||||
"""Attempt to load test fixtures when in DEBUG mode."""
|
|
||||||
if settings.DEBUG:
|
|
||||||
try:
|
|
||||||
call_command("load")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(e)
|
|
||||||
|
|
12
src/registrar/templates/admin/change_form.html
Normal file
12
src/registrar/templates/admin/change_form.html
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
|
||||||
|
{% comment %} Replace the Django ul markup with a div. We'll edit the child markup accordingly in change_form_object_tools {% endcomment %}
|
||||||
|
{% block object-tools %}
|
||||||
|
{% if change and not is_popup %}
|
||||||
|
<div class="object-tools">
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
20
src/registrar/templates/admin/change_form_object_tools.html
Normal file
20
src/registrar/templates/admin/change_form_object_tools.html
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
{% comment %} Replace li with p for more semantic HTML if we have a single child {% endcomment %}
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{% url opts|admin_urlname:'history' original.pk|admin_urlquote as history_url %}
|
||||||
|
{% if has_absolute_url %}
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-0 padding-0">
|
||||||
|
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -24,3 +24,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %}
|
||||||
|
{% block object-tools %}
|
||||||
|
<div class="object-tools">
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
13
src/registrar/templates/admin/change_list_object_tools.html
Normal file
13
src/registrar/templates/admin/change_list_object_tools.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% load i18n admin_urls %}
|
||||||
|
|
||||||
|
{% comment %} Replace li with p for more semantic HTML {% endcomment %}
|
||||||
|
{% block object-tools-items %}
|
||||||
|
{% if has_add_permission %}
|
||||||
|
<p class="margin-0 padding-0">
|
||||||
|
{% url cl.opts|admin_urlname:'add' as add_url %}
|
||||||
|
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
|
||||||
|
{% blocktranslate with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktranslate %}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -17,7 +17,7 @@ Load our custom filters to extract info from the django generated markup.
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
{% if results.0.form %}
|
{% if results.0|contains_checkbox %}
|
||||||
{# .gov - hardcode the select all checkbox #}
|
{# .gov - hardcode the select all checkbox #}
|
||||||
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||||
<div class="text">
|
<div class="text">
|
||||||
|
@ -60,12 +60,11 @@ Load our custom filters to extract info from the django generated markup.
|
||||||
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
<tr>
|
||||||
|
|
||||||
{% with result_value=result.0|extract_value %}
|
{% with result_value=result.0|extract_value %}
|
||||||
{% with result_label=result.1|extract_a_text %}
|
{% with result_label=result.1|extract_a_text %}
|
||||||
<td>
|
<td>
|
||||||
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_label|default:result_value }}" class="action-select">
|
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}" class="action-select">
|
||||||
<label class="usa-sr-only" for="{{ result_label|default:result_value }}">{{ result_label|default:'label' }}</label>
|
<label class="usa-sr-only" for="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}">{{ result_label|default:'label' }}</label>
|
||||||
</td>
|
</td>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
|
|
||||||
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
|
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
|
||||||
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
||||||
{% with www_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.requested_domain %}
|
{% input_with_errors forms.0.requested_domain %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -75,7 +75,7 @@
|
||||||
{% with attr_aria_describedby="alt_domain_instructions" %}
|
{% with attr_aria_describedby="alt_domain_instructions" %}
|
||||||
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
{# attr_validate / validate="domain" invokes code in get-gov.js #}
|
||||||
{# attr_auto_validate likewise triggers behavior in get-gov.js #}
|
{# attr_auto_validate likewise triggers behavior in get-gov.js #}
|
||||||
{% with www_gov=True attr_validate="domain" attr_auto_validate=True %}
|
{% with append_gov=True attr_validate="domain" attr_auto_validate=True %}
|
||||||
{% for form in forms.1 %}
|
{% for form in forms.1 %}
|
||||||
{% input_with_errors form.alternative_domain %}
|
{% input_with_errors form.alternative_domain %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
{% elif domainapplication.status == 'in review' %} In Review
|
{% elif domainapplication.status == 'in review' %} In Review
|
||||||
{% elif domainapplication.status == 'rejected' %} Rejected
|
{% elif domainapplication.status == 'rejected' %} Rejected
|
||||||
{% elif domainapplication.status == 'submitted' %} Submitted
|
{% elif domainapplication.status == 'submitted' %} Submitted
|
||||||
|
{% elif domainapplication.status == 'ineligible' %} Ineligible
|
||||||
{% else %}ERROR Please contact technical support/dev
|
{% else %}ERROR Please contact technical support/dev
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
@ -83,6 +84,14 @@
|
||||||
{% 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 website for your organization' value=domainapplication.current_websites.all list='true' heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if domainapplication.requested_domain %}
|
||||||
|
{% include "includes/summary_item.html" with title='.gov domain' value=domainapplication.requested_domain heading_level=heading_level %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if domainapplication.alternative_domains.all %}
|
||||||
|
{% include "includes/summary_item.html" with title='Alternative domains' value=domainapplication.alternative_domains.all list='true' heading_level=heading_level %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if domainapplication.purpose %}
|
{% if domainapplication.purpose %}
|
||||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=domainapplication.purpose heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Purpose of your domain' value=domainapplication.purpose heading_level=heading_level %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -149,12 +149,12 @@
|
||||||
<button type="button" class="usa-nav__close">
|
<button type="button" class="usa-nav__close">
|
||||||
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
|
<img src="/public/img/usa-icons/close.svg" role="img" alt="Close" />
|
||||||
</button>
|
</button>
|
||||||
<ul class="usa-nav__primary usa-accordion">
|
<ul class="usa-nav__primary usa-accordion display-flex flex-align-center">
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% if user.is_authenticated %}
|
{% if user.is_authenticated %}
|
||||||
<span>{{ user.email }}</span>
|
<span>{{ user.email }}</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-nav__primary-item display-flex flex-align-center">
|
<li class="usa-nav__primary-item display-flex flex-align-center margin-left-2">
|
||||||
<span class="text-base"> | </span>
|
<span class="text-base"> | </span>
|
||||||
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
<a href="{% url 'logout' %}"><span class="text-primary">Sign out</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
{% extends 'admin/change_form.html' %}
|
{% extends 'admin/change_form.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script type="application/javascript" src="{% static 'js/get-gov-admin.js' %}" defer></script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block field_sets %}
|
{% block field_sets %}
|
||||||
<div class="submit-row">
|
<div class="submit-row">
|
||||||
|
<input id="manageDomainSubmitButton" type="submit" value="Manage Domain" name="_edit_domain">
|
||||||
<input type="submit" value="Place hold" name="_place_client_hold">
|
<input type="submit" value="Place hold" name="_place_client_hold">
|
||||||
<input type="submit" value="Get the email" name="_get_security_email">
|
<input type="submit" value="Get the email" name="_get_security_email">
|
||||||
<input type="submit" value="Set New Security Contact" name="_set_security_contact">
|
<input type="submit" value="Set New Security Contact" name="_set_security_contact">
|
||||||
|
|
|
@ -10,8 +10,7 @@
|
||||||
<h1>Authorizing official</h1>
|
<h1>Authorizing official</h1>
|
||||||
|
|
||||||
<p>Your authorizing official is the person within your organization who can
|
<p>Your authorizing official is the person within your organization who can
|
||||||
authorize domain requests. This is generally the highest-ranking or
|
authorize domain requests. This person must be in a role of significant, executive responsibility within the organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
|
||||||
highest-elected official in your organization. Read more about <a class="usa-link" href="{% public_site_url 'domains/eligibility/#you-must-have-approval-from-an-authorizing-official-within-your-organization' %}">who can serve as an authorizing official</a>.</p>
|
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="grid-container">
|
<div class="grid-container">
|
||||||
<div class="grid-row">
|
<div class="grid-row">
|
||||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
{% if not is_analyst_or_superuser or not analyst_action or analyst_action_location != domain.pk %}
|
||||||
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
text-primary-darker text-semibold"
|
text-primary-darker text-semibold"
|
||||||
>
|
>
|
||||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
<div class="tablet:grid-col-3">
|
<div class="tablet:grid-col-3">
|
||||||
|
@ -20,15 +22,26 @@
|
||||||
<div class="tablet:grid-col-9">
|
<div class="tablet:grid-col-9">
|
||||||
<main id="main-content" class="grid-container">
|
<main id="main-content" class="grid-container">
|
||||||
|
|
||||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
||||||
|
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
||||||
|
<div class="usa-alert__body">
|
||||||
|
<h4 class="usa-alert__heading larger-font-sizing">Attention!</h4>
|
||||||
|
<p class="usa-alert__text ">
|
||||||
|
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||||
Back to manage your domains
|
Back to manage your domains
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
{# messages block is under the back breadcrumb link #}
|
{# messages block is under the back breadcrumb link #}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
|
|
|
@ -7,7 +7,8 @@
|
||||||
|
|
||||||
<h1>Domain contact information</h1>
|
<h1>Domain contact information</h1>
|
||||||
|
|
||||||
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. Changing your contact information here won’t affect your Login.gov account information.</p>
|
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> However, it won’t affect your Login.gov account information.
|
||||||
|
</p>
|
||||||
|
|
||||||
{% include "includes/required_fields.html" %}
|
{% include "includes/required_fields.html" %}
|
||||||
|
|
||||||
|
|
26
src/registrar/templates/emails/domain_request_withdrawn.txt
Normal file
26
src/registrar/templates/emails/domain_request_withdrawn.txt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||||
|
Hi {{ application.submitter.first_name }}.
|
||||||
|
|
||||||
|
Your .gov domain request has been withdrawn.
|
||||||
|
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||||
|
REQUEST #: {{ application.id }}
|
||||||
|
STATUS: Withdrawn
|
||||||
|
|
||||||
|
|
||||||
|
YOU CAN EDIT YOUR WITHDRAWN REQUEST
|
||||||
|
|
||||||
|
The details of your withdrawn request are included below. You can edit and resubmit this application by logging into the registrar. <https://manage.get.gov/>.
|
||||||
|
|
||||||
|
THANK YOU
|
||||||
|
|
||||||
|
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||||
|
|
||||||
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
{% include 'emails/includes/application_summary.txt' %}
|
||||||
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
The .gov team
|
||||||
|
Contact us: <https://get.gov/contact/>
|
||||||
|
Visit <https://get.gov>
|
||||||
|
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
||||||
|
Your .gov domain request has been withdrawn
|
|
@ -19,7 +19,7 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Registered 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">
|
||||||
<caption class="sr-only">Your registered domains</caption>
|
<caption class="sr-only">Your registered domains</caption>
|
||||||
|
@ -43,13 +43,13 @@
|
||||||
<td data-label="Status">{{ domain.application_status|title }}</td>
|
<td data-label="Status">{{ domain.application_status|title }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url "domain" pk=domain.pk %}">
|
<a href="{% url "domain" pk=domain.pk %}">
|
||||||
<svg
|
<svg
|
||||||
class="usa-icon"
|
class="usa-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
role="img"
|
role="img"
|
||||||
width="24"
|
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">{{ domain.name }}</span>
|
Manage <span class="usa-sr-only">{{ domain.name }}</span>
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Active 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">
|
||||||
<caption class="sr-only">Your domain applications</caption>
|
<caption class="sr-only">Your domain applications</caption>
|
||||||
|
@ -115,15 +115,18 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p>You don't have any active domain requests right now</p>
|
<p>You don't have any active domain requests right now</p>
|
||||||
{% endif %}
|
|
||||||
<p><a href="{% url 'application:' %}" class="usa-button">Start a new domain request</a></p>
|
<p><a href="{% url 'application:' %}" class="usa-button">Start a new domain request</a></p>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{# Note: Reimplement this after MVP.. #}
|
||||||
|
<!--
|
||||||
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
<section class="section--outlined tablet:grid-col-11 desktop:grid-col-10">
|
||||||
<h2>Archived domains</h2>
|
<h2>Archived domains</h2>
|
||||||
<p>You don't have any archived domains</p>
|
<p>You don't have any archived domains</p>
|
||||||
</section>
|
</section>
|
||||||
|
-->
|
||||||
|
|
||||||
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
||||||
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
||||||
|
@ -132,8 +135,8 @@
|
||||||
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
||||||
Export domains as csv
|
Export domains as csv
|
||||||
</a>
|
</a>
|
||||||
</section> -->
|
</section>
|
||||||
|
-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %} {# not user.is_authenticated #}
|
{% else %} {# not user.is_authenticated #}
|
||||||
|
|
|
@ -55,15 +55,13 @@ error messages, if necessary.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if www_gov %}
|
{% if append_gov %}
|
||||||
<div class="display-flex flex-align-center">
|
<div class="display-flex flex-align-center">
|
||||||
<span class="padding-top-05 padding-right-2px">www.</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# this is the input field, itself #}
|
{# this is the input field, itself #}
|
||||||
{% include widget.template_name %}
|
{% include widget.template_name %}
|
||||||
|
|
||||||
{% if www_gov %}
|
{% if append_gov %}
|
||||||
<span class="padding-top-05 padding-left-2px">.gov </span>
|
<span class="padding-top-05 padding-left-2px">.gov </span>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -40,3 +40,11 @@ def slice_after(value, substring):
|
||||||
result = value[index + len(substring) :]
|
result = value[index + len(substring) :]
|
||||||
return result
|
return result
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def contains_checkbox(html_list):
|
||||||
|
for html_string in html_list:
|
||||||
|
if re.search(r'<input[^>]*type="checkbox"', html_string):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
|
@ -518,3 +518,11 @@ def multiple_unalphabetical_domain_objects(
|
||||||
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||||
applications.append(application)
|
applications.append(application)
|
||||||
return applications
|
return applications
|
||||||
|
|
||||||
|
|
||||||
|
def generic_domain_object(domain_type, object_name):
|
||||||
|
"""Returns a generic domain object of
|
||||||
|
domain_type 'application', 'information', or 'invitation'"""
|
||||||
|
mock = AuditedAdminMockData()
|
||||||
|
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||||
|
return application
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
from django.test import TestCase, RequestFactory, Client
|
from django.test import TestCase, RequestFactory, Client
|
||||||
from django.contrib.admin.sites import AdminSite
|
from django.contrib.admin.sites import AdminSite
|
||||||
|
from django.urls import reverse
|
||||||
|
|
||||||
from registrar.admin import (
|
from registrar.admin import (
|
||||||
|
DomainAdmin,
|
||||||
DomainApplicationAdmin,
|
DomainApplicationAdmin,
|
||||||
ListHeaderAdmin,
|
ListHeaderAdmin,
|
||||||
MyUserAdmin,
|
MyUserAdmin,
|
||||||
|
@ -12,16 +14,19 @@ from registrar.models import (
|
||||||
DomainInformation,
|
DomainInformation,
|
||||||
User,
|
User,
|
||||||
DomainInvitation,
|
DomainInvitation,
|
||||||
|
Domain,
|
||||||
)
|
)
|
||||||
from .common import (
|
from .common import (
|
||||||
completed_application,
|
completed_application,
|
||||||
|
generic_domain_object,
|
||||||
mock_user,
|
mock_user,
|
||||||
create_superuser,
|
create_superuser,
|
||||||
create_user,
|
create_user,
|
||||||
multiple_unalphabetical_domain_objects,
|
multiple_unalphabetical_domain_objects,
|
||||||
)
|
)
|
||||||
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
@ -35,6 +40,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.site = AdminSite()
|
self.site = AdminSite()
|
||||||
self.factory = RequestFactory()
|
self.factory = RequestFactory()
|
||||||
|
self.admin = DomainApplicationAdmin(
|
||||||
|
model=DomainApplication, admin_site=self.site
|
||||||
|
)
|
||||||
|
self.superuser = create_superuser()
|
||||||
|
self.staffuser = create_user()
|
||||||
|
|
||||||
@boto3_mocking.patching
|
@boto3_mocking.patching
|
||||||
def test_save_model_sends_submitted_email(self):
|
def test_save_model_sends_submitted_email(self):
|
||||||
|
@ -54,14 +64,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.SUBMITTED
|
application.status = DomainApplication.SUBMITTED
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = mock_client_instance.send_email.call_args
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
@ -100,14 +107,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.IN_REVIEW
|
application.status = DomainApplication.IN_REVIEW
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = mock_client_instance.send_email.call_args
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
@ -146,14 +150,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.APPROVED
|
application.status = DomainApplication.APPROVED
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = mock_client_instance.send_email.call_args
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
@ -187,14 +188,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.APPROVED
|
application.status = DomainApplication.APPROVED
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Test that approved domain exists and equals requested domain
|
# Test that approved domain exists and equals requested domain
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -219,14 +217,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.ACTION_NEEDED
|
application.status = DomainApplication.ACTION_NEEDED
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = mock_client_instance.send_email.call_args
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
@ -268,14 +263,11 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create an instance of the model admin
|
|
||||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
|
||||||
|
|
||||||
# Modify the application's property
|
# Modify the application's property
|
||||||
application.status = DomainApplication.REJECTED
|
application.status = DomainApplication.REJECTED
|
||||||
|
|
||||||
# Use the model admin's save_model method
|
# Use the model admin's save_model method
|
||||||
model_admin.save_model(request, application, form=None, change=True)
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
# Access the arguments passed to send_email
|
# Access the arguments passed to send_email
|
||||||
call_args = mock_client_instance.send_email.call_args
|
call_args = mock_client_instance.send_email.call_args
|
||||||
|
@ -296,6 +288,155 @@ class TestDomainApplicationAdmin(TestCase):
|
||||||
# Perform assertions on the mock call itself
|
# Perform assertions on the mock call itself
|
||||||
mock_client_instance.send_email.assert_called_once()
|
mock_client_instance.send_email.assert_called_once()
|
||||||
|
|
||||||
|
def test_save_model_sets_restricted_status_on_user(self):
|
||||||
|
# make sure there is no user with this email
|
||||||
|
EMAIL = "mayor@igorville.gov"
|
||||||
|
User.objects.filter(email=EMAIL).delete()
|
||||||
|
|
||||||
|
# Create a sample application
|
||||||
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
|
||||||
|
# Create a mock request
|
||||||
|
request = self.factory.post(
|
||||||
|
"/admin/registrar/domainapplication/{}/change/".format(application.pk)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Modify the application's property
|
||||||
|
application.status = DomainApplication.INELIGIBLE
|
||||||
|
|
||||||
|
# Use the model admin's save_model method
|
||||||
|
self.admin.save_model(request, application, form=None, change=True)
|
||||||
|
|
||||||
|
# Test that approved domain exists and equals requested domain
|
||||||
|
self.assertEqual(application.creator.status, "restricted")
|
||||||
|
|
||||||
|
def test_readonly_when_restricted_creator(self):
|
||||||
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
application.creator.status = User.RESTRICTED
|
||||||
|
application.creator.save()
|
||||||
|
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
readonly_fields = self.admin.get_readonly_fields(request, application)
|
||||||
|
|
||||||
|
expected_fields = [
|
||||||
|
"id",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
"status",
|
||||||
|
"creator",
|
||||||
|
"investigator",
|
||||||
|
"organization_type",
|
||||||
|
"federally_recognized_tribe",
|
||||||
|
"state_recognized_tribe",
|
||||||
|
"tribe_name",
|
||||||
|
"federal_agency",
|
||||||
|
"federal_type",
|
||||||
|
"is_election_board",
|
||||||
|
"organization_name",
|
||||||
|
"address_line1",
|
||||||
|
"address_line2",
|
||||||
|
"city",
|
||||||
|
"state_territory",
|
||||||
|
"zipcode",
|
||||||
|
"urbanization",
|
||||||
|
"type_of_work",
|
||||||
|
"more_organization_information",
|
||||||
|
"authorizing_official",
|
||||||
|
"approved_domain",
|
||||||
|
"requested_domain",
|
||||||
|
"submitter",
|
||||||
|
"purpose",
|
||||||
|
"no_other_contacts_rationale",
|
||||||
|
"anything_else",
|
||||||
|
"is_policy_acknowledged",
|
||||||
|
"current_websites",
|
||||||
|
"other_contacts",
|
||||||
|
"alternative_domains",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
def test_readonly_fields_for_analyst(self):
|
||||||
|
request = self.factory.get("/") # Use the correct method and path
|
||||||
|
request.user = self.staffuser
|
||||||
|
|
||||||
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
|
|
||||||
|
expected_fields = [
|
||||||
|
"creator",
|
||||||
|
"type_of_work",
|
||||||
|
"more_organization_information",
|
||||||
|
"address_line1",
|
||||||
|
"address_line2",
|
||||||
|
"zipcode",
|
||||||
|
"requested_domain",
|
||||||
|
"alternative_domains",
|
||||||
|
"purpose",
|
||||||
|
"submitter",
|
||||||
|
"no_other_contacts_rationale",
|
||||||
|
"anything_else",
|
||||||
|
"is_policy_acknowledged",
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
def test_readonly_fields_for_superuser(self):
|
||||||
|
request = self.factory.get("/") # Use the correct method and path
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
readonly_fields = self.admin.get_readonly_fields(request)
|
||||||
|
|
||||||
|
expected_fields = []
|
||||||
|
|
||||||
|
self.assertEqual(readonly_fields, expected_fields)
|
||||||
|
|
||||||
|
def test_saving_when_restricted_creator(self):
|
||||||
|
# Create an instance of the model
|
||||||
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
application.creator.status = User.RESTRICTED
|
||||||
|
application.creator.save()
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.get("/")
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.error") as mock_error:
|
||||||
|
# Simulate saving the model
|
||||||
|
self.admin.save_model(request, application, None, False)
|
||||||
|
|
||||||
|
# Assert that the error message was called with the correct argument
|
||||||
|
mock_error.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"This action is not permitted for applications "
|
||||||
|
+ "with a restricted creator.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Assert that the status has not changed
|
||||||
|
self.assertEqual(application.status, DomainApplication.IN_REVIEW)
|
||||||
|
|
||||||
|
def test_change_view_with_restricted_creator(self):
|
||||||
|
# Create an instance of the model
|
||||||
|
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||||
|
application.creator.status = User.RESTRICTED
|
||||||
|
application.creator.save()
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.warning") as mock_warning:
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.get(
|
||||||
|
"/admin/your_app/domainapplication/{}/change/".format(application.pk)
|
||||||
|
)
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
self.admin.display_restricted_warning(request, application)
|
||||||
|
|
||||||
|
# Assert that the error message was called with the correct argument
|
||||||
|
mock_warning.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"Cannot edit an application with a restricted creator.",
|
||||||
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
|
@ -375,7 +516,6 @@ class ListHeaderAdminTest(TestCase):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
self.superuser.delete()
|
|
||||||
|
|
||||||
|
|
||||||
class MyUserAdminTest(TestCase):
|
class MyUserAdminTest(TestCase):
|
||||||
|
@ -636,3 +776,129 @@ class AuditedAdminTest(TestCase):
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
DomainApplication.objects.all().delete()
|
DomainApplication.objects.all().delete()
|
||||||
DomainInvitation.objects.all().delete()
|
DomainInvitation.objects.all().delete()
|
||||||
|
|
||||||
|
|
||||||
|
class DomainSessionVariableTest(TestCase):
|
||||||
|
"""Test cases for session variables in Django Admin"""
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
self.admin = DomainAdmin(Domain, None)
|
||||||
|
self.client = Client(HTTP_HOST="localhost:8080")
|
||||||
|
|
||||||
|
def test_session_vars_set_correctly(self):
|
||||||
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information = generic_domain_object("information", "session")
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
self.populate_session_values(request, dummy_domain_information.domain)
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(
|
||||||
|
request.session["analyst_action_location"],
|
||||||
|
dummy_domain_information.domain.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_vars_set_correctly_hardcoded_domain(self):
|
||||||
|
"""Checks if session variables are being set correctly"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information: Domain = generic_domain_object(
|
||||||
|
"information", "session"
|
||||||
|
)
|
||||||
|
dummy_domain_information.domain.pk = 1
|
||||||
|
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
self.populate_session_values(request, dummy_domain_information.domain)
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request.session["analyst_action_location"], 1)
|
||||||
|
|
||||||
|
def test_session_variables_reset_correctly(self):
|
||||||
|
"""Checks if incorrect session variables get overridden"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information = generic_domain_object("information", "session")
|
||||||
|
request = self.get_factory_post_edit_domain(dummy_domain_information.domain.pk)
|
||||||
|
|
||||||
|
self.populate_session_values(
|
||||||
|
request, dummy_domain_information.domain, preload_bad_data=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(
|
||||||
|
request.session["analyst_action_location"],
|
||||||
|
dummy_domain_information.domain.pk,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_session_variables_retain_information(self):
|
||||||
|
"""Checks to see if session variables retain old information"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
dummy_domain_information_list = multiple_unalphabetical_domain_objects(
|
||||||
|
"information"
|
||||||
|
)
|
||||||
|
for item in dummy_domain_information_list:
|
||||||
|
request = self.get_factory_post_edit_domain(item.domain.pk)
|
||||||
|
self.populate_session_values(request, item.domain)
|
||||||
|
|
||||||
|
self.assertEqual(request.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request.session["analyst_action_location"], item.domain.pk)
|
||||||
|
|
||||||
|
def test_session_variables_concurrent_requests(self):
|
||||||
|
"""Simulates two requests at once"""
|
||||||
|
|
||||||
|
p = "adminpass"
|
||||||
|
self.client.login(username="superuser", password=p)
|
||||||
|
|
||||||
|
info_first = generic_domain_object("information", "session")
|
||||||
|
info_second = generic_domain_object("information", "session2")
|
||||||
|
|
||||||
|
request_first = self.get_factory_post_edit_domain(info_first.domain.pk)
|
||||||
|
request_second = self.get_factory_post_edit_domain(info_second.domain.pk)
|
||||||
|
|
||||||
|
self.populate_session_values(request_first, info_first.domain, True)
|
||||||
|
self.populate_session_values(request_second, info_second.domain, True)
|
||||||
|
|
||||||
|
# Check if anything got nulled out
|
||||||
|
self.assertNotEqual(request_first.session["analyst_action"], None)
|
||||||
|
self.assertNotEqual(request_second.session["analyst_action"], None)
|
||||||
|
self.assertNotEqual(request_first.session["analyst_action_location"], None)
|
||||||
|
self.assertNotEqual(request_second.session["analyst_action_location"], None)
|
||||||
|
|
||||||
|
# Check if they are both the same action 'type'
|
||||||
|
self.assertEqual(request_first.session["analyst_action"], "edit")
|
||||||
|
self.assertEqual(request_second.session["analyst_action"], "edit")
|
||||||
|
|
||||||
|
# Check their locations, and ensure they aren't the same across both
|
||||||
|
self.assertNotEqual(
|
||||||
|
request_first.session["analyst_action_location"],
|
||||||
|
request_second.session["analyst_action_location"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def populate_session_values(self, request, domain_object, preload_bad_data=False):
|
||||||
|
"""Boilerplate for creating mock sessions"""
|
||||||
|
request.user = self.client
|
||||||
|
request.session = SessionStore()
|
||||||
|
request.session.create()
|
||||||
|
if preload_bad_data:
|
||||||
|
request.session["analyst_action"] = "invalid"
|
||||||
|
request.session["analyst_action_location"] = "bad location"
|
||||||
|
self.admin.response_change(request, domain_object)
|
||||||
|
|
||||||
|
def get_factory_post_edit_domain(self, primary_key):
|
||||||
|
"""Posts to registrar domain change
|
||||||
|
with the edit domain button 'clicked',
|
||||||
|
then returns the factory object"""
|
||||||
|
return self.factory.post(
|
||||||
|
reverse("admin:registrar_domain_change", args=(primary_key,)),
|
||||||
|
{"_edit_domain": "true"},
|
||||||
|
follow=True,
|
||||||
|
)
|
||||||
|
|
|
@ -171,6 +171,15 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.submit()
|
application.submit()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_submitted(self):
|
||||||
|
"""Create an application with status ineligible and call submit
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.submit()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_in_review(self):
|
def test_transition_not_allowed_started_in_review(self):
|
||||||
"""Create an application with status started and call in_review
|
"""Create an application with status started and call in_review
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -225,6 +234,15 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.in_review()
|
application.in_review()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_in_review(self):
|
||||||
|
"""Create an application with status ineligible and call in_review
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.in_review()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_action_needed(self):
|
def test_transition_not_allowed_started_action_needed(self):
|
||||||
"""Create an application with status started and call action_needed
|
"""Create an application with status started and call action_needed
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -270,6 +288,15 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.action_needed()
|
application.action_needed()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_action_needed(self):
|
||||||
|
"""Create an application with status ineligible and call action_needed
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.action_needed()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_approved(self):
|
def test_transition_not_allowed_started_approved(self):
|
||||||
"""Create an application with status started and call approve
|
"""Create an application with status started and call approve
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -351,6 +378,15 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.withdraw()
|
application.withdraw()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_withdrawn(self):
|
||||||
|
"""Create an application with status ineligible and call withdraw
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.withdraw()
|
||||||
|
|
||||||
def test_transition_not_allowed_started_rejected(self):
|
def test_transition_not_allowed_started_rejected(self):
|
||||||
"""Create an application with status started and call reject
|
"""Create an application with status started and call reject
|
||||||
against transition rules"""
|
against transition rules"""
|
||||||
|
@ -396,6 +432,69 @@ class TestDomainApplication(TestCase):
|
||||||
with self.assertRaises(TransitionNotAllowed):
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
application.reject()
|
application.reject()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_rejected(self):
|
||||||
|
"""Create an application with status ineligible and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_started_ineligible(self):
|
||||||
|
"""Create an application with status started and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.STARTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_submitted_ineligible(self):
|
||||||
|
"""Create an application with status submitted and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_action_needed_ineligible(self):
|
||||||
|
"""Create an application with status action needed and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_withdrawn_ineligible(self):
|
||||||
|
"""Create an application with status withdrawn and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_rejected_ineligible(self):
|
||||||
|
"""Create an application with status rejected and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.REJECTED)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
def test_transition_not_allowed_ineligible_ineligible(self):
|
||||||
|
"""Create an application with status ineligible and call reject
|
||||||
|
against transition rules"""
|
||||||
|
|
||||||
|
application = completed_application(status=DomainApplication.INELIGIBLE)
|
||||||
|
|
||||||
|
with self.assertRaises(TransitionNotAllowed):
|
||||||
|
application.reject_with_prejudice()
|
||||||
|
|
||||||
|
|
||||||
class TestPermissions(TestCase):
|
class TestPermissions(TestCase):
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from registrar.templatetags.custom_filters import (
|
||||||
extract_a_text,
|
extract_a_text,
|
||||||
find_index,
|
find_index,
|
||||||
slice_after,
|
slice_after,
|
||||||
|
contains_checkbox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -83,3 +84,21 @@ class CustomFiltersTestCase(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
result, value
|
result, value
|
||||||
) # Should return the original value if substring not found
|
) # Should return the original value if substring not found
|
||||||
|
|
||||||
|
def test_contains_checkbox_with_checkbox(self):
|
||||||
|
# Test the filter when HTML list contains a checkbox
|
||||||
|
html_list = [
|
||||||
|
'<input type="checkbox" name="_selected_action">',
|
||||||
|
"<div>Some other HTML content</div>",
|
||||||
|
]
|
||||||
|
result = contains_checkbox(html_list)
|
||||||
|
self.assertTrue(result) # Expecting True
|
||||||
|
|
||||||
|
def test_contains_checkbox_without_checkbox(self):
|
||||||
|
# Test the filter when HTML list does not contain a checkbox
|
||||||
|
html_list = [
|
||||||
|
"<div>Some HTML content without checkbox</div>",
|
||||||
|
"<p>More HTML content</p>",
|
||||||
|
]
|
||||||
|
result = contains_checkbox(html_list)
|
||||||
|
self.assertFalse(result) # Expecting False
|
||||||
|
|
|
@ -101,6 +101,18 @@ class LoggedInTests(TestWithUser):
|
||||||
"What kind of U.S.-based government organization do you represent?",
|
"What kind of U.S.-based government organization do you represent?",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_domain_application_form_with_ineligible_user(self):
|
||||||
|
"""Application form not accessible for an ineligible user.
|
||||||
|
This test should be solid enough since all application wizard
|
||||||
|
views share the same permissions class"""
|
||||||
|
self.user.status = User.RESTRICTED
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get("/register/", follow=True)
|
||||||
|
print(response.status_code)
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationTests(TestWithUser, WebTest):
|
class DomainApplicationTests(TestWithUser, WebTest):
|
||||||
|
|
||||||
|
@ -1423,6 +1435,18 @@ class TestDomainDetail(TestWithDomainPermissions, WebTest):
|
||||||
success_page, "The security email for this domain have been updated"
|
success_page, "The security email for this domain have been updated"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_domain_overview_blocked_for_ineligible_user(self):
|
||||||
|
"""We could easily duplicate this test for all domain management
|
||||||
|
views, but a single url test should be solid enough since all domain
|
||||||
|
management pages share the same permissions class"""
|
||||||
|
self.user.status = User.RESTRICTED
|
||||||
|
self.user.save()
|
||||||
|
home_page = self.app.get("/")
|
||||||
|
self.assertContains(home_page, "igorville.gov")
|
||||||
|
with less_console_noise():
|
||||||
|
response = self.client.get(reverse("domain", kwargs={"pk": self.domain.id}))
|
||||||
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
class TestApplicationStatus(TestWithUser, WebTest):
|
class TestApplicationStatus(TestWithUser, WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -1437,6 +1461,28 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
||||||
)
|
)
|
||||||
application.save()
|
application.save()
|
||||||
|
|
||||||
|
home_page = self.app.get("/")
|
||||||
|
self.assertContains(home_page, "city.gov")
|
||||||
|
# click the "Manage" link
|
||||||
|
detail_page = home_page.click("Manage")
|
||||||
|
self.assertContains(detail_page, "city.gov")
|
||||||
|
self.assertContains(detail_page, "city1.gov")
|
||||||
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
|
self.assertContains(detail_page, "Admin Tester")
|
||||||
|
self.assertContains(detail_page, "Status:")
|
||||||
|
|
||||||
|
def test_application_status_with_ineligible_user(self):
|
||||||
|
"""Checking application status page whith a blocked user.
|
||||||
|
The user should still have access to view."""
|
||||||
|
self.user.status = "ineligible"
|
||||||
|
self.user.save()
|
||||||
|
|
||||||
|
application = completed_application(
|
||||||
|
status=DomainApplication.SUBMITTED, user=self.user
|
||||||
|
)
|
||||||
|
application.save()
|
||||||
|
|
||||||
home_page = self.app.get("/")
|
home_page = self.app.get("/")
|
||||||
self.assertContains(home_page, "city.gov")
|
self.assertContains(home_page, "city.gov")
|
||||||
# click the "Manage" link
|
# click the "Manage" link
|
||||||
|
@ -1459,6 +1505,7 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
||||||
# click the "Manage" link
|
# click the "Manage" link
|
||||||
detail_page = home_page.click("Manage")
|
detail_page = home_page.click("Manage")
|
||||||
self.assertContains(detail_page, "city.gov")
|
self.assertContains(detail_page, "city.gov")
|
||||||
|
self.assertContains(detail_page, "city1.gov")
|
||||||
self.assertContains(detail_page, "Chief Tester")
|
self.assertContains(detail_page, "Chief Tester")
|
||||||
self.assertContains(detail_page, "testy@town.com")
|
self.assertContains(detail_page, "testy@town.com")
|
||||||
self.assertContains(detail_page, "Admin Tester")
|
self.assertContains(detail_page, "Admin Tester")
|
||||||
|
@ -1500,3 +1547,18 @@ class TestApplicationStatus(TestWithUser, WebTest):
|
||||||
reverse(url_name, kwargs={"pk": application.pk})
|
reverse(url_name, kwargs={"pk": application.pk})
|
||||||
)
|
)
|
||||||
self.assertEqual(page.status_code, 403)
|
self.assertEqual(page.status_code, 403)
|
||||||
|
|
||||||
|
def test_approved_application_not_in_active_requests(self):
|
||||||
|
"""An approved application is not shown in the Active
|
||||||
|
Requests table on home.html."""
|
||||||
|
application = completed_application(
|
||||||
|
status=DomainApplication.APPROVED, user=self.user
|
||||||
|
)
|
||||||
|
application.save()
|
||||||
|
|
||||||
|
home_page = self.app.get("/")
|
||||||
|
# This works in our test environment because creating
|
||||||
|
# an approved application here does not generate a
|
||||||
|
# domain object, so we do not expect to see 'city.gov'
|
||||||
|
# in either the Domains or Requests tables.
|
||||||
|
self.assertNotContains(home_page, "city.gov")
|
||||||
|
|
|
@ -12,7 +12,7 @@ from registrar.models import DomainApplication
|
||||||
from registrar.utility import StrEnum
|
from registrar.utility import StrEnum
|
||||||
from registrar.views.utility import StepsHelper
|
from registrar.views.utility import StepsHelper
|
||||||
|
|
||||||
from .utility import DomainApplicationPermissionView
|
from .utility import DomainApplicationPermissionView, ApplicationWizardPermissionView
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class Step(StrEnum):
|
||||||
REVIEW = "review"
|
REVIEW = "review"
|
||||||
|
|
||||||
|
|
||||||
class ApplicationWizard(TemplateView):
|
class ApplicationWizard(ApplicationWizardPermissionView, TemplateView):
|
||||||
"""
|
"""
|
||||||
A common set of methods and configuration.
|
A common set of methods and configuration.
|
||||||
|
|
||||||
|
@ -60,6 +60,8 @@ class ApplicationWizard(TemplateView):
|
||||||
although not without consulting the base implementation, first.
|
although not without consulting the base implementation, first.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
template_name = ""
|
||||||
|
|
||||||
# uniquely namespace the wizard in urls.py
|
# uniquely namespace the wizard in urls.py
|
||||||
# (this is not seen _in_ urls, only for Django's internal naming)
|
# (this is not seen _in_ urls, only for Django's internal naming)
|
||||||
# NB: this is included here for reference. Do not change it without
|
# NB: this is included here for reference. Do not change it without
|
||||||
|
|
|
@ -79,6 +79,7 @@ class DomainOrgNameAddressView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The organization name and mailing address has been updated."
|
self.request, "The organization name and mailing address has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -121,6 +122,7 @@ class DomainAuthorizingOfficialView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The authorizing official for this domain has been updated."
|
self.request, "The authorizing official for this domain has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -191,6 +193,7 @@ class DomainNameserversView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The name servers for this domain have been updated."
|
self.request, "The name servers for this domain have been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(formset)
|
return super().form_valid(formset)
|
||||||
|
|
||||||
|
@ -231,6 +234,7 @@ class DomainYourContactInformationView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "Your contact information for this domain has been updated."
|
self.request, "Your contact information for this domain has been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
@ -278,6 +282,7 @@ class DomainSecurityEmailView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, "The security email for this domain have been updated."
|
self.request, "The security email for this domain have been updated."
|
||||||
)
|
)
|
||||||
|
|
||||||
# superclass has the redirect
|
# superclass has the redirect
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
@ -353,6 +358,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||||
messages.success(
|
messages.success(
|
||||||
self.request, f"Invited {email_address} to this domain."
|
self.request, f"Invited {email_address} to this domain."
|
||||||
)
|
)
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
|
@ -374,6 +380,7 @@ class DomainAddUserView(DomainPermissionView, FormMixin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
messages.success(self.request, f"Added user {requested_email}.")
|
messages.success(self.request, f"Added user {requested_email}.")
|
||||||
|
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,10 @@ def index(request):
|
||||||
context = {}
|
context = {}
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
applications = DomainApplication.objects.filter(creator=request.user)
|
applications = DomainApplication.objects.filter(creator=request.user)
|
||||||
context["domain_applications"] = applications
|
# Let's exclude the approved applications since our
|
||||||
|
# domain_applications context will be used to populate
|
||||||
|
# the active applications table
|
||||||
|
context["domain_applications"] = applications.exclude(status="approved")
|
||||||
|
|
||||||
domains = request.user.permissions.values(
|
domains = request.user.permissions.values(
|
||||||
"role",
|
"role",
|
||||||
|
|
|
@ -5,4 +5,5 @@ from .permission_views import (
|
||||||
DomainPermissionView,
|
DomainPermissionView,
|
||||||
DomainApplicationPermissionView,
|
DomainApplicationPermissionView,
|
||||||
DomainInvitationPermissionDeleteView,
|
DomainInvitationPermissionDeleteView,
|
||||||
|
ApplicationWizardPermissionView,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,16 @@
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
|
|
||||||
from registrar.models import UserDomainRole, DomainApplication, DomainInvitation
|
from registrar.models import (
|
||||||
|
DomainApplication,
|
||||||
|
DomainInvitation,
|
||||||
|
DomainInformation,
|
||||||
|
UserDomainRole,
|
||||||
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PermissionsLoginMixin(PermissionRequiredMixin):
|
class PermissionsLoginMixin(PermissionRequiredMixin):
|
||||||
|
@ -25,27 +34,80 @@ class DomainPermission(PermissionsLoginMixin):
|
||||||
up from the domain's primary key in self.kwargs["pk"]
|
up from the domain's primary key in self.kwargs["pk"]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# ticket 806
|
|
||||||
# if self.request.user is staff or admin and
|
|
||||||
# domain.application__status = 'approved' or 'rejected' or 'action needed'
|
|
||||||
# return True
|
|
||||||
|
|
||||||
if not self.request.user.is_authenticated:
|
if not self.request.user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if self.request.user.is_restricted():
|
||||||
|
return False
|
||||||
|
|
||||||
|
pk = self.kwargs["pk"]
|
||||||
|
# If pk is none then something went very wrong...
|
||||||
|
if pk is None:
|
||||||
|
raise ValueError("Primary key is None")
|
||||||
|
|
||||||
|
if self.can_access_other_user_domains(pk):
|
||||||
|
return True
|
||||||
|
|
||||||
# user needs to have a role on the domain
|
# user needs to have a role on the domain
|
||||||
if not UserDomainRole.objects.filter(
|
if not UserDomainRole.objects.filter(
|
||||||
user=self.request.user, domain__id=self.kwargs["pk"]
|
user=self.request.user, domain__id=pk
|
||||||
).exists():
|
).exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ticket 796
|
|
||||||
# if domain.application__status != 'approved'
|
|
||||||
# return false
|
|
||||||
|
|
||||||
# if we need to check more about the nature of role, do it here.
|
# if we need to check more about the nature of role, do it here.
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def can_access_other_user_domains(self, pk):
|
||||||
|
"""Checks to see if an authorized user (staff or superuser)
|
||||||
|
can access a domain that they did not create or was invited to.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check if the user is permissioned...
|
||||||
|
user_is_analyst_or_superuser = (
|
||||||
|
self.request.user.is_staff or self.request.user.is_superuser
|
||||||
|
)
|
||||||
|
|
||||||
|
if not user_is_analyst_or_superuser:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if the user is attempting a valid edit action.
|
||||||
|
# In other words, if the analyst/admin did not click
|
||||||
|
# the 'Manage Domain' button in /admin,
|
||||||
|
# then they cannot access this page.
|
||||||
|
session = self.request.session
|
||||||
|
can_do_action = (
|
||||||
|
"analyst_action" in session
|
||||||
|
and "analyst_action_location" in session
|
||||||
|
and session["analyst_action_location"] == pk
|
||||||
|
)
|
||||||
|
|
||||||
|
if not can_do_action:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Analysts may manage domains, when they are in these statuses:
|
||||||
|
valid_domain_statuses = [
|
||||||
|
DomainApplication.APPROVED,
|
||||||
|
DomainApplication.IN_REVIEW,
|
||||||
|
DomainApplication.REJECTED,
|
||||||
|
DomainApplication.ACTION_NEEDED,
|
||||||
|
# Edge case - some domains do not have
|
||||||
|
# a status or DomainInformation... aka a status of 'None'.
|
||||||
|
# It is necessary to access those to correct errors.
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
|
||||||
|
requested_domain = None
|
||||||
|
if DomainInformation.objects.filter(id=pk).exists():
|
||||||
|
requested_domain = DomainInformation.objects.get(id=pk)
|
||||||
|
|
||||||
|
if requested_domain.domain_application.status not in valid_domain_statuses:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Valid session keys exist,
|
||||||
|
# the user is permissioned,
|
||||||
|
# and it is in a valid status
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DomainApplicationPermission(PermissionsLoginMixin):
|
class DomainApplicationPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
|
@ -71,6 +133,23 @@ class DomainApplicationPermission(PermissionsLoginMixin):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationWizardPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
|
"""Does the logged-in user have permission to start or edit an application?"""
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Check if this user has permission to start or edit an application.
|
||||||
|
|
||||||
|
The user is in self.request.user
|
||||||
|
"""
|
||||||
|
|
||||||
|
# The user has an ineligible flag
|
||||||
|
if self.request.user.is_restricted():
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationPermission(PermissionsLoginMixin):
|
class DomainInvitationPermission(PermissionsLoginMixin):
|
||||||
|
|
||||||
"""Does the logged-in user have access to this domain invitation?
|
"""Does the logged-in user have access to this domain invitation?
|
||||||
|
|
|
@ -2,15 +2,18 @@
|
||||||
|
|
||||||
import abc # abstract base class
|
import abc # abstract base class
|
||||||
|
|
||||||
from django.views.generic import DetailView, DeleteView
|
from django.views.generic import DetailView, DeleteView, TemplateView
|
||||||
|
|
||||||
from registrar.models import Domain, DomainApplication, DomainInvitation
|
from registrar.models import Domain, DomainApplication, DomainInvitation
|
||||||
|
|
||||||
from .mixins import (
|
from .mixins import (
|
||||||
DomainPermission,
|
DomainPermission,
|
||||||
DomainApplicationPermission,
|
DomainApplicationPermission,
|
||||||
DomainInvitationPermission,
|
DomainInvitationPermission,
|
||||||
|
ApplicationWizardPermission,
|
||||||
)
|
)
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
|
@ -26,6 +29,22 @@ class DomainPermissionView(DomainPermission, DetailView, abc.ABC):
|
||||||
# variable name in template context for the model object
|
# variable name in template context for the model object
|
||||||
context_object_name = "domain"
|
context_object_name = "domain"
|
||||||
|
|
||||||
|
# Adds context information for user permissions
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
user = self.request.user
|
||||||
|
context["is_analyst_or_superuser"] = user.is_staff or user.is_superuser
|
||||||
|
# Stored in a variable for the linter
|
||||||
|
action = "analyst_action"
|
||||||
|
action_location = "analyst_action_location"
|
||||||
|
# Flag to see if an analyst is attempting to make edits
|
||||||
|
if action in self.request.session:
|
||||||
|
context[action] = self.request.session[action]
|
||||||
|
if action_location in self.request.session:
|
||||||
|
context[action_location] = self.request.session[action_location]
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
# Abstract property enforces NotImplementedError on an attribute.
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
@property
|
@property
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
|
@ -53,6 +72,23 @@ class DomainApplicationPermissionView(DomainApplicationPermission, DetailView, a
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ApplicationWizardPermissionView(
|
||||||
|
ApplicationWizardPermission, TemplateView, abc.ABC
|
||||||
|
):
|
||||||
|
|
||||||
|
"""Abstract base view for the application form that enforces permissions
|
||||||
|
|
||||||
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
|
`template_name`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Abstract property enforces NotImplementedError on an attribute.
|
||||||
|
@property
|
||||||
|
@abc.abstractmethod
|
||||||
|
def template_name(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class DomainInvitationPermissionDeleteView(
|
class DomainInvitationPermissionDeleteView(
|
||||||
DomainInvitationPermission, DeleteView, abc.ABC
|
DomainInvitationPermission, DeleteView, abc.ABC
|
||||||
):
|
):
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
|
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
|
||||||
# get-gov.js contains suspicious word "from" as in `Array.from()`
|
# get-gov.js contains suspicious word "from" as in `Array.from()`
|
||||||
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
|
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
|
||||||
# Ignore wording of "TODO"
|
# Ignores suspicious word "TODO"
|
||||||
10027 OUTOFSCOPE http://app:8080.*$
|
10027 OUTOFSCOPE http://app:8080.*$
|
||||||
10028 FAIL (Open Redirect - Passive/beta)
|
10028 FAIL (Open Redirect - Passive/beta)
|
||||||
10029 FAIL (Cookie Poisoning - Passive/beta)
|
10029 FAIL (Cookie Poisoning - Passive/beta)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue