mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge remote-tracking branch 'origin/main' into nl/2036-reorder-request-status-dropdown-options
This commit is contained in:
commit
a6c74e06c1
46 changed files with 2564 additions and 175 deletions
|
@ -405,3 +405,9 @@ This function is triggered by the post_save event on the User model, designed to
|
|||
1. For New Users: Upon the creation of a new user, it checks for an existing `Contact` by email. If no matching contact is found, it creates a new Contact using the user's details from Login.gov. If a matching contact is found, it associates this contact with the user. In cases where multiple contacts with the same email exist, it logs a warning and associates the first contact found.
|
||||
|
||||
2. For Existing Users: For users logging in subsequent times, the function ensures that any updates from Login.gov are applied to the associated User record. However, it does not alter any existing Contact records.
|
||||
|
||||
## Disable email sending (toggling the disable_email_sending flag)
|
||||
1. On the app, navigate to `\admin`.
|
||||
2. Under models, click `Waffle flags`.
|
||||
3. Click the `disable_email_sending` record. This should exist by default, if not - create one with that name.
|
||||
4. (Important) Set the field `everyone` to `Yes`. This field overrides all other settings
|
|
@ -1,18 +1,29 @@
|
|||
# Export / Import Tables
|
||||
|
||||
A means is provided to export and import individual tables from
|
||||
A means is provided to export and import tables from
|
||||
one environment to another. This allows for replication of
|
||||
production data in a development environment. Import and export
|
||||
are provided through the django admin interface, through a modified
|
||||
library, django-import-export. Each supported model has an Import
|
||||
and an Export button on the list view.
|
||||
are provided through a modified library, django-import-export.
|
||||
Simple scripts are provided as detailed below.
|
||||
|
||||
### Export
|
||||
|
||||
When exporting models from the source environment, make sure that
|
||||
no filters are selected. This will ensure that all rows of the model
|
||||
are exported. Due to database dependencies, the following models
|
||||
need to be exported:
|
||||
To export from the source environment, run the following command from src directory:
|
||||
manage.py export_tables
|
||||
|
||||
Connect to the source sandbox and run the command:
|
||||
cf ssh {source-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py export_tables
|
||||
|
||||
example exporting from getgov-stable:
|
||||
cf ssh getgov-stable
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py export_tables
|
||||
|
||||
This exports a file, exported_tables.zip, to the tmp directory
|
||||
|
||||
For reference, the zip file will contain the following tables in csv form:
|
||||
|
||||
* User
|
||||
* Contact
|
||||
|
@ -25,6 +36,20 @@ need to be exported:
|
|||
* Host
|
||||
* HostIP
|
||||
|
||||
After exporting the file from the target environment, scp the exported_tables.zip
|
||||
file from the target environment to local. Run the below commands from local.
|
||||
|
||||
Get passcode by running:
|
||||
cf ssh-code
|
||||
|
||||
scp file from source app to local file:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {source-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip {local_file_path}
|
||||
when prompted, supply the passcode retrieved in the 'cf ssh-code' command
|
||||
|
||||
example copying from stable to local cwd:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-stable --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip .
|
||||
|
||||
|
||||
### Import
|
||||
|
||||
When importing into the target environment, if the target environment
|
||||
|
@ -34,7 +59,18 @@ that there are no database conflicts on import.
|
|||
|
||||
#### Preparing Target Environment
|
||||
|
||||
Delete all rows from tables in the following order through django admin:
|
||||
In order to delete all rows from the appropriate tables, run the following
|
||||
command:
|
||||
cf ssh {target-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py clean_tables
|
||||
|
||||
example cleaning getgov-backup:
|
||||
cf ssh getgov-backup
|
||||
/tmp/lifecycle/backup
|
||||
./manage.py clean_tables
|
||||
|
||||
For reference, this deletes all rows from the following tables:
|
||||
|
||||
* DomainInformation
|
||||
* DomainRequest
|
||||
|
@ -48,10 +84,34 @@ Delete all rows from tables in the following order through django admin:
|
|||
|
||||
#### Importing into Target Environment
|
||||
|
||||
Once target environment is prepared, files can be imported in the following
|
||||
order:
|
||||
Once target environment is prepared, files can be imported.
|
||||
|
||||
* User (After importing User table, you need to delete all rows from Contact table before importing Contacts)
|
||||
To scp the exported_tables.zip file from local to the sandbox, run the following:
|
||||
|
||||
Get passcode by running:
|
||||
cf ssh-code
|
||||
|
||||
scp file from local to target app:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {target-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 {local_file_path} ssh.fr.cloud.gov:app/tmp/exported_tables.zip
|
||||
when prompted, supply the passcode retrieved in the 'cf ssh-code' command
|
||||
|
||||
example copy of local file in tmp to getgov-backup:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-backup --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 tmp/exported_tables.zip ssh.fr.cloud.gov:app/tmp/exported_tables.zip
|
||||
|
||||
|
||||
Then connect to a shell in the target environment, and run the following import command:
|
||||
cf ssh {target-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py import_tables
|
||||
|
||||
example cleaning getgov-backup:
|
||||
cf ssh getgov-backup
|
||||
/tmp/lifecycle/backup
|
||||
./manage.py import_tables
|
||||
|
||||
For reference, this imports tables in the following order:
|
||||
|
||||
* User
|
||||
* Contact
|
||||
* Domain
|
||||
* Host
|
||||
|
|
|
@ -2245,9 +2245,46 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
return response
|
||||
|
||||
|
||||
class PublicContactAdmin(ListHeaderAdmin):
|
||||
class PublicContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
import/export file"""
|
||||
|
||||
class Meta:
|
||||
model = models.PublicContact
|
||||
|
||||
def import_row(self, row, instance_loader, using_transactions=True, dry_run=False, raise_errors=None, **kwargs):
|
||||
"""Override kwargs skip_epp_save and set to True"""
|
||||
kwargs["skip_epp_save"] = True
|
||||
return super().import_row(
|
||||
row,
|
||||
instance_loader,
|
||||
using_transactions=using_transactions,
|
||||
dry_run=dry_run,
|
||||
raise_errors=raise_errors,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def save_instance(self, instance, is_create, using_transactions=True, dry_run=False):
|
||||
"""Override save_instance setting skip_epp_save to True"""
|
||||
self.before_save_instance(instance, using_transactions, dry_run)
|
||||
if self._meta.use_bulk:
|
||||
if is_create:
|
||||
self.create_instances.append(instance)
|
||||
else:
|
||||
self.update_instances.append(instance)
|
||||
elif not using_transactions and dry_run:
|
||||
# we don't have transactions and we want to do a dry_run
|
||||
pass
|
||||
else:
|
||||
instance.save(skip_epp_save=True)
|
||||
self.after_save_instance(instance, using_transactions, dry_run)
|
||||
|
||||
|
||||
class PublicContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
"""Custom PublicContact admin class."""
|
||||
|
||||
resource_classes = [PublicContactResource]
|
||||
|
||||
change_form_template = "django/admin/email_clipboard_change_form.html"
|
||||
autocomplete_fields = ["domain"]
|
||||
|
||||
|
@ -2305,6 +2342,8 @@ class UserGroupAdmin(AuditedAdmin):
|
|||
|
||||
|
||||
class WaffleFlagAdmin(FlagAdmin):
|
||||
"""Custom admin implementation of django-waffle's Flag class"""
|
||||
|
||||
class Meta:
|
||||
"""Contains meta information about this class"""
|
||||
|
||||
|
@ -2338,6 +2377,6 @@ admin.site.register(models.VerifiedByStaff, VerifiedByStaffAdmin)
|
|||
# Register our custom waffle implementations
|
||||
admin.site.register(models.WaffleFlag, WaffleFlagAdmin)
|
||||
|
||||
# Unregister Sample and Switch from the waffle library
|
||||
admin.site.unregister(Sample)
|
||||
# Unregister Switch and Sample from the waffle library
|
||||
admin.site.unregister(Switch)
|
||||
admin.site.unregister(Sample)
|
||||
|
|
|
@ -1248,3 +1248,125 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
loadDomainRequests(1);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that hooks up the edit buttons on the finish-user-setup page
|
||||
*/
|
||||
(function finishUserSetupListener() {
|
||||
|
||||
function getInputField(fieldName){
|
||||
return document.querySelector(`#id_${fieldName}`)
|
||||
}
|
||||
|
||||
// Shows the hidden input field and hides the readonly one
|
||||
function showInputFieldHideReadonlyField(fieldName, button) {
|
||||
let inputField = getInputField(fieldName)
|
||||
let readonlyField = document.querySelector(`#${fieldName}__edit-button-readonly`)
|
||||
|
||||
readonlyField.classList.toggle('display-none');
|
||||
inputField.classList.toggle('display-none');
|
||||
|
||||
// Toggle the bold style on the grid row
|
||||
let gridRow = button.closest(".grid-col-2").closest(".grid-row")
|
||||
if (gridRow){
|
||||
gridRow.classList.toggle("bold-usa-label")
|
||||
}
|
||||
}
|
||||
|
||||
function handleFullNameField(fieldName = "full_name") {
|
||||
// Remove the display-none class from the nearest parent div
|
||||
let nameFieldset = document.querySelector("#profile-name-group");
|
||||
if (nameFieldset){
|
||||
nameFieldset.classList.remove("display-none");
|
||||
}
|
||||
|
||||
// Hide the "full_name" field
|
||||
let inputField = getInputField(fieldName);
|
||||
if (inputField) {
|
||||
inputFieldParentDiv = inputField.closest("div");
|
||||
if (inputFieldParentDiv) {
|
||||
inputFieldParentDiv.classList.add("display-none");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditButtonClick(fieldName, button){
|
||||
button.addEventListener('click', function() {
|
||||
// Lock the edit button while this operation occurs
|
||||
button.disabled = true
|
||||
|
||||
if (fieldName == "full_name"){
|
||||
handleFullNameField();
|
||||
}else {
|
||||
showInputFieldHideReadonlyField(fieldName, button);
|
||||
}
|
||||
|
||||
// Hide the button itself
|
||||
button.classList.add("display-none");
|
||||
|
||||
// Unlock after it completes
|
||||
button.disabled = false
|
||||
});
|
||||
}
|
||||
|
||||
function setupListener(){
|
||||
document.querySelectorAll('[id$="__edit-button"]').forEach(function(button) {
|
||||
// Get the "{field_name}" and "edit-button"
|
||||
let fieldIdParts = button.id.split("__")
|
||||
if (fieldIdParts && fieldIdParts.length > 0){
|
||||
let fieldName = fieldIdParts[0]
|
||||
|
||||
// When the edit button is clicked, show the input field under it
|
||||
handleEditButtonClick(fieldName, button);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function showInputOnErrorFields(){
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get all input elements within the form
|
||||
let form = document.querySelector("#finish-profile-setup-form");
|
||||
let inputs = form ? form.querySelectorAll("input") : null;
|
||||
if (!inputs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let fullNameButtonClicked = false
|
||||
inputs.forEach(function(input) {
|
||||
let fieldName = input.name;
|
||||
let errorMessage = document.querySelector(`#id_${fieldName}__error-message`);
|
||||
|
||||
// If no error message is found, do nothing
|
||||
if (!fieldName || !errorMessage) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let editButton = document.querySelector(`#${fieldName}__edit-button`);
|
||||
if (editButton){
|
||||
// Show the input field of the field that errored out
|
||||
editButton.click();
|
||||
}
|
||||
|
||||
// If either the full_name field errors out,
|
||||
// or if any of its associated fields do - show all name related fields.
|
||||
let nameFields = ["first_name", "middle_name", "last_name"];
|
||||
if (nameFields.includes(fieldName) && !fullNameButtonClicked){
|
||||
// Click the full name button if any of its related fields error out
|
||||
fullNameButton = document.querySelector("#full_name__edit-button");
|
||||
if (fullNameButton) {
|
||||
fullNameButton.click();
|
||||
fullNameButtonClicked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Hookup all edit buttons to the `handleEditButtonClick` function
|
||||
setupListener();
|
||||
|
||||
// Show the input fields if an error exists
|
||||
showInputOnErrorFields();
|
||||
})();
|
||||
|
|
|
@ -163,6 +163,7 @@ html[data-theme="dark"] {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
#branding h1 a:link, #branding h1 a:visited {
|
||||
color: var(--primary-fg);
|
||||
}
|
||||
|
@ -194,6 +195,18 @@ div#content > h2 {
|
|||
}
|
||||
}
|
||||
|
||||
.change-form {
|
||||
.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;
|
||||
}
|
||||
.usa-table td {
|
||||
border-bottom: 1px solid var(--hairline-color);
|
||||
}
|
||||
}
|
||||
|
||||
#nav-sidebar {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
|
||||
/* Styles for making visible to screen reader / AT users only. */
|
||||
.sr-only {
|
||||
|
@ -169,3 +170,44 @@ abbr[title] {
|
|||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-with-edit-button {
|
||||
svg.usa-icon {
|
||||
width: 1.5em !important;
|
||||
height: 1.5em !important;
|
||||
color: #{$dhs-green};
|
||||
position: absolute;
|
||||
}
|
||||
&.input-with-edit-button__error {
|
||||
svg.usa-icon {
|
||||
color: #{$dhs-red};
|
||||
}
|
||||
div.readonly-field {
|
||||
color: #{$dhs-red};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We need to deviate from some default USWDS styles here
|
||||
// in this particular case, so we have to override this.
|
||||
.usa-form .usa-button.readonly-edit-button {
|
||||
margin-top: 0px !important;
|
||||
padding-top: 0px !important;
|
||||
svg {
|
||||
width: 1.25em !important;
|
||||
height: 1.25em !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Define some styles for the .gov header/logo
|
||||
.usa-logo button {
|
||||
color: #{$dhs-dark-gray-85};
|
||||
font-weight: 700;
|
||||
font-family: family('sans');
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.usa-logo button.usa-button--unstyled.disabled-button:hover{
|
||||
color: #{$dhs-dark-gray-85};
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
|
||||
/* Make "placeholder" links visually obvious */
|
||||
a[href$="todo"]::after {
|
||||
|
@ -7,11 +8,16 @@ a[href$="todo"]::after {
|
|||
content: " [link TBD]";
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
|
||||
a.usa-link.usa-link--always-blue {
|
||||
color: #{$dhs-blue};
|
||||
}
|
||||
|
||||
a.breadcrumb__back {
|
||||
display:flex;
|
||||
align-items: center;
|
||||
margin-bottom: units(2.5);
|
||||
color: #{$dhs-blue};
|
||||
&:visited {
|
||||
color: color('primary');
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ $dhs-gray-10: #fcfdfd;
|
|||
|
||||
/*--- Dark Gray ---*/
|
||||
$dhs-dark-gray-90: #040404;
|
||||
$dhs-dark-gray-85: #1b1b1b;
|
||||
$dhs-dark-gray-80: #19191a;
|
||||
$dhs-dark-gray-70: #2f2f30;
|
||||
$dhs-dark-gray-60: #444547;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
|
||||
.usa-form .usa-button {
|
||||
margin-top: units(3);
|
||||
|
@ -26,6 +27,34 @@
|
|||
}
|
||||
}
|
||||
|
||||
.usa-form-editable {
|
||||
border-top: 2px #{$dhs-dark-gray-15} solid;
|
||||
|
||||
.bold-usa-label label.usa-label{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.bold-usa-label label.usa-label{
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.usa-form-editable--no-border {
|
||||
border-top: None;
|
||||
margin-top: 0px !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.usa-form-editable > .usa-form-group:first-of-type {
|
||||
margin-top: unset;
|
||||
}
|
||||
|
||||
@media (min-width: 35em) {
|
||||
.usa-form--largest {
|
||||
max-width: 35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-form-group--unstyled-error {
|
||||
margin-left: 0;
|
||||
padding-left: 0;
|
||||
|
@ -52,4 +81,4 @@ legend.float-left-tablet + button.float-right-tablet {
|
|||
background-color: var(--body-fg);
|
||||
color: var(--close-button-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,3 +24,7 @@
|
|||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
#extended-logo .usa-tooltip__body {
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ MIDDLEWARE = [
|
|||
# django-cors-headers: listen to cors responses
|
||||
"corsheaders.middleware.CorsMiddleware",
|
||||
# custom middleware to stop caching from CloudFront
|
||||
"registrar.no_cache_middleware.NoCacheMiddleware",
|
||||
"registrar.registrar_middleware.NoCacheMiddleware",
|
||||
# serve static assets in production
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
# provide security enhancements to the request/response cycle
|
||||
|
@ -188,6 +188,7 @@ MIDDLEWARE = [
|
|||
"auditlog.middleware.AuditlogMiddleware",
|
||||
# Used for waffle feature flags
|
||||
"waffle.middleware.WaffleMiddleware",
|
||||
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||
|
@ -326,7 +327,7 @@ SERVER_EMAIL = "root@get.gov"
|
|||
# endregion
|
||||
|
||||
# region: Waffle feature flags-----------------------------------------------------------###
|
||||
# If Waffle encounters a reference to a flag that is not in the database, should Waffle create the flag?
|
||||
# If Waffle encounters a reference to a flag that is not in the database, create the flag automagically.
|
||||
WAFFLE_CREATE_MISSING_FLAGS = True
|
||||
|
||||
# The model that will be used to keep track of flags. Extends AbstractUserFlag.
|
||||
|
|
|
@ -180,6 +180,11 @@ urlpatterns = [
|
|||
views.DomainAddUserView.as_view(),
|
||||
name="domain-users-add",
|
||||
),
|
||||
path(
|
||||
"finish-profile-setup",
|
||||
views.FinishProfileSetupView.as_view(),
|
||||
name="finish-user-profile-setup",
|
||||
),
|
||||
path(
|
||||
"user-profile",
|
||||
views.UserProfileView.as_view(),
|
||||
|
|
|
@ -60,4 +60,35 @@ class UserProfileForm(forms.ModelForm):
|
|||
}
|
||||
self.fields["phone"].error_messages["required"] = "Enter your phone number."
|
||||
|
||||
if self.instance and self.instance.phone:
|
||||
self.fields["phone"].initial = self.instance.phone.as_national
|
||||
|
||||
DomainHelper.disable_field(self.fields["email"], disable_required=True)
|
||||
|
||||
|
||||
class FinishSetupProfileForm(UserProfileForm):
|
||||
"""Form for updating user profile."""
|
||||
|
||||
full_name = forms.CharField(required=True, label="Full name")
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
# Remove the full name property
|
||||
if "full_name" in cleaned_data:
|
||||
# Delete the full name element as its purely decorative.
|
||||
# We include it as a normal Charfield for all the advantages
|
||||
# and utility that it brings, but we're playing pretend.
|
||||
del cleaned_data["full_name"]
|
||||
return cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Override the inerited __init__ method to update the fields."""
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Set custom form label for email
|
||||
self.fields["email"].label = "Organization email"
|
||||
self.fields["title"].label = "Title or role in your organization"
|
||||
|
||||
# Define the "full_name" value
|
||||
self.fields["full_name"].initial = self.instance.get_formatted_name()
|
||||
|
|
68
src/registrar/management/commands/clean_tables.py
Normal file
68
src/registrar/management/commands/clean_tables.py
Normal file
|
@ -0,0 +1,68 @@
|
|||
import logging
|
||||
from django.conf import settings
|
||||
from django.core.management import BaseCommand
|
||||
from django.apps import apps
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.management.commands.utility.terminal_helper import TerminalHelper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Clean tables in database to prepare for import."
|
||||
|
||||
def handle(self, **options):
|
||||
"""Delete all rows from a list of tables"""
|
||||
|
||||
if settings.IS_PRODUCTION:
|
||||
logger.error("clean_tables cannot be run in production")
|
||||
return
|
||||
|
||||
TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=True,
|
||||
info_to_inspect="""
|
||||
This script will delete all rows from the following tables:
|
||||
* Contact
|
||||
* Domain
|
||||
* DomainInformation
|
||||
* DomainRequest
|
||||
* DraftDomain
|
||||
* Host
|
||||
* HostIp
|
||||
* PublicContact
|
||||
* User
|
||||
* Website
|
||||
""",
|
||||
prompt_title="Do you wish to proceed with these changes?",
|
||||
)
|
||||
|
||||
table_names = [
|
||||
"DomainInformation",
|
||||
"DomainRequest",
|
||||
"PublicContact",
|
||||
"Domain",
|
||||
"User",
|
||||
"Contact",
|
||||
"Website",
|
||||
"DraftDomain",
|
||||
"HostIp",
|
||||
"Host",
|
||||
]
|
||||
|
||||
for table_name in table_names:
|
||||
self.clean_table(table_name)
|
||||
|
||||
def clean_table(self, table_name):
|
||||
"""Delete all rows in the given table"""
|
||||
try:
|
||||
# Get the model class dynamically
|
||||
model = apps.get_model("registrar", table_name)
|
||||
# Use a transaction to ensure database integrity
|
||||
with transaction.atomic():
|
||||
model.objects.all().delete()
|
||||
logger.info(f"Successfully cleaned table {table_name}")
|
||||
except LookupError:
|
||||
logger.error(f"Model for table {table_name} not found.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning table {table_name}: {e}")
|
64
src/registrar/management/commands/export_tables.py
Normal file
64
src/registrar/management/commands/export_tables.py
Normal file
|
@ -0,0 +1,64 @@
|
|||
import logging
|
||||
import os
|
||||
import pyzipper
|
||||
from django.core.management import BaseCommand
|
||||
import registrar.admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Exports tables in csv format to zip file in tmp directory."
|
||||
|
||||
def handle(self, **options):
|
||||
"""Generates CSV files for specified tables and creates a zip archive"""
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"HostIp",
|
||||
"Host",
|
||||
"PublicContact",
|
||||
]
|
||||
|
||||
# Ensure the tmp directory exists
|
||||
os.makedirs("tmp", exist_ok=True)
|
||||
|
||||
for table_name in table_names:
|
||||
self.export_table(table_name)
|
||||
|
||||
# Create a zip file containing all the CSV files
|
||||
zip_filename = "tmp/exported_tables.zip"
|
||||
with pyzipper.AESZipFile(zip_filename, "w", compression=pyzipper.ZIP_DEFLATED) as zipf:
|
||||
for table_name in table_names:
|
||||
csv_filename = f"tmp/{table_name}.csv"
|
||||
if os.path.exists(csv_filename):
|
||||
zipf.write(csv_filename, os.path.basename(csv_filename))
|
||||
logger.info(f"Added {csv_filename} to zip archive {zip_filename}")
|
||||
|
||||
# Remove the CSV files after adding them to the zip file
|
||||
for table_name in table_names:
|
||||
csv_filename = f"tmp/{table_name}.csv"
|
||||
if os.path.exists(csv_filename):
|
||||
os.remove(csv_filename)
|
||||
logger.info(f"Removed temporary file {csv_filename}")
|
||||
|
||||
def export_table(self, table_name):
|
||||
"""Export a given table to a csv file in the tmp directory"""
|
||||
resourcename = f"{table_name}Resource"
|
||||
try:
|
||||
resourceclass = getattr(registrar.admin, resourcename)
|
||||
dataset = resourceclass().export()
|
||||
filename = f"tmp/{table_name}.csv"
|
||||
with open(filename, "w") as outputfile:
|
||||
outputfile.write(dataset.csv)
|
||||
logger.info(f"Successfully exported {table_name} to {filename}")
|
||||
except AttributeError:
|
||||
logger.error(f"Resource class {resourcename} not found in registrar.admin")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to export {table_name}: {e}")
|
104
src/registrar/management/commands/import_tables.py
Normal file
104
src/registrar/management/commands/import_tables.py
Normal file
|
@ -0,0 +1,104 @@
|
|||
import logging
|
||||
import os
|
||||
import pyzipper
|
||||
import tablib
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.core.management import BaseCommand
|
||||
import registrar.admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Imports tables from a zip file, exported_tables.zip, containing CSV files in the tmp directory."
|
||||
|
||||
def handle(self, **options):
|
||||
"""Extracts CSV files from a zip archive and imports them into the respective tables"""
|
||||
|
||||
if settings.IS_PRODUCTION:
|
||||
logger.error("import_tables cannot be run in production")
|
||||
return
|
||||
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"Host",
|
||||
"HostIp",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"PublicContact",
|
||||
]
|
||||
|
||||
# Ensure the tmp directory exists
|
||||
os.makedirs("tmp", exist_ok=True)
|
||||
|
||||
# Unzip the file
|
||||
zip_filename = "tmp/exported_tables.zip"
|
||||
if not os.path.exists(zip_filename):
|
||||
logger.error(f"Zip file {zip_filename} does not exist.")
|
||||
return
|
||||
|
||||
with pyzipper.AESZipFile(zip_filename, "r") as zipf:
|
||||
zipf.extractall("tmp")
|
||||
logger.info(f"Extracted zip file {zip_filename} into tmp directory")
|
||||
|
||||
# Import each CSV file
|
||||
for table_name in table_names:
|
||||
self.import_table(table_name)
|
||||
|
||||
def import_table(self, table_name):
|
||||
"""Import data from a CSV file into the given table"""
|
||||
|
||||
resourcename = f"{table_name}Resource"
|
||||
csv_filename = f"tmp/{table_name}.csv"
|
||||
try:
|
||||
if not os.path.exists(csv_filename):
|
||||
logger.error(f"CSV file {csv_filename} not found.")
|
||||
return
|
||||
|
||||
# if table_name is Contact, clean the table first
|
||||
# User table is loaded before Contact, and signals create
|
||||
# rows in Contact table which break the import, so need
|
||||
# to be cleaned again before running import on Contact table
|
||||
if table_name == "Contact":
|
||||
self.clean_table(table_name)
|
||||
|
||||
resourceclass = getattr(registrar.admin, resourcename)
|
||||
resource_instance = resourceclass()
|
||||
with open(csv_filename, "r") as csvfile:
|
||||
dataset = tablib.Dataset().load(csvfile.read(), format="csv")
|
||||
result = resource_instance.import_data(dataset, dry_run=False, skip_epp_save=True)
|
||||
|
||||
if result.has_errors():
|
||||
logger.error(f"Errors occurred while importing {csv_filename}: {result.row_errors()}")
|
||||
else:
|
||||
logger.info(f"Successfully imported {csv_filename} into {table_name}")
|
||||
|
||||
except AttributeError:
|
||||
logger.error(f"Resource class {resourcename} not found in registrar.admin")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to import {csv_filename}: {e}")
|
||||
finally:
|
||||
if os.path.exists(csv_filename):
|
||||
os.remove(csv_filename)
|
||||
logger.info(f"Removed temporary file {csv_filename}")
|
||||
|
||||
def clean_table(self, table_name):
|
||||
"""Delete all rows in the given table"""
|
||||
try:
|
||||
# Get the model class dynamically
|
||||
model = apps.get_model("registrar", table_name)
|
||||
# Use a transaction to ensure database integrity
|
||||
with transaction.atomic():
|
||||
model.objects.all().delete()
|
||||
logger.info(f"Successfully cleaned table {table_name}")
|
||||
except LookupError:
|
||||
logger.error(f"Model for table {table_name} not found.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning table {table_name}: {e}")
|
|
@ -938,3 +938,131 @@ class DomainRequest(TimeStampedModel):
|
|||
for field in opts.many_to_many:
|
||||
data[field.name] = field.value_from_object(self)
|
||||
return data
|
||||
|
||||
def _is_federal_complete(self):
|
||||
# Federal -> "Federal government branch" page can't be empty + Federal Agency selection can't be None
|
||||
return not (self.federal_type is None or self.federal_agency is None)
|
||||
|
||||
def _is_interstate_complete(self):
|
||||
# Interstate -> "About your organization" page can't be empty
|
||||
return self.about_your_organization is not None
|
||||
|
||||
def _is_state_or_territory_complete(self):
|
||||
# State -> ""Election office" page can't be empty
|
||||
return self.is_election_board is not None
|
||||
|
||||
def _is_tribal_complete(self):
|
||||
# Tribal -> "Tribal name" and "Election office" page can't be empty
|
||||
return self.tribe_name is not None and self.is_election_board is not None
|
||||
|
||||
def _is_county_complete(self):
|
||||
# County -> "Election office" page can't be empty
|
||||
return self.is_election_board is not None
|
||||
|
||||
def _is_city_complete(self):
|
||||
# City -> "Election office" page can't be empty
|
||||
return self.is_election_board is not None
|
||||
|
||||
def _is_special_district_complete(self):
|
||||
# Special District -> "Election office" and "About your organization" page can't be empty
|
||||
return self.is_election_board is not None and self.about_your_organization is not None
|
||||
|
||||
def _is_organization_name_and_address_complete(self):
|
||||
return not (
|
||||
self.organization_name is None
|
||||
and self.address_line1 is None
|
||||
and self.city is None
|
||||
and self.state_territory is None
|
||||
and self.zipcode is None
|
||||
)
|
||||
|
||||
def _is_authorizing_official_complete(self):
|
||||
return self.authorizing_official is not None
|
||||
|
||||
def _is_requested_domain_complete(self):
|
||||
return self.requested_domain is not None
|
||||
|
||||
def _is_purpose_complete(self):
|
||||
return self.purpose is not None
|
||||
|
||||
def _is_submitter_complete(self):
|
||||
return self.submitter is not None
|
||||
|
||||
def _has_other_contacts_and_filled(self):
|
||||
# Other Contacts Radio button is Yes and if all required fields are filled
|
||||
return (
|
||||
self.has_other_contacts()
|
||||
and self.other_contacts.filter(
|
||||
first_name__isnull=False,
|
||||
last_name__isnull=False,
|
||||
title__isnull=False,
|
||||
email__isnull=False,
|
||||
phone__isnull=False,
|
||||
).exists()
|
||||
)
|
||||
|
||||
def _has_no_other_contacts_gives_rationale(self):
|
||||
# Other Contacts Radio button is No and a rationale is provided
|
||||
return self.has_other_contacts() is False and self.no_other_contacts_rationale is not None
|
||||
|
||||
def _is_other_contacts_complete(self):
|
||||
if self._has_other_contacts_and_filled() or self._has_no_other_contacts_gives_rationale():
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cisa_rep_and_email_check(self):
|
||||
# Has a CISA rep + email is NOT empty or NOT an empty string OR doesn't have CISA rep
|
||||
return (
|
||||
self.has_cisa_representative is True
|
||||
and self.cisa_representative_email is not None
|
||||
and self.cisa_representative_email != ""
|
||||
) or self.has_cisa_representative is False
|
||||
|
||||
def _anything_else_radio_button_and_text_field_check(self):
|
||||
# Anything else boolean is True + filled text field and it's not an empty string OR the boolean is No
|
||||
return (
|
||||
self.has_anything_else_text is True and self.anything_else is not None and self.anything_else != ""
|
||||
) or self.has_anything_else_text is False
|
||||
|
||||
def _is_additional_details_complete(self):
|
||||
return self._cisa_rep_and_email_check() and self._anything_else_radio_button_and_text_field_check()
|
||||
|
||||
def _is_policy_acknowledgement_complete(self):
|
||||
return self.is_policy_acknowledged is not None
|
||||
|
||||
def _is_general_form_complete(self):
|
||||
return (
|
||||
self._is_organization_name_and_address_complete()
|
||||
and self._is_authorizing_official_complete()
|
||||
and self._is_requested_domain_complete()
|
||||
and self._is_purpose_complete()
|
||||
and self._is_submitter_complete()
|
||||
and self._is_other_contacts_complete()
|
||||
and self._is_additional_details_complete()
|
||||
and self._is_policy_acknowledgement_complete()
|
||||
)
|
||||
|
||||
def _form_complete(self):
|
||||
match self.generic_org_type:
|
||||
case DomainRequest.OrganizationChoices.FEDERAL:
|
||||
is_complete = self._is_federal_complete()
|
||||
case DomainRequest.OrganizationChoices.INTERSTATE:
|
||||
is_complete = self._is_interstate_complete()
|
||||
case DomainRequest.OrganizationChoices.STATE_OR_TERRITORY:
|
||||
is_complete = self._is_state_or_territory_complete()
|
||||
case DomainRequest.OrganizationChoices.TRIBAL:
|
||||
is_complete = self._is_tribal_complete()
|
||||
case DomainRequest.OrganizationChoices.COUNTY:
|
||||
is_complete = self._is_county_complete()
|
||||
case DomainRequest.OrganizationChoices.CITY:
|
||||
is_complete = self._is_city_complete()
|
||||
case DomainRequest.OrganizationChoices.SPECIAL_DISTRICT:
|
||||
is_complete = self._is_special_district_complete()
|
||||
case _:
|
||||
# NOTE: Shouldn't happen, this is only if somehow they didn't choose an org type
|
||||
is_complete = False
|
||||
|
||||
if not is_complete or not self._is_general_form_complete():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
|
@ -98,6 +98,24 @@ class User(AbstractUser):
|
|||
help_text="The means through which this user was verified",
|
||||
)
|
||||
|
||||
@property
|
||||
def finished_setup(self):
|
||||
"""
|
||||
Tracks if the user finished their profile setup or not. This is so
|
||||
we can globally enforce that new users provide additional account information before proceeding.
|
||||
"""
|
||||
|
||||
# Change this to self once the user and contact objects are merged.
|
||||
# For now, since they are linked, lets test on the underlying contact object.
|
||||
user_info = self.contact # noqa
|
||||
user_values = [
|
||||
user_info.first_name,
|
||||
user_info.last_name,
|
||||
user_info.title,
|
||||
user_info.phone,
|
||||
]
|
||||
return None not in user_values
|
||||
|
||||
def __str__(self):
|
||||
# this info is pulled from Login.gov
|
||||
if self.first_name or self.last_name:
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
"""Middleware to add Cache-control: no-cache to every response.
|
||||
|
||||
Used to force Cloudfront caching to leave us alone while we develop
|
||||
better caching responses.
|
||||
"""
|
||||
|
||||
|
||||
class NoCacheMiddleware:
|
||||
"""Middleware to add a single header to every response."""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response["Cache-Control"] = "no-cache"
|
||||
return response
|
100
src/registrar/registrar_middleware.py
Normal file
100
src/registrar/registrar_middleware.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
"""
|
||||
Contains middleware used in settings.py
|
||||
"""
|
||||
|
||||
from urllib.parse import parse_qs
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||
|
||||
|
||||
class NoCacheMiddleware:
|
||||
"""
|
||||
Middleware to add Cache-control: no-cache to every response.
|
||||
|
||||
Used to force Cloudfront caching to leave us alone while we develop
|
||||
better caching responses.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
response["Cache-Control"] = "no-cache"
|
||||
return response
|
||||
|
||||
|
||||
class CheckUserProfileMiddleware:
|
||||
"""
|
||||
Checks if the current user has finished_setup = False.
|
||||
If they do, redirect them to the setup page regardless of where they are in
|
||||
the application.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
self.setup_page = reverse("finish-user-profile-setup")
|
||||
self.logout_page = reverse("logout")
|
||||
self.excluded_pages = [
|
||||
self.setup_page,
|
||||
self.logout_page,
|
||||
]
|
||||
|
||||
def __call__(self, request):
|
||||
response = self.get_response(request)
|
||||
return response
|
||||
|
||||
def process_view(self, request, view_func, view_args, view_kwargs):
|
||||
"""Runs pre-processing logic for each view. Checks for the
|
||||
finished_setup flag on the current user. If they haven't done so,
|
||||
then we redirect them to the finish setup page."""
|
||||
# Check that the user is "opted-in" to the profile feature flag
|
||||
has_profile_feature_flag = flag_is_active(request, "profile_feature")
|
||||
|
||||
# If they aren't, skip this check entirely
|
||||
if not has_profile_feature_flag:
|
||||
return None
|
||||
|
||||
if request.user.is_authenticated:
|
||||
if hasattr(request.user, "finished_setup") and not request.user.finished_setup:
|
||||
return self._handle_setup_not_finished(request)
|
||||
|
||||
# Continue processing the view
|
||||
return None
|
||||
|
||||
def _handle_setup_not_finished(self, request):
|
||||
"""Redirects the given user to the finish setup page.
|
||||
|
||||
We set the "redirect" query param equal to where the user wants to go.
|
||||
|
||||
If the user wants to go to '/request/', then we set that
|
||||
information in the query param.
|
||||
|
||||
Otherwise, we assume they want to go to the home page.
|
||||
"""
|
||||
|
||||
# In some cases, we don't want to redirect to home. This handles that.
|
||||
# Can easily be generalized if need be, but for now lets keep this easy to read.
|
||||
custom_redirect = "domain-request:" if request.path == "/request/" else None
|
||||
|
||||
# Don't redirect on excluded pages (such as the setup page itself)
|
||||
if not any(request.path.startswith(page) for page in self.excluded_pages):
|
||||
|
||||
# Preserve the original query parameters, and coerce them into a dict
|
||||
query_params = parse_qs(request.META["QUERY_STRING"])
|
||||
|
||||
# Set the redirect value to our redirect location
|
||||
if custom_redirect is not None:
|
||||
query_params["redirect"] = custom_redirect
|
||||
|
||||
# Add our new query param, while preserving old ones
|
||||
new_setup_page = replace_url_queryparams(self.setup_page, query_params) if query_params else self.setup_page
|
||||
|
||||
return HttpResponseRedirect(new_setup_page)
|
||||
else:
|
||||
# Process the view as normal
|
||||
return None
|
|
@ -139,11 +139,7 @@
|
|||
<div class="usa-nav-container">
|
||||
<div class="usa-navbar">
|
||||
{% block logo %}
|
||||
<div class="usa-logo display-inline-block" id="extended-logo">
|
||||
<strong class="usa-logo__text" >
|
||||
<a href="{% url 'home' %}">.gov Registrar </a>
|
||||
</strong>
|
||||
</div>
|
||||
{% include "includes/gov_extended_logo.html" with logo_clickable=True %}
|
||||
{% endblock %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
</div>
|
||||
|
@ -160,7 +156,8 @@
|
|||
{% if has_profile_feature_flag %}
|
||||
<li class="usa-nav__primary-item">
|
||||
{% url 'user-profile' as user_profile_url %}
|
||||
<a class="usa-nav-link {% if request.path == user_profile_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
|
||||
{% url 'finish-user-profile-setup' as finish_setup_url %}
|
||||
<a class="usa-nav-link {% if request.path == user_profile_url or request.path == finish_setup_url %}usa-current{% endif %}" href="{{ user_profile_url }}">
|
||||
<span class="text-primary">Your profile</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -206,7 +203,9 @@
|
|||
</div>
|
||||
{% endblock wrapper%}
|
||||
|
||||
{% include "includes/footer.html" %}
|
||||
{% block footer %}
|
||||
{% include "includes/footer.html" with show_manage_your_domains=True %}
|
||||
{% endblock footer %}
|
||||
</div> <!-- /#wrapper -->
|
||||
|
||||
{% block init_js %}{% endblock %}{# useful for vars and other initializations #}
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
aria-describedby="Are you sure you want to submit a domain request?"
|
||||
data-force-action
|
||||
>
|
||||
{% include 'includes/modal.html' with modal_heading=modal_heading|safe modal_description="Once you submit this request, you won’t be able to edit it until we review it. You’ll only be able to withdraw your request." modal_button=modal_button|safe %}
|
||||
{% include 'includes/modal.html' with is_domain_request_form=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
|
||||
</div>
|
||||
|
||||
{% block after_form_content %}{% endblock %}
|
||||
|
|
|
@ -25,11 +25,11 @@
|
|||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.generic_org_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -37,7 +37,7 @@
|
|||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
|
@ -47,7 +47,7 @@
|
|||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -66,7 +66,7 @@
|
|||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='Incomplete' %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -74,7 +74,7 @@
|
|||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -86,7 +86,7 @@
|
|||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -107,7 +107,7 @@
|
|||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe%}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
|
||||
|
@ -123,7 +123,7 @@
|
|||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -135,7 +135,7 @@
|
|||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -148,7 +148,7 @@
|
|||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
@ -157,7 +157,7 @@
|
|||
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"Incomplete" %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title sub_header_text='CISA regional representative' value=domain_request.cisa_representative_email heading_level=heading_level editable=True edit_link=domain_request_url custom_text_for_value_none='No' %}
|
||||
{% endwith %}
|
||||
|
||||
|
|
20
src/registrar/templates/finish_profile_setup.html
Normal file
20
src/registrar/templates/finish_profile_setup.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
{% extends "profile.html" %}
|
||||
|
||||
{% load static form_helpers url_helpers field_helpers %}
|
||||
{% block title %} Finish setting up your profile | {% endblock %}
|
||||
|
||||
{# Disable the redirect #}
|
||||
{% block logo %}
|
||||
{% include "includes/gov_extended_logo.html" with logo_clickable=confirm_changes %}
|
||||
{% endblock %}
|
||||
|
||||
{# Add the new form #}
|
||||
{% block content_bottom %}
|
||||
{% include "includes/finish_profile_form.html" with form=form %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock content_bottom %}
|
||||
|
||||
{% block footer %}
|
||||
{% include "includes/footer.html" with show_manage_your_domains=confirm_changes %}
|
||||
{% endblock footer %}
|
89
src/registrar/templates/includes/finish_profile_form.html
Normal file
89
src/registrar/templates/includes/finish_profile_form.html
Normal file
|
@ -0,0 +1,89 @@
|
|||
{% extends 'includes/profile_form.html' %}
|
||||
|
||||
{% load static url_helpers %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block profile_header %}
|
||||
<h1>Finish setting up your profile</h1>
|
||||
{% endblock profile_header %}
|
||||
|
||||
{% block profile_blurb %}
|
||||
<p>
|
||||
We <a class="usa-link usa-link--always-blue" href="{% public_site_url 'domains/requirements/#keep-your-contact-information-updated' %}" target="_blank">require</a>
|
||||
that you maintain accurate contact information.
|
||||
The details you provide will only be used to support the administration of .gov and won’t be made public.
|
||||
</p>
|
||||
|
||||
<h2>What contact information should we use to reach you?</h2>
|
||||
<p>
|
||||
Review the details below and update any required information.
|
||||
Note that editing this information won’t affect your Login.gov account information.
|
||||
</p>
|
||||
|
||||
{# We use a var called 'remove_margin_top' rather than 'add_margin_top' because this is more useful as a default #}
|
||||
{% include "includes/required_fields.html" with remove_margin_top=True %}
|
||||
|
||||
{% endblock profile_blurb %}
|
||||
|
||||
{% block profile_form %}
|
||||
<form id="finish-profile-setup-form" class="usa-form usa-form--largest" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
<fieldset class="usa-fieldset">
|
||||
<legend class="usa-sr-only">
|
||||
Your contact information
|
||||
</legend>
|
||||
|
||||
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %}
|
||||
{% input_with_errors form.full_name %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="profile-name-group" class="display-none" role="group">
|
||||
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-2" %}
|
||||
{% input_with_errors form.first_name %}
|
||||
{% endwith %}
|
||||
|
||||
{% with group_classes="usa-form-editable padding-top-2" %}
|
||||
{% input_with_errors form.middle_name %}
|
||||
{% endwith %}
|
||||
|
||||
{% with group_classes="usa-form-editable padding-top-2" %}
|
||||
{% input_with_errors form.last_name %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||
{% with show_readonly=True add_class="display-none" group_classes="usa-form-editable usa-form-editable padding-top-2 bold-usa-label" %}
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||
{% with link_text="Get help with your Login.gov account" target_blank=True do_not_show_max_chars=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %}
|
||||
{% input_with_errors form.title %}
|
||||
{% endwith %}
|
||||
|
||||
{% with show_edit_button=True show_readonly=True group_classes="usa-form-editable padding-top-2" %}
|
||||
{% with add_class="usa-input--medium" %}
|
||||
{% input_with_errors form.phone %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
</fieldset>
|
||||
<div>
|
||||
|
||||
<button type="submit" name="contact_setup_save_button" class="usa-button ">
|
||||
Save
|
||||
</button>
|
||||
{% if confirm_changes and going_to_specific_page %}
|
||||
<button type="submit" name="contact_setup_submit_button" class="usa-button usa-button--outline">
|
||||
{{redirect_button_text }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock profile_form %}
|
|
@ -26,10 +26,12 @@
|
|||
>
|
||||
<address class="usa-footer__address">
|
||||
<div class="usa-footer__contact-info grid-row grid-gap-md">
|
||||
{% if show_manage_your_domains %}
|
||||
<div class="grid-col-auto">
|
||||
<a class="usa-link" rel="noopener noreferrer" href="{% url 'home' %}">Manage your domains</a>
|
||||
</div>
|
||||
<span class=""> | </span>
|
||||
{% endif %}
|
||||
<div class="grid-col-auto">
|
||||
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'help/' %}">Help </a>
|
||||
</div>
|
||||
|
|
17
src/registrar/templates/includes/gov_extended_logo.html
Normal file
17
src/registrar/templates/includes/gov_extended_logo.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
{# Q: For reviewers -- What should this file be called? #}
|
||||
|
||||
<div class="usa-logo display-inline-block" id="extended-logo">
|
||||
<strong class="usa-logo__text" >
|
||||
{% if logo_clickable %}
|
||||
<a href="{% url 'home' %}">.gov Registrar </a>
|
||||
{% else %}
|
||||
<button
|
||||
class="usa-button--unstyled disabled-button usa-tooltip"
|
||||
data-position="bottom"
|
||||
title="Before you can manage your domains, we need you to add contact information."
|
||||
data-tooltip="true"
|
||||
role="button"
|
||||
>.gov Registrar</button>
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
|
@ -2,7 +2,7 @@
|
|||
Template include for form fields with classes and their corresponding
|
||||
error messages, if necessary.
|
||||
{% endcomment %}
|
||||
|
||||
{% load static field_helpers url_helpers %}
|
||||
{% load custom_filters %}
|
||||
|
||||
{% load widget_tweaks %}
|
||||
|
@ -27,7 +27,11 @@ error messages, if necessary.
|
|||
{% endif %}
|
||||
|
||||
{% if not field.widget_type == "checkbox" %}
|
||||
{% include "django/forms/label.html" %}
|
||||
{% if show_edit_button %}
|
||||
{% include "includes/label_with_edit_button.html" with bold_label=True %}
|
||||
{% else %}
|
||||
{% include "django/forms/label.html" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if sublabel_text %}
|
||||
|
@ -58,6 +62,11 @@ error messages, if necessary.
|
|||
{% if append_gov %}
|
||||
<div class="display-flex flex-align-center">
|
||||
{% endif %}
|
||||
|
||||
{% if show_readonly %}
|
||||
{% include "includes/readonly_input.html" %}
|
||||
{% endif %}
|
||||
|
||||
{# this is the input field, itself #}
|
||||
{% include widget.template_name %}
|
||||
|
||||
|
|
14
src/registrar/templates/includes/label_with_edit_button.html
Normal file
14
src/registrar/templates/includes/label_with_edit_button.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
{% load static field_helpers url_helpers %}
|
||||
<div class="grid-row {% if bold_label %}bold-usa-label{% endif %}">
|
||||
<div class="grid-col">
|
||||
{% include "django/forms/label.html" %}
|
||||
</div>
|
||||
<div class="grid-col-2 text-right">
|
||||
<button type="button" id="{{field.name}}__edit-button" class="usa-button usa-button--unstyled readonly-edit-button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#edit"></use>
|
||||
</svg>Edit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
|
@ -18,12 +18,18 @@
|
|||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
{% if not_form %}
|
||||
<li class="usa-button-group__item">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ modal_button }}
|
||||
</form>
|
||||
</li>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="usa-button-group__item">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ modal_button }}
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="usa-button-group__item">
|
||||
{% comment %} The cancel button the DS form actually triggers a context change in the view,
|
||||
in addition to being a close modal hook {% endcomment %}
|
||||
|
@ -39,7 +45,7 @@
|
|||
Cancel
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
{% elif not is_domain_request_form or review_form_is_complete %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
|
|
50
src/registrar/templates/includes/profile_form.html
Normal file
50
src/registrar/templates/includes/profile_form.html
Normal file
|
@ -0,0 +1,50 @@
|
|||
{% load static url_helpers %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block profile_header %}
|
||||
<h1>Your profile</h1>
|
||||
{% endblock profile_header %}
|
||||
|
||||
{% block profile_blurb %}
|
||||
<p>We <a class="usa-link usa-link--always-blue" href="{% public_site_url 'domains/requirements/#keep-your-contact-information-updated' %}" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.</p>
|
||||
|
||||
<h2>Contact information</h2>
|
||||
<p>Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.</p>
|
||||
{% include "includes/required_fields.html" %}
|
||||
{% endblock profile_blurb %}
|
||||
|
||||
|
||||
{% block profile_form %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
||||
{% input_with_errors form.middle_name %}
|
||||
|
||||
{% input_with_errors form.last_name %}
|
||||
|
||||
{% input_with_errors form.title %}
|
||||
|
||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||
{% with link_text="Get help with your Login.gov account" %}
|
||||
{% with target_blank=True %}
|
||||
{% with do_not_show_max_chars=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% with add_class="usa-input--medium" %}
|
||||
{% input_with_errors form.phone %}
|
||||
{% endwith %}
|
||||
|
||||
<button type="submit" class="usa-button">Save</button>
|
||||
</form>
|
||||
{% endblock profile_form %}
|
18
src/registrar/templates/includes/readonly_input.html
Normal file
18
src/registrar/templates/includes/readonly_input.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
{% load static field_helpers url_helpers custom_filters %}
|
||||
|
||||
<div id="{{field.name}}__edit-button-readonly" class="margin-top-2 margin-bottom-1 input-with-edit-button {% if not field.value and field.field.required %}input-with-edit-button__error{% endif %}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
{% if field.value or not field.field.required %}
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
|
||||
{%elif not field.value %}
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
||||
{%endif %}
|
||||
</svg>
|
||||
<div class="display-inline padding-left-05 margin-left-3 readonly-field {% if not field.field.required %}text-base{% endif %}">
|
||||
{% if field.name != "phone" %}
|
||||
{{ field.value }}
|
||||
{% else %}
|
||||
{{ field.value|format_phone }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -1,3 +1,3 @@
|
|||
<p class="margin-top-3">
|
||||
<p class="{% if not remove_margin_top %}margin-top-3 {% endif %}">
|
||||
<em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||
</p>
|
||||
|
|
|
@ -1,16 +1,13 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
|
||||
{% block title %}
|
||||
Edit your User Profile |
|
||||
{% endblock title %}
|
||||
{% load static url_helpers %}
|
||||
{% load field_helpers %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
{# messages block #}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
||||
|
@ -21,61 +18,28 @@ Edit your User Profile |
|
|||
{% endfor %}
|
||||
{% endif %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
|
||||
<a href="{% if not return_to_request %}{% url 'home' %}{% else %}{% url 'domain-request:' %}{% endif %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
{% if not return_to_request %}
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Back to manage your domains
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Go back to your domain request
|
||||
</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<h1>Your profile</h1>
|
||||
<p>We <a href="{% public_site_url 'domains/requirements/#what-.gov-domain-registrants-must-do' %}" target="_blank">require</a> that you maintain accurate contact information. The details you provide will only be used to support the administration of .gov and won’t be made public.</p>
|
||||
|
||||
<h2>Contact information</h2>
|
||||
<p>Review the details below and update any required information. Note that editing this information won’t affect your Login.gov account information.</p>
|
||||
{% if show_back_button %}
|
||||
<a href="{% if not return_to_request %}{% url 'home' %}{% else %}{% url 'domain-request:' %}{% endif %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
{% if not return_to_request %}
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
{{ profile_back_button_text }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Go back to your domain request
|
||||
</p>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
||||
{% input_with_errors form.middle_name %}
|
||||
|
||||
{% input_with_errors form.last_name %}
|
||||
|
||||
{% input_with_errors form.title %}
|
||||
|
||||
{% public_site_url "help/account-management/#get-help-with-login.gov" as login_help_url %}
|
||||
|
||||
{% with link_href=login_help_url %}
|
||||
{% with sublabel_text="We recommend using your work email for your .gov account. If the wrong email is displayed below, you’ll need to update your Login.gov account and log back in. Get help with your Login.gov account." %}
|
||||
{% with link_text="Get help with your Login.gov account" %}
|
||||
{% with target_blank=True %}
|
||||
{% with do_not_show_max_chars=True %}
|
||||
{% input_with_errors form.email %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
||||
{% with add_class="usa-input--medium" %}
|
||||
{% input_with_errors form.phone %}
|
||||
{% endwith %}
|
||||
|
||||
<button type="submit" class="usa-button">Save</button>
|
||||
</form>
|
||||
</main>
|
||||
{% endblock content %}
|
||||
|
||||
{% block content_bottom %}
|
||||
{% include "includes/profile_form.html" with form=form %}
|
||||
</div>
|
||||
</main>
|
||||
{% endblock content_bottom %}
|
||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
|||
from django import template
|
||||
import re
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from phonenumber_field.phonenumber import PhoneNumber
|
||||
|
||||
register = template.Library()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -133,3 +134,14 @@ def get_region(state):
|
|||
return regions.get(state.upper(), "N/A")
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@register.filter
|
||||
def format_phone(value):
|
||||
"""Converts a phonenumber to a national format"""
|
||||
if value:
|
||||
phone_number = value
|
||||
if isinstance(value, str):
|
||||
phone_number = PhoneNumber.from_string(value)
|
||||
return phone_number.as_national
|
||||
return value
|
||||
|
|
|
@ -26,6 +26,7 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
add_group_class: append to input element's surrounding tag's `class` attribute
|
||||
attr_* - adds or replaces any single html attribute for the input
|
||||
add_error_attr_* - like `attr_*` but only if field.errors is not empty
|
||||
show_edit_button: shows a simple edit button, and adds display-none to the input field.
|
||||
|
||||
Example usage:
|
||||
```
|
||||
|
@ -91,6 +92,12 @@ def input_with_errors(context, field=None): # noqa: C901
|
|||
elif key == "add_group_class":
|
||||
group_classes.append(value)
|
||||
|
||||
elif key == "show_edit_button":
|
||||
# Hide the primary input field.
|
||||
# Used such that we can toggle it with JS
|
||||
if "display-none" not in classes:
|
||||
classes.append("display-none")
|
||||
|
||||
attrs["id"] = field.auto_id
|
||||
|
||||
# do some work for various edge cases
|
||||
|
|
|
@ -3,10 +3,12 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
from django.test import TestCase
|
||||
from waffle.testutils import override_flag
|
||||
from registrar.utility import email
|
||||
from registrar.utility.email import send_templated_email
|
||||
from .common import completed_domain_request, less_console_noise
|
||||
|
||||
from datetime import datetime
|
||||
from registrar.utility import email
|
||||
import boto3_mocking # type: ignore
|
||||
|
||||
|
||||
|
@ -15,6 +17,24 @@ class TestEmails(TestCase):
|
|||
self.mock_client_class = MagicMock()
|
||||
self.mock_client = self.mock_client_class.return_value
|
||||
|
||||
@boto3_mocking.patching
|
||||
@override_flag("disable_email_sending", active=True)
|
||||
def test_disable_email_flag(self):
|
||||
"""Test if the 'disable_email_sending' stops emails from being sent"""
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
expected_message = "Email sending is disabled due to"
|
||||
with self.assertRaisesRegex(email.EmailSendingError, expected_message):
|
||||
send_templated_email(
|
||||
"test content",
|
||||
"test subject",
|
||||
"doesnotexist@igorville.com",
|
||||
context={"domain_request": self},
|
||||
bcc_address=None,
|
||||
)
|
||||
|
||||
# Assert that an email wasn't sent
|
||||
self.assertFalse(self.mock_client.send_email.called)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_submission_confirmation(self):
|
||||
"""Submission confirmation email works."""
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
import copy
|
||||
from datetime import date, datetime, time
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils import timezone
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from django.utils.module_loading import import_string
|
||||
import logging
|
||||
import pyzipper
|
||||
from registrar.management.commands.clean_tables import Command as CleanTablesCommand
|
||||
from registrar.models import (
|
||||
User,
|
||||
Domain,
|
||||
|
@ -18,14 +21,15 @@ from registrar.models import (
|
|||
PublicContact,
|
||||
FederalAgency,
|
||||
)
|
||||
|
||||
from django.core.management import call_command
|
||||
from unittest.mock import patch, call
|
||||
import tablib
|
||||
from unittest.mock import patch, call, MagicMock, mock_open
|
||||
from epplibwrapper import commands, common
|
||||
|
||||
from .common import MockEppLib, less_console_noise, completed_domain_request
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TestPopulateVerificationType(MockEppLib):
|
||||
"""Tests for the populate_organization_type script"""
|
||||
|
@ -767,3 +771,340 @@ class TestDiscloseEmails(MockEppLib):
|
|||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestCleanTables(TestCase):
|
||||
"""Test the clean_tables script"""
|
||||
|
||||
def setUp(self):
|
||||
self.command = CleanTablesCommand()
|
||||
self.logger_patcher = patch("registrar.management.commands.clean_tables.logger")
|
||||
self.logger_mock = self.logger_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.logger_patcher.stop()
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
def test_command_logs_error_in_production(self):
|
||||
"""Test that the handle method does not process in production"""
|
||||
with less_console_noise():
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
self.logger_mock.error.assert_called_with("clean_tables cannot be run in production")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
def test_command_cleans_tables(self):
|
||||
"""test that the handle method functions properly to clean tables"""
|
||||
with less_console_noise():
|
||||
with patch("django.apps.apps.get_model") as get_model_mock:
|
||||
model_mock = MagicMock()
|
||||
get_model_mock.return_value = model_mock
|
||||
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
|
||||
table_names = [
|
||||
"DomainInformation",
|
||||
"DomainRequest",
|
||||
"PublicContact",
|
||||
"Domain",
|
||||
"User",
|
||||
"Contact",
|
||||
"Website",
|
||||
"DraftDomain",
|
||||
"HostIp",
|
||||
"Host",
|
||||
]
|
||||
|
||||
# Check that each model's delete method was called
|
||||
for table_name in table_names:
|
||||
get_model_mock.assert_any_call("registrar", table_name)
|
||||
model_mock.objects.all().delete.assert_called()
|
||||
|
||||
self.logger_mock.info.assert_any_call("Successfully cleaned table DomainInformation")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
def test_command_handles_nonexistent_model(self):
|
||||
"""Test that exceptions for non existent models are handled properly within the handle method"""
|
||||
with less_console_noise():
|
||||
with patch("django.apps.apps.get_model", side_effect=LookupError):
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
# Assert that the error message was logged for any of the table names
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainInformation not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DomainRequest not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table PublicContact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Domain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table User not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Contact not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Website not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table DraftDomain not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table HostIp not found.")
|
||||
self.logger_mock.error.assert_any_call("Model for table Host not found.")
|
||||
|
||||
@override_settings(IS_PRODUCTION=False)
|
||||
def test_command_logs_other_exceptions(self):
|
||||
"""Test that generic exceptions are handled properly in the handle method"""
|
||||
with less_console_noise():
|
||||
with patch("django.apps.apps.get_model") as get_model_mock:
|
||||
model_mock = MagicMock()
|
||||
get_model_mock.return_value = model_mock
|
||||
model_mock.objects.all().delete.side_effect = Exception("Some error")
|
||||
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit", # noqa
|
||||
return_value=True,
|
||||
):
|
||||
call_command("clean_tables")
|
||||
|
||||
self.logger_mock.error.assert_any_call("Error cleaning table DomainInformation: Some error")
|
||||
|
||||
|
||||
class TestExportTables(MockEppLib):
|
||||
"""Test the export_tables script"""
|
||||
|
||||
def setUp(self):
|
||||
self.logger_patcher = patch("registrar.management.commands.export_tables.logger")
|
||||
self.logger_mock = self.logger_patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.logger_patcher.stop()
|
||||
|
||||
@patch("registrar.management.commands.export_tables.os.makedirs")
|
||||
@patch("registrar.management.commands.export_tables.os.path.exists")
|
||||
@patch("registrar.management.commands.export_tables.os.remove")
|
||||
@patch("registrar.management.commands.export_tables.pyzipper.AESZipFile")
|
||||
@patch("registrar.management.commands.export_tables.getattr")
|
||||
@patch("builtins.open", new_callable=mock_open, read_data=b"mock_csv_data")
|
||||
@patch("django.utils.translation.trans_real._translations", {})
|
||||
@patch("django.utils.translation.trans_real.translation")
|
||||
def test_handle(
|
||||
self, mock_translation, mock_file, mock_getattr, mock_zipfile, mock_remove, mock_path_exists, mock_makedirs
|
||||
):
|
||||
"""test that the handle method properly exports tables"""
|
||||
with less_console_noise():
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
|
||||
# Mock os.path.exists to always return True
|
||||
mock_path_exists.return_value = True
|
||||
|
||||
# Mock the resource class and its export method
|
||||
mock_resource_class = MagicMock()
|
||||
mock_dataset = MagicMock()
|
||||
mock_dataset.csv = b"mock_csv_data"
|
||||
mock_resource_class().export.return_value = mock_dataset
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
|
||||
# Mock translation function to return a dummy translation object
|
||||
mock_translation.return_value = MagicMock()
|
||||
|
||||
call_command("export_tables")
|
||||
|
||||
# Check that os.makedirs was called once to create the tmp directory
|
||||
mock_makedirs.assert_called_once_with("tmp", exist_ok=True)
|
||||
|
||||
# Check that the export_table function was called for each table
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"HostIp",
|
||||
"Host",
|
||||
"PublicContact",
|
||||
]
|
||||
|
||||
# Check that the CSV file was written
|
||||
for table_name in table_names:
|
||||
mock_file().write.assert_any_call(b"mock_csv_data")
|
||||
# Check that os.path.exists was called
|
||||
mock_path_exists.assert_any_call(f"tmp/{table_name}.csv")
|
||||
# Check that os.remove was called
|
||||
mock_remove.assert_any_call(f"tmp/{table_name}.csv")
|
||||
|
||||
# Check that the zipfile was created and files were added
|
||||
mock_zipfile.assert_called_once_with("tmp/exported_tables.zip", "w", compression=pyzipper.ZIP_DEFLATED)
|
||||
zipfile_instance = mock_zipfile.return_value.__enter__.return_value
|
||||
for table_name in table_names:
|
||||
zipfile_instance.write.assert_any_call(f"tmp/{table_name}.csv", f"{table_name}.csv")
|
||||
|
||||
# Verify logging for added files
|
||||
for table_name in table_names:
|
||||
self.logger_mock.info.assert_any_call(
|
||||
f"Added tmp/{table_name}.csv to zip archive tmp/exported_tables.zip"
|
||||
)
|
||||
|
||||
# Verify logging for removed files
|
||||
for table_name in table_names:
|
||||
self.logger_mock.info.assert_any_call(f"Removed temporary file tmp/{table_name}.csv")
|
||||
|
||||
@patch("registrar.management.commands.export_tables.getattr")
|
||||
def test_export_table_handles_missing_resource_class(self, mock_getattr):
|
||||
"""Test that missing resource classes are handled properly in the handle method"""
|
||||
with less_console_noise():
|
||||
mock_getattr.side_effect = AttributeError
|
||||
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("NonExistentTable")
|
||||
|
||||
self.logger_mock.error.assert_called_with(
|
||||
"Resource class NonExistentTableResource not found in registrar.admin"
|
||||
)
|
||||
|
||||
@patch("registrar.management.commands.export_tables.getattr")
|
||||
def test_export_table_handles_generic_exception(self, mock_getattr):
|
||||
"""Test that general exceptions in the handle method are handled correctly"""
|
||||
with less_console_noise():
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_class().export.side_effect = Exception("Test Exception")
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
|
||||
# Import the command to avoid any locale or gettext issues
|
||||
command_class = import_string("registrar.management.commands.export_tables.Command")
|
||||
command_instance = command_class()
|
||||
command_instance.export_table("TestTable")
|
||||
|
||||
self.logger_mock.error.assert_called_with("Failed to export TestTable: Test Exception")
|
||||
|
||||
|
||||
class TestImportTables(TestCase):
|
||||
"""Test the import_tables script"""
|
||||
|
||||
@patch("registrar.management.commands.import_tables.os.makedirs")
|
||||
@patch("registrar.management.commands.import_tables.os.path.exists")
|
||||
@patch("registrar.management.commands.import_tables.os.remove")
|
||||
@patch("registrar.management.commands.import_tables.pyzipper.AESZipFile")
|
||||
@patch("registrar.management.commands.import_tables.tablib.Dataset")
|
||||
@patch("registrar.management.commands.import_tables.open", create=True)
|
||||
@patch("registrar.management.commands.import_tables.logger")
|
||||
@patch("registrar.management.commands.import_tables.getattr")
|
||||
@patch("django.apps.apps.get_model")
|
||||
def test_handle(
|
||||
self,
|
||||
mock_get_model,
|
||||
mock_getattr,
|
||||
mock_logger,
|
||||
mock_open,
|
||||
mock_dataset,
|
||||
mock_zipfile,
|
||||
mock_remove,
|
||||
mock_path_exists,
|
||||
mock_makedirs,
|
||||
):
|
||||
"""Test that the handle method properly imports tables"""
|
||||
with less_console_noise():
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
|
||||
# Mock os.path.exists to always return True
|
||||
mock_path_exists.return_value = True
|
||||
|
||||
# Mock the zipfile to have extractall return None
|
||||
mock_zipfile_instance = mock_zipfile.return_value.__enter__.return_value
|
||||
mock_zipfile_instance.extractall.return_value = None
|
||||
|
||||
# Mock the CSV file content
|
||||
csv_content = b"mock_csv_data"
|
||||
|
||||
# Mock the open function to return a mock file
|
||||
mock_open.return_value.__enter__.return_value.read.return_value = csv_content
|
||||
|
||||
# Mock the Dataset class and its load method to return a dataset
|
||||
mock_dataset_instance = MagicMock(spec=tablib.Dataset)
|
||||
with patch(
|
||||
"registrar.management.commands.import_tables.tablib.Dataset.load", return_value=mock_dataset_instance
|
||||
):
|
||||
# Mock the resource class and its import method
|
||||
mock_resource_class = MagicMock()
|
||||
mock_resource_instance = MagicMock()
|
||||
mock_result = MagicMock()
|
||||
mock_result.has_errors.return_value = False
|
||||
mock_resource_instance.import_data.return_value = mock_result
|
||||
mock_resource_class.return_value = mock_resource_instance
|
||||
mock_getattr.return_value = mock_resource_class
|
||||
|
||||
# Call the command
|
||||
call_command("import_tables")
|
||||
|
||||
# Check that os.makedirs was called once to create the tmp directory
|
||||
mock_makedirs.assert_called_once_with("tmp", exist_ok=True)
|
||||
|
||||
# Check that os.path.exists was called once for the zip file
|
||||
mock_path_exists.assert_any_call("tmp/exported_tables.zip")
|
||||
|
||||
# Check that pyzipper.AESZipFile was called once to open the zip file
|
||||
mock_zipfile.assert_called_once_with("tmp/exported_tables.zip", "r")
|
||||
|
||||
# Check that extractall was called once to extract the zip file contents
|
||||
mock_zipfile_instance.extractall.assert_called_once_with("tmp")
|
||||
|
||||
# Check that the import_table function was called for each table
|
||||
table_names = [
|
||||
"User",
|
||||
"Contact",
|
||||
"Domain",
|
||||
"DomainRequest",
|
||||
"DomainInformation",
|
||||
"UserDomainRole",
|
||||
"DraftDomain",
|
||||
"Website",
|
||||
"HostIp",
|
||||
"Host",
|
||||
"PublicContact",
|
||||
]
|
||||
# Check that os.path.exists was called for each table
|
||||
for table_name in table_names:
|
||||
mock_path_exists.assert_any_call(f"tmp/{table_name}.csv")
|
||||
|
||||
# Check that clean_tables is called for Contact
|
||||
mock_get_model.assert_any_call("registrar", "Contact")
|
||||
model_mock = mock_get_model.return_value
|
||||
model_mock.objects.all().delete.assert_called()
|
||||
|
||||
# Check that logger.info was called for each successful import
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Successfully imported tmp/{table_name}.csv into {table_name}")
|
||||
|
||||
# Check that logger.error was not called for resource class not found
|
||||
mock_logger.error.assert_not_called()
|
||||
|
||||
# Check that os.remove was called for each CSV file
|
||||
for table_name in table_names:
|
||||
mock_remove.assert_any_call(f"tmp/{table_name}.csv")
|
||||
|
||||
# Check that logger.info was called for each CSV file removal
|
||||
for table_name in table_names:
|
||||
mock_logger.info.assert_any_call(f"Removed temporary file tmp/{table_name}.csv")
|
||||
|
||||
@patch("registrar.management.commands.import_tables.logger")
|
||||
@patch("registrar.management.commands.import_tables.os.makedirs")
|
||||
@patch("registrar.management.commands.import_tables.os.path.exists")
|
||||
def test_handle_zip_file_not_found(self, mock_path_exists, mock_makedirs, mock_logger):
|
||||
"""Test the handle method when the zip file doesn't exist"""
|
||||
with less_console_noise():
|
||||
# Mock os.makedirs to do nothing
|
||||
mock_makedirs.return_value = None
|
||||
|
||||
# Mock os.path.exists to return False
|
||||
mock_path_exists.return_value = False
|
||||
|
||||
call_command("import_tables")
|
||||
|
||||
# Check that logger.error was called with the correct message
|
||||
mock_logger.error.assert_called_once_with("Zip file tmp/exported_tables.zip does not exist.")
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from django.test import TestCase
|
||||
from django.db.utils import IntegrityError
|
||||
from unittest.mock import patch
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from registrar.models import (
|
||||
Contact,
|
||||
|
@ -1602,3 +1603,369 @@ class TestDomainInformationCustomSave(TestCase):
|
|||
)
|
||||
self.assertEqual(domain_information_election.is_election_board, True)
|
||||
self.assertEqual(domain_information_election.generic_org_type, DomainRequest.OrganizationChoices.CITY)
|
||||
|
||||
|
||||
class TestDomainRequestIncomplete(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email
|
||||
)
|
||||
ao, _ = Contact.objects.get_or_create(
|
||||
first_name="Meowy",
|
||||
last_name="Meoward",
|
||||
title="Chief Cat",
|
||||
email="meoward@chiefcat.com",
|
||||
phone="(206) 206 2060",
|
||||
)
|
||||
draft_domain, _ = DraftDomain.objects.get_or_create(name="MeowardMeowardMeoward.gov")
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy you",
|
||||
last_name="Tester you",
|
||||
title="Admin Tester",
|
||||
email="testy-admin@town.com",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(555) 555 5557",
|
||||
)
|
||||
alt, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="MeowardMeowardMeoward.com")
|
||||
self.domain_request = DomainRequest.objects.create(
|
||||
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||
federal_type="executive",
|
||||
federal_agency=FederalAgency.objects.get(agency="AMTRAK"),
|
||||
about_your_organization="Some description",
|
||||
is_election_board=True,
|
||||
tribe_name="Some tribe name",
|
||||
organization_name="Some organization",
|
||||
address_line1="address 1",
|
||||
state_territory="CA",
|
||||
zipcode="94044",
|
||||
authorizing_official=ao,
|
||||
requested_domain=draft_domain,
|
||||
purpose="Some purpose",
|
||||
submitter=you,
|
||||
no_other_contacts_rationale=None,
|
||||
has_cisa_representative=True,
|
||||
cisa_representative_email="somerep@cisa.com",
|
||||
has_anything_else_text=True,
|
||||
anything_else="Anything else",
|
||||
is_policy_acknowledged=True,
|
||||
creator=self.user,
|
||||
)
|
||||
|
||||
self.domain_request.other_contacts.add(other)
|
||||
self.domain_request.current_websites.add(current)
|
||||
self.domain_request.alternative_domains.add(alt)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
DomainRequest.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
def test_is_federal_complete(self):
|
||||
self.assertTrue(self.domain_request._is_federal_complete())
|
||||
self.domain_request.federal_type = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_federal_complete())
|
||||
|
||||
def test_is_interstate_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.INTERSTATE
|
||||
self.domain_request.about_your_organization = "Something something about your organization"
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_interstate_complete())
|
||||
self.domain_request.about_your_organization = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_interstate_complete())
|
||||
|
||||
def test_is_state_or_territory_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.STATE_OR_TERRITORY
|
||||
self.domain_request.is_election_board = True
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_state_or_territory_complete())
|
||||
|
||||
def test_is_tribal_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.TRIBAL
|
||||
self.domain_request.tribe_name = "Tribe Name"
|
||||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_tribal_complete())
|
||||
self.domain_request.tribe_name = None
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertFalse(self.domain_request._is_tribal_complete())
|
||||
|
||||
def test_is_county_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.COUNTY
|
||||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_county_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_county_complete())
|
||||
|
||||
def test_is_city_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.CITY
|
||||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_city_complete())
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertTrue(self.domain_request._is_city_complete())
|
||||
|
||||
def test_is_special_district_complete(self):
|
||||
self.domain_request.generic_org_type = DomainRequest.OrganizationChoices.SPECIAL_DISTRICT
|
||||
self.domain_request.about_your_organization = "Something something about your organization"
|
||||
self.domain_request.is_election_board = False
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_special_district_complete())
|
||||
self.domain_request.about_your_organization = None
|
||||
self.domain_request.is_election_board = None
|
||||
self.domain_request.save()
|
||||
# is_election_board will overwrite to False bc of _update_org_type_from_generic_org_and_election
|
||||
self.assertFalse(self.domain_request._is_special_district_complete())
|
||||
|
||||
def test_is_organization_name_and_address_complete(self):
|
||||
self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
|
||||
self.domain_request.organization_name = None
|
||||
self.domain_request.address_line1 = None
|
||||
self.domain_request.save()
|
||||
self.assertTrue(self.domain_request._is_organization_name_and_address_complete())
|
||||
|
||||
def test_is_authorizing_official_complete(self):
|
||||
self.assertTrue(self.domain_request._is_authorizing_official_complete())
|
||||
self.domain_request.authorizing_official = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_authorizing_official_complete())
|
||||
|
||||
def test_is_requested_domain_complete(self):
|
||||
self.assertTrue(self.domain_request._is_requested_domain_complete())
|
||||
self.domain_request.requested_domain = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_requested_domain_complete())
|
||||
|
||||
def test_is_purpose_complete(self):
|
||||
self.assertTrue(self.domain_request._is_purpose_complete())
|
||||
self.domain_request.purpose = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_purpose_complete())
|
||||
|
||||
def test_is_submitter_complete(self):
|
||||
self.assertTrue(self.domain_request._is_submitter_complete())
|
||||
self.domain_request.submitter = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_submitter_complete())
|
||||
|
||||
def test_is_other_contacts_complete_missing_one_field(self):
|
||||
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||
contact = self.domain_request.other_contacts.first()
|
||||
contact.first_name = None
|
||||
contact.save()
|
||||
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||
|
||||
def test_is_other_contacts_complete_all_none(self):
|
||||
self.domain_request.other_contacts.clear()
|
||||
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||
|
||||
def test_is_other_contacts_False_and_has_rationale(self):
|
||||
# Click radio button "No" for no other contacts and give rationale
|
||||
self.domain_request.other_contacts.clear()
|
||||
self.domain_request.other_contacts.exists = False
|
||||
self.domain_request.no_other_contacts_rationale = "Some rationale"
|
||||
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||
|
||||
def test_is_other_contacts_False_and_NO_rationale(self):
|
||||
# Click radio button "No" for no other contacts and DONT give rationale
|
||||
self.domain_request.other_contacts.clear()
|
||||
self.domain_request.other_contacts.exists = False
|
||||
self.domain_request.no_other_contacts_rationale = None
|
||||
self.assertFalse(self.domain_request._is_other_contacts_complete())
|
||||
|
||||
def test_is_additional_details_complete(self):
|
||||
test_cases = [
|
||||
# CISA Rep - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - Yes
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - No
|
||||
# Anything Else Text - No
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": False,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - Yes
|
||||
# Anything Else Radio - None
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_email": "some@cisarepemail.com",
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
"expected": False,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - None
|
||||
# Anything Else Radio - None
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
"expected": False,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - None
|
||||
# Anything Else Radio - No
|
||||
# Anything Else Text - No
|
||||
# sync_yes_no will override has_cisa_representative to be False if cisa_representative_email is None
|
||||
# therefore, our expected will be True
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": False,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - None
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - Yes
|
||||
# Email - None
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - Yes
|
||||
{
|
||||
"has_cisa_representative": True,
|
||||
# Above will be overridden to False if cisa_rep_email is None bc of sync_yes_no_form_fields
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - No
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - Yes
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": "Some text",
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - No
|
||||
# Anything Else Radio - Yes
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": True,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - No
|
||||
# Anything Else Radio - None
|
||||
# Anything Else Text - None
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
# Above is both None, so it does NOT get overwritten
|
||||
"expected": False,
|
||||
},
|
||||
# CISA Rep - No
|
||||
# Anything Else Radio - No
|
||||
# Anything Else Text - No
|
||||
{
|
||||
"has_cisa_representative": False,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": False,
|
||||
"anything_else": None,
|
||||
"expected": True,
|
||||
},
|
||||
# CISA Rep - None
|
||||
# Anything Else Radio - None
|
||||
{
|
||||
"has_cisa_representative": None,
|
||||
"cisa_representative_email": None,
|
||||
"has_anything_else_text": None,
|
||||
"anything_else": None,
|
||||
"expected": False,
|
||||
},
|
||||
]
|
||||
for case in test_cases:
|
||||
with self.subTest(case=case):
|
||||
self.domain_request.has_cisa_representative = case["has_cisa_representative"]
|
||||
self.domain_request.cisa_representative_email = case["cisa_representative_email"]
|
||||
self.domain_request.has_anything_else_text = case["has_anything_else_text"]
|
||||
self.domain_request.anything_else = case["anything_else"]
|
||||
self.domain_request.save()
|
||||
self.domain_request.refresh_from_db()
|
||||
self.assertEqual(
|
||||
self.domain_request._is_additional_details_complete(),
|
||||
case["expected"],
|
||||
msg=f"Failed for case: {case}",
|
||||
)
|
||||
|
||||
def test_is_policy_acknowledgement_complete(self):
|
||||
self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
|
||||
self.domain_request.is_policy_acknowledged = False
|
||||
self.assertTrue(self.domain_request._is_policy_acknowledgement_complete())
|
||||
self.domain_request.is_policy_acknowledged = None
|
||||
self.assertFalse(self.domain_request._is_policy_acknowledgement_complete())
|
||||
|
||||
def test_form_complete(self):
|
||||
self.assertTrue(self.domain_request._form_complete())
|
||||
self.domain_request.generic_org_type = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._form_complete())
|
||||
|
|
|
@ -20,6 +20,7 @@ from django.urls import reverse
|
|||
from registrar.models import (
|
||||
DomainRequest,
|
||||
DomainInformation,
|
||||
Website,
|
||||
)
|
||||
from waffle.testutils import override_flag
|
||||
import logging
|
||||
|
@ -54,8 +55,19 @@ class TestWithUser(MockEppLib):
|
|||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
phone = "8003111234"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, first_name=first_name, last_name=last_name, email=email
|
||||
username=username, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||
)
|
||||
title = "test title"
|
||||
self.user.contact.title = title
|
||||
self.user.contact.save()
|
||||
|
||||
username_incomplete = "test_user_incomplete"
|
||||
first_name_2 = "Incomplete"
|
||||
email_2 = "unicorn@igorville.com"
|
||||
self.incomplete_user = get_user_model().objects.create(
|
||||
username=username_incomplete, first_name=first_name_2, email=email_2
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -64,6 +76,7 @@ class TestWithUser(MockEppLib):
|
|||
DomainRequest.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
self.user.delete()
|
||||
self.incomplete_user.delete()
|
||||
|
||||
|
||||
class TestEnvironmentVariablesEffects(TestCase):
|
||||
|
@ -474,6 +487,137 @@ class HomeTests(TestWithUser):
|
|||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class FinishUserProfileTests(TestWithUser, WebTest):
|
||||
"""A series of tests that target the finish setup page for user profile"""
|
||||
|
||||
# csrf checks do not work well with WebTest.
|
||||
# We disable them here.
|
||||
csrf_checks = False
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user.title = None
|
||||
self.user.save()
|
||||
self.client.force_login(self.user)
|
||||
self.domain, _ = Domain.objects.get_or_create(name="sampledomain.gov", state=Domain.State.READY)
|
||||
self.role, _ = UserDomainRole.objects.get_or_create(
|
||||
user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
PublicContact.objects.filter(domain=self.domain).delete()
|
||||
self.role.delete()
|
||||
self.domain.delete()
|
||||
Domain.objects.all().delete()
|
||||
Website.objects.all().delete()
|
||||
Contact.objects.all().delete()
|
||||
|
||||
def _set_session_cookie(self):
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
def _submit_form_webtest(self, form, follow=False):
|
||||
page = form.submit()
|
||||
self._set_session_cookie()
|
||||
return page.follow() if follow else page
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_with_profile_feature_on(self):
|
||||
"""Tests that a new user is redirected to the profile setup page when profile_feature is on"""
|
||||
self.app.set_user(self.incomplete_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
# This will redirect the user to the setup page.
|
||||
# Follow implicity checks if our redirect is working.
|
||||
finish_setup_page = self.app.get(reverse("home")).follow()
|
||||
self._set_session_cookie()
|
||||
|
||||
# Assert that we're on the right page
|
||||
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
||||
|
||||
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
||||
|
||||
self.assertEqual(finish_setup_page.status_code, 200)
|
||||
|
||||
# We're missing a phone number, so the page should tell us that
|
||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
finish_setup_form["phone"] = "(201) 555-0123"
|
||||
finish_setup_form["title"] = "CEO"
|
||||
finish_setup_form["last_name"] = "example"
|
||||
save_page = self._submit_form_webtest(finish_setup_form, follow=True)
|
||||
|
||||
self.assertEqual(save_page.status_code, 200)
|
||||
self.assertContains(save_page, "Your profile has been updated.")
|
||||
|
||||
# Try to navigate back to the home page.
|
||||
# This is the same as clicking the back button.
|
||||
completed_setup_page = self.app.get(reverse("home"))
|
||||
self.assertContains(completed_setup_page, "Manage your domain")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_goes_to_domain_request_with_profile_feature_on(self):
|
||||
"""Tests that a new user is redirected to the domain request page when profile_feature is on"""
|
||||
|
||||
self.app.set_user(self.incomplete_user.username)
|
||||
with override_flag("profile_feature", active=True):
|
||||
# This will redirect the user to the setup page
|
||||
finish_setup_page = self.app.get(reverse("domain-request:")).follow()
|
||||
self._set_session_cookie()
|
||||
|
||||
# Assert that we're on the right page
|
||||
self.assertContains(finish_setup_page, "Finish setting up your profile")
|
||||
|
||||
finish_setup_page = self._submit_form_webtest(finish_setup_page.form)
|
||||
|
||||
self.assertEqual(finish_setup_page.status_code, 200)
|
||||
|
||||
# We're missing a phone number, so the page should tell us that
|
||||
self.assertContains(finish_setup_page, "Enter your phone number.")
|
||||
|
||||
# Check for the name of the save button
|
||||
self.assertContains(finish_setup_page, "contact_setup_save_button")
|
||||
|
||||
# Add a phone number
|
||||
finish_setup_form = finish_setup_page.form
|
||||
finish_setup_form["phone"] = "(201) 555-0123"
|
||||
finish_setup_form["title"] = "CEO"
|
||||
finish_setup_form["last_name"] = "example"
|
||||
completed_setup_page = self._submit_form_webtest(finish_setup_page.form, follow=True)
|
||||
|
||||
self.assertEqual(completed_setup_page.status_code, 200)
|
||||
|
||||
# Assert that we're on the domain request page
|
||||
self.assertNotContains(completed_setup_page, "Finish setting up your profile")
|
||||
self.assertNotContains(completed_setup_page, "What contact information should we use to reach you?")
|
||||
|
||||
self.assertContains(completed_setup_page, "You’re about to start your .gov domain request")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_with_profile_feature_off(self):
|
||||
"""Tests that a new user is not redirected to the profile setup page when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/")
|
||||
self.assertNotContains(response, "Finish setting up your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_user_goes_to_domain_request_with_profile_feature_off(self):
|
||||
"""Tests that a new user is redirected to the domain request page
|
||||
when profile_feature is off but not the setup page"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/request/")
|
||||
|
||||
self.assertNotContains(response, "Finish setting up your profile")
|
||||
self.assertNotContains(response, "What contact information should we use to reach you?")
|
||||
|
||||
self.assertContains(response, "You’re about to start your .gov domain request")
|
||||
|
||||
|
||||
class UserProfileTests(TestWithUser, WebTest):
|
||||
"""A series of tests that target your profile functionality"""
|
||||
|
||||
|
@ -502,7 +646,7 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
assume that the same test results hold true for 401 and 403."""
|
||||
with override_flag("profile_feature", active=True):
|
||||
with self.assertRaises(Exception):
|
||||
response = self.client.get(reverse("home"))
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
|
@ -522,42 +666,42 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
def test_home_page_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of home page when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get("/")
|
||||
response = self.client.get("/", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_home_page_main_nav_with_profile_feature_off(self):
|
||||
"""test that Your profile is not in main nav of home page when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/")
|
||||
response = self.client.get("/", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_request_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of new request when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get("/request/")
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_new_request_main_nav_with_profile_feature_off(self):
|
||||
"""test that Your profile is not in main nav of new request when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/request/")
|
||||
response = self.client.get("/request/", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_main_nav_with_profile_feature_on(self):
|
||||
"""test that Your profile is in main nav of user profile when profile_feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get("/user-profile")
|
||||
response = self.client.get("/user-profile", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_profile_returns_404_when_feature_off(self):
|
||||
"""test that Your profile returns 404 when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get("/user-profile")
|
||||
response = self.client.get("/user-profile", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -582,14 +726,14 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
def test_domain_your_contact_information_when_profile_feature_off(self):
|
||||
"""test that Your contact information is accessible when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information")
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||
self.assertContains(response, "Your contact information")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_your_contact_information_when_profile_feature_on(self):
|
||||
"""test that Your contact information is not accessible when profile feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information")
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -606,9 +750,9 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
submitter=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||
self.assertContains(response, "Your profile")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -625,9 +769,9 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
submitter=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw")
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}/withdraw", follow=True)
|
||||
self.assertNotContains(response, "Your profile")
|
||||
# cleanup
|
||||
domain_request.delete()
|
||||
|
@ -642,13 +786,6 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
profile_form = profile_page.form
|
||||
profile_page = profile_form.submit()
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
# assert that first result contains errors
|
||||
self.assertContains(profile_page, "Enter your title")
|
||||
self.assertContains(profile_page, "Enter your phone number")
|
||||
profile_form = profile_page.form
|
||||
profile_form["title"] = "sample title"
|
||||
profile_form["phone"] = "(201) 555-1212"
|
||||
profile_page = profile_form.submit()
|
||||
|
|
|
@ -467,6 +467,323 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# check that any new pages are added to this test
|
||||
self.assertEqual(num_pages, num_pages_tested)
|
||||
|
||||
@boto3_mocking.patching
|
||||
def test_domain_request_form_submission_incomplete(self):
|
||||
num_pages_tested = 0
|
||||
# skipping elections, type_of_work, tribal_government
|
||||
|
||||
intro_page = self.app.get(reverse("domain-request:"))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
intro_form = intro_page.forms[0]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
intro_result = intro_form.submit()
|
||||
|
||||
# follow first redirect
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
type_page = intro_result.follow()
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# ---- TYPE PAGE ----
|
||||
type_form = type_page.forms[0]
|
||||
type_form["generic_org_type-generic_org_type"] = "federal"
|
||||
# test next button and validate data
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
type_result = type_form.submit()
|
||||
# should see results in db
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.generic_org_type, "federal")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(type_result.status_code, 302)
|
||||
self.assertEqual(type_result["Location"], "/request/organization_federal/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- 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.forms[0]
|
||||
federal_form["organization_federal-federal_type"] = "executive"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
federal_result = federal_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.federal_type, "executive")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(federal_result.status_code, 302)
|
||||
self.assertEqual(federal_result["Location"], "/request/organization_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- ORG CONTACT PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
org_contact_page = federal_result.follow()
|
||||
org_contact_form = org_contact_page.forms[0]
|
||||
# federal agency so we have to fill in federal_agency
|
||||
federal_agency, _ = FederalAgency.objects.get_or_create(agency="General Services Administration")
|
||||
org_contact_form["organization_contact-federal_agency"] = federal_agency.id
|
||||
org_contact_form["organization_contact-organization_name"] = "Testorg"
|
||||
org_contact_form["organization_contact-address_line1"] = "address 1"
|
||||
org_contact_form["organization_contact-address_line2"] = "address 2"
|
||||
org_contact_form["organization_contact-city"] = "NYC"
|
||||
org_contact_form["organization_contact-state_territory"] = "NY"
|
||||
org_contact_form["organization_contact-zipcode"] = "10002"
|
||||
org_contact_form["organization_contact-urbanization"] = "URB Royal Oaks"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
org_contact_result = org_contact_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.organization_name, "Testorg")
|
||||
self.assertEqual(domain_request.address_line1, "address 1")
|
||||
self.assertEqual(domain_request.address_line2, "address 2")
|
||||
self.assertEqual(domain_request.city, "NYC")
|
||||
self.assertEqual(domain_request.state_territory, "NY")
|
||||
self.assertEqual(domain_request.zipcode, "10002")
|
||||
self.assertEqual(domain_request.urbanization, "URB Royal Oaks")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(org_contact_result.status_code, 302)
|
||||
self.assertEqual(org_contact_result["Location"], "/request/authorizing_official/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- AUTHORIZING OFFICIAL PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
ao_page = org_contact_result.follow()
|
||||
ao_form = ao_page.forms[0]
|
||||
ao_form["authorizing_official-first_name"] = "Testy ATO"
|
||||
ao_form["authorizing_official-last_name"] = "Tester ATO"
|
||||
ao_form["authorizing_official-title"] = "Chief Tester"
|
||||
ao_form["authorizing_official-email"] = "testy@town.com"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
ao_result = ao_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.authorizing_official.first_name, "Testy ATO")
|
||||
self.assertEqual(domain_request.authorizing_official.last_name, "Tester ATO")
|
||||
self.assertEqual(domain_request.authorizing_official.title, "Chief Tester")
|
||||
self.assertEqual(domain_request.authorizing_official.email, "testy@town.com")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(ao_result.status_code, 302)
|
||||
self.assertEqual(ao_result["Location"], "/request/current_sites/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- CURRENT SITES PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_page = ao_result.follow()
|
||||
current_sites_form = current_sites_page.forms[0]
|
||||
current_sites_form["current_sites-0-website"] = "www.city.com"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_result = current_sites_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(
|
||||
domain_request.current_websites.filter(website="http://www.city.com").count(),
|
||||
1,
|
||||
)
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(current_sites_result.status_code, 302)
|
||||
self.assertEqual(current_sites_result["Location"], "/request/dotgov_domain/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- DOTGOV DOMAIN PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
dotgov_page = current_sites_result.follow()
|
||||
dotgov_form = dotgov_page.forms[0]
|
||||
dotgov_form["dotgov_domain-requested_domain"] = "city"
|
||||
dotgov_form["dotgov_domain-0-alternative_domain"] = "city1"
|
||||
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
dotgov_result = dotgov_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.requested_domain.name, "city.gov")
|
||||
self.assertEqual(domain_request.alternative_domains.filter(website="city1.gov").count(), 1)
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(dotgov_result.status_code, 302)
|
||||
self.assertEqual(dotgov_result["Location"], "/request/purpose/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- PURPOSE PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
purpose_page = dotgov_result.follow()
|
||||
purpose_form = purpose_page.forms[0]
|
||||
purpose_form["purpose-purpose"] = "For all kinds of things."
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
purpose_result = purpose_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.purpose, "For all kinds of things.")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(purpose_result.status_code, 302)
|
||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- YOUR CONTACT INFO PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_page = purpose_result.follow()
|
||||
your_contact_form = your_contact_page.forms[0]
|
||||
|
||||
your_contact_form["your_contact-first_name"] = "Testy you"
|
||||
your_contact_form["your_contact-last_name"] = "Tester you"
|
||||
your_contact_form["your_contact-title"] = "Admin Tester"
|
||||
your_contact_form["your_contact-email"] = "testy-admin@town.com"
|
||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_result = your_contact_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.submitter.first_name, "Testy you")
|
||||
self.assertEqual(domain_request.submitter.last_name, "Tester you")
|
||||
self.assertEqual(domain_request.submitter.title, "Admin Tester")
|
||||
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
|
||||
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(your_contact_result.status_code, 302)
|
||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- OTHER CONTACTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
other_contacts_page = your_contact_result.follow()
|
||||
|
||||
# This page has 3 forms in 1.
|
||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||
other_contacts_form = other_contacts_page.forms[0]
|
||||
|
||||
other_contacts_form["other_contacts-has_other_contacts"] = "True"
|
||||
|
||||
other_contacts_form["other_contacts-0-first_name"] = "Testy2"
|
||||
other_contacts_form["other_contacts-0-last_name"] = "Tester2"
|
||||
other_contacts_form["other_contacts-0-title"] = "Another Tester"
|
||||
other_contacts_form["other_contacts-0-email"] = "testy2@town.com"
|
||||
other_contacts_form["other_contacts-0-phone"] = "(201) 555 5557"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
other_contacts_result = other_contacts_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(
|
||||
domain_request.other_contacts.filter(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Another Tester",
|
||||
email="testy2@town.com",
|
||||
phone="(201) 555 5557",
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(other_contacts_result.status_code, 302)
|
||||
self.assertEqual(other_contacts_result["Location"], "/request/additional_details/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- ADDITIONAL DETAILS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
additional_details_page = other_contacts_result.follow()
|
||||
additional_details_form = additional_details_page.forms[0]
|
||||
|
||||
# load inputs with test data
|
||||
|
||||
additional_details_form["additional_details-has_cisa_representative"] = "True"
|
||||
additional_details_form["additional_details-has_anything_else_text"] = "True"
|
||||
additional_details_form["additional_details-cisa_representative_email"] = "FakeEmail@gmail.com"
|
||||
additional_details_form["additional_details-anything_else"] = "Nothing else."
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
additional_details_result = additional_details_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.cisa_representative_email, "FakeEmail@gmail.com")
|
||||
self.assertEqual(domain_request.anything_else, "Nothing else.")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(additional_details_result.status_code, 302)
|
||||
self.assertEqual(additional_details_result["Location"], "/request/requirements/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- REQUIREMENTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requirements_page = additional_details_result.follow()
|
||||
requirements_form = requirements_page.forms[0]
|
||||
|
||||
requirements_form["requirements-is_policy_acknowledged"] = True
|
||||
|
||||
# Before we go to the review page, let's remove some of the data from the request:
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
|
||||
domain_request.generic_org_type = None
|
||||
domain_request.save()
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requirements_result = requirements_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
self.assertEqual(domain_request.is_policy_acknowledged, True)
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(requirements_result.status_code, 302)
|
||||
self.assertEqual(requirements_result["Location"], "/request/review/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- REVIEW AND FINSIHED PAGES ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
review_page = requirements_result.follow()
|
||||
# review_form = review_page.forms[0]
|
||||
|
||||
# Review page contains all the previously entered data
|
||||
# Let's make sure the long org name is displayed
|
||||
self.assertNotContains(review_page, "Federal")
|
||||
# self.assertContains(review_page, "Executive")
|
||||
self.assertContains(review_page, "Incomplete", count=1)
|
||||
|
||||
# We can't test the modal itself as it relies on JS for init and triggering,
|
||||
# but we can test for the existence of its trigger:
|
||||
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||
# And the existence of the modal's data parked and ready for the js init.
|
||||
# The next assert also tests for the passed requested domain context from
|
||||
# the view > domain_request_form > modal
|
||||
self.assertNotContains(review_page, "You are about to submit a domain request for city.gov")
|
||||
self.assertContains(review_page, "Your request form is incomplete")
|
||||
|
||||
# This is the start of a test to check an existing domain_request, it currently
|
||||
# does not work and results in errors as noted in:
|
||||
# https://github.com/cisagov/getgov/pull/728
|
||||
|
@ -2385,7 +2702,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
|
||||
review_page = self.app.get(reverse("domain-request:review"))
|
||||
self.assertContains(review_page, "toggle-submit-domain-request")
|
||||
self.assertContains(review_page, "You are about to submit an incomplete request")
|
||||
self.assertContains(review_page, "Your request form is incomplete")
|
||||
|
||||
|
||||
class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.template.loader import get_template
|
|||
from email.mime.application import MIMEApplication
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from waffle import flag_is_active
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -35,6 +36,11 @@ def send_templated_email(
|
|||
|
||||
Raises EmailSendingError if SES client could not be accessed
|
||||
"""
|
||||
|
||||
if flag_is_active(None, "disable_email_sending") and not settings.IS_PRODUCTION: # type: ignore
|
||||
message = "Could not send email. Email sending is disabled due to flag 'disable_email_sending'."
|
||||
raise EmailSendingError(message)
|
||||
|
||||
template = get_template(template_name)
|
||||
email_body = template.render(context=context)
|
||||
|
||||
|
|
|
@ -14,6 +14,6 @@ from .domain import (
|
|||
DomainInvitationDeleteView,
|
||||
DomainDeleteUserView,
|
||||
)
|
||||
from .user_profile import UserProfileView
|
||||
from .user_profile import UserProfileView, FinishProfileSetupView
|
||||
from .health import *
|
||||
from .index import *
|
||||
|
|
|
@ -379,30 +379,45 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
|
||||
def get_context_data(self):
|
||||
"""Define context for access on all wizard pages."""
|
||||
# Build the submit button that we'll pass to the modal.
|
||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||
# Concatenate the modal header that we'll pass to the modal.
|
||||
if self.domain_request.requested_domain:
|
||||
modal_heading = "You are about to submit a domain request for " + str(self.domain_request.requested_domain)
|
||||
else:
|
||||
modal_heading = "You are about to submit an incomplete request"
|
||||
|
||||
has_profile_flag = flag_is_active(self.request, "profile_feature")
|
||||
logger.debug("PROFILE FLAG is %s" % has_profile_flag)
|
||||
|
||||
context = {
|
||||
"form_titles": self.TITLES,
|
||||
"steps": self.steps,
|
||||
# Add information about which steps should be unlocked
|
||||
"visited": self.storage.get("step_history", []),
|
||||
"is_federal": self.domain_request.is_federal(),
|
||||
"modal_button": modal_button,
|
||||
"modal_heading": modal_heading,
|
||||
# Use the profile waffle feature flag to toggle profile features throughout domain requests
|
||||
"has_profile_feature_flag": has_profile_flag,
|
||||
"user": self.request.user,
|
||||
}
|
||||
return context
|
||||
context_stuff = {}
|
||||
if DomainRequest._form_complete(self.domain_request):
|
||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||
context_stuff = {
|
||||
"not_form": False,
|
||||
"form_titles": self.TITLES,
|
||||
"steps": self.steps,
|
||||
"visited": self.storage.get("step_history", []),
|
||||
"is_federal": self.domain_request.is_federal(),
|
||||
"modal_button": modal_button,
|
||||
"modal_heading": "You are about to submit a domain request for "
|
||||
+ str(self.domain_request.requested_domain),
|
||||
"modal_description": "Once you submit this request, you won’t be able to edit it until we review it.\
|
||||
You’ll only be able to withdraw your request.",
|
||||
"review_form_is_complete": True,
|
||||
# Use the profile waffle feature flag to toggle profile features throughout domain requests
|
||||
"has_profile_feature_flag": has_profile_flag,
|
||||
"user": self.request.user,
|
||||
}
|
||||
else: # form is not complete
|
||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||
context_stuff = {
|
||||
"not_form": True,
|
||||
"form_titles": self.TITLES,
|
||||
"steps": self.steps,
|
||||
"visited": self.storage.get("step_history", []),
|
||||
"is_federal": self.domain_request.is_federal(),
|
||||
"modal_button": modal_button,
|
||||
"modal_heading": "Your request form is incomplete",
|
||||
"modal_description": 'This request cannot be submitted yet.\
|
||||
Return to the request and visit the steps that are marked as "incomplete."',
|
||||
"review_form_is_complete": False,
|
||||
"has_profile_feature_flag": has_profile_flag,
|
||||
"user": self.request.user,
|
||||
}
|
||||
return context_stuff
|
||||
|
||||
def get_step_list(self) -> list:
|
||||
"""Dynamically generated list of steps in the form wizard."""
|
||||
|
@ -670,6 +685,8 @@ class Review(DomainRequestWizard):
|
|||
forms = [] # type: ignore
|
||||
|
||||
def get_context_data(self):
|
||||
if DomainRequest._form_complete(self.domain_request) is False:
|
||||
logger.warning("User arrived at review page with an incomplete form.")
|
||||
context = super().get_context_data()
|
||||
context["Step"] = Step.__members__
|
||||
context["domain_request"] = self.domain_request
|
||||
|
|
|
@ -2,13 +2,16 @@
|
|||
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
import logging
|
||||
from urllib.parse import parse_qs, unquote
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
from django.contrib import messages
|
||||
from django.views.generic.edit import FormMixin
|
||||
from registrar.forms.user_profile import UserProfileForm
|
||||
from django.urls import reverse
|
||||
from registrar.forms.user_profile import UserProfileForm, FinishSetupProfileForm
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from registrar.models import (
|
||||
Contact,
|
||||
)
|
||||
|
@ -16,6 +19,8 @@ from registrar.models.utility.generic_helper import replace_url_queryparams
|
|||
from registrar.views.utility.permission_views import UserProfilePermissionView
|
||||
from waffle.decorators import flag_is_active, waffle_flag
|
||||
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_protect
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -59,6 +64,11 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
context = super().get_context_data(**kwargs)
|
||||
# This is a django waffle flag which toggles features based off of the "flag" table
|
||||
context["has_profile_feature_flag"] = flag_is_active(self.request, "profile_feature")
|
||||
|
||||
# The text for the back button on this page
|
||||
context["profile_back_button_text"] = "Go to manage your domains"
|
||||
context["show_back_button"] = True
|
||||
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
|
@ -99,3 +109,145 @@ class UserProfileView(UserProfilePermissionView, FormMixin):
|
|||
if hasattr(user, "contact"): # Check if the user has a contact instance
|
||||
return user.contact
|
||||
return None
|
||||
|
||||
|
||||
class FinishProfileSetupView(UserProfileView):
|
||||
"""This view forces the user into providing additional details that
|
||||
we may have missed from Login.gov"""
|
||||
|
||||
class RedirectType(Enum):
|
||||
"""
|
||||
Enums for each type of redirection. Enforces behaviour on `get_redirect_url()`.
|
||||
|
||||
- HOME: We want to redirect to reverse("home")
|
||||
- BACK_TO_SELF: We want to redirect back to this page
|
||||
- TO_SPECIFIC_PAGE: We want to redirect to the page specified in the queryparam "redirect"
|
||||
- COMPLETE_SETUP: Indicates that we want to navigate BACK_TO_SELF, but the subsequent
|
||||
redirect after the next POST should be either HOME or TO_SPECIFIC_PAGE
|
||||
"""
|
||||
|
||||
HOME = "home"
|
||||
TO_SPECIFIC_PAGE = "domain_request"
|
||||
BACK_TO_SELF = "back_to_self"
|
||||
COMPLETE_SETUP = "complete_setup"
|
||||
|
||||
@classmethod
|
||||
def get_all_redirect_types(cls) -> list[str]:
|
||||
"""Returns the value of every redirect type defined in this enum."""
|
||||
return [r.value for r in cls]
|
||||
|
||||
template_name = "finish_profile_setup.html"
|
||||
form_class = FinishSetupProfileForm
|
||||
model = Contact
|
||||
|
||||
all_redirect_types = RedirectType.get_all_redirect_types()
|
||||
redirect_type: RedirectType
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
# Hide the back button by default
|
||||
context["show_back_button"] = False
|
||||
|
||||
if self.redirect_type == self.RedirectType.COMPLETE_SETUP:
|
||||
context["confirm_changes"] = True
|
||||
|
||||
if "redirect_viewname" not in self.session:
|
||||
context["show_back_button"] = True
|
||||
else:
|
||||
context["going_to_specific_page"] = True
|
||||
context["redirect_button_text"] = "Continue to your request"
|
||||
|
||||
return context
|
||||
|
||||
@method_decorator(csrf_protect)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
"""
|
||||
Handles dispatching of the view, applying CSRF protection and checking the 'profile_feature' flag.
|
||||
|
||||
This method sets the redirect type based on the 'redirect' query parameter,
|
||||
defaulting to BACK_TO_SELF if not provided.
|
||||
It updates the session with the redirect view name if the redirect type is TO_SPECIFIC_PAGE.
|
||||
|
||||
Returns:
|
||||
HttpResponse: The response generated by the parent class's dispatch method.
|
||||
"""
|
||||
|
||||
# Update redirect type based on the query parameter if present
|
||||
default_redirect_value = self.RedirectType.BACK_TO_SELF.value
|
||||
redirect_value = request.GET.get("redirect", default_redirect_value)
|
||||
|
||||
if redirect_value in self.all_redirect_types:
|
||||
# If the redirect value is a preexisting value in our enum, set it to that.
|
||||
self.redirect_type = self.RedirectType(redirect_value)
|
||||
else:
|
||||
# If the redirect type is undefined, then we assume that we are specifying a particular page to redirect to.
|
||||
self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE
|
||||
|
||||
# Store the page that we want to redirect to for later use
|
||||
request.session["redirect_viewname"] = str(redirect_value)
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""Form submission posts to this view."""
|
||||
self._refresh_session_and_object(request)
|
||||
form = self.form_class(request.POST, instance=self.object)
|
||||
|
||||
# Get the current form and validate it
|
||||
if form.is_valid():
|
||||
if "contact_setup_save_button" in request.POST:
|
||||
# Logic for when the 'Save' button is clicked
|
||||
self.redirect_type = self.RedirectType.COMPLETE_SETUP
|
||||
elif "contact_setup_submit_button" in request.POST:
|
||||
specific_redirect = "redirect_viewname" in self.session
|
||||
self.redirect_type = self.RedirectType.TO_SPECIFIC_PAGE if specific_redirect else self.RedirectType.HOME
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the nameservers page for the domain."""
|
||||
return self.get_redirect_url()
|
||||
|
||||
def get_redirect_url(self):
|
||||
"""
|
||||
Returns a URL string based on the current value of self.redirect_type.
|
||||
|
||||
Depending on self.redirect_type, constructs a base URL and appends a
|
||||
'redirect' query parameter. Handles different redirection types such as
|
||||
HOME, BACK_TO_SELF, COMPLETE_SETUP, and TO_SPECIFIC_PAGE.
|
||||
|
||||
Returns:
|
||||
str: The full URL with the appropriate query parameters.
|
||||
"""
|
||||
|
||||
# These redirect types redirect to the same page
|
||||
self_redirect = [self.RedirectType.BACK_TO_SELF, self.RedirectType.COMPLETE_SETUP]
|
||||
|
||||
# Maps the redirect type to a URL
|
||||
base_url = ""
|
||||
try:
|
||||
if self.redirect_type in self_redirect:
|
||||
base_url = reverse("finish-user-profile-setup")
|
||||
elif self.redirect_type == self.RedirectType.TO_SPECIFIC_PAGE:
|
||||
# We only allow this session value to use viewnames,
|
||||
# because this restricts what can be redirected to.
|
||||
desired_view = self.session["redirect_viewname"]
|
||||
self.session.pop("redirect_viewname")
|
||||
base_url = reverse(desired_view)
|
||||
else:
|
||||
base_url = reverse("home")
|
||||
except NoReverseMatch as err:
|
||||
logger.error(f"get_redirect_url -> Could not find the specified page. Err: {err}")
|
||||
|
||||
query_params = {}
|
||||
|
||||
# Quote cleans up the value so that it can be used in a url
|
||||
if self.redirect_type and self.redirect_type.value:
|
||||
query_params["redirect"] = quote(self.redirect_type.value)
|
||||
|
||||
# Generate the full url from the given query params
|
||||
full_url = replace_url_queryparams(base_url, query_params)
|
||||
return full_url
|
||||
|
|
|
@ -296,7 +296,6 @@ class UserDeleteDomainRolePermission(PermissionsLoginMixin):
|
|||
domain_pk = self.kwargs["pk"]
|
||||
user_pk = self.kwargs["user_pk"]
|
||||
|
||||
# Check if the user is authenticated
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
|
@ -393,6 +392,8 @@ class UserProfilePermission(PermissionsLoginMixin):
|
|||
|
||||
If the user is authenticated, they have access
|
||||
"""
|
||||
|
||||
# Check if the user is authenticated
|
||||
if not self.request.user.is_authenticated:
|
||||
return False
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue