mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-24 21:29:53 +02:00
Merge remote-tracking branch 'origin' into ab/645-implement-test-cases-registry-integration
This commit is contained in:
commit
c9cadd3401
57 changed files with 2238 additions and 218 deletions
12
src/package-lock.json
generated
12
src/package-lock.json
generated
|
@ -4086,9 +4086,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/normalize-package-data/node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
|
@ -10148,9 +10148,9 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import logging
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from django.urls import reverse
|
||||
from registrar.models.utility.admin_sort_fields import AdminSortFields
|
||||
from . import models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AuditedAdmin(admin.ModelAdmin):
|
||||
|
||||
class AuditedAdmin(admin.ModelAdmin, AdminSortFields):
|
||||
"""Custom admin to make auditing easier."""
|
||||
|
||||
def history_view(self, request, object_id, extra_context=None):
|
||||
|
@ -23,9 +23,13 @@ class AuditedAdmin(admin.ModelAdmin):
|
|||
)
|
||||
)
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
"""Used to sort dropdown fields alphabetically but can be expanded upon"""
|
||||
form_field = super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
return self.form_field_order_helper(form_field, db_field)
|
||||
|
||||
|
||||
class ListHeaderAdmin(AuditedAdmin):
|
||||
|
||||
"""Custom admin to add a descriptive subheader to list views."""
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
|
@ -93,12 +97,29 @@ class UserContactInline(admin.StackedInline):
|
|||
model = models.Contact
|
||||
|
||||
|
||||
class MyUserAdmin(UserAdmin):
|
||||
class MyUserAdmin(BaseUserAdmin):
|
||||
|
||||
"""Custom user admin class to use our inlines."""
|
||||
|
||||
inlines = [UserContactInline]
|
||||
|
||||
def get_list_display(self, request):
|
||||
if not request.user.is_superuser:
|
||||
# Customize the list display for staff users
|
||||
return ("email", "first_name", "last_name", "is_staff", "is_superuser")
|
||||
|
||||
# Use the default list display for non-staff users
|
||||
return super().get_list_display(request)
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
if not request.user.is_superuser:
|
||||
# If the user doesn't have permission to change the model,
|
||||
# show a read-only fieldset
|
||||
return ((None, {"fields": []}),)
|
||||
|
||||
# If the user has permission to change the model, show all fields
|
||||
return super().get_fieldsets(request, obj)
|
||||
|
||||
|
||||
class HostIPInline(admin.StackedInline):
|
||||
|
||||
|
@ -224,7 +245,6 @@ class DomainAdmin(ListHeaderAdmin):
|
|||
|
||||
|
||||
class ContactAdmin(ListHeaderAdmin):
|
||||
|
||||
"""Custom contact admin class to add search."""
|
||||
|
||||
search_fields = ["email", "first_name", "last_name"]
|
||||
|
@ -336,18 +356,27 @@ class DomainApplicationAdmin(ListHeaderAdmin):
|
|||
pass
|
||||
elif obj.status == models.DomainApplication.SUBMITTED:
|
||||
# This is an fsm in model which will throw an error if the
|
||||
# transition condition is violated, so we call it on the
|
||||
# original object which has the right status value, and pass
|
||||
# the updated object which contains the up-to-date data
|
||||
# for the side effects (like an email send). Same
|
||||
# comment applies to original_obj method calls below.
|
||||
original_obj.submit(updated_domain_application=obj)
|
||||
elif obj.status == models.DomainApplication.INVESTIGATING:
|
||||
original_obj.in_review(updated_domain_application=obj)
|
||||
# transition condition is violated, so we roll back the
|
||||
# status to what it was before the admin user changed it and
|
||||
# let the fsm method set it. Same comment applies to
|
||||
# transition method calls below.
|
||||
obj.status = original_obj.status
|
||||
obj.submit()
|
||||
elif obj.status == models.DomainApplication.IN_REVIEW:
|
||||
obj.status = original_obj.status
|
||||
obj.in_review()
|
||||
elif obj.status == models.DomainApplication.ACTION_NEEDED:
|
||||
obj.status = original_obj.status
|
||||
obj.action_needed()
|
||||
elif obj.status == models.DomainApplication.APPROVED:
|
||||
original_obj.approve(updated_domain_application=obj)
|
||||
obj.status = original_obj.status
|
||||
obj.approve()
|
||||
elif obj.status == models.DomainApplication.WITHDRAWN:
|
||||
original_obj.withdraw()
|
||||
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")
|
||||
|
||||
|
|
172
src/registrar/assets/sass/_theme/_admin.scss
Normal file
172
src/registrar/assets/sass/_theme/_admin.scss
Normal file
|
@ -0,0 +1,172 @@
|
|||
@use "cisa_colors" as *;
|
||||
@use "uswds-core" as *;
|
||||
|
||||
// We'll use Django's CSS vars: https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#theming-support
|
||||
// and assign USWDS theme vars whenever possible
|
||||
// If needed (see below), we'll use the USWDS hex value
|
||||
// As a last resort, we'll use CISA colors to supplement the palette
|
||||
:root,
|
||||
html[data-theme="light"] {
|
||||
--primary: #{$theme-color-primary};
|
||||
--secondary: #{$theme-color-primary-darkest};
|
||||
--accent: #{$theme-color-accent-cool};
|
||||
// --primary-fg: #fff;
|
||||
|
||||
// USWDS theme vars that are set to a token, such as #{$theme-color-base-darker}
|
||||
// would interpolate to 'gray-cool-70' and output invalid CSS, so we use the hex
|
||||
// source value instead: https://designsystem.digital.gov/design-tokens/color/system-tokens/
|
||||
--body-fg: #3d4551;
|
||||
// --body-bg: #fff;
|
||||
--body-quiet-color: #{$theme-color-base-dark};
|
||||
// --body-loud-color: #000;
|
||||
|
||||
--header-color: var( --primary-fg);
|
||||
--header-branding-color: var( --primary-fg);
|
||||
// --header-bg: var(--secondary);
|
||||
// --header-link-color: var(--primary-fg);
|
||||
|
||||
--breadcrumbs-fg: #{$theme-color-accent-cool-lightest};
|
||||
// --breadcrumbs-link-fg: var(--body-bg);
|
||||
--breadcrumbs-bg: #{$theme-color-primary-dark};
|
||||
|
||||
// #{$theme-link-color} would interpolate to 'primary', so we use the source value instead
|
||||
--link-fg: #{$theme-color-primary};
|
||||
--link-hover-color: #{$theme-color-primary-darker};
|
||||
// $theme-link-visited-color - violet-70v
|
||||
--link-selected-fg: #54278f;
|
||||
|
||||
--hairline-color: #{$dhs-gray-15};
|
||||
// $theme-color-base-lightest - gray-5
|
||||
--border-color: #f0f0f0;
|
||||
|
||||
--error-fg: #{$theme-color-error};
|
||||
|
||||
--message-success-bg: #{$theme-color-success-lighter};
|
||||
// $theme-color-warning-lighter - yellow-5
|
||||
--message-warning-bg: #faf3d1;
|
||||
--message-error-bg: #{$theme-color-error-lighter};
|
||||
|
||||
--darkened-bg: #{$dhs-gray-15}; /* A bit darker than --body-bg */
|
||||
--selected-bg: var(--border-color); /* E.g. selected table cells */
|
||||
--selected-row: var(--message-warning-bg);
|
||||
|
||||
// --button-fg: #fff;
|
||||
// --button-bg: var(--secondary);
|
||||
--button-hover-bg: #{$theme-color-primary-darker};
|
||||
--default-button-bg: #{$theme-color-primary-dark};
|
||||
--default-button-hover-bg: #{$theme-color-primary-darkest};
|
||||
// #{$theme-color-base} - 'gray-cool-50'
|
||||
--close-button-bg: #71767a;
|
||||
// #{$theme-color-base-darker} - 'gray-cool-70'
|
||||
--close-button-hover-bg: #3d4551;
|
||||
--delete-button-bg: #{$theme-color-error};
|
||||
--delete-button-hover-bg: #{$theme-color-error-dark};
|
||||
|
||||
// --object-tools-fg: var(--button-fg);
|
||||
// --object-tools-bg: var(--close-button-bg);
|
||||
// --object-tools-hover-bg: var(--close-button-hover-bg);
|
||||
}
|
||||
|
||||
// Fold dark theme settings into our main CSS
|
||||
// https://docs.djangoproject.com/en/4.2/ref/contrib/admin/#theming-support > dark theme note
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root,
|
||||
html[data-theme="dark"] {
|
||||
// Edit the primary to meet accessibility requ.
|
||||
--primary: #23485a;
|
||||
--primary-fg: #f7f7f7;
|
||||
|
||||
--body-fg: #eeeeee;
|
||||
--body-bg: #121212;
|
||||
--body-quiet-color: #e0e0e0;
|
||||
--body-loud-color: #ffffff;
|
||||
|
||||
--breadcrumbs-link-fg: #e0e0e0;
|
||||
--breadcrumbs-bg: var(--primary);
|
||||
|
||||
--link-fg: #81d4fa;
|
||||
--link-hover-color: #4ac1f7;
|
||||
--link-selected-fg: #6f94c6;
|
||||
|
||||
--hairline-color: #272727;
|
||||
--border-color: #353535;
|
||||
|
||||
--error-fg: #e35f5f;
|
||||
--message-success-bg: #006b1b;
|
||||
--message-warning-bg: #583305;
|
||||
--message-error-bg: #570808;
|
||||
|
||||
--darkened-bg: #212121;
|
||||
--selected-bg: #1b1b1b;
|
||||
--selected-row: #00363a;
|
||||
|
||||
--close-button-bg: #333333;
|
||||
--close-button-hover-bg: #666666;
|
||||
}
|
||||
|
||||
// Dark mode django (bug due to scss cascade) and USWDS tables
|
||||
.change-list .usa-table,
|
||||
.change-list .usa-table--striped tbody tr:nth-child(odd) td,
|
||||
.change-list .usa-table--borderless thead th,
|
||||
.change-list .usa-table thead td,
|
||||
.change-list .usa-table thead th,
|
||||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
}
|
||||
|
||||
// Firefox needs this to be specifically set
|
||||
html[data-theme="dark"] {
|
||||
.change-list .usa-table,
|
||||
.change-list .usa-table--striped tbody tr:nth-child(odd) td,
|
||||
.change-list .usa-table--borderless thead th,
|
||||
.change-list .usa-table thead td,
|
||||
.change-list .usa-table thead th,
|
||||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
}
|
||||
|
||||
#branding h1 a:link, #branding h1 a:visited {
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
||||
#branding h1,
|
||||
h1, h2, h3 {
|
||||
font-weight: font-weight('bold');
|
||||
}
|
||||
|
||||
table > caption > a {
|
||||
font-weight: font-weight('bold');
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
.usa-table--striped tbody tr:nth-child(odd) td,
|
||||
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||
.usa-table td,
|
||||
.usa-table th {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
#nav-sidebar {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
// 'Delete button' layout bug
|
||||
.submit-row a.deletelink {
|
||||
height: auto!important;
|
||||
}
|
||||
|
||||
// Keep th from collapsing
|
||||
.min-width-25 {
|
||||
min-width: 25px;
|
||||
}
|
||||
.min-width-81 {
|
||||
min-width: 81px;
|
||||
}
|
|
@ -9,3 +9,7 @@
|
|||
/*--------------------------------------------------
|
||||
--- Custom Styles ---------------------------------*/
|
||||
@forward "uswds-theme-custom-styles";
|
||||
|
||||
/*--------------------------------------------------
|
||||
--- Admin ---------------------------------*/
|
||||
@forward "admin";
|
||||
|
|
|
@ -571,12 +571,16 @@ SECURE_SSL_REDIRECT = True
|
|||
ALLOWED_HOSTS = [
|
||||
"getgov-stable.app.cloud.gov",
|
||||
"getgov-staging.app.cloud.gov",
|
||||
"getgov-nl.app.cloud.gov",
|
||||
"getgov-rh.app.cloud.gov",
|
||||
"getgov-za.app.cloud.gov",
|
||||
"getgov-gd.app.cloud.gov",
|
||||
"getgov-rb.app.cloud.gov",
|
||||
"getgov-ko.app.cloud.gov",
|
||||
"getgov-ab.app.cloud.gov",
|
||||
"getgov-bl.app.cloud.gov",
|
||||
"getgov-rjm.app.cloud.gov",
|
||||
"getgov-dk.app.cloud.gov",
|
||||
"get.gov",
|
||||
]
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ For more information see:
|
|||
https://docs.djangoproject.com/en/4.0/topics/http/urls/
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
from django.views.generic import RedirectView
|
||||
|
@ -45,6 +44,10 @@ for step, view in [
|
|||
|
||||
urlpatterns = [
|
||||
path("", views.index, name="home"),
|
||||
path(
|
||||
"admin/logout/",
|
||||
RedirectView.as_view(pattern_name="logout", permanent=False),
|
||||
),
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
"application/<id>/edit/",
|
||||
|
@ -114,20 +117,6 @@ urlpatterns = [
|
|||
),
|
||||
]
|
||||
|
||||
|
||||
if not settings.DEBUG:
|
||||
urlpatterns += [
|
||||
# redirect to login.gov
|
||||
path(
|
||||
"admin/login/", RedirectView.as_view(pattern_name="login", permanent=False)
|
||||
),
|
||||
# redirect to login.gov
|
||||
path(
|
||||
"admin/logout/",
|
||||
RedirectView.as_view(pattern_name="logout", permanent=False),
|
||||
),
|
||||
]
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
# DEBUG = False even when these apps have been loaded because settings.DEBUG
|
||||
# was actually True. Instead, let's add these URLs any time we are able to
|
||||
|
|
|
@ -57,6 +57,21 @@ class UserFixture:
|
|||
"first_name": "Ryan",
|
||||
"last_name": "Brooks",
|
||||
},
|
||||
{
|
||||
"username": "30001ee7-0467-4df2-8db2-786e79606060",
|
||||
"first_name": "Zander",
|
||||
"last_name": "Adkinson",
|
||||
},
|
||||
{
|
||||
"username": "bb21f687-c773-4df3-9243-111cfd4c0be4",
|
||||
"first_name": "Paul",
|
||||
"last_name": "Kuykendall",
|
||||
},
|
||||
{
|
||||
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
|
||||
"first_name": "Rebecca",
|
||||
"last_name": "Hsieh",
|
||||
},
|
||||
]
|
||||
|
||||
STAFF = [
|
||||
|
@ -64,12 +79,28 @@ class UserFixture:
|
|||
"username": "319c490d-453b-43d9-bc4d-7d6cd8ff6844",
|
||||
"first_name": "Rachid-Analyst",
|
||||
"last_name": "Mrad-Analyst",
|
||||
"email": "rachid.mrad@gmail.com",
|
||||
},
|
||||
{
|
||||
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
|
||||
"first_name": "Alysia-Analyst",
|
||||
"last_name": "Alysia-Analyst",
|
||||
},
|
||||
{
|
||||
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
||||
"first_name": "Zander-Analyst",
|
||||
"last_name": "Adkinson-Analyst",
|
||||
},
|
||||
{
|
||||
"username": "57ab5847-7789-49fe-a2f9-21d38076d699",
|
||||
"first_name": "Paul-Analyst",
|
||||
"last_name": "Kuykendall-Analyst",
|
||||
},
|
||||
{
|
||||
"username": "e474e7a9-71ca-449d-833c-8a6e094dd117",
|
||||
"first_name": "Rebecca-Analyst",
|
||||
"last_name": "Hsieh-Analyst",
|
||||
},
|
||||
]
|
||||
|
||||
STAFF_PERMISSIONS = [
|
||||
|
@ -85,6 +116,7 @@ class UserFixture:
|
|||
"permissions": ["change_domainapplication"],
|
||||
},
|
||||
{"app_label": "registrar", "model": "domain", "permissions": ["view_domain"]},
|
||||
{"app_label": "registrar", "model": "user", "permissions": ["view_user"]},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -98,6 +130,8 @@ class UserFixture:
|
|||
user.is_superuser = True
|
||||
user.first_name = admin["first_name"]
|
||||
user.last_name = admin["last_name"]
|
||||
if "email" in admin.keys():
|
||||
user.email = admin["email"]
|
||||
user.is_staff = True
|
||||
user.is_active = True
|
||||
user.save()
|
||||
|
@ -115,6 +149,8 @@ class UserFixture:
|
|||
user.is_superuser = False
|
||||
user.first_name = staff["first_name"]
|
||||
user.last_name = staff["last_name"]
|
||||
if "email" in admin.keys():
|
||||
user.email = admin["email"]
|
||||
user.is_staff = True
|
||||
user.is_active = True
|
||||
|
||||
|
@ -201,11 +237,11 @@ class DomainApplicationFixture:
|
|||
"organization_name": "Example - Submitted but pending Investigation",
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"status": "in review",
|
||||
"organization_name": "Example - In Investigation",
|
||||
},
|
||||
{
|
||||
"status": "investigating",
|
||||
"status": "in review",
|
||||
"organization_name": "Example - Approved",
|
||||
},
|
||||
{
|
||||
|
@ -377,9 +413,9 @@ class DomainFixture(DomainApplicationFixture):
|
|||
return
|
||||
|
||||
for user in users:
|
||||
# approve one of each users investigating status domains
|
||||
# approve one of each users in review status domains
|
||||
application = DomainApplication.objects.filter(
|
||||
creator=user, status=DomainApplication.INVESTIGATING
|
||||
creator=user, status=DomainApplication.IN_REVIEW
|
||||
).last()
|
||||
logger.debug(f"Approving {application} for {user}")
|
||||
application.approve()
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 4.2.2 on 2023-07-12 21:31
|
||||
# Generated by Django 4.2.2 on 2023-07-13 17:56
|
||||
# hand merged
|
||||
|
||||
from django.db import migrations
|
||||
import django_fsm
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0027_alter_domaininformation_address_line1_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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"),
|
||||
],
|
||||
default="started",
|
||||
max_length=50,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -18,18 +18,22 @@ class DomainApplication(TimeStampedModel):
|
|||
|
||||
"""A registrant's application for a new domain."""
|
||||
|
||||
# #### Contants for choice fields ####
|
||||
# #### Constants for choice fields ####
|
||||
STARTED = "started"
|
||||
SUBMITTED = "submitted"
|
||||
INVESTIGATING = "investigating"
|
||||
IN_REVIEW = "in review"
|
||||
ACTION_NEEDED = "action needed"
|
||||
APPROVED = "approved"
|
||||
WITHDRAWN = "withdrawn"
|
||||
REJECTED = "rejected"
|
||||
STATUS_CHOICES = [
|
||||
(STARTED, STARTED),
|
||||
(SUBMITTED, SUBMITTED),
|
||||
(INVESTIGATING, INVESTIGATING),
|
||||
(IN_REVIEW, IN_REVIEW),
|
||||
(ACTION_NEEDED, ACTION_NEEDED),
|
||||
(APPROVED, APPROVED),
|
||||
(WITHDRAWN, WITHDRAWN),
|
||||
(REJECTED, REJECTED),
|
||||
]
|
||||
|
||||
class StateTerritoryChoices(models.TextChoices):
|
||||
|
@ -497,16 +501,13 @@ class DomainApplication(TimeStampedModel):
|
|||
except EmailSendingError:
|
||||
logger.warning("Failed to send confirmation email", exc_info=True)
|
||||
|
||||
@transition(field="status", source=[STARTED, WITHDRAWN], target=SUBMITTED)
|
||||
def submit(self, updated_domain_application=None):
|
||||
@transition(
|
||||
field="status", source=[STARTED, ACTION_NEEDED, WITHDRAWN], target=SUBMITTED
|
||||
)
|
||||
def submit(self):
|
||||
"""Submit an application that is started.
|
||||
|
||||
As a side effect, an email notification is sent.
|
||||
|
||||
This method is called in admin.py on the original application
|
||||
which has the correct status value, but is passed the changed
|
||||
application which has the up-to-date data that we'll use
|
||||
in the email."""
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
# check our conditions here inside the `submit` method so that we
|
||||
# can raise more informative exceptions
|
||||
|
@ -522,53 +523,46 @@ class DomainApplication(TimeStampedModel):
|
|||
if not DraftDomain.string_could_be_domain(self.requested_domain.name):
|
||||
raise ValueError("Requested domain is not a valid domain name.")
|
||||
|
||||
if updated_domain_application is not None:
|
||||
# A DomainApplication is being passed to this method (ie from admin)
|
||||
updated_domain_application._send_status_update_email(
|
||||
"submission confirmation",
|
||||
"emails/submission_confirmation.txt",
|
||||
"emails/submission_confirmation_subject.txt",
|
||||
)
|
||||
else:
|
||||
# Or this method is called with the right application
|
||||
# for context, ie from views/application.py
|
||||
self._send_status_update_email(
|
||||
"submission confirmation",
|
||||
"emails/submission_confirmation.txt",
|
||||
"emails/submission_confirmation_subject.txt",
|
||||
)
|
||||
self._send_status_update_email(
|
||||
"submission confirmation",
|
||||
"emails/submission_confirmation.txt",
|
||||
"emails/submission_confirmation_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=SUBMITTED, target=INVESTIGATING)
|
||||
def in_review(self, updated_domain_application):
|
||||
@transition(field="status", source=SUBMITTED, target=IN_REVIEW)
|
||||
def in_review(self):
|
||||
"""Investigate an application that has been submitted.
|
||||
|
||||
As a side effect, an email notification is sent.
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
This method is called in admin.py on the original application
|
||||
which has the correct status value, but is passed the changed
|
||||
application which has the up-to-date data that we'll use
|
||||
in the email."""
|
||||
|
||||
updated_domain_application._send_status_update_email(
|
||||
self._send_status_update_email(
|
||||
"application in review",
|
||||
"emails/status_change_in_review.txt",
|
||||
"emails/status_change_in_review_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=APPROVED)
|
||||
def approve(self, updated_domain_application=None):
|
||||
@transition(field="status", source=[IN_REVIEW, REJECTED], target=ACTION_NEEDED)
|
||||
def action_needed(self):
|
||||
"""Send back an application that is under investigation or rejected.
|
||||
|
||||
As a side effect, an email notification is sent."""
|
||||
|
||||
self._send_status_update_email(
|
||||
"action needed",
|
||||
"emails/status_change_action_needed.txt",
|
||||
"emails/status_change_action_needed_subject.txt",
|
||||
)
|
||||
|
||||
@transition(
|
||||
field="status", source=[SUBMITTED, IN_REVIEW, REJECTED], target=APPROVED
|
||||
)
|
||||
def approve(self):
|
||||
"""Approve an application that has been submitted.
|
||||
|
||||
This has substantial side-effects because it creates another database
|
||||
object for the approved Domain and makes the user who created the
|
||||
application into an admin on that domain. It also triggers an email
|
||||
notification.
|
||||
|
||||
This method is called in admin.py on the original application
|
||||
which has the correct status value, but is passed the changed
|
||||
application which has the up-to-date data that we'll use
|
||||
in the email.
|
||||
"""
|
||||
notification."""
|
||||
|
||||
# create the domain
|
||||
Domain = apps.get_model("registrar.Domain")
|
||||
|
@ -587,24 +581,28 @@ class DomainApplication(TimeStampedModel):
|
|||
user=self.creator, domain=created_domain, role=UserDomainRole.Roles.ADMIN
|
||||
)
|
||||
|
||||
if updated_domain_application is not None:
|
||||
# A DomainApplication is being passed to this method (ie from admin)
|
||||
updated_domain_application._send_status_update_email(
|
||||
"application approved",
|
||||
"emails/status_change_approved.txt",
|
||||
"emails/status_change_approved_subject.txt",
|
||||
)
|
||||
else:
|
||||
self._send_status_update_email(
|
||||
"application approved",
|
||||
"emails/status_change_approved.txt",
|
||||
"emails/status_change_approved_subject.txt",
|
||||
)
|
||||
self._send_status_update_email(
|
||||
"application approved",
|
||||
"emails/status_change_approved.txt",
|
||||
"emails/status_change_approved_subject.txt",
|
||||
)
|
||||
|
||||
@transition(field="status", source=[SUBMITTED, INVESTIGATING], target=WITHDRAWN)
|
||||
@transition(field="status", source=[SUBMITTED, IN_REVIEW], target=WITHDRAWN)
|
||||
def withdraw(self):
|
||||
"""Withdraw an application that has been submitted."""
|
||||
|
||||
@transition(field="status", source=[IN_REVIEW, APPROVED], target=REJECTED)
|
||||
def reject(self):
|
||||
"""Reject an application that has been submitted.
|
||||
|
||||
As a side effect, an email notification is sent, similar to in_review"""
|
||||
|
||||
self._send_status_update_email(
|
||||
"action needed",
|
||||
"emails/status_change_rejected.txt",
|
||||
"emails/status_change_rejected_subject.txt",
|
||||
)
|
||||
|
||||
# ## Form policies ###
|
||||
#
|
||||
# These methods control what questions need to be answered by applicants
|
||||
|
|
63
src/registrar/models/utility/admin_form_order_helper.py
Normal file
63
src/registrar/models/utility/admin_form_order_helper.py
Normal file
|
@ -0,0 +1,63 @@
|
|||
import logging
|
||||
from typing import Dict
|
||||
from django.forms import ModelChoiceField
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SortingDict:
|
||||
"""Stores a sorting dictionary object"""
|
||||
|
||||
_sorting_dict: Dict[type, type] = {}
|
||||
|
||||
def __init__(self, model_list, sort_list):
|
||||
self._sorting_dict = {
|
||||
"dropDownSelected": self.convert_list_to_dict(model_list),
|
||||
"sortBy": sort_list,
|
||||
}
|
||||
|
||||
# Used in __init__ for model_list for performance reasons
|
||||
def convert_list_to_dict(self, value_list):
|
||||
"""Used internally to convert model_list to a dictionary"""
|
||||
return {item: item for item in value_list}
|
||||
|
||||
def get_dict(self):
|
||||
"""Grabs the associated dictionary item,
|
||||
has two fields: 'dropDownSelected': model_list and 'sortBy': sort_list"""
|
||||
# This should never happen so we need to log this
|
||||
if self._sorting_dict is None:
|
||||
raise ValueError("_sorting_dict was None")
|
||||
return self._sorting_dict
|
||||
|
||||
|
||||
class AdminFormOrderHelper:
|
||||
"""A helper class to order a dropdown field in Django Admin,
|
||||
takes the fields you want to order by as an array"""
|
||||
|
||||
# Used to keep track of how we want to order_by certain FKs
|
||||
_sorting_list: list[SortingDict] = []
|
||||
|
||||
def __init__(self, sort: list[SortingDict]):
|
||||
self._sorting_list = sort
|
||||
|
||||
def get_ordered_form_field(self, form_field, db_field) -> ModelChoiceField | None:
|
||||
"""Orders the queryset for a ModelChoiceField
|
||||
based on the order_by_dict dictionary"""
|
||||
_order_by_list = []
|
||||
|
||||
for item in self._sorting_list:
|
||||
item_dict = item.get_dict()
|
||||
drop_down_selected = item_dict.get("dropDownSelected")
|
||||
sort_by = item_dict.get("sortBy")
|
||||
|
||||
if db_field.name in drop_down_selected:
|
||||
_order_by_list = sort_by
|
||||
# Exit loop when order_by_list is found
|
||||
break
|
||||
|
||||
# Only order if we choose to do so
|
||||
# noqa for the linter... reduces readability otherwise
|
||||
if _order_by_list is not None and _order_by_list != []: # noqa
|
||||
form_field.queryset = form_field.queryset.order_by(*_order_by_list)
|
||||
|
||||
return form_field
|
27
src/registrar/models/utility/admin_sort_fields.py
Normal file
27
src/registrar/models/utility/admin_sort_fields.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
from registrar.models.utility.admin_form_order_helper import (
|
||||
AdminFormOrderHelper,
|
||||
SortingDict,
|
||||
)
|
||||
|
||||
|
||||
class AdminSortFields:
|
||||
# Used to keep track of how we want to order_by certain FKs
|
||||
foreignkey_orderby_dict: list[SortingDict] = [
|
||||
# foreign_key - order_by
|
||||
# Handles fields that are sorted by 'first_name / last_name
|
||||
SortingDict(
|
||||
["submitter", "authorizing_official", "investigator", "creator", "user"],
|
||||
["first_name", "last_name"],
|
||||
),
|
||||
# Handles fields that are sorted by 'name'
|
||||
SortingDict(["domain", "requested_domain"], ["name"]),
|
||||
SortingDict(["domain_application"], ["requested_domain__name"]),
|
||||
]
|
||||
|
||||
# For readability purposes, but can be replaced with a one liner
|
||||
def form_field_order_helper(self, form_field, db_field):
|
||||
"""A shorthand for AdminFormOrderHelper(foreignkey_orderby_dict)
|
||||
.get_ordered_form_field(form_field, db_field)"""
|
||||
|
||||
form = AdminFormOrderHelper(self.foreignkey_orderby_dict)
|
||||
return form.get_ordered_form_field(form_field, db_field)
|
56
src/registrar/templates/admin/app_list.html
Normal file
56
src/registrar/templates/admin/app_list.html
Normal file
|
@ -0,0 +1,56 @@
|
|||
{% load i18n %}
|
||||
|
||||
{% if app_list %}
|
||||
{% for app in app_list %}
|
||||
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
||||
<table>
|
||||
<caption>
|
||||
<a href="{{ app.app_url }}" class="section" title="{% blocktranslate with name=app.name %}Models in the {{ name }} application{% endblocktranslate %}">{{ app.name }}</a>
|
||||
</caption>
|
||||
|
||||
{# .gov override #}
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Model</th>
|
||||
<th><span class="display-inline-block min-width-25">Add</span></th>
|
||||
{% if show_changelinks %}
|
||||
<th>
|
||||
<span class="display-inline-block min-width-81">
|
||||
{% translate 'View/Change' %}</th>
|
||||
</span>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
{# end .gov override #}
|
||||
|
||||
{% for model in app.models %}
|
||||
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
|
||||
{% if model.admin_url %}
|
||||
<th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}>{{ model.name }}</a></th>
|
||||
{% else %}
|
||||
<th scope="row">{{ model.name }}</th>
|
||||
{% endif %}
|
||||
|
||||
{% if model.add_url %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink">{% translate 'Add' %}</a></td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.admin_url and show_changelinks %}
|
||||
{% if model.view_only %}
|
||||
<td><a href="{{ model.admin_url }}" class="viewlink">{% translate 'View' %}</a></td>
|
||||
{% else %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% translate 'Change' %}</a></td>
|
||||
{% endif %}
|
||||
{% elif show_changelinks %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
{% endif %}
|
38
src/registrar/templates/admin/base_site.html
Normal file
38
src/registrar/templates/admin/base_site.html
Normal file
|
@ -0,0 +1,38 @@
|
|||
{% extends "admin/base.html" %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Django site admin') }}{% endblock %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}
|
||||
<link rel="stylesheet" type="text/css" href="{% static "css/styles.css" %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block branding %}
|
||||
<h1 id="site-name"><a href="{% url 'admin:index' %}">.gov admin</a></h1>
|
||||
{% if user.is_anonymous %}
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% comment %}
|
||||
This was copied from the 'userlinks' template, with a few minor changes.
|
||||
You can find that here:
|
||||
https://github.com/django/django/blob/d25f3892114466d689fd6936f79f3bd9a9acc30e/django/contrib/admin/templates/admin/base.html#L59
|
||||
{% endcomment %}
|
||||
{% block userlinks %}
|
||||
{% if site_url %}
|
||||
<a href="{{ site_url }}">{% translate 'View site' %}</a> /
|
||||
{% endif %}
|
||||
{% if user.is_active and user.is_staff %}
|
||||
{% url 'django-admindocs-docroot' as docsroot %}
|
||||
{% if docsroot %}
|
||||
<a href="{{ docsroot }}">{% translate 'Documentation' %}</a> /
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if user.has_usable_password %}
|
||||
<a href="{% url 'admin:password_change' %}">{% translate 'Change password' %}</a> /
|
||||
{% endif %}
|
||||
<a href="{% url 'admin:logout' %}" id="admin-logout-button">{% translate 'Log out' %}</a>
|
||||
{% include "admin/color_theme_toggle.html" %}
|
||||
{% endblock %}
|
||||
{% block nav-global %}{% endblock %}
|
112
src/registrar/templates/admin/change_list_results.html
Normal file
112
src/registrar/templates/admin/change_list_results.html
Normal file
|
@ -0,0 +1,112 @@
|
|||
{% load i18n static %}
|
||||
|
||||
{% comment %}
|
||||
.gov override
|
||||
Load our custom filters to extract info from the django generated markup.
|
||||
{% endcomment %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% if result_hidden_fields %}
|
||||
<div class="hiddenfields">{# DIV for HTML validation #}
|
||||
{% for item in result_hidden_fields %}{{ item }}{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if results %}
|
||||
<div class="results override-change_list_results">
|
||||
<table id="result_list" class="usa-table usa-table--borderless usa-table--striped">
|
||||
<thead>
|
||||
<tr>
|
||||
|
||||
{% if results.0.form %}
|
||||
{# .gov - hardcode the select all checkbox #}
|
||||
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||
<div class="text">
|
||||
<span>
|
||||
<input type="checkbox" name="_selected_action" id="action-toggle">
|
||||
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
||||
</span>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
</th>
|
||||
{# .gov - don't let django generate the select all checkbox #}
|
||||
{% for header in result_headers|slice:"1:" %}
|
||||
<th scope="col"{{ header.class_attrib }}>
|
||||
{% if header.sortable %}
|
||||
{% if header.sort_priority > 0 %}
|
||||
<div class="sortoptions">
|
||||
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
|
||||
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
|
||||
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
|
||||
<div class="clear"></div>
|
||||
</th>{% endfor %}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% comment %}
|
||||
.gov - hardcode the row checkboxes using the custom filters to extract
|
||||
the value attribute's value, and a label based on the anchor elements's
|
||||
text. Then edit the for loop to keep django from generating the row select
|
||||
checkboxes.
|
||||
{% endcomment %}
|
||||
|
||||
{% for result in results %}
|
||||
{% if result.form.non_field_errors %}
|
||||
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
|
||||
{% with result_value=result.0|extract_value %}
|
||||
{% with result_label=result.1|extract_a_text %}
|
||||
<td>
|
||||
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_label|default:result_value }}" class="action-select">
|
||||
<label class="usa-sr-only" for="{{ result_label|default:result_value }}">{{ result_label|default:'label' }}</label>
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% for item in result|slice:"1:" %}
|
||||
{{ item }}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% else %} {# results doesn't have a form as its first element #}
|
||||
|
||||
{% for header in result_headers %}
|
||||
<th scope="col"{{ header.class_attrib }}>
|
||||
{% if header.sortable %}
|
||||
{% if header.sort_priority > 0 %}
|
||||
<div class="sortoptions">
|
||||
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
|
||||
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
|
||||
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="text">{% if header.sortable %}<a href="{{ header.url_primary }}">{{ header.text|capfirst }}</a>{% else %}<span>{{ header.text|capfirst }}</span>{% endif %}</div>
|
||||
<div class="clear"></div>
|
||||
</th>{% endfor %}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for result in results %}
|
||||
{% if result.form.non_field_errors %}
|
||||
<tr><td colspan="{{ result|length }}">{{ result.form.non_field_errors }}</td></tr>
|
||||
{% endif %}
|
||||
<tr>{% for item in result %}{{ item }}{% endfor %}</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
|
@ -6,13 +6,13 @@
|
|||
Who is the authorizing official for your organization?
|
||||
</h2>
|
||||
|
||||
<p>Your authorizing official is the person within your organization who can authorize your domain request. This is generally the highest-ranking or highest-elected official in your organization.</p>
|
||||
<p>Your authorizing official is the person within your organization who can authorize your domain request. This person must be in a role of significant, executive responsibility within the organization.</p>
|
||||
|
||||
<div class="ao_example">
|
||||
{% include "includes/ao_example.html" %}
|
||||
</div>
|
||||
|
||||
<p>We might contact your authorizing official, or their office, to double check that they approve this request. Read more about <a 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>
|
||||
<p>We typically don’t reach out to the authorizing official, but if contact is necessary, our practice is to coordinate first with you, the requestor. Read more about <a 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>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -6,7 +6,10 @@
|
|||
Is your organization an election office? <abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||
</h2>
|
||||
|
||||
<p>Answer “yes” if the primary purpose of your organization is to manage elections.</p>
|
||||
<p>An election office is a government entity whose <em>primary</em> responsibility is overseeing elections and/or conducting voter registration.</p>
|
||||
|
||||
<p>Answer “yes” only if the <em>main purpose</em> of your organization is to serve as an election office.</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
{% load static field_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>We’d like to contact other employees in your organization about your domain request. For example, they could be involved in managing your organization or its technical infrastructure. <strong>This information will help us assess your eligibility for a .gov domain.</strong> These contacts should be in addition to you and your authorizing official. They should be employees of your organization.</p>
|
||||
<p>To help us assess your eligibility for a .gov domain, please provide contact information for other employees from your organization.
|
||||
<ul class="usa-list">
|
||||
<li>They should be clearly and publicly affiliated with your organization and familiar with your domain request. </li>
|
||||
<li>They don't need to be involved with the technical management of your domain (although they can be). </li>
|
||||
<li>We typically don’t reach out to these employees, but if contact is necessary, our practice is to coordinate first with you, the requestor. </li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -20,8 +20,9 @@
|
|||
Status:
|
||||
</span>
|
||||
{% if domainapplication.status == 'approved' %} Approved
|
||||
{% elif domainapplication.status == 'investigating' %} In Review
|
||||
{% elif domainapplication.status == 'submitted' %} Received
|
||||
{% elif domainapplication.status == 'in review' %} In Review
|
||||
{% elif domainapplication.status == 'rejected' %} Rejected
|
||||
{% elif domainapplication.status == 'submitted' %} Submitted
|
||||
{% else %}ERROR Please contact technical support/dev
|
||||
{% endif %}
|
||||
</p>
|
||||
|
|
|
@ -3,7 +3,17 @@
|
|||
|
||||
|
||||
{% block form_fields %}
|
||||
{% input_with_errors forms.0.tribe_name %}
|
||||
|
||||
{% with sublabel_text="Please include the entire name of your tribe as recognized by the Bureau of Indian Affairs." %}
|
||||
{% with link_text="Bureau of Indian Affairs" %}
|
||||
{% with link_href="https://www.federalregister.gov/documents/2023/01/12/2023-00504/indian-entities-recognized-by-and-eligible-to-receive-services-from-the-united-states-bureau-of" %}
|
||||
{% with target_blank="true" %}
|
||||
{% input_with_errors forms.0.tribe_name %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend class="usa-legend">
|
||||
<p>Is your organization a federally-recognized tribe or a state-recognized tribe? Check all that apply.
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi {{ application.submitter.first_name }}.
|
||||
|
||||
We've identified an action needed to complete the review of your .gov domain request.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.updated_at|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
STATUS: Action needed
|
||||
|
||||
|
||||
NEED TO MAKE CHANGES?
|
||||
|
||||
If you need to change your request you have to first withdraw it. Once you
|
||||
withdraw the request you can edit it and submit it again. Changing your request
|
||||
might add to the wait time. Learn more about withdrawing your request.
|
||||
<https://get.gov/help/domain-requests/#withdraw-your-domain-request>.
|
||||
|
||||
|
||||
NEXT STEPS
|
||||
|
||||
- You will receive a separate email from our team that provides details about the action needed.
|
||||
You may need to update your application or provide additional information.
|
||||
|
||||
- If you do not receive a separate email with these details within one business day, please contact us:
|
||||
<https://forms.office.com/pages/responsepage.aspx?id=bOfNPG2UEkq7evydCEI1SqHke9Gh6wJEl3kQ5EjWUKlUQzRJWDlBNTBCQUxTTzBaNlhTWURSSTBLTC4u>
|
||||
|
||||
|
||||
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 @@
|
|||
Action needed for your .gov domain request
|
32
src/registrar/templates/emails/status_change_rejected.txt
Normal file
32
src/registrar/templates/emails/status_change_rejected.txt
Normal file
|
@ -0,0 +1,32 @@
|
|||
{% 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 rejected.
|
||||
|
||||
DOMAIN REQUESTED: {{ application.requested_domain.name }}
|
||||
REQUEST RECEIVED ON: {{ application.updated_at|date }}
|
||||
REQUEST #: {{ application.id }}
|
||||
STATUS: Rejected
|
||||
|
||||
|
||||
YOU CAN SUBMIT A NEW REQUEST
|
||||
|
||||
The details of your request are included below. If your organization is eligible for a .gov
|
||||
domain and you meet our other requirements, you can submit a new request. Learn
|
||||
more about .gov domains <https://get.gov/help/domains/>.
|
||||
|
||||
|
||||
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 rejected
|
|
@ -33,6 +33,8 @@
|
|||
</thead>
|
||||
<tbody>
|
||||
{% for domain in domains %}
|
||||
{% comment %} ticket 796
|
||||
{% if domain.application_status == "approved" or (domain.application does not exist) %} {% endcomment %}
|
||||
<tr>
|
||||
<th th scope="row" role="rowheader" data-label="Domain name">
|
||||
{{ domain.name }}
|
||||
|
@ -88,7 +90,7 @@
|
|||
<td data-sort-value="{{ application.created_at|date:"U" }}" data-label="Date created">{{ application.created_at|date }}</td>
|
||||
<td data-label="Status">{{ application.status|title }}</td>
|
||||
<td>
|
||||
{% if application.status == "started" or application.status == "withdrawn" %}
|
||||
{% if application.status == "started" or application.status == "action needed" or application.status == "withdrawn" %}
|
||||
<a href="{% url 'edit-application' application.pk %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
|
@ -123,13 +125,14 @@
|
|||
<p>You don't have any archived domains</p>
|
||||
</section>
|
||||
|
||||
<section class="tablet:grid-col-11 desktop:grid-col-10">
|
||||
<!-- Note: Uncomment below when this is being implemented post-MVP -->
|
||||
<!-- <section class="tablet:grid-col-11 desktop:grid-col-10">
|
||||
<h2 class="padding-top-1 mobile-lg:padding-top-3"> Export domains</h2>
|
||||
<p>Download a list of your domains and their statuses as a csv file.</p>
|
||||
<a href="{% url 'todo' %}" class="usa-button usa-button--outline">
|
||||
Export domains as csv
|
||||
</a>
|
||||
</section>
|
||||
</section> -->
|
||||
|
||||
</div>
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ Template include for form fields with classes and their corresponding
|
|||
error messages, if necessary.
|
||||
{% endcomment %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% if widget.attrs.maxlength %}
|
||||
|
@ -29,8 +31,19 @@ error messages, if necessary.
|
|||
{% endif %}
|
||||
|
||||
{% if sublabel_text %}
|
||||
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">{{ sublabel_text }}</p>
|
||||
{% endif %}
|
||||
<p id="{{ widget.attrs.id }}__sublabel" class="text-base margin-top-2px margin-bottom-1">
|
||||
{% comment %} If the link_text appears more than once, the first instance will be a link and the other instances will be ignored {% endcomment %}
|
||||
{% if link_text and link_text in sublabel_text %}
|
||||
{% with link_index=sublabel_text|find_index:link_text %}
|
||||
{{ sublabel_text|slice:link_index }}
|
||||
{% comment %} HTML will convert a new line into a space, resulting with a space before the fullstop in case link_text is at the end of sublabel_text, hence the unfortunate line below {% endcomment %}
|
||||
<a {% if target_blank == "true" %}target="_blank" {% endif %}href="{{ link_href }}">{{ link_text }}</a>{% with sublabel_part_after=sublabel_text|slice_after:link_text %}{{ sublabel_part_after }}{% endwith %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{{ sublabel_text }}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if field.errors %}
|
||||
<div id="{{ widget.attrs.id }}__error-message">
|
||||
|
|
42
src/registrar/templatetags/custom_filters.py
Normal file
42
src/registrar/templatetags/custom_filters.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from django import template
|
||||
import re
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.filter(name="extract_value")
|
||||
def extract_value(html_input):
|
||||
match = re.search(r'value="([^"]*)"', html_input)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
@register.filter
|
||||
def extract_a_text(value):
|
||||
# Use regex to extract the text within the <a> tag
|
||||
pattern = r"<a\b[^>]*>(.*?)</a>"
|
||||
match = re.search(pattern, value)
|
||||
if match:
|
||||
extracted_text = match.group(1)
|
||||
else:
|
||||
extracted_text = ""
|
||||
|
||||
return extracted_text
|
||||
|
||||
|
||||
@register.filter
|
||||
def find_index(haystack, needle):
|
||||
try:
|
||||
return haystack.index(needle)
|
||||
except ValueError:
|
||||
return -1
|
||||
|
||||
|
||||
@register.filter
|
||||
def slice_after(value, substring):
|
||||
index = value.find(substring)
|
||||
if index != -1:
|
||||
result = value[index + len(substring) :]
|
||||
return result
|
||||
return value
|
|
@ -2,13 +2,26 @@ import os
|
|||
import logging
|
||||
|
||||
from contextlib import contextmanager
|
||||
import random
|
||||
from string import ascii_uppercase
|
||||
from unittest.mock import Mock
|
||||
from typing import List, Dict
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model, login
|
||||
|
||||
from registrar.models import Contact, DraftDomain, Website, DomainApplication, User
|
||||
from registrar.models import (
|
||||
Contact,
|
||||
DraftDomain,
|
||||
Website,
|
||||
DomainApplication,
|
||||
DomainInvitation,
|
||||
User,
|
||||
DomainInformation,
|
||||
Domain,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_handlers():
|
||||
|
@ -88,6 +101,339 @@ class MockSESClient(Mock):
|
|||
self.EMAILS_SENT.append({"args": args, "kwargs": kwargs})
|
||||
|
||||
|
||||
class AuditedAdminMockData:
|
||||
"""Creates simple data mocks for AuditedAdminTest.
|
||||
Can likely be more generalized, but the primary purpose of this class is to simplify
|
||||
mock data creation, especially for lists of items,
|
||||
by making the assumption that for most use cases we don't have to worry about
|
||||
data 'accuracy' ('testy 2' is not an accurate first_name for example), we just care about
|
||||
implementing some kind of patterning, especially with lists of items.
|
||||
|
||||
Two variables are used across multiple functions:
|
||||
|
||||
*item_name* - Used in patterning. Will be appended en masse to multiple str fields,
|
||||
like first_name. For example, item_name 'egg' will return a user object of:
|
||||
|
||||
first_name: 'egg first_name:user',
|
||||
last_name: 'egg last_name:user',
|
||||
username: 'egg username:user'
|
||||
|
||||
where 'user' is the short_hand
|
||||
|
||||
*short_hand* - Used in patterning. Certain fields will have ':{shorthand}' appended to it,
|
||||
as a way to optionally include metadata in the str itself. Can be further expanded on.
|
||||
Came from a bug where different querysets used in testing would effectively be 'anonymized', wherein
|
||||
it would only display a list of types, but not include the variable name.
|
||||
""" # noqa
|
||||
|
||||
# Constants for different domain object types
|
||||
INFORMATION = "information"
|
||||
APPLICATION = "application"
|
||||
INVITATION = "invitation"
|
||||
|
||||
def dummy_user(self, item_name, short_hand):
|
||||
"""Creates a dummy user object,
|
||||
but with a shorthand and support for multiple"""
|
||||
user = User.objects.get_or_create(
|
||||
first_name="{} first_name:{}".format(item_name, short_hand),
|
||||
last_name="{} last_name:{}".format(item_name, short_hand),
|
||||
username="{} username:{}".format(item_name, short_hand),
|
||||
)[0]
|
||||
return user
|
||||
|
||||
def dummy_contact(self, item_name, short_hand):
|
||||
"""Creates a dummy contact object"""
|
||||
contact = Contact.objects.get_or_create(
|
||||
first_name="{} first_name:{}".format(item_name, short_hand),
|
||||
last_name="{} last_name:{}".format(item_name, short_hand),
|
||||
title="{} title:{}".format(item_name, short_hand),
|
||||
email="{}testy@town.com".format(item_name),
|
||||
phone="(555) 555 5555",
|
||||
)[0]
|
||||
return contact
|
||||
|
||||
def dummy_draft_domain(self, item_name, prebuilt=False):
|
||||
"""
|
||||
Creates a dummy DraftDomain object
|
||||
Args:
|
||||
item_name (str): Value for 'name' in a DraftDomain object.
|
||||
prebuilt (boolean): Determines return type.
|
||||
Returns:
|
||||
DraftDomain: Where name = 'item_name'. If prebuilt = True, then
|
||||
name will be "city{}.gov".format(item_name).
|
||||
"""
|
||||
if prebuilt:
|
||||
item_name = "city{}.gov".format(item_name)
|
||||
return DraftDomain.objects.get_or_create(name=item_name)[0]
|
||||
|
||||
def dummy_domain(self, item_name, prebuilt=False):
|
||||
"""
|
||||
Creates a dummy domain object
|
||||
Args:
|
||||
item_name (str): Value for 'name' in a Domain object.
|
||||
prebuilt (boolean): Determines return type.
|
||||
Returns:
|
||||
Domain: Where name = 'item_name'. If prebuilt = True, then
|
||||
domain name will be "city{}.gov".format(item_name).
|
||||
"""
|
||||
if prebuilt:
|
||||
item_name = "city{}.gov".format(item_name)
|
||||
return Domain.objects.get_or_create(name=item_name)[0]
|
||||
|
||||
def dummy_website(self, item_name):
|
||||
"""
|
||||
Creates a dummy website object
|
||||
Args:
|
||||
item_name (str): Value for 'website' in a Website object.
|
||||
Returns:
|
||||
Website: Where website = 'item_name'.
|
||||
"""
|
||||
return Website.objects.get_or_create(website=item_name)[0]
|
||||
|
||||
def dummy_alt(self, item_name):
|
||||
"""
|
||||
Creates a dummy website object for alternates
|
||||
Args:
|
||||
item_name (str): Value for 'website' in a Website object.
|
||||
Returns:
|
||||
Website: Where website = "cityalt{}.gov".format(item_name).
|
||||
"""
|
||||
return self.dummy_website(item_name="cityalt{}.gov".format(item_name))
|
||||
|
||||
def dummy_current(self, item_name):
|
||||
"""
|
||||
Creates a dummy website object for current
|
||||
Args:
|
||||
item_name (str): Value for 'website' in a Website object.
|
||||
prebuilt (boolean): Determines return type.
|
||||
Returns:
|
||||
Website: Where website = "city{}.gov".format(item_name)
|
||||
"""
|
||||
return self.dummy_website(item_name="city{}.com".format(item_name))
|
||||
|
||||
def get_common_domain_arg_dictionary(
|
||||
self,
|
||||
item_name,
|
||||
org_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
):
|
||||
"""
|
||||
Generates a generic argument dict for most domains
|
||||
Args:
|
||||
item_name (str): A shared str value appended to first_name, last_name,
|
||||
organization_name, address_line1, address_line2,
|
||||
title, email, and username.
|
||||
|
||||
org_type (str - optional): Sets a domains org_type
|
||||
|
||||
federal_type (str - optional): Sets a domains federal_type
|
||||
|
||||
purpose (str - optional): Sets a domains purpose
|
||||
Returns:
|
||||
Dictionary: {
|
||||
organization_type: str,
|
||||
federal_type: str,
|
||||
purpose: str,
|
||||
organization_name: str = "{} organization".format(item_name),
|
||||
address_line1: str = "{} address_line1".format(item_name),
|
||||
address_line2: str = "{} address_line2".format(item_name),
|
||||
is_policy_acknowledged: boolean = True,
|
||||
state_territory: str = "NY",
|
||||
zipcode: str = "10002",
|
||||
type_of_work: str = "e-Government",
|
||||
anything_else: str = "There is more",
|
||||
authorizing_official: Contact = self.dummy_contact(item_name, "authorizing_official"),
|
||||
submitter: Contact = self.dummy_contact(item_name, "submitter"),
|
||||
creator: User = self.dummy_user(item_name, "creator"),
|
||||
}
|
||||
""" # noqa
|
||||
common_args = dict(
|
||||
organization_type=org_type,
|
||||
federal_type=federal_type,
|
||||
purpose=purpose,
|
||||
organization_name="{} organization".format(item_name),
|
||||
address_line1="{} address_line1".format(item_name),
|
||||
address_line2="{} address_line2".format(item_name),
|
||||
is_policy_acknowledged=True,
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
type_of_work="e-Government",
|
||||
anything_else="There is more",
|
||||
authorizing_official=self.dummy_contact(item_name, "authorizing_official"),
|
||||
submitter=self.dummy_contact(item_name, "submitter"),
|
||||
creator=self.dummy_user(item_name, "creator"),
|
||||
)
|
||||
return common_args
|
||||
|
||||
def dummy_kwarg_boilerplate(
|
||||
self,
|
||||
domain_type,
|
||||
item_name,
|
||||
status=DomainApplication.STARTED,
|
||||
org_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
):
|
||||
"""
|
||||
Returns a prebuilt kwarg dictionary for DomainApplication,
|
||||
DomainInformation, or DomainInvitation.
|
||||
Args:
|
||||
domain_type (str): is either 'application', 'information',
|
||||
or 'invitation'.
|
||||
|
||||
item_name (str): A shared str value appended to first_name, last_name,
|
||||
organization_name, address_line1, address_line2,
|
||||
title, email, and username.
|
||||
|
||||
status (str - optional): Defines the status for DomainApplication,
|
||||
e.g. DomainApplication.STARTED
|
||||
|
||||
org_type (str - optional): Sets a domains org_type
|
||||
|
||||
federal_type (str - optional): Sets a domains federal_type
|
||||
|
||||
purpose (str - optional): Sets a domains purpose
|
||||
Returns:
|
||||
dict: Returns a dictionary structurally consistent with the expected input
|
||||
of either DomainApplication, DomainInvitation, or DomainInformation
|
||||
based on the 'domain_type' field.
|
||||
""" # noqa
|
||||
common_args = self.get_common_domain_arg_dictionary(
|
||||
item_name, org_type, federal_type, purpose
|
||||
)
|
||||
full_arg_dict = None
|
||||
match domain_type:
|
||||
case self.APPLICATION:
|
||||
full_arg_dict = dict(
|
||||
**common_args,
|
||||
requested_domain=self.dummy_draft_domain(item_name),
|
||||
investigator=self.dummy_user(item_name, "investigator"),
|
||||
status=status,
|
||||
)
|
||||
case self.INFORMATION:
|
||||
domain_app = self.create_full_dummy_domain_application(item_name)
|
||||
full_arg_dict = dict(
|
||||
**common_args,
|
||||
domain=self.dummy_domain(item_name, True),
|
||||
domain_application=domain_app,
|
||||
)
|
||||
case self.INVITATION:
|
||||
full_arg_dict = dict(
|
||||
email="test_mail@mail.com",
|
||||
domain=self.dummy_domain(item_name, True),
|
||||
status=DomainInvitation.INVITED,
|
||||
)
|
||||
return full_arg_dict
|
||||
|
||||
def create_full_dummy_domain_application(
|
||||
self, item_name, status=DomainApplication.STARTED
|
||||
):
|
||||
"""Creates a dummy domain application object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||
self.APPLICATION, item_name, status
|
||||
)
|
||||
application = DomainApplication.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)[0]
|
||||
return application
|
||||
|
||||
def create_full_dummy_domain_information(
|
||||
self, item_name, status=DomainApplication.STARTED
|
||||
):
|
||||
"""Creates a dummy domain information object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||
self.INFORMATION, item_name, status
|
||||
)
|
||||
application = DomainInformation.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)[0]
|
||||
return application
|
||||
|
||||
def create_full_dummy_domain_invitation(
|
||||
self, item_name, status=DomainApplication.STARTED
|
||||
):
|
||||
"""Creates a dummy domain invitation object"""
|
||||
domain_application_kwargs = self.dummy_kwarg_boilerplate(
|
||||
self.INVITATION, item_name, status
|
||||
)
|
||||
application = DomainInvitation.objects.get_or_create(
|
||||
**domain_application_kwargs
|
||||
)[0]
|
||||
|
||||
return application
|
||||
|
||||
def create_full_dummy_domain_object(
|
||||
self,
|
||||
domain_type,
|
||||
item_name,
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
has_alternative_gov_domain=True,
|
||||
status=DomainApplication.STARTED,
|
||||
):
|
||||
"""A helper to create a dummy domain application object"""
|
||||
application = None
|
||||
match domain_type:
|
||||
case self.APPLICATION:
|
||||
application = self.create_full_dummy_domain_application(
|
||||
item_name, status
|
||||
)
|
||||
case self.INVITATION:
|
||||
application = self.create_full_dummy_domain_invitation(
|
||||
item_name, status
|
||||
)
|
||||
case self.INFORMATION:
|
||||
application = self.create_full_dummy_domain_information(
|
||||
item_name, status
|
||||
)
|
||||
case _:
|
||||
raise ValueError("Invalid domain_type, must conform to given constants")
|
||||
|
||||
if has_other_contacts and domain_type != self.INVITATION:
|
||||
other = self.dummy_contact(item_name, "other")
|
||||
application.other_contacts.add(other)
|
||||
if has_current_website and domain_type == self.APPLICATION:
|
||||
current = self.dummy_current(item_name)
|
||||
application.current_websites.add(current)
|
||||
if has_alternative_gov_domain and domain_type == self.APPLICATION:
|
||||
alt = self.dummy_alt(item_name)
|
||||
application.alternative_domains.add(alt)
|
||||
|
||||
return application
|
||||
|
||||
|
||||
def mock_user():
|
||||
"""A simple user."""
|
||||
user_kwargs = dict(
|
||||
id=4,
|
||||
first_name="Rachid",
|
||||
last_name="Mrad",
|
||||
)
|
||||
mock_user, _ = User.objects.get_or_create(**user_kwargs)
|
||||
return mock_user
|
||||
|
||||
|
||||
def create_superuser():
|
||||
User = get_user_model()
|
||||
p = "adminpass"
|
||||
return User.objects.create_superuser(
|
||||
username="superuser",
|
||||
email="admin@example.com",
|
||||
password=p,
|
||||
)
|
||||
|
||||
|
||||
def create_user():
|
||||
User = get_user_model()
|
||||
p = "userpass"
|
||||
return User.objects.create_user(
|
||||
username="staffuser",
|
||||
email="user@example.com",
|
||||
password=p,
|
||||
)
|
||||
|
||||
|
||||
def completed_application(
|
||||
has_other_contacts=True,
|
||||
has_current_website=True,
|
||||
|
@ -111,15 +457,15 @@ def completed_application(
|
|||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
|
@ -159,14 +505,16 @@ def completed_application(
|
|||
return application
|
||||
|
||||
|
||||
def mock_user():
|
||||
"""A simple user."""
|
||||
user_kwargs = dict(
|
||||
id=4,
|
||||
first_name="Rachid",
|
||||
last_name="Mrad",
|
||||
)
|
||||
def multiple_unalphabetical_domain_objects(
|
||||
domain_type=AuditedAdminMockData.APPLICATION,
|
||||
):
|
||||
"""Returns a list of generic domain objects for testing purposes"""
|
||||
applications = []
|
||||
list_of_letters = list(ascii_uppercase)
|
||||
random.shuffle(list_of_letters)
|
||||
|
||||
user, _ = User.objects.get_or_create(**user_kwargs)
|
||||
|
||||
return user
|
||||
mock = AuditedAdminMockData()
|
||||
for object_name in list_of_letters:
|
||||
application = mock.create_full_dummy_domain_object(domain_type, object_name)
|
||||
applications.append(application)
|
||||
return applications
|
||||
|
|
|
@ -1,34 +1,40 @@
|
|||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from registrar.admin import DomainApplicationAdmin, ListHeaderAdmin
|
||||
from registrar.models import DomainApplication, DomainInformation, User
|
||||
from .common import completed_application, mock_user
|
||||
|
||||
from registrar.admin import (
|
||||
DomainApplicationAdmin,
|
||||
ListHeaderAdmin,
|
||||
MyUserAdmin,
|
||||
AuditedAdmin,
|
||||
)
|
||||
from registrar.models import (
|
||||
DomainApplication,
|
||||
DomainInformation,
|
||||
User,
|
||||
DomainInvitation,
|
||||
)
|
||||
from .common import (
|
||||
completed_application,
|
||||
mock_user,
|
||||
create_superuser,
|
||||
create_user,
|
||||
multiple_unalphabetical_domain_objects,
|
||||
)
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from django.conf import settings
|
||||
from unittest.mock import MagicMock
|
||||
import boto3_mocking # type: ignore
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestDomainApplicationAdmin(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None)
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
username = "admin"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
p = "adminpassword"
|
||||
User = get_user_model()
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username=username,
|
||||
first_name=first_name,
|
||||
last_name=last_name,
|
||||
email=email,
|
||||
password=p,
|
||||
)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_submitted_email(self):
|
||||
|
@ -76,9 +82,6 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
# Cleanup
|
||||
application.delete()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_in_review_email(self):
|
||||
# make sure there is no user with this email
|
||||
|
@ -101,7 +104,7 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.INVESTIGATING
|
||||
application.status = DomainApplication.IN_REVIEW
|
||||
|
||||
# Use the model admin's save_model method
|
||||
model_admin.save_model(request, application, form=None, change=True)
|
||||
|
@ -125,9 +128,6 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
# Cleanup
|
||||
application.delete()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_approved_email(self):
|
||||
# make sure there is no user with this email
|
||||
|
@ -139,7 +139,7 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# Create a sample application
|
||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
|
@ -174,15 +174,146 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
# Cleanup
|
||||
if DomainInformation.objects.get(id=application.pk) is not None:
|
||||
DomainInformation.objects.get(id=application.pk).delete()
|
||||
application.delete()
|
||||
def test_save_model_sets_approved_domain(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)
|
||||
)
|
||||
|
||||
# Create an instance of the model admin
|
||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.APPROVED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
model_admin.save_model(request, application, form=None, change=True)
|
||||
|
||||
# Test that approved domain exists and equals requested domain
|
||||
self.assertEqual(
|
||||
application.requested_domain.name, application.approved_domain.name
|
||||
)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_action_needed_email(self):
|
||||
# make sure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# 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)
|
||||
)
|
||||
|
||||
# Create an instance of the model admin
|
||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.ACTION_NEEDED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
model_admin.save_model(request, application, form=None, change=True)
|
||||
|
||||
# Access the arguments passed to send_email
|
||||
call_args = mock_client_instance.send_email.call_args
|
||||
args, kwargs = call_args
|
||||
|
||||
# Retrieve the email details from the arguments
|
||||
from_email = kwargs.get("FromEmailAddress")
|
||||
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||
email_content = kwargs["Content"]
|
||||
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||
|
||||
# Assert or perform other checks on the email details
|
||||
expected_string = (
|
||||
"We've identified an action needed to complete the "
|
||||
"review of your .gov domain request."
|
||||
)
|
||||
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||
self.assertEqual(to_email, EMAIL)
|
||||
self.assertIn(expected_string, email_body)
|
||||
|
||||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_save_model_sends_rejected_email(self):
|
||||
# make sure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
mock_client = MagicMock()
|
||||
mock_client_instance = mock_client.return_value
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
# 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)
|
||||
)
|
||||
|
||||
# Create an instance of the model admin
|
||||
model_admin = DomainApplicationAdmin(DomainApplication, self.site)
|
||||
|
||||
# Modify the application's property
|
||||
application.status = DomainApplication.REJECTED
|
||||
|
||||
# Use the model admin's save_model method
|
||||
model_admin.save_model(request, application, form=None, change=True)
|
||||
|
||||
# Access the arguments passed to send_email
|
||||
call_args = mock_client_instance.send_email.call_args
|
||||
args, kwargs = call_args
|
||||
|
||||
# Retrieve the email details from the arguments
|
||||
from_email = kwargs.get("FromEmailAddress")
|
||||
to_email = kwargs["Destination"]["ToAddresses"][0]
|
||||
email_content = kwargs["Content"]
|
||||
email_body = email_content["Simple"]["Body"]["Text"]["Data"]
|
||||
|
||||
# Assert or perform other checks on the email details
|
||||
expected_string = "Your .gov domain request has been rejected."
|
||||
self.assertEqual(from_email, settings.DEFAULT_FROM_EMAIL)
|
||||
self.assertEqual(to_email, EMAIL)
|
||||
self.assertIn(expected_string, email_body)
|
||||
|
||||
# Perform assertions on the mock call itself
|
||||
mock_client_instance.send_email.assert_called_once()
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
|
||||
class ListHeaderAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.admin = ListHeaderAdmin(model=DomainApplication, admin_site=None)
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.superuser = create_superuser()
|
||||
|
||||
def test_changelist_view(self):
|
||||
# Have to get creative to get past linter
|
||||
p = "adminpassword"
|
||||
self.client.login(username="admin", password=p)
|
||||
p = "adminpass"
|
||||
self.client.login(username="superuser", password=p)
|
||||
|
||||
# Mock a user
|
||||
user = mock_user()
|
||||
|
@ -241,6 +372,267 @@ class TestDomainApplicationAdmin(TestCase):
|
|||
|
||||
def tearDown(self):
|
||||
# delete any applications too
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
self.superuser.delete()
|
||||
|
||||
|
||||
class MyUserAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
admin_site = AdminSite()
|
||||
self.admin = MyUserAdmin(model=get_user_model(), admin_site=admin_site)
|
||||
|
||||
def test_list_display_without_username(self):
|
||||
request = self.client.request().wsgi_request
|
||||
request.user = create_user()
|
||||
|
||||
list_display = self.admin.get_list_display(request)
|
||||
expected_list_display = (
|
||||
"email",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
)
|
||||
|
||||
self.assertEqual(list_display, expected_list_display)
|
||||
self.assertNotIn("username", list_display)
|
||||
|
||||
def test_get_fieldsets_superuser(self):
|
||||
request = self.client.request().wsgi_request
|
||||
request.user = create_superuser()
|
||||
fieldsets = self.admin.get_fieldsets(request)
|
||||
expected_fieldsets = super(MyUserAdmin, self.admin).get_fieldsets(request)
|
||||
self.assertEqual(fieldsets, expected_fieldsets)
|
||||
|
||||
def test_get_fieldsets_non_superuser(self):
|
||||
request = self.client.request().wsgi_request
|
||||
request.user = create_user()
|
||||
fieldsets = self.admin.get_fieldsets(request)
|
||||
expected_fieldsets = ((None, {"fields": []}),)
|
||||
self.assertEqual(fieldsets, expected_fieldsets)
|
||||
|
||||
def tearDown(self):
|
||||
User.objects.all().delete()
|
||||
|
||||
|
||||
class AuditedAdminTest(TestCase):
|
||||
def setUp(self):
|
||||
self.site = AdminSite()
|
||||
self.factory = RequestFactory()
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
||||
def order_by_desired_field_helper(
|
||||
self, obj_to_sort: AuditedAdmin, request, field_name, *obj_names
|
||||
):
|
||||
formatted_sort_fields = []
|
||||
for obj in obj_names:
|
||||
formatted_sort_fields.append("{}__{}".format(field_name, obj))
|
||||
|
||||
ordered_list = list(
|
||||
obj_to_sort.get_queryset(request)
|
||||
.order_by(*formatted_sort_fields)
|
||||
.values_list(*formatted_sort_fields)
|
||||
)
|
||||
|
||||
return ordered_list
|
||||
|
||||
def test_alphabetically_sorted_fk_fields_domain_application(self):
|
||||
tested_fields = [
|
||||
DomainApplication.authorizing_official.field,
|
||||
DomainApplication.submitter.field,
|
||||
DomainApplication.investigator.field,
|
||||
DomainApplication.creator.field,
|
||||
DomainApplication.requested_domain.field,
|
||||
]
|
||||
|
||||
# Creates multiple domain applications - review status does not matter
|
||||
applications = multiple_unalphabetical_domain_objects("application")
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domainapplication/{}/change/".format(applications[0].pk)
|
||||
)
|
||||
|
||||
model_admin = AuditedAdmin(DomainApplication, self.site)
|
||||
|
||||
sorted_fields = []
|
||||
# Typically we wouldn't want two nested for fields,
|
||||
# but both fields are of a fixed length.
|
||||
# For test case purposes, this should be performant.
|
||||
for field in tested_fields:
|
||||
isNamefield: bool = field == DomainApplication.requested_domain.field
|
||||
if isNamefield:
|
||||
sorted_fields = ["name"]
|
||||
else:
|
||||
sorted_fields = ["first_name", "last_name"]
|
||||
# We want both of these to be lists, as it is richer test wise.
|
||||
|
||||
desired_order = self.order_by_desired_field_helper(
|
||||
model_admin, request, field.name, *sorted_fields
|
||||
)
|
||||
current_sort_order = list(
|
||||
model_admin.formfield_for_foreignkey(field, request).queryset
|
||||
)
|
||||
|
||||
# Conforms to the same object structure as desired_order
|
||||
current_sort_order_coerced_type = []
|
||||
|
||||
# This is necessary as .queryset and get_queryset
|
||||
# return lists of different types/structures.
|
||||
# We need to parse this data and coerce them into the same type.
|
||||
for contact in current_sort_order:
|
||||
if not isNamefield:
|
||||
first = contact.first_name
|
||||
last = contact.last_name
|
||||
else:
|
||||
first = contact.name
|
||||
last = None
|
||||
|
||||
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
|
||||
if name_tuple is not None:
|
||||
current_sort_order_coerced_type.append(name_tuple)
|
||||
|
||||
self.assertEqual(
|
||||
desired_order,
|
||||
current_sort_order_coerced_type,
|
||||
"{} is not ordered alphabetically".format(field.name),
|
||||
)
|
||||
|
||||
def test_alphabetically_sorted_fk_fields_domain_information(self):
|
||||
tested_fields = [
|
||||
DomainInformation.authorizing_official.field,
|
||||
DomainInformation.submitter.field,
|
||||
DomainInformation.creator.field,
|
||||
(DomainInformation.domain.field, ["name"]),
|
||||
(DomainInformation.domain_application.field, ["requested_domain__name"]),
|
||||
]
|
||||
# Creates multiple domain applications - review status does not matter
|
||||
applications = multiple_unalphabetical_domain_objects("information")
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domaininformation/{}/change/".format(applications[0].pk)
|
||||
)
|
||||
|
||||
model_admin = AuditedAdmin(DomainInformation, self.site)
|
||||
|
||||
sorted_fields = []
|
||||
# Typically we wouldn't want two nested for fields,
|
||||
# but both fields are of a fixed length.
|
||||
# For test case purposes, this should be performant.
|
||||
for field in tested_fields:
|
||||
isOtherOrderfield: bool = isinstance(field, tuple)
|
||||
field_obj = None
|
||||
if isOtherOrderfield:
|
||||
sorted_fields = field[1]
|
||||
field_obj = field[0]
|
||||
else:
|
||||
sorted_fields = ["first_name", "last_name"]
|
||||
field_obj = field
|
||||
# We want both of these to be lists, as it is richer test wise.
|
||||
desired_order = self.order_by_desired_field_helper(
|
||||
model_admin, request, field_obj.name, *sorted_fields
|
||||
)
|
||||
current_sort_order = list(
|
||||
model_admin.formfield_for_foreignkey(field_obj, request).queryset
|
||||
)
|
||||
|
||||
# Conforms to the same object structure as desired_order
|
||||
current_sort_order_coerced_type = []
|
||||
|
||||
# This is necessary as .queryset and get_queryset
|
||||
# return lists of different types/structures.
|
||||
# We need to parse this data and coerce them into the same type.
|
||||
for obj in current_sort_order:
|
||||
last = None
|
||||
if not isOtherOrderfield:
|
||||
first = obj.first_name
|
||||
last = obj.last_name
|
||||
elif field_obj == DomainInformation.domain.field:
|
||||
first = obj.name
|
||||
elif field_obj == DomainInformation.domain_application.field:
|
||||
first = obj.requested_domain.name
|
||||
|
||||
name_tuple = self.coerced_fk_field_helper(
|
||||
first, last, field_obj.name, ":"
|
||||
)
|
||||
if name_tuple is not None:
|
||||
current_sort_order_coerced_type.append(name_tuple)
|
||||
|
||||
self.assertEqual(
|
||||
desired_order,
|
||||
current_sort_order_coerced_type,
|
||||
"{} is not ordered alphabetically".format(field_obj.name),
|
||||
)
|
||||
|
||||
def test_alphabetically_sorted_fk_fields_domain_invitation(self):
|
||||
tested_fields = [DomainInvitation.domain.field]
|
||||
|
||||
# Creates multiple domain applications - review status does not matter
|
||||
applications = multiple_unalphabetical_domain_objects("invitation")
|
||||
|
||||
# Create a mock request
|
||||
request = self.factory.post(
|
||||
"/admin/registrar/domaininvitation/{}/change/".format(applications[0].pk)
|
||||
)
|
||||
|
||||
model_admin = AuditedAdmin(DomainInvitation, self.site)
|
||||
|
||||
sorted_fields = []
|
||||
# Typically we wouldn't want two nested for fields,
|
||||
# but both fields are of a fixed length.
|
||||
# For test case purposes, this should be performant.
|
||||
for field in tested_fields:
|
||||
sorted_fields = ["name"]
|
||||
# We want both of these to be lists, as it is richer test wise.
|
||||
|
||||
desired_order = self.order_by_desired_field_helper(
|
||||
model_admin, request, field.name, *sorted_fields
|
||||
)
|
||||
current_sort_order = list(
|
||||
model_admin.formfield_for_foreignkey(field, request).queryset
|
||||
)
|
||||
|
||||
# Conforms to the same object structure as desired_order
|
||||
current_sort_order_coerced_type = []
|
||||
|
||||
# This is necessary as .queryset and get_queryset
|
||||
# return lists of different types/structures.
|
||||
# We need to parse this data and coerce them into the same type.
|
||||
for contact in current_sort_order:
|
||||
first = contact.name
|
||||
last = None
|
||||
|
||||
name_tuple = self.coerced_fk_field_helper(first, last, field.name, ":")
|
||||
if name_tuple is not None:
|
||||
current_sort_order_coerced_type.append(name_tuple)
|
||||
|
||||
self.assertEqual(
|
||||
desired_order,
|
||||
current_sort_order_coerced_type,
|
||||
"{} is not ordered alphabetically".format(field.name),
|
||||
)
|
||||
|
||||
def coerced_fk_field_helper(
|
||||
self, first_name, last_name, field_name, queryset_shorthand
|
||||
):
|
||||
"""Handles edge cases for test cases"""
|
||||
if first_name is None:
|
||||
raise ValueError("Invalid value for first_name, must be defined")
|
||||
|
||||
returned_tuple = (first_name, last_name)
|
||||
# Handles edge case for names - structured strangely
|
||||
if last_name is None:
|
||||
return (first_name,)
|
||||
|
||||
if first_name.split(queryset_shorthand)[1] == field_name:
|
||||
return returned_tuple
|
||||
else:
|
||||
return None
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainApplication.objects.all().delete()
|
||||
DomainInvitation.objects.all().delete()
|
||||
|
|
|
@ -144,11 +144,11 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
||||
def test_transition_not_allowed_investigating_submitted(self):
|
||||
"""Create an application with status investigating and call submit
|
||||
def test_transition_not_allowed_in_review_submitted(self):
|
||||
"""Create an application with status in review and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
@ -162,7 +162,16 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
||||
def test_transition_not_allowed_started_investigating(self):
|
||||
def test_transition_not_allowed_rejected_submitted(self):
|
||||
"""Create an application with status rejected and call submit
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.submit()
|
||||
|
||||
def test_transition_not_allowed_started_in_review(self):
|
||||
"""Create an application with status started and call in_review
|
||||
against transition rules"""
|
||||
|
||||
|
@ -171,16 +180,16 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_investigating_investigating(self):
|
||||
"""Create an application with status investigating and call in_review
|
||||
def test_transition_not_allowed_in_review_in_review(self):
|
||||
"""Create an application with status in review and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.INVESTIGATING)
|
||||
application = completed_application(status=DomainApplication.IN_REVIEW)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_approved_investigating(self):
|
||||
def test_transition_not_allowed_approved_in_review(self):
|
||||
"""Create an application with status approved and call in_review
|
||||
against transition rules"""
|
||||
|
||||
|
@ -189,7 +198,25 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_investigating(self):
|
||||
def test_transition_not_allowed_action_needed_in_review(self):
|
||||
"""Create an application with status action needed and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_rejected_in_review(self):
|
||||
"""Create an application with status rejected and call in_review
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_in_review(self):
|
||||
"""Create an application with status withdrawn and call in_review
|
||||
against transition rules"""
|
||||
|
||||
|
@ -198,6 +225,51 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.in_review()
|
||||
|
||||
def test_transition_not_allowed_started_action_needed(self):
|
||||
"""Create an application with status started and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.STARTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_submitted_action_needed(self):
|
||||
"""Create an application with status submitted and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.SUBMITTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_action_needed_action_needed(self):
|
||||
"""Create an application with status action needed and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_approved_action_needed(self):
|
||||
"""Create an application with status approved and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.APPROVED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_action_needed(self):
|
||||
"""Create an application with status withdrawn and call action_needed
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.WITHDRAWN)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.action_needed()
|
||||
|
||||
def test_transition_not_allowed_started_approved(self):
|
||||
"""Create an application with status started and call approve
|
||||
against transition rules"""
|
||||
|
@ -216,6 +288,15 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
||||
def test_transition_not_allowed_action_needed_approved(self):
|
||||
"""Create an application with status action needed and call approve
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.approve()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_approved(self):
|
||||
"""Create an application with status withdrawn and call approve
|
||||
against transition rules"""
|
||||
|
@ -243,6 +324,24 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
||||
def test_transition_not_allowed_action_needed_withdrawn(self):
|
||||
"""Create an application with status action needed and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.ACTION_NEEDED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
||||
def test_transition_not_allowed_rejected_withdrawn(self):
|
||||
"""Create an application with status rejected and call withdraw
|
||||
against transition rules"""
|
||||
|
||||
application = completed_application(status=DomainApplication.REJECTED)
|
||||
|
||||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_withdrawn(self):
|
||||
"""Create an application with status withdrawn and call withdraw
|
||||
against transition rules"""
|
||||
|
@ -252,6 +351,51 @@ class TestDomainApplication(TestCase):
|
|||
with self.assertRaises(TransitionNotAllowed):
|
||||
application.withdraw()
|
||||
|
||||
def test_transition_not_allowed_started_rejected(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()
|
||||
|
||||
def test_transition_not_allowed_submitted_rejected(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()
|
||||
|
||||
def test_transition_not_allowed_action_needed_rejected(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()
|
||||
|
||||
def test_transition_not_allowed_withdrawn_rejected(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()
|
||||
|
||||
def test_transition_not_allowed_rejected_rejected(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()
|
||||
|
||||
|
||||
class TestPermissions(TestCase):
|
||||
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.template import Context, Template
|
||||
from registrar.templatetags.custom_filters import (
|
||||
extract_value,
|
||||
extract_a_text,
|
||||
find_index,
|
||||
slice_after,
|
||||
)
|
||||
|
||||
|
||||
class TestTemplateTags(TestCase):
|
||||
|
@ -29,3 +35,51 @@ class TestTemplateTags(TestCase):
|
|||
self.assertTrue(result.startswith(settings.GETGOV_PUBLIC_SITE_URL))
|
||||
# slash-slash host slash directory slash page
|
||||
self.assertEqual(result.count("/"), 4)
|
||||
|
||||
|
||||
class CustomFiltersTestCase(TestCase):
|
||||
def test_extract_value_filter(self):
|
||||
html_input = (
|
||||
'<input type="checkbox" name="_selected_action" value="123" '
|
||||
'id="label_123" class="action-select">'
|
||||
)
|
||||
result = extract_value(html_input)
|
||||
self.assertEqual(result, "123")
|
||||
|
||||
html_input = (
|
||||
'<input type="checkbox" name="_selected_action" value="abc" '
|
||||
'id="label_123" class="action-select">'
|
||||
)
|
||||
result = extract_value(html_input)
|
||||
self.assertEqual(result, "abc")
|
||||
|
||||
def test_extract_a_text_filter(self):
|
||||
input_text = '<a href="#">Link Text</a>'
|
||||
result = extract_a_text(input_text)
|
||||
self.assertEqual(result, "Link Text")
|
||||
|
||||
input_text = '<a href="/example">Another Link</a>'
|
||||
result = extract_a_text(input_text)
|
||||
self.assertEqual(result, "Another Link")
|
||||
|
||||
def test_find_index(self):
|
||||
haystack = "Hello, World!"
|
||||
needle = "lo"
|
||||
result = find_index(haystack, needle)
|
||||
self.assertEqual(result, 3)
|
||||
|
||||
needle = "XYZ"
|
||||
result = find_index(haystack, needle)
|
||||
self.assertEqual(result, -1)
|
||||
|
||||
def test_slice_after(self):
|
||||
value = "Hello, World!"
|
||||
substring = "lo"
|
||||
result = slice_after(value, substring)
|
||||
self.assertEqual(result, ", World!")
|
||||
|
||||
substring = "XYZ"
|
||||
result = slice_after(value, substring)
|
||||
self.assertEqual(
|
||||
result, value
|
||||
) # Should return the original value if substring not found
|
||||
|
|
|
@ -145,7 +145,6 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# ---- TYPE PAGE ----
|
||||
type_form = type_page.form
|
||||
type_form["organization_type-organization_type"] = "federal"
|
||||
|
||||
# test next button and validate data
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
type_result = type_page.form.submit()
|
||||
|
@ -161,6 +160,7 @@ class DomainApplicationTests(TestWithUser, WebTest):
|
|||
# ---- FEDERAL BRANCH PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
federal_page = type_result.follow()
|
||||
federal_form = federal_page.form
|
||||
federal_form["organization_federal-federal_type"] = "executive"
|
||||
|
|
|
@ -67,7 +67,7 @@ class ApplicationWizard(TemplateView):
|
|||
URL_NAMESPACE = "application"
|
||||
# name for accessing /application/<id>/edit
|
||||
EDIT_URL_NAME = "edit-application"
|
||||
|
||||
NEW_URL_NAME = "/register/"
|
||||
# We need to pass our human-readable step titles as context to the templates.
|
||||
TITLES = {
|
||||
Step.ORGANIZATION_TYPE: _("Type of organization"),
|
||||
|
@ -144,6 +144,7 @@ class ApplicationWizard(TemplateView):
|
|||
self._application = DomainApplication.objects.create(
|
||||
creator=self.request.user, # type: ignore
|
||||
)
|
||||
|
||||
self.storage["application_id"] = self._application.id
|
||||
return self._application
|
||||
|
||||
|
@ -195,7 +196,6 @@ class ApplicationWizard(TemplateView):
|
|||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""This method handles GET requests."""
|
||||
|
||||
current_url = resolve(request.path_info).url_name
|
||||
|
||||
# if user visited via an "edit" url, associate the id of the
|
||||
|
@ -213,12 +213,15 @@ class ApplicationWizard(TemplateView):
|
|||
# send users "to the application wizard" without needing to
|
||||
# know which view is first in the list of steps.
|
||||
if self.__class__ == ApplicationWizard:
|
||||
# if starting a new application, clear the storage
|
||||
if request.path_info == self.NEW_URL_NAME:
|
||||
del self.storage
|
||||
|
||||
return self.goto(self.steps.first)
|
||||
|
||||
self.steps.current = current_url
|
||||
context = self.get_context_data()
|
||||
context["forms"] = self.get_forms()
|
||||
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def get_all_forms(self, **kwargs) -> list:
|
||||
|
@ -242,7 +245,6 @@ class ApplicationWizard(TemplateView):
|
|||
and from the database if `use_db` is True (provided that record exists).
|
||||
An empty form will be provided if neither of those are true.
|
||||
"""
|
||||
|
||||
kwargs = {
|
||||
"files": files,
|
||||
"prefix": self.steps.current,
|
||||
|
|
|
@ -24,6 +24,12 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
The user is in self.request.user and the domain needs to be looked
|
||||
up from the domain's primary key in self.kwargs["pk"]
|
||||
"""
|
||||
|
||||
# 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:
|
||||
return False
|
||||
|
||||
|
@ -33,6 +39,10 @@ class DomainPermission(PermissionsLoginMixin):
|
|||
).exists():
|
||||
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.
|
||||
return True
|
||||
|
||||
|
|
|
@ -3,15 +3,15 @@ asgiref==3.7.2 ; python_version >= '3.7'
|
|||
boto3==1.26.145
|
||||
botocore==1.29.145 ; python_version >= '3.7'
|
||||
cachetools==5.3.1
|
||||
certifi==2023.5.7 ; python_version >= '3.6'
|
||||
certifi==2023.7.22 ; python_version >= '3.6'
|
||||
cfenv==0.5.3
|
||||
cffi==1.15.1
|
||||
charset-normalizer==3.1.0 ; python_full_version >= '3.7.0'
|
||||
cryptography==41.0.1 ; python_version >= '3.7'
|
||||
cryptography==41.0.3 ; python_version >= '3.7'
|
||||
defusedxml==0.7.1 ; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
|
||||
dj-database-url==2.0.0
|
||||
dj-email-url==1.0.6
|
||||
django==4.2.1
|
||||
django==4.2.3
|
||||
django-allow-cidr==0.6.0
|
||||
django-auditlog==2.3.0
|
||||
django-cache-url==3.4.4
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
10027 OUTOFSCOPE http://app:8080/public/js/uswds-init.min.js
|
||||
# get-gov.js contains suspicious word "from" as in `Array.from()`
|
||||
10027 OUTOFSCOPE http://app:8080/public/js/get-gov.js
|
||||
# Ignore wording of "TODO"
|
||||
10027 OUTOFSCOPE http://app:8080.*$
|
||||
10028 FAIL (Open Redirect - Passive/beta)
|
||||
10029 FAIL (Cookie Poisoning - Passive/beta)
|
||||
10030 FAIL (User Controllable Charset - Passive/beta)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue