merge main

This commit is contained in:
David Kennedy 2025-01-03 13:20:52 -05:00
commit d2f1d1f5a8
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
66 changed files with 1204 additions and 852 deletions

View file

@ -629,6 +629,51 @@ export function initRejectedEmail() {
});
}
/**
* A function that handles the suborganzation and requested suborganization fields and buttons.
* - Fieldwise: Hooks to the sub_organization, suborganization_city, and suborganization_state_territory fields.
* On change, this function checks if any of these fields are not empty:
* sub_organization, suborganization_city, and suborganization_state_territory.
* If they aren't, then we show the "clear" button. If they are, then we hide it because we don't need it.
*
* - Buttonwise: Hooks to the #clear-requested-suborganization button.
* On click, this will clear the input value of sub_organization, suborganization_city, and suborganization_state_territory.
*/
function handleSuborgFieldsAndButtons() {
const requestedSuborganizationField = document.getElementById("id_requested_suborganization");
const suborganizationCity = document.getElementById("id_suborganization_city");
const suborganizationStateTerritory = document.getElementById("id_suborganization_state_territory");
const rejectButton = document.querySelector("#clear-requested-suborganization");
// Ensure that every variable is present before proceeding
if (!requestedSuborganizationField || !suborganizationCity || !suborganizationStateTerritory || !rejectButton) {
console.warn("handleSuborganizationSelection() => Could not find required fields.")
return;
}
function handleRejectButtonVisibility() {
if (requestedSuborganizationField.value || suborganizationCity.value || suborganizationStateTerritory.value) {
showElement(rejectButton);
}else {
hideElement(rejectButton)
}
}
function handleRejectButton() {
// Clear the text fields
requestedSuborganizationField.value = "";
suborganizationCity.value = "";
suborganizationStateTerritory.value = "";
// Update button visibility after clearing
handleRejectButtonVisibility();
}
rejectButton.addEventListener("click", handleRejectButton)
requestedSuborganizationField.addEventListener("blur", handleRejectButtonVisibility);
suborganizationCity.addEventListener("blur", handleRejectButtonVisibility);
suborganizationStateTerritory.addEventListener("change", handleRejectButtonVisibility);
}
/**
* A function for dynamic DomainRequest fields
*/
@ -636,5 +681,6 @@ export function initDynamicDomainRequestFields(){
const domainRequestPage = document.getElementById("domainrequest_form");
if (domainRequestPage) {
handlePortfolioSelection();
handleSuborgFieldsAndButtons();
}
}

View file

@ -49,6 +49,13 @@ export function handlePortfolioSelection(
const portfolioUrbanizationField = document.querySelector(".field-portfolio_urbanization");
const portfolioUrbanization = portfolioUrbanizationField.querySelector(".readonly");
const portfolioJsonUrl = document.getElementById("portfolio_json_url")?.value || null;
// These requested suborganization fields only exist on the domain request page
const rejectSuborganizationButton = document.querySelector("#clear-requested-suborganization");
const requestedSuborganizationFieldInput = document.getElementById("id_requested_suborganization");
const suborganizationCityInput = document.getElementById("id_suborganization_city");
const suborganizationStateTerritoryInput = document.getElementById("id_suborganization_state_territory");
// Global var to track page load
let isPageLoading = true;
/**
@ -469,11 +476,28 @@ export function handlePortfolioSelection(
if (requestedSuborganizationField) showElement(requestedSuborganizationField);
if (suborganizationCity) showElement(suborganizationCity);
if (suborganizationStateTerritory) showElement(suborganizationStateTerritory);
// == LOGIC FOR THE DOMAIN REQUEST PAGE == //
// Handle rejectSuborganizationButton (display of the clear requested suborg button).
// Basically, this button should only be visible when we have data for suborg, city, and state_territory.
// The function handleSuborgFieldsAndButtons() in domain-request-form.js handles doing this same logic
// but on field input for city, state_territory, and the suborg field.
// If it doesn't exist, don't do anything.
if (rejectSuborganizationButton){
if (requestedSuborganizationFieldInput?.value || suborganizationCityInput?.value || suborganizationStateTerritoryInput?.value) {
showElement(rejectSuborganizationButton);
}else {
hideElement(rejectSuborganizationButton);
}
}
} else {
// Hide suborganization request fields if suborganization is selected
if (requestedSuborganizationField) hideElement(requestedSuborganizationField);
if (suborganizationCity) hideElement(suborganizationCity);
if (suborganizationStateTerritory) hideElement(suborganizationStateTerritory);
if (suborganizationStateTerritory) hideElement(suborganizationStateTerritory);
// == LOGIC FOR THE DOMAIN REQUEST PAGE == //
if (rejectSuborganizationButton) hideElement(rejectSuborganizationButton);
}
}

View file

@ -176,7 +176,16 @@ html[data-theme="dark"] {
color: var(--primary-fg);
}
// Reset the USWDS styles for alerts
@include at-media(desktop) {
.dashboard .usa-alert__body--widescreen {
padding-left: 4rem !important;
}
.dashboard .usa-alert__body--widescreen::before {
left: 1.5rem !important;
}
}
#branding h1,
h1, h2, h3,

View file

@ -1,21 +1,18 @@
@use "uswds-core" as *;
@use "base" as *;
// Fixes some font size disparities with the Figma
// for usa-alert alert elements
.usa-alert {
.usa-alert__heading.larger-font-sizing {
font-size: units(3);
}
}
.usa-alert__text.measure-none {
max-width: measure(none);
}
/*----------------
Alert Layout
-----------------*/
// The icon was off center for some reason
// Fixes that issue
@media (min-width: 64em){
@include at-media(desktop) {
// NOTE: !important is used because _font.scss overrides this
.usa-alert__body {
max-width: $widescreen-max-width !important;
}
.usa-alert--warning{
.usa-alert__body::before {
left: 1rem !important;
@ -24,13 +21,29 @@
.usa-alert__body.margin-left-1 {
margin-left: 0.5rem!important;
}
.usa-alert__body--widescreen::before {
left: 4rem !important;
}
.usa-alert__body--widescreen {
padding-left: 7rem!important;
}
}
// NOTE: !important is used because _font.scss overrides this
.usa-alert__body--widescreen {
max-width: $widescreen-max-width !important;
/*----------------
Alert Fonts
-----------------*/
// Fixes some font size disparities with the Figma
// for usa-alert alert elements
.usa-alert {
.usa-alert__heading.larger-font-sizing {
font-size: 1.5rem;
}
}
/*----------------
Alert Coloring
-----------------*/
.usa-site-alert--hot-pink {
.usa-alert {
background-color: $hot-pink;

View file

@ -2,6 +2,8 @@
@use "cisa_colors" as *;
$widescreen-max-width: 1920px;
$widescreen-x-padding: 4.5rem;
$hot-pink: #FFC3F9;
/* Styles for making visible to screen reader / AT users only. */
@ -253,6 +255,15 @@ abbr[title] {
max-width: $widescreen-max-width;
}
// This is used in cases where we want to align content to widescreen margins
// but we don't want the content itself to have widescreen widths
@include at-media(desktop) {
.padding-x--widescreen {
padding-left: $widescreen-x-padding !important;
padding-right: $widescreen-x-padding !important;
}
}
.margin-right-neg-4px {
margin-right: -4px;
}
@ -267,3 +278,7 @@ abbr[title] {
height: 1.5em;
width: 1.5em;
}
.maxw-fit-content {
max-width: fit-content;
}

View file

@ -6,3 +6,21 @@
.usa-identifier__container--widescreen {
max-width: $widescreen-max-width !important;
}
// NOTE: !important is used because we are overriding default
// USWDS paddings in a few locations
@include at-media(desktop) {
.grid-container--widescreen {
padding-left: $widescreen-x-padding !important;
padding-right: $widescreen-x-padding !important;
}
}
// matches max-width to equal the max-width of .grid-container
// used to trick the eye into thinking we have left-aligned a
// regular grid-container within a widescreen (see instances
// where is_widescreen_centered is used in the html).
.max-width--grid-container {
max-width: 960px;
}

View file

@ -110,8 +110,8 @@
}
}
.usa-nav__secondary {
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
right: 3rem;
right: 1rem;
padding-right: $widescreen-x-padding;
color: color('white');
bottom: 4.3rem;
.usa-nav-link,

View file

@ -251,7 +251,7 @@ TEMPLATES = [
"registrar.context_processors.org_user_status",
"registrar.context_processors.add_path_to_context",
"registrar.context_processors.portfolio_permissions",
"registrar.context_processors.is_widescreen_mode",
"registrar.context_processors.is_widescreen_centered",
],
},
},

View file

@ -109,31 +109,21 @@ def portfolio_permissions(request):
return portfolio_context
def is_widescreen_mode(request):
widescreen_paths = [] # If this list is meant to include specific paths, populate it.
portfolio_widescreen_paths = [
def is_widescreen_centered(request):
include_paths = [
"/domains/",
"/requests/",
"/request/",
"/no-organization-requests/",
"/no-organization-domains/",
"/domain-request/",
"/members/",
]
# widescreen_paths can be a bear as it trickles down sub-urls. exclude_paths gives us a way out.
exclude_paths = [
"/domains/edit",
"members/new-member/",
]
# Check if the current path matches a widescreen path or the root path.
is_widescreen = any(path in request.path for path in widescreen_paths) or request.path == "/"
is_excluded = any(exclude_path in request.path for exclude_path in exclude_paths)
# Check if the user is an organization user and the path matches portfolio paths.
is_portfolio_widescreen = (
hasattr(request.user, "is_org_user")
and request.user.is_org_user(request)
and any(path in request.path for path in portfolio_widescreen_paths)
and not any(exclude_path in request.path for exclude_path in exclude_paths)
)
# Check if the current path matches a path in included_paths or the root path.
is_widescreen_centered = any(path in request.path for path in include_paths) or request.path == "/"
# Return a dictionary with the widescreen mode status.
return {"is_widescreen_mode": is_widescreen or is_portfolio_widescreen}
return {"is_widescreen_centered": is_widescreen_centered and not is_excluded}

View file

@ -17,6 +17,7 @@ from registrar.models import Contact, DomainRequest, DraftDomain, Domain, Federa
from registrar.templatetags.url_helpers import public_site_url
from registrar.utility.enums import ValidationReturnType
from registrar.utility.constants import BranchChoices
from django.core.exceptions import ValidationError
logger = logging.getLogger(__name__)
@ -78,6 +79,20 @@ class RequestingEntityForm(RegistrarForm):
# Otherwise just return the suborg as normal
return self.cleaned_data.get("sub_organization")
def clean_requested_suborganization(self):
name = self.cleaned_data.get("requested_suborganization")
if (
name
and Suborganization.objects.filter(
name__iexact=name, portfolio=self.domain_request.portfolio, name__isnull=False, portfolio__isnull=False
).exists()
):
raise ValidationError(
"This suborganization already exists. "
"Choose a new name, or select it directly if you would like to use it."
)
return name
def full_clean(self):
"""Validation logic to remove the custom suborganization value before clean is triggered.
Without this override, the form will throw an 'invalid option' error."""
@ -114,7 +129,7 @@ class RequestingEntityForm(RegistrarForm):
if requesting_entity_is_suborganization == "True":
if is_requesting_new_suborganization:
# Validate custom suborganization fields
if not cleaned_data.get("requested_suborganization"):
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
if not cleaned_data.get("suborganization_city"):
self.add_error("suborganization_city", "Enter the city where your suborganization is located.")

View file

@ -12,6 +12,7 @@ from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTy
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.utility.constants import BranchChoices
from auditlog.models import LogEntry
from django.core.exceptions import ValidationError
from registrar.utility.waffle import flag_is_active_for_user
@ -671,6 +672,59 @@ class DomainRequest(TimeStampedModel):
# Store original values for caching purposes. Used to compare them on save.
self._cache_status_and_status_reasons()
def clean(self):
"""
Validates suborganization-related fields in two scenarios:
1. New suborganization request: Prevents duplicate names within same portfolio
2. Partial suborganization data: Enforces a all-or-nothing rule for city/state/name fields
when portfolio exists without selected suborganization
Add new domain request validation rules here to ensure they're
enforced during both model save and form submission.
Not presently used on the domain request wizard, though.
"""
super().clean()
# Validation logic for a suborganization request
if self.is_requesting_new_suborganization():
# Raise an error if this suborganization already exists
Suborganization = apps.get_model("registrar.Suborganization")
if (
self.requested_suborganization
and Suborganization.objects.filter(
name__iexact=self.requested_suborganization,
portfolio=self.portfolio,
name__isnull=False,
portfolio__isnull=False,
).exists()
):
# Add a field-level error to requested_suborganization.
# To pass in field-specific errors, we need to embed a dict of
# field: validationerror then pass that into a validation error itself.
# This is slightly confusing, but it just adds it at that level.
msg = (
"This suborganization already exists. "
"Choose a new name, or select it directly if you would like to use it."
)
errors = {"requested_suborganization": ValidationError(msg)}
raise ValidationError(errors)
elif self.portfolio and not self.sub_organization:
# You cannot create a new suborganization without these fields
required_suborg_fields = {
"requested_suborganization": self.requested_suborganization,
"suborganization_city": self.suborganization_city,
"suborganization_state_territory": self.suborganization_state_territory,
}
# If at least one value is populated, enforce a all-or-nothing rule
if any(bool(value) for value in required_suborg_fields.values()):
# Find which fields are empty and throw an error on the field
errors = {}
for field_name, value in required_suborg_fields.items():
if not value:
errors[field_name] = ValidationError(
"This field is required when creating a new suborganization.",
)
raise ValidationError(errors)
def save(self, *args, **kwargs):
"""Save override for custom properties"""
self.sync_organization_type()
@ -690,6 +744,18 @@ class DomainRequest(TimeStampedModel):
# Update the cached values after saving
self._cache_status_and_status_reasons()
def create_requested_suborganization(self):
"""Creates the requested suborganization.
Adds the name, portfolio, city, and state_territory fields.
Returns the created suborganization."""
Suborganization = apps.get_model("registrar.Suborganization")
return Suborganization.objects.create(
name=self.requested_suborganization,
portfolio=self.portfolio,
city=self.suborganization_city,
state_territory=self.suborganization_state_territory,
)
def send_custom_status_update_email(self, status):
"""Helper function to send out a second status email when the status remains the same,
but the reason has changed."""
@ -784,7 +850,9 @@ class DomainRequest(TimeStampedModel):
return True
def delete_and_clean_up_domain(self, called_from):
# Delete the approved domain
try:
# Clean up the approved domain
domain_state = self.approved_domain.state
# Only reject if it exists on EPP
if domain_state != Domain.State.UNKNOWN:
@ -796,6 +864,39 @@ class DomainRequest(TimeStampedModel):
logger.error(err)
logger.error(f"Can't query an approved domain while attempting {called_from}")
# Delete the suborg as long as this is the only place it is used
self._cleanup_dangling_suborg()
def _cleanup_dangling_suborg(self):
"""Deletes the existing suborg if its only being used by the deleted record"""
# Nothing to delete, so we just smile and walk away
if self.sub_organization is None:
return
Suborganization = apps.get_model("registrar.Suborganization")
# Stored as so because we need to set the reference to none first,
# so we can't just use the self.sub_organization property
suborg = Suborganization.objects.get(id=self.sub_organization.id)
requests = suborg.request_sub_organization
domain_infos = suborg.information_sub_organization
# Check if this is the only reference to the suborganization
if requests.count() != 1 or domain_infos.count() > 1:
return
# Remove the suborganization reference from request.
self.sub_organization = None
self.save()
# Remove the suborganization reference from domain if it exists.
if domain_infos.count() == 1:
domain_infos.update(sub_organization=None)
# Delete the now-orphaned suborganization
logger.info(f"_cleanup_dangling_suborg() -> Deleting orphan suborganization: {suborg}")
suborg.delete()
def _send_status_update_email(
self,
new_status,
@ -984,6 +1085,7 @@ class DomainRequest(TimeStampedModel):
if self.status == self.DomainRequestStatus.APPROVED:
self.delete_and_clean_up_domain("action_needed")
elif self.status == self.DomainRequestStatus.REJECTED:
self.rejection_reason = None
@ -1014,8 +1116,16 @@ class DomainRequest(TimeStampedModel):
domain request into an admin on that domain. It also triggers an email
notification."""
should_save = False
if self.federal_agency is None:
self.federal_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
should_save = True
if self.is_requesting_new_suborganization():
self.sub_organization = self.create_requested_suborganization()
should_save = True
if should_save:
self.save()
# create the domain
@ -1148,7 +1258,7 @@ class DomainRequest(TimeStampedModel):
def is_requesting_new_suborganization(self) -> bool:
"""Determines if a user is trying to request
a new suborganization using the domain request form, rather than one that already exists.
Used for the RequestingEntity page.
Used for the RequestingEntity page and on DomainInformation.create_from_da().
Returns True if a sub_organization does not exist and if requested_suborganization,
suborganization_city, and suborganization_state_territory all exist.

View file

@ -5,8 +5,8 @@
{% block title %}{% translate "Unauthorized | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grow-gap">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="grid-row grow-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "You are not authorized to view this page" %}

View file

@ -5,8 +5,8 @@
{% block title %}{% translate "Forbidden | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grow-gap">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="grid-row grow-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "You're not authorized to view this page." %}

View file

@ -5,8 +5,8 @@
{% block title %}{% translate "Page not found | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grid-gap">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "We couldnt find that page" %}

View file

@ -5,8 +5,8 @@
{% block title %}{% translate "Server error | " %}{% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-row grid-gap">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
<h1>
{% translate "We're having some trouble." %}

View file

@ -39,7 +39,7 @@
{% for model in app.models %}
<tr class="model-{{ model.object_name|lower }}{% if model.admin_url in request.path|urlencode %} current-model{% endif %}">
{% if model.admin_url %}
<th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}>{{ model.name }}</a></th>
<th scope="row"><a href="{{ model.admin_url }}"{% if model.admin_url in request.path|urlencode %} aria-current="page"{% endif %}">{{ model.name }}</a></th>
{% else %}
<th scope="row">{{ model.name }}</th>
{% endif %}

View file

@ -61,7 +61,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
{% if field.field.help_text %}
{# .gov override #}
{% block help_text %}
<div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div class="help"{% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}">
<div>{{ field.field.help_text|safe }}</div>
</div>
{% endblock help_text %}

View file

@ -43,7 +43,7 @@
<select name="selected_user" id="selected_user" class="admin-combobox margin-top-0" onchange="this.form.submit()">
<option value="">Select a user</option>
{% for user in other_users %}
<option value="{{ user.pk }}" {% if selected_user and user.pk == selected_user.pk %}selected{% endif %}>
<option value="{{ user.pk }}" {% if selected_user and user.pk == selected_user.pk %}selected{% endif %}">
{{ user.first_name }} {{ user.last_name }}
</option>
{% endfor %}

View file

@ -97,7 +97,7 @@
<section class="usa-banner" aria-label="Official website of the United States government">
<div class="usa-accordion">
<header class="usa-banner__header">
<div class="usa-banner__inner {% if is_widescreen_mode %} usa-banner__inner--widescreen {% endif %}">
<div class="usa-banner__inner usa-banner__inner--widescreen padding-x--widescreen">
<div class="grid-col-auto">
<img class="usa-banner__header-flag" src="{% static 'img/us_flag_small.png' %}" alt="U.S. flag" />
</div>
@ -113,7 +113,7 @@
</button>
</div>
</header>
<div class="usa-banner__content usa-accordion__content" id="gov-banner-default">
<div class="usa-banner__content usa-accordion__content padding-x--widescreen margin-x-0" id="gov-banner-default">
<div class="grid-row grid-gap-lg">
<div class="usa-banner__guidance tablet:grid-col-6">
<img class="usa-banner__icon usa-media-block__img" src="{% static 'img/icon-dot-gov.svg' %}" role="img"
@ -159,14 +159,14 @@
{% block wrapper %}
{% block wrapperdiv %}
<div id="wrapper">
<div id="wrapper" class="wrapper--padding-top-6">
{% endblock wrapperdiv %}
{% block messages %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
{% if 'base' in message.extra_tags %}
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}>
<li{% if message.tags %} class="{{ message.tags }}" {% endif %}">
{{ message }}
</li>
{% endif %}

View file

@ -11,7 +11,7 @@
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li {% if message.tags %} class="{{ message.tags }}" {% endif %}>
<li {% if message.tags %} class="{{ message.tags }}" {% endif %}">
{{ message }}
</li>
{% endfor %}

View file

@ -321,6 +321,22 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
{% else %}
<input id="last-sent-rejection-email-content" class="display-none" value="None">
{% endif %}
{% elif field.field.name == "requested_suborganization" %}
{{ field.field }}
<div class="requested-suborganization--clear-button">
<button
id="clear-requested-suborganization"
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled"
type="button"
>
<svg
class="usa-icon"
>
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
Clear requested suborganization
</button>
</div>
{% else %}
{{ field.field }}
{% endif %}

View file

@ -1,6 +1,6 @@
{% comment %} This view provides a detail button that can be used to show/hide content {% endcomment %}
<details class="margin-top-1 dja-detail-table" aria-role="button" {% if start_open %}open{% else %}closed{% endif %}>
<details class="margin-top-1 dja-detail-table" aria-role="button" {% if start_open %}open{% else %}closed{% endif %}">
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
{% block detail_content %}

View file

@ -11,7 +11,7 @@
{% endblock %}
{% block help_text %}
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}>
<div class="help margin-bottom-1" {% if field.field.id_for_label %} id="{{ field.field.id_for_label }}_helptext"{% endif %}">
{% if field.field.name == "state" %}
<div>{{ state_help_message }}</div>
{% else %}

View file

@ -6,14 +6,14 @@
<ul class="mulitple-choice">
{% for choice in choices %}
{% if choice.reset %}
<li{% if choice.selected %} class="selected"{% endif %}>
<li{% if choice.selected %} class="selected"{% endif %}">
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
</li>
{% endif %}
{% endfor %}
{% for choice in choices %}
{% if not choice.reset %}
<li{% if choice.selected %} class="selected"{% endif %}>
<li{% if choice.selected %} class="selected"{% endif %}">
{% if choice.selected and choice.exclude_query_string %}
<a class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">

View file

@ -4,10 +4,10 @@
{% block title %}{{ domain.name }} | {% endblock %}
{% block content %}
<div class="grid-container">
<div class="grid-container grid-container--widescreen">
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-3 ">
<p class="font-body-md margin-top-0 margin-bottom-2
text-primary-darker text-semibold domain-name-wrap"
>

View file

@ -3,8 +3,8 @@
{% block title %}{{form_titles|get_item:steps.current}} | Request a .gov | {% endblock %}
{% block content %}
<div class="grid-container">
<div class="grid-row grid-gap">
<div class="grid-container grid-container--widescreen">
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-3">
{% include 'domain_request_sidebar.html' %}
</div>

View file

@ -4,41 +4,42 @@
{% block title %} Start a request | {% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="grid-row {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
<form class="usa-form usa-form--extra-large" method="post" novalidate>
{% csrf_token %}
<form class="usa-form usa-form--extra-large" method="post" novalidate>
{% csrf_token %}
<h1>Youre about to start your .gov domain request.</h1>
<p>You dont have to complete the process in one session. You can save what you enter and come back to it when youre ready.</p>
{% if portfolio %}
<p>Well use the information you provide to verify your domain request meets our guidelines.</p>
{% else %}
<p>Well use the information you provide to verify your organizations eligibility for a .gov domain. Well also verify that the domain you request meets our guidelines.</p>
{% endif %}
<h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p>
<h2>How well reach you</h2>
<p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:start" class="usa-link">your profile</a> to make updates.</p>
{% include "includes/profile_information.html" with user=user%}
<h1>Youre about to start your .gov domain request.</h1>
<p>You dont have to complete the process in one session. You can save what you enter and come back to it when youre ready.</p>
{% if portfolio %}
<p>Well use the information you provide to verify your domain request meets our guidelines.</p>
{% else %}
<p>Well use the information you provide to verify your organizations eligibility for a .gov domain. Well also verify that the domain you request meets our guidelines.</p>
{% endif %}
<h2>Time to complete the form</h2>
<p>If you have <a href="{% public_site_url 'domains/before/#information-you%E2%80%99ll-need-to-complete-the-domain-request-form' %}" target="_blank" class="usa-link">all the information you need</a>,
completing your domain request might take around 15 minutes.</p>
<h2>How well reach you</h2>
<p>While reviewing your domain request, we may need to reach out with questions. Well also email you when we complete our review. If the contact information below is not correct, visit <a href="{% url 'user-profile' %}?redirect=domain-request:start" class="usa-link">your profile</a> to make updates.</p>
{% include "includes/profile_information.html" with user=user%}
{% block form_buttons %}
<div class="stepnav">
<button
type="submit"
name="submit_button"
value="intro_acknowledge"
class="usa-button"
>Continue</button>
</div>
{% endblock %}
{% block form_buttons %}
<div class="stepnav">
<button
type="submit"
name="submit_button"
value="intro_acknowledge"
class="usa-button"
>Continue</button>
</div>
{% endblock %}
</form>
</form>
<div class="caption margin-top-5"><a href="{% public_site_url 'privacy-policy#pra' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Paperwork Reduction Act statement</a> (OMB control number: 1670-0049; expiration date: 10/31/2026)</div>
</div>
<div class="caption margin-top-5"><a href="{% public_site_url 'privacy-policy#pra' %}" target="_blank" rel="noopener noreferrer" class="usa-link">Paperwork Reduction Act statement</a> (OMB control number: 1670-0049; expiration date: 10/31/2026)</div>
</div>
</main>
{% endblock %}

View file

@ -8,18 +8,20 @@
{% endblock wrapperdiv %}
{% block content %}
<div class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
<div class="grid-container grid-container--widescreen">
<div class="grid-row {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
<h1>Withdraw request for {{ DomainRequest.requested_domain.name }}?</h1>
<h1>Withdraw request for {{ DomainRequest.requested_domain.name }}?</h1>
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -18,7 +18,7 @@
<li class="usa-sidenav__item">
{% url 'domain-dns' pk=domain.id as url %}
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}>
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}">
DNS
</a>
{% if request.path|startswith:url %}

View file

@ -5,12 +5,12 @@
{% block title %} Home | {% endblock %}
{% block content %}
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<main id="main-content" class="grid-container grid-container--widescreen">
{% if user.is_authenticated %}
{# the entire logged in page goes here #}
{% block homepage_content %}
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
<div class="tablet:grid-col-11 desktop:grid-col-10 {% if is_widescreen_centered %}tablet:grid-offset-1{% endif %}">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}

View file

@ -1,6 +1,6 @@
<div class="margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert usa-alert--error">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h4 class="usa-alert__heading">
Header
</h4>

View file

@ -1,6 +1,6 @@
<section class="usa-site-alert usa-site-alert--info margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h4 class="usa-alert__heading">
Header
</h4>

View file

@ -1,6 +1,6 @@
<section class="usa-site-alert usa-site-alert--emergency usa-site-alert--hot-pink margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %} {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %} usa-alert__body--widescreen">
<p class="usa-alert__text maxw-none">
<strong>Attention:</strong> You are on a test site.
</p>

View file

@ -1,6 +1,6 @@
<section class="usa-site-alert usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h3 class="usa-alert__heading">
Service disruption
</h3>

View file

@ -1,6 +1,6 @@
<section class="usa-site-alert usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h3 class="usa-alert__heading">
Header here
</h3>

View file

@ -1,6 +1,6 @@
<section class="usa-site-alert usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h3 class="usa-alert__heading">
System outage
</h3>

View file

@ -1,6 +1,6 @@
<div class="margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert usa-alert--warning">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<h4 class="usa-alert__heading">
Header
</h4>

View file

@ -1,236 +0,0 @@
{% load custom_filters %}
{% load static url_helpers %}
<main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
{% block breadcrumb %}
{% if portfolio %}
{% url 'domain-requests' as url %}
{% else %}
{% url 'home' as url %}
{% endif %}
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
{% if portfolio %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
{% else %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
{% endif %}
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<span>New domain request</span>
{% else %}
<span>{{ DomainRequest.requested_domain.name }}</span>
{% endif %}
</li>
</ol>
</nav>
{% endblock breadcrumb %}
{% block header %}
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<h1>New domain request</h1>
{% else %}
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
{% endif %}
{% endblock header %}
{% block status_summary %}
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
role="region"
aria-labelledby="summary-box-key-information"
>
<div class="usa-summary-box__body">
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
id="summary-box-key-information"
>
<span class="text-bold text-primary-darker">
Status:
</span>
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
</p>
</div>
</div>
<br>
{% endblock status_summary %}
{% block status_metadata %}
{% if portfolio %}
{% if DomainRequest.creator %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
</p>
{% else %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% endif %}
{% endif %}
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
{% comment %}
These are intentionally seperated this way.
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
Leave it this way until we've solidified our requirements.
{% endcomment %}
{% if DomainRequest.status == statuses.STARTED %}
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
<p class="margin-top-1">
{% comment %}
A newly created domain request will not have a value for last_status update.
This is because the status never really updated.
However, if this somehow goes back to started we can default to displaying that new date.
{% endcomment %}
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
</p>
{% endwith %}
{% elif DomainRequest.status == statuses.SUBMITTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.REJECTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
</p>
{% elif DomainRequest.status == statuses.WITHDRAWN %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
</p>
{% else %}
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% endif %}
{% endwith %}
{% endblock status_metadata %}
{% block status_blurb %}
{% if DomainRequest.is_awaiting_review %}
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
{% endif %}
{% endblock status_blurb %}
{% block modify_request %}
{% if DomainRequest.is_withdrawable %}
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
Withdraw request</a>
</p>
{% endif %}
{% endblock modify_request %}
</div>
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
{% block request_summary_header %}
<h2 class="text-primary-darker"> Summary of your domain request </h2>
{% endblock request_summary_header%}
{% block request_summary %}
{% with heading_level='h3' %}
{% with org_type=DomainRequest.get_generic_org_type_display %}
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
{% endwith %}
{% if DomainRequest.tribe_name %}
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
{% if DomainRequest.federally_recognized_tribe %}
<p>Federally-recognized tribe</p>
{% endif %}
{% if DomainRequest.state_recognized_tribe %}
<p>State-recognized tribe</p>
{% endif %}
{% endif %}
{% if DomainRequest.get_federal_type_display %}
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
{% endif %}
{% if DomainRequest.is_election_board %}
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
{% endwith %}
{% endif %}
{% if DomainRequest.organization_name %}
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.about_your_organization %}
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
{% endif %}
{% if DomainRequest.senior_official %}
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.current_websites.all %}
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.requested_domain %}
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
{% endif %}
{% if DomainRequest.alternative_domains.all %}
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.purpose %}
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
{% endif %}
{% if DomainRequest.creator %}
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.other_contacts.all %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
{% else %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
{% endif %}
{# We always show this field even if None #}
{% if DomainRequest %}
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if DomainRequest.cisa_representative_first_name %}
{{ DomainRequest.get_formatted_cisa_rep_name }}
{% else %}
No
{% endif %}
</ul>
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if DomainRequest.anything_else %}
{{DomainRequest.anything_else}}
{% else %}
No
{% endif %}
</ul>
{% endif %}
{% endwith %}
{% endblock request_summary%}
</div>
</main>

View file

@ -4,8 +4,8 @@
{% url 'get_domain_requests_json' as url %}
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
<section class="section-outlined domain-requests {% if portfolio %}section-outlined--border-base-light{% endif %}" id="domain-requests">
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %}section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
{% if not portfolio %}
<h2 id="domain-requests-header" class="display-inline-block">Domain requests</h2>
{% else %}
@ -13,7 +13,7 @@
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %}
<div class="section-outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %} {% if is_widescreen_mode %} section-outlined__search--widescreen {% endif %}">
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
<section aria-label="Domain requests search component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}

View file

@ -12,7 +12,7 @@
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<p class="usa-alert__text maxw-none">
{% if num_expiring_domains == 1%}
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
@ -33,7 +33,7 @@
<!-- Embedding the portfolio value in a data attribute -->
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %}
<div class="section-outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %} {% if is_widescreen_mode %} section-outlined__search--widescreen {% endif %}">
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
<section aria-label="Domains search component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}
@ -78,7 +78,7 @@
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
<div class="usa-alert">
<div class="usa-alert__body {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
<div class="usa-alert__body usa-alert__body--widescreen">
<p class="usa-alert__text maxw-none">
{% if num_expiring_domains == 1%}
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>

View file

@ -3,7 +3,7 @@
<footer class="usa-footer">
<div class="usa-footer__secondary-section">
<div class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<div class="grid-container grid-container--widescreen">
<div class="grid-row grid-gap">
<div
class="
@ -24,7 +24,7 @@
<div class="usa-footer__contact-links
mobile-lg:grid-col-6 flex-align-self-center"
>
<address class="usa-footer__address">
<address class="usa-footer__address maxw-none">
<div class="usa-footer__contact-info grid-row grid-gap-md">
{% if show_manage_your_domains %}
<div class="grid-col-auto">
@ -51,7 +51,7 @@
class="usa-identifier__section usa-identifier__section--masthead"
aria-label="Agency identifier"
>
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
<div class="usa-identifier__container usa-identifier__container--widescreen padding-x--widescreen">
<div class="usa-identifier__logos">
<a rel="noopener noreferrer" target="_blank" href="https://www.cisa.gov" class="usa-identifier__logo"
><img
@ -77,7 +77,7 @@
class="usa-identifier__section usa-identifier__section--required-links"
aria-label="Important links"
>
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
<div class="usa-identifier__container usa-identifier__container--widescreen padding-x--widescreen">
<ul class="usa-identifier__required-links-list">
<li class="usa-identifier__required-links-item">
<a rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/' %}"
@ -119,7 +119,7 @@
class="usa-identifier__section usa-identifier__section--usagov"
aria-label="U.S. government information and services"
>
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
<div class="usa-identifier__container usa-identifier__container--widescreen padding-x--widescreen">
<div class="usa-identifier__usagov-description">
Looking for U.S. government information and services?
</div>

View file

@ -1,7 +1,7 @@
{% load static %}
<header class="usa-header usa-header--basic">
<div class="usa-nav-container {% if is_widescreen_mode %} usa-nav-container--widescreen {% endif %}">
<div class="usa-nav-container usa-nav-container--widescreen padding-x--widescreen">
<div class="usa-navbar">
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
<button type="button" class="usa-menu-btn">Menu</button>

View file

@ -2,13 +2,13 @@
{% load custom_filters %}
<header class="usa-header usa-header--extended">
<div class="usa-navbar {% if is_widescreen_mode %} usa-navbar--widescreen {% endif %}">
<div class="usa-navbar usa-navbar--widescreen padding-x--widescreen">
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
<button type="button" class="usa-menu-btn">Menu</button>
</div>
{% block usa_nav %}
<nav class="usa-nav" aria-label="Primary navigation">
<div class="usa-nav__inner {% if is_widescreen_mode %} usa-nav__inner--widescreen {% endif %}">
<div class="usa-nav__inner usa-nav__inner--widescreen padding-x--widescreen">
<button type="button" class="usa-nav__close">
<img src="{%static 'img/usa-icons/close.svg'%}" role="img" alt="Close" />
</button>

View file

@ -8,7 +8,7 @@
<section class="section-outlined members margin-top-0 section-outlined--border-base-light" id="members">
<div class="section-outlined__header margin-bottom-3 grid-row">
<!-- ---------- SEARCH ---------- -->
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 {% if is_widescreen_mode %} section-outlined__search--widescreen {% endif %}">
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen">
<section aria-label="Members search component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %}

View file

@ -1,218 +1,219 @@
{% load custom_filters %}
{% load static url_helpers %}
<main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
{% block breadcrumb %}
{% if portfolio %}
{% url 'domain-requests' as url %}
{% else %}
{% url 'home' as url %}
{% endif %}
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="{% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="grid-col desktop:grid-col-8 desktop:grid-offset-2 ">
{% block breadcrumb %}
{% if portfolio %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
{% url 'domain-requests' as url %}
{% else %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
{% url 'home' as url %}
{% endif %}
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<span>New domain request</span>
{% else %}
<span>{{ DomainRequest.requested_domain.name }}</span>
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
{% if portfolio %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
{% else %}
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
{% endif %}
</li>
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<span>New domain request</span>
{% else %}
<span>{{ DomainRequest.requested_domain.name }}</span>
{% endif %}
</li>
</ol>
</nav>
{% endblock breadcrumb %}
{% block header %}
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<h1>New domain request</h1>
{% else %}
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
{% endif %}
{% endblock header %}
{% block status_summary %}
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
role="region"
aria-labelledby="summary-box-key-information"
>
<div class="usa-summary-box__body">
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
id="summary-box-key-information"
>
<span class="text-bold text-primary-darker">
Status:
</span>
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
</p>
</div>
</div>
<br>
{% endblock status_summary %}
{% block status_metadata %}
{% if portfolio %}
{% if DomainRequest.creator %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
</p>
{% else %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% endif %}
{% endif %}
</li>
</ol>
</nav>
{% endblock breadcrumb %}
{% block header %}
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
<h1>New domain request</h1>
{% else %}
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
{% endif %}
{% endblock header %}
{% block status_summary %}
<div
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
role="region"
aria-labelledby="summary-box-key-information"
>
<div class="usa-summary-box__body">
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
id="summary-box-key-information"
>
<span class="text-bold text-primary-darker">
Status:
</span>
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
</p>
</div>
</div>
<br>
{% endblock status_summary %}
{% block status_metadata %}
{% if portfolio %}
{% if DomainRequest.creator %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
</p>
{% else %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% endif %}
{% endif %}
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
{% comment %}
These are intentionally seperated this way.
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
Leave it this way until we've solidified our requirements.
{% endcomment %}
{% if DomainRequest.status == statuses.STARTED %}
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
<p class="margin-top-1">
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
{% comment %}
A newly created domain request will not have a value for last_status update.
This is because the status never really updated.
However, if this somehow goes back to started we can default to displaying that new date.
These are intentionally seperated this way.
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
Leave it this way until we've solidified our requirements.
{% endcomment %}
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
</p>
{% endwith %}
{% elif DomainRequest.status == statuses.SUBMITTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.REJECTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
</p>
{% elif DomainRequest.status == statuses.WITHDRAWN %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
</p>
{% else %}
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% endif %}
{% endwith %}
{% endblock status_metadata %}
{% block status_blurb %}
{% if DomainRequest.is_awaiting_review %}
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
{% endif %}
{% endblock status_blurb %}
{% block modify_request %}
{% if DomainRequest.is_withdrawable %}
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
Withdraw request</a>
{% if DomainRequest.status == statuses.STARTED %}
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
<p class="margin-top-1">
{% comment %}
A newly created domain request will not have a value for last_status update.
This is because the status never really updated.
However, if this somehow goes back to started we can default to displaying that new date.
{% endcomment %}
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
</p>
{% endif %}
{% endblock modify_request %}
</div>
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
{% block request_summary_header %}
<h2 class="text-primary-darker"> Summary of your domain request </h2>
{% endblock request_summary_header%}
{% block request_summary %}
{% if portfolio %}
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
{% else %}
{% with heading_level='h3' %}
{% with org_type=DomainRequest.get_generic_org_type_display %}
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
{% endwith %}
{% if DomainRequest.tribe_name %}
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
{% if DomainRequest.federally_recognized_tribe %}
<p>Federally-recognized tribe</p>
{% endif %}
{% if DomainRequest.state_recognized_tribe %}
<p>State-recognized tribe</p>
{% endif %}
{% endif %}
{% if DomainRequest.get_federal_type_display %}
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
{% endif %}
{% if DomainRequest.is_election_board %}
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
{% endwith %}
{% endif %}
{% if DomainRequest.organization_name %}
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.about_your_organization %}
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
{% endif %}
{% if DomainRequest.senior_official %}
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.current_websites.all %}
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.requested_domain %}
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
{% endif %}
{% if DomainRequest.alternative_domains.all %}
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.purpose %}
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
{% endif %}
{% if DomainRequest.creator %}
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.other_contacts.all %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
{% elif DomainRequest.status == statuses.SUBMITTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% elif DomainRequest.status == statuses.REJECTED %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
</p>
{% elif DomainRequest.status == statuses.WITHDRAWN %}
<p class="margin-top-1 margin-bottom-1">
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
</p>
<p class="margin-top-1">
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
</p>
{% else %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
<p class="margin-top-1">
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
</p>
{% endif %}
{% endwith %}
{% endblock status_metadata %}
{% block status_blurb %}
{% if DomainRequest.is_awaiting_review %}
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
{% endif %}
{% endblock status_blurb %}
{% block modify_request %}
{% if DomainRequest.is_withdrawable %}
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
Withdraw request</a>
</p>
{% endif %}
{% endblock modify_request %}
</div>
<div class="grid-col maxw-fit-content desktop:grid-offset-2 ">
{% block request_summary_header %}
<h2 class="text-primary-darker"> Summary of your domain request </h2>
{% endblock request_summary_header%}
{% block request_summary %}
{% if portfolio %}
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
{% else %}
{% with heading_level='h3' %}
{% with org_type=DomainRequest.get_generic_org_type_display %}
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
{% endwith %}
{% if DomainRequest.tribe_name %}
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
{% if DomainRequest.federally_recognized_tribe %}
<p>Federally-recognized tribe</p>
{% endif %}
{% if DomainRequest.state_recognized_tribe %}
<p>State-recognized tribe</p>
{% endif %}
{% endif %}
{% if DomainRequest.get_federal_type_display %}
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
{% endif %}
{% if DomainRequest.is_election_board %}
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
{% endwith %}
{% endif %}
{% if DomainRequest.organization_name %}
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.about_your_organization %}
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
{% endif %}
{% if DomainRequest.senior_official %}
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.current_websites.all %}
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.requested_domain %}
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
{% endif %}
{% if DomainRequest.alternative_domains.all %}
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.purpose %}
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
{% endif %}
{% if DomainRequest.creator %}
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
{% endif %}
{% if DomainRequest.other_contacts.all %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
{% else %}
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
{% endif %}
{# We always show this field even if None #}
{% if DomainRequest %}
@ -236,5 +237,6 @@
{% endwith %}
{% endif %}
{% endblock request_summary%}
</div>
</div>
</main>

View file

@ -1,3 +1,3 @@
<p class="{% if not remove_margin_top %}margin-top-3 {% endif %}">
<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>

View file

@ -1,5 +1,9 @@
{% load static field_helpers url_helpers %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% if can_edit %}
{% include "includes/form_errors.html" with form=form %}
{% endif %}

View file

@ -4,26 +4,26 @@
<div id="wrapper" class="{% block wrapper_class %}wrapper--padding-top-6{% endblock %}">
{% block content %}
<main class="grid-container {% if is_widescreen_mode %} grid-container--widescreen{% endif %}">
<main class="grid-container grid-container--widescreen">
{% if user.is_authenticated %}
{# the entire logged in page goes here #}
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div class="grid-row {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="tablet:grid-col-11 desktop:grid-col-10 {% if is_widescreen_centered %}tablet:grid-offset-1{% endif %}">
{% block portfolio_content %}{% endblock %}
</div>
{% else %} {# not user.is_authenticated #}
{# the entire logged out page goes here #}
<p><a class="usa-button" href="{% url 'login' %}">
Sign in
</a></p>
{% endif %}
</div>
{% else %} {# not user.is_authenticated #}
{# the entire logged out page goes here #}
<p><a class="usa-button" href="{% url 'login' %}">
Sign in
</a></p>
{% endif %}
</div>
</main>
{% endblock content%}

View file

@ -9,9 +9,11 @@
{% endblock %}
{% block portfolio_content %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
{% endblock messages%}
<div id="main-content">
<h1 id="domains-header">Domains</h1>

View file

@ -8,7 +8,12 @@ Organization member
{% load static %}
{% block portfolio_content %}
<div id="main-content">
<div id="main-content" class="{% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% url 'members' as url %}
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">

View file

@ -6,7 +6,12 @@
{% load static %}
{% block portfolio_content %}
<div id="main-content">
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% url 'members' as url %}
{% if portfolio_permission %}

View file

@ -6,7 +6,12 @@
{% load static %}
{% block portfolio_content %}
<div id="main-content">
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% url 'members' as url %}
{% if portfolio_permission %}

View file

@ -11,82 +11,89 @@
{% block portfolio_content %}
{% include "includes/form_errors.html" with form=form %}
<!-- Navigation breadcrumbs -->
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<a href="{% url 'members' %}" class="usa-breadcrumb__link"><span>Members</span></a>
</li>
<li class="usa-breadcrumb__list-item">
{% if member %}
{% url 'member' pk=member.pk as back_url %}
{% elif invitation %}
{% url 'invitedmember' pk=invitation.pk as back_url %}
{% endif %}
<a href="{{ back_url }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
</li>
{% comment %} Manage members {% endcomment %}
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Member access and permissions</span>
</li>
</ol>
</nav>
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
<!-- Navigation breadcrumbs -->
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Domain request breadcrumb">
<ol class="usa-breadcrumb__list">
<li class="usa-breadcrumb__list-item">
<a href="{% url 'members' %}" class="usa-breadcrumb__link"><span>Members</span></a>
</li>
<li class="usa-breadcrumb__list-item">
{% if member %}
{% url 'member' pk=member.pk as back_url %}
{% elif invitation %}
{% url 'invitedmember' pk=invitation.pk as back_url %}
{% endif %}
<a href="{{ back_url }}" class="usa-breadcrumb__link"><span>Manage member</span></a>
</li>
{% comment %} Manage members {% endcomment %}
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
<span>Member access and permissions</span>
</li>
</ol>
</nav>
<!-- Page header -->
<h1>Member access and permissions</h1>
<!-- Page header -->
<h1>Member access and permissions</h1>
{% include "includes/required_fields.html" with remove_margin_top=True %}
{% include "includes/required_fields.html" with remove_margin_top=True %}
<form class="usa-form usa-form--large" method="post" id="member_form" novalidate>
{% csrf_token %}
<form class="usa-form usa-form--large" method="post" id="member_form" novalidate>
{% csrf_token %}
<fieldset class="usa-fieldset">
<legend>
{% if member and member.email or invitation and invitation.email %}
<h2 class="margin-top-1">Member email</h2>
{% else %}
<h2 class="margin-top-1">Member</h2>
{% endif %}
</legend>
<p class="margin-top-0">
{% comment %}
Show member email if possible, then invitation email.
If neither of these are true, show the name or as a last resort just "None".
{% endcomment %}
{% if member %}
{% if member.email %}
{{ member.email }}
{% else %}
{{ member.get_formatted_name }}
{% endif %}
{% elif invitation %}
{% if invitation.email %}
{{ invitation.email }}
{% else %}
None
{% endif %}
{% endif %}
</p>
<!-- Member email -->
</fieldset>
<!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset">
<legend>
{% if member and member.email or invitation and invitation.email %}
<h2 class="margin-top-1">Member email</h2>
{% else %}
<h2 class="margin-top-1">Member</h2>
{% endif %}
<h2 class="margin-top-0">Member Access</h2>
</legend>
<p class="margin-top-0">
{% comment %}
Show member email if possible, then invitation email.
If neither of these are true, show the name or as a last resort just "None".
{% endcomment %}
{% if member %}
{% if member.email %}
{{ member.email }}
{% else %}
{{ member.get_formatted_name }}
{% endif %}
{% elif invitation %}
{% if invitation.email %}
{{ invitation.email }}
{% else %}
None
{% endif %}
{% endif %}
</p>
<!-- Member email -->
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors form.role %}
{% endwith %}
</fieldset>
<!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset">
<legend>
<h2 class="margin-top-0">Member Access</h2>
</legend>
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors form.role %}
{% endwith %}
</fieldset>
<!-- Admin access form -->
<div id="member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<!-- Admin access form -->
<div id="member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<h3 class="summary-item__title
text-primary-dark
@ -104,10 +111,10 @@
{% endwith %}
</div>
<!-- Basic access form -->
<div id="member-basic-permissions" class="margin-top-2">
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<!-- Basic access form -->
<div id="member-basic-permissions" class="margin-top-2">
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<h3 class="margin-bottom-0 summary-item__title text-primary-dark">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
@ -115,18 +122,19 @@
{% endwith %}
</div>
<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<a
type="button"
href="{{ back_url }}"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel editing member"
>
Cancel
</a>
<button type="submit" class="usa-button">Update Member</button>
</div>
</form>
<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<a
type="button"
href="{{ back_url }}"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel editing member"
>
Cancel
</a>
<button type="submit" class="usa-button">Update Member</button>
</div>
</form>
</div>
{% endblock portfolio_content%}

View file

@ -10,6 +10,11 @@
{% block portfolio_content %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
<div id="main-content">
<div id="toggleable-alert" class="usa-alert usa-alert--slim margin-bottom-2 display-none">
<div class="usa-alert__body usa-alert__body--widescreen">

View file

@ -10,11 +10,12 @@
{% block portfolio_content %}
<!-- Form mesages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
<div id="main-content" class=" {% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
<!-- Navigation breadcrumbs -->
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Domain request breadcrumb">
@ -28,44 +29,44 @@
</ol>
</nav>
<!-- Page header -->
{% block new_member_header %}
<h1>Add a new member</h1>
{% endblock new_member_header %}
<!-- Page header -->
{% block new_member_header %}
<h1>Add a new member</h1>
{% endblock new_member_header %}
{% include "includes/required_fields.html" %}
{% include "includes/required_fields.html" %}
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Email</h2>
</legend>
<!-- Member email -->
{% csrf_token %}
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.email %}
{% endwith %}
</fieldset>
<form class="usa-form usa-form--large" method="post" id="add_member_form" novalidate>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Email</h2>
</legend>
<!-- Member email -->
{% csrf_token %}
{% with group_classes="usa-form-editable usa-form-editable--no-border padding-top-0" %}
{% input_with_errors form.email %}
{% endwith %}
</fieldset>
<!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Member Access</h2>
</legend>
<!-- Member access radio buttons (Toggles other sections) -->
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Member Access</h2>
</legend>
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
<em>Select the level of access for this member. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% input_with_errors form.role %}
{% endwith %}
</fieldset>
</fieldset>
<!-- Admin access form -->
<div id="new-member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<!-- Admin access form -->
<div id="new-member-admin-permissions" class="margin-top-2">
<h2>Admin access permissions</h2>
<p>Member permissions available for admin-level acccess.</p>
<h3 class="summary-item__title
text-primary-dark
@ -83,10 +84,10 @@
{% endwith %}
</div>
<!-- Basic access form -->
<div id="new-member-basic-permissions" class="margin-top-2">
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<!-- Basic access form -->
<div id="new-member-basic-permissions" class="margin-top-2">
<h2>Basic member permissions</h2>
<p>Member permissions available for basic-level acccess.</p>
<h3 class="margin-bottom-0">Organization domain requests</h3>
{% with group_classes="usa-form-editable usa-form-editable--no-border bg-gray-1 padding-top-0" %}
@ -94,26 +95,26 @@
{% endwith %}
</div>
<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<a
type="button"
href="{% url 'members' %}"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel adding new member"
>Cancel
</a>
<a
id="invite_member_trigger"
href="#invite-member-modal"
class="usa-button usa-button--outline margin-top-1 display-none"
aria-controls="invite-member-modal"
data-open-modal
>Trigger invite member modal</a>
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
</div>
</form>
<!-- Submit/cancel buttons -->
<div class="margin-top-3">
<a
type="button"
href="{% url 'members' %}"
class="usa-button usa-button--outline"
name="btn-cancel-click"
aria-label="Cancel adding new member"
>Cancel
</a>
<a
id="invite_member_trigger"
href="#invite-member-modal"
class="usa-button usa-button--outline margin-top-1 display-none"
aria-controls="invite-member-modal"
data-open-modal
>Trigger invite member modal</a>
<button id="invite_new_member_submit" type="submit" class="usa-button">Invite Member</button>
</div>
</form>
<div
class="usa-modal"
@ -133,43 +134,44 @@
<h4 class="text-primary">Email</h4>
<p class="margin-top-0" id="modalEmail"></p>
<h4 class="text-primary">Member Access</h4>
<p class="margin-top-0" id="modalAccessLevel"></p>
<h4 class="text-primary">Member Access</h4>
<p class="margin-top-0" id="modalAccessLevel"></p>
<!-- Dynamic Permissions Details -->
<div id="permission_details"></div>
</div>
<!-- Dynamic Permissions Details -->
<div id="permission_details"></div>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
</li>
<li class="usa-button-group__item">
<button
type="button"
class="usa-button usa-button--unstyled"
data-close-modal
onclick="closeModal()"
>
Cancel
</button>
</li>
</ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
onclick="closeModal()"
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{% static 'img/sprite.svg' %}#close"></use>
</svg>
</button>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button id="confirm_new_member_submit" type="submit" class="usa-button">Yes, invite member</button>
</li>
<li class="usa-button-group__item">
<button
type="button"
class="usa-button usa-button--unstyled"
data-close-modal
onclick="closeModal()"
>
Cancel
</button>
</li>
</ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
onclick="closeModal()"
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{% static 'img/sprite.svg' %}#close"></use>
</svg>
</button>
</div>
</div>
</div>

View file

@ -4,8 +4,13 @@
{% block title %} Domains | {% endblock %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% block portfolio_content %}
<div id="main-content">
<div id="main-content" class="{% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<h1 id="domains-header">Domains</h1>
<section class="section-outlined">
<div class="section-outlined__header margin-bottom-3">

View file

@ -4,7 +4,13 @@
{% block title %} Domain Requests | {% endblock %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
{% block portfolio_content %}
<div id="main-content" class="{% if not is_widescreen_centered %}desktop:grid-offset-2{% endif %}">
<h1 id="domains-header">Domain requests</h1>
<section class="section-outlined">
<div class="section-outlined__header margin-bottom-3">
@ -27,4 +33,5 @@
{% endif %}
</div>
</section>
</div>
{% endblock %}

View file

@ -18,12 +18,11 @@
</div>
<div class="tablet:grid-col-9" id="main-content">
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<!-- Form messages -->
{% include "includes/form_errors.html" with form=form %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock messages%}
<h1>Organization</h1>

View file

@ -9,9 +9,11 @@
{% endblock %}
{% block portfolio_content %}
<!-- Form messages -->
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
{% endblock messages%}
<div id="main-content">
<h1 id="domain-requests-header" class="margin-bottom-1">Domain requests</h1>

View file

@ -6,9 +6,6 @@
{% load static %}
{% block portfolio_content %}
{% block messages %}
{% include "includes/form_messages.html" %}
{% endblock %}
<div class="grid-row grid-gap">
<div class="tablet:grid-col-3">

View file

@ -11,90 +11,92 @@ Edit your User Profile |
{% endblock %}
{% block content %}
<main id="main-content" class="grid-container">
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
{% if messages %}
{% for message in messages %}
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}
{% include "includes/form_errors.html" with form=form %}
<main id="main-content" class="grid-container grid-container--widescreen">
<div class="{% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
<div class="desktop:grid-col-8 desktop:grid-offset-2">
{% if messages %}
{% for message in messages %}
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
<div class="usa-alert__body">
{{ message }}
</div>
</div>
{% endfor %}
{% endif %}
{% include "includes/form_errors.html" with form=form %}
{% if show_back_button %}
<a href="{% url form.initial.redirect %}" 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>
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
{{ profile_back_button_text }}
</p>
</a>
{% endif %}
{% if show_confirmation_modal %}
<a
href="#toggle-confirmation-modal"
class="usa-button display-none show-confirmation-modal"
aria-controls="toggle-confirmation-modal"
data-open-modal
>Open confirmation modal</a>
<div
class="usa-modal usa-modal--lg is-visible"
id="toggle-confirmation-modal"
aria-labelledby="Add contact information"
aria-describedby="Add contact information"
data-force-action
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading">
Add contact information
</h2>
<div class="usa-prose">
<p>
.Gov domain registrants must maintain accurate contact information in the .gov registrar.
Before you can manage your domain, we need you to add your contact information.
</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button
type="button"
class="usa-button padding-105 text-center"
data-close-modal
>
{% if show_back_button %}
<a href="{% url form.initial.redirect %}" 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>
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
{{ profile_back_button_text }}
</p>
</a>
{% endif %}
{% if show_confirmation_modal %}
<a
href="#toggle-confirmation-modal"
class="usa-button display-none show-confirmation-modal"
aria-controls="toggle-confirmation-modal"
data-open-modal
>Open confirmation modal</a>
<div
class="usa-modal usa-modal--lg is-visible"
id="toggle-confirmation-modal"
aria-labelledby="Add contact information"
aria-describedby="Add contact information"
data-force-action
>
<div class="usa-modal__content">
<div class="usa-modal__main">
<h2 class="usa-modal__heading">
Add contact information
</button>
</li>
</ul>
</h2>
<div class="usa-prose">
<p>
.Gov domain registrants must maintain accurate contact information in the .gov registrar.
Before you can manage your domain, we need you to add your contact information.
</p>
</div>
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
<button
type="button"
class="usa-button padding-105 text-center"
data-close-modal
>
Add contact information
</button>
</li>
</ul>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
</button>
</div>
</div>
<button
type="button"
class="usa-button usa-modal__close"
aria-label="Close this window"
data-close-modal
>
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg>
</button>
</div>
{% endif %}
{% endblock content %}
{% block content_bottom %}
{% include "includes/profile_form.html" with form=form %}
</div>
{% endif %}
{% endblock content %}
{% block content_bottom %}
{% include "includes/profile_form.html" with form=form %}
</div>
</div>
</main>
{% endblock content_bottom %}

View file

@ -1034,6 +1034,10 @@ def completed_domain_request( # noqa
action_needed_reason=None,
portfolio=None,
organization_name=None,
sub_organization=None,
requested_suborganization=None,
suborganization_city=None,
suborganization_state_territory=None,
):
"""A completed domain request."""
if not user:
@ -1098,6 +1102,18 @@ def completed_domain_request( # noqa
if portfolio:
domain_request_kwargs["portfolio"] = portfolio
if sub_organization:
domain_request_kwargs["sub_organization"] = sub_organization
if requested_suborganization:
domain_request_kwargs["requested_suborganization"] = requested_suborganization
if suborganization_city:
domain_request_kwargs["suborganization_city"] = suborganization_city
if suborganization_state_territory:
domain_request_kwargs["suborganization_state_territory"] = suborganization_state_territory
domain_request, _ = DomainRequest.objects.get_or_create(**domain_request_kwargs)
if has_other_contacts:

View file

@ -1,5 +1,7 @@
from datetime import datetime
from django.forms import ValidationError
from django.utils import timezone
from waffle.testutils import override_flag
import re
from django.test import RequestFactory, Client, TestCase, override_settings
from django.contrib.admin.sites import AdminSite
@ -24,6 +26,7 @@ from registrar.models import (
SeniorOfficial,
Portfolio,
AllowedEmail,
Suborganization,
)
from .common import (
MockSESClient,
@ -82,6 +85,7 @@ class TestDomainRequestAdmin(MockEppLib):
Contact.objects.all().delete()
Website.objects.all().delete()
SeniorOfficial.objects.all().delete()
Suborganization.objects.all().delete()
Portfolio.objects.all().delete()
self.mock_client.EMAILS_SENT.clear()
@ -91,6 +95,83 @@ class TestDomainRequestAdmin(MockEppLib):
User.objects.all().delete()
AllowedEmail.objects.all().delete()
@override_flag("organization_feature", active=True)
@less_console_noise_decorator
def test_clean_validates_duplicate_suborganization(self):
"""Tests that clean() prevents duplicate suborganization names within the same portfolio"""
# Create a portfolio and existing suborganization
portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
# Create an existing suborganization
Suborganization.objects.create(name="Existing Suborg", portfolio=portfolio)
# Create a domain request trying to use the same suborganization name
# (intentionally lowercase)
domain_request = completed_domain_request(
name="test1234.gov",
portfolio=portfolio,
requested_suborganization="existing suborg",
suborganization_city="Rome",
suborganization_state_territory=DomainRequest.StateTerritoryChoices.OHIO,
)
# Assert that the validation error is raised
with self.assertRaises(ValidationError) as err:
domain_request.clean()
self.assertIn("This suborganization already exists", str(err.exception))
# Test that a different name is allowed. Should not raise a error.
domain_request.requested_suborganization = "New Suborg"
domain_request.clean()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
def test_clean_validates_partial_suborganization_fields(self):
"""Tests that clean() enforces all-or-nothing rule for suborganization fields"""
portfolio = Portfolio.objects.create(organization_name="Test Portfolio", creator=self.superuser)
# Create domain request with only city filled out
domain_request = completed_domain_request(
name="test1234.gov",
portfolio=portfolio,
suborganization_city="Test City",
)
# Assert validation error is raised with correct missing fields
with self.assertRaises(ValidationError) as err:
domain_request.clean()
error_dict = err.exception.error_dict
expected_missing = ["requested_suborganization", "suborganization_state_territory"]
# Verify correct fields are flagged as required
self.assertEqual(sorted(error_dict.keys()), sorted(expected_missing))
# Verify error message
for field in expected_missing:
self.assertEqual(
str(error_dict[field][0].message), "This field is required when creating a new suborganization."
)
# When all data is passed in, this should validate correctly
domain_request.requested_suborganization = "Complete Suborg"
domain_request.suborganization_state_territory = DomainRequest.StateTerritoryChoices.OHIO
# Assert that no ValidationError is raised
try:
domain_request.clean()
except ValidationError as e:
self.fail(f"ValidationError was raised unexpectedly: {e}")
# Also ensure that no validation error is raised if nothing is passed in at all
domain_request.suborganization_city = None
domain_request.requested_suborganization = None
domain_request.suborganization_state_territory = None
try:
domain_request.clean()
except ValidationError as e:
self.fail(f"ValidationError was raised unexpectedly: {e}")
@less_console_noise_decorator
def test_domain_request_senior_official_is_alphabetically_sorted(self):
"""Tests if the senior offical dropdown is alphanetically sorted in the django admin display"""

View file

@ -15,6 +15,7 @@ from registrar.models import (
FederalAgency,
AllowedEmail,
Portfolio,
Suborganization,
)
import boto3_mocking
@ -23,6 +24,8 @@ from registrar.utility.errors import FSMDomainRequestError
from .common import (
MockSESClient,
create_user,
create_superuser,
less_console_noise,
completed_domain_request,
set_domain_request_investigators,
@ -1070,3 +1073,142 @@ class TestDomainRequest(TestCase):
)
self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type)
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency)
class TestDomainRequestSuborganization(TestCase):
"""Tests for the suborganization fields on domain requests"""
def setUp(self):
super().setUp()
self.user = create_user()
self.superuser = create_superuser()
def tearDown(self):
super().tearDown()
DomainInformation.objects.all().delete()
DomainRequest.objects.all().delete()
Domain.objects.all().delete()
Suborganization.objects.all().delete()
Portfolio.objects.all().delete()
@less_console_noise_decorator
def test_approve_creates_requested_suborganization(self):
"""Test that approving a domain request with a requested suborganization creates it"""
portfolio = Portfolio.objects.create(organization_name="Test Org", creator=self.user)
domain_request = completed_domain_request(
name="test.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
requested_suborganization="Boom",
suborganization_city="Explody town",
suborganization_state_territory=DomainRequest.StateTerritoryChoices.OHIO,
)
domain_request.investigator = self.superuser
domain_request.save()
domain_request.approve()
created_suborg = Suborganization.objects.filter(
name="Boom",
city="Explody town",
state_territory=DomainRequest.StateTerritoryChoices.OHIO,
portfolio=portfolio,
).first()
self.assertIsNotNone(created_suborg)
self.assertEqual(domain_request.sub_organization, created_suborg)
@less_console_noise_decorator
def test_approve_without_requested_suborganization_makes_no_changes(self):
"""Test that approving without a requested suborganization doesn't create one"""
portfolio = Portfolio.objects.create(organization_name="Test Org", creator=self.user)
domain_request = completed_domain_request(
name="test.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
)
domain_request.investigator = self.superuser
domain_request.save()
initial_suborg_count = Suborganization.objects.count()
domain_request.approve()
self.assertEqual(Suborganization.objects.count(), initial_suborg_count)
self.assertIsNone(domain_request.sub_organization)
@less_console_noise_decorator
def test_approve_with_existing_suborganization_makes_no_changes(self):
"""Test that approving with an existing suborganization doesn't create a new one"""
portfolio = Portfolio.objects.create(organization_name="Test Org", creator=self.user)
existing_suborg = Suborganization.objects.create(name="Existing Division", portfolio=portfolio)
domain_request = completed_domain_request(
name="test.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
sub_organization=existing_suborg,
)
domain_request.investigator = self.superuser
domain_request.save()
initial_suborg_count = Suborganization.objects.count()
domain_request.approve()
self.assertEqual(Suborganization.objects.count(), initial_suborg_count)
self.assertEqual(domain_request.sub_organization, existing_suborg)
@less_console_noise_decorator
def test_cleanup_dangling_suborg_with_single_reference(self):
"""Test that a suborganization is deleted when it's only referenced once"""
portfolio = Portfolio.objects.create(organization_name="Test Org", creator=self.user)
suborg = Suborganization.objects.create(name="Test Division", portfolio=portfolio)
domain_request = completed_domain_request(
name="test.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
sub_organization=suborg,
)
domain_request.approve()
# set it back to in review
domain_request.in_review()
domain_request.refresh_from_db()
# Verify the suborganization was deleted
self.assertFalse(Suborganization.objects.filter(id=suborg.id).exists())
self.assertIsNone(domain_request.sub_organization)
@less_console_noise_decorator
def test_cleanup_dangling_suborg_with_multiple_references(self):
"""Test that a suborganization is preserved when it has multiple references"""
portfolio = Portfolio.objects.create(organization_name="Test Org", creator=self.user)
suborg = Suborganization.objects.create(name="Test Division", portfolio=portfolio)
# Create two domain requests using the same suborganization
domain_request1 = completed_domain_request(
name="test1.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
sub_organization=suborg,
)
domain_request2 = completed_domain_request(
name="test2.gov",
portfolio=portfolio,
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
sub_organization=suborg,
)
domain_request1.approve()
domain_request2.approve()
# set one back to in review
domain_request1.in_review()
domain_request1.refresh_from_db()
# Verify the suborganization still exists
self.assertTrue(Suborganization.objects.filter(id=suborg.id).exists())
self.assertEqual(domain_request1.sub_organization, suborg)
self.assertEqual(domain_request2.sub_organization, suborg)

View file

@ -2576,6 +2576,46 @@ class TestRequestingEntity(WebTest):
User.objects.all().delete()
super().tearDown()
@less_console_noise_decorator
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
def test_form_validates_duplicate_suborganization(self):
"""Tests that form validation prevents duplicate suborganization names within the same portfolio"""
# Create an existing suborganization
suborganization = Suborganization.objects.create(name="Existing Suborg", portfolio=self.portfolio)
# Start the domain request process
response = self.app.get(reverse("domain-request:start"))
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
# Navigate past the intro page
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
form = response.forms[0]
response = form.submit().follow()
# Fill out the requesting entity form
form = response.forms[0]
form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = "True"
form["portfolio_requesting_entity-is_requesting_new_suborganization"] = "True"
form["portfolio_requesting_entity-requested_suborganization"] = suborganization.name.lower()
form["portfolio_requesting_entity-suborganization_city"] = "Eggnog"
form["portfolio_requesting_entity-suborganization_state_territory"] = DomainRequest.StateTerritoryChoices.OHIO
# Submit form and verify error
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit()
self.assertContains(response, "This suborganization already exists")
# Test that a different name is allowed
form["portfolio_requesting_entity-requested_suborganization"] = "New Suborg"
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
response = form.submit().follow()
# Verify successful submission by checking we're on the next page
self.assertContains(response, "Current websites")
@override_flag("organization_feature", active=True)
@override_flag("organization_requests", active=True)
@less_console_noise_decorator

View file

@ -417,7 +417,7 @@ class MemberExport(BaseExport):
# Adding a order_by increases output predictability.
# Doesn't matter as much for normal use, but makes tests easier.
# We should also just be ordering by default anyway.
members = permissions.union(invitations).order_by("email_display")
members = permissions.union(invitations).order_by("email_display", "member_display", "first_name", "last_name")
return convert_queryset_to_dict(members, is_model=False)
@classmethod