Merge branch 'main' into XX/documentation-for-cloning-workflow

This commit is contained in:
Matt-Spence 2025-01-06 10:44:57 -05:00 committed by GitHub
commit 04e73ce42b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
141 changed files with 5837 additions and 2551 deletions

View file

@ -4,7 +4,6 @@ Pull requests should be titled in the format of `#issue_number: Descriptive name
Pull requests including a migration should be suffixed with ` - MIGRATION`
After creating a pull request, pull request submitters should:
- Add at least 2 developers as PR reviewers (only 1 will need to approve).
- Message on Slack or in standup to notify the team that a PR is ready for review.
- If any model was updated to modify/add/delete columns, run makemigrations and commit the associated migrations file.

View file

@ -4,7 +4,7 @@ verify_ssl = true
name = "pypi"
[packages]
django = "4.2.10"
django = "4.2.17"
cfenv = "*"
django-cors-headers = "*"
pycryptodomex = "*"

1318
src/Pipfile.lock generated

File diff suppressed because it is too large Load diff

53
src/package-lock.json generated
View file

@ -6921,16 +6921,6 @@
"validate-npm-package-license": "^3.0.1"
}
},
"node_modules/normalize-package-data/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
@ -7307,39 +7297,6 @@
"node": ">= 12"
}
},
"node_modules/pa11y/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pa11y/node_modules/semver": {
"version": "7.3.8",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz",
"integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==",
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/pa11y/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/parse-filepath": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz",
@ -8888,13 +8845,15 @@
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/semver-greatest-satisfied-range": {

View file

@ -22,5 +22,8 @@
"sass-loader": "^12.6.0",
"webpack": "^5.96.1",
"webpack-stream": "^7.0.0"
},
"overrides": {
"semver": "^7.5.3"
}
}

View file

@ -21,10 +21,14 @@ from registrar.utility.admin_helpers import (
get_field_links_as_list,
)
from django.conf import settings
from django.contrib.messages import get_messages
from django.contrib.admin.helpers import AdminForm
from django.shortcuts import redirect
from django_fsm import get_available_FIELD_transitions, FSMField
from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
from registrar.utility.email import EmailSendingError
from registrar.utility.email_invitations import send_portfolio_invitation_email
from waffle.decorators import flag_is_active
from django.contrib import admin, messages
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
@ -37,7 +41,7 @@ from waffle.admin import FlagAdmin
from waffle.models import Sample, Switch
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
from registrar.utility.constants import BranchChoices
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes, MissingEmailError
from registrar.utility.waffle import flag_is_active_for_user
from registrar.views.utility.mixins import OrderableFieldsMixin
from django.contrib.admin.views.main import ORDER_VAR
@ -1312,6 +1316,8 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"]
search_help_text = "Search by first name, last name, email, or portfolio."
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
def get_roles(self, obj):
readable_roles = obj.get_readable_roles()
return ", ".join(readable_roles)
@ -1468,7 +1474,7 @@ class PortfolioInvitationAdmin(ListHeaderAdmin):
autocomplete_fields = ["portfolio"]
change_form_template = "django/admin/email_clipboard_change_form.html"
change_form_template = "django/admin/portfolio_invitation_change_form.html"
# Select portfolio invitations to change -> Portfolio invitations
def changelist_view(self, request, extra_context=None):
@ -1478,6 +1484,118 @@ class PortfolioInvitationAdmin(ListHeaderAdmin):
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def save_model(self, request, obj, form, change):
"""
Override the save_model method.
Only send email on creation of the PortfolioInvitation object. Not on updates.
Emails sent to requested user / email.
When exceptions are raised, return without saving model.
"""
if not change: # Only send email if this is a new PortfolioInvitation (creation)
portfolio = obj.portfolio
requested_email = obj.email
requestor = request.user
permission_exists = UserPortfolioPermission.objects.filter(
user__email=requested_email, portfolio=portfolio, user__email__isnull=False
).exists()
try:
if not permission_exists:
# if permission does not exist for a user with requested_email, send email
send_portfolio_invitation_email(email=requested_email, requestor=requestor, portfolio=portfolio)
messages.success(request, f"{requested_email} has been invited.")
else:
messages.warning(request, "User is already a member of this portfolio.")
except Exception as e:
# when exception is raised, handle and do not save the model
self._handle_exceptions(e, request, obj)
return
# Call the parent save method to save the object
super().save_model(request, obj, form, change)
def _handle_exceptions(self, exception, request, obj):
"""Handle exceptions raised during the process.
Log warnings / errors, and message errors to the user.
"""
if isinstance(exception, EmailSendingError):
logger.warning(
"Could not sent email invitation to %s for portfolio %s (EmailSendingError)",
obj.email,
obj.portfolio,
exc_info=True,
)
messages.error(request, "Could not send email invitation. Portfolio invitation not saved.")
elif isinstance(exception, MissingEmailError):
messages.error(request, str(exception))
logger.error(
f"Can't send email to '{obj.email}' for portfolio '{obj.portfolio}'. "
f"No email exists for the requestor.",
exc_info=True,
)
else:
logger.warning("Could not send email invitation (Other Exception)", exc_info=True)
messages.error(request, "Could not send email invitation. Portfolio invitation not saved.")
def response_add(self, request, obj, post_url_continue=None):
"""
Override response_add to handle rendering when exceptions are raised during add model.
Normal flow on successful save_model on add is to redirect to changelist_view.
If there are errors, flow is modified to instead render change form.
"""
# Check if there are any error or warning messages in the `messages` framework
storage = get_messages(request)
has_errors = any(message.level_tag in ["error", "warning"] for message in storage)
if has_errors:
# Re-render the change form if there are errors or warnings
# Prepare context for rendering the change form
# Get the model form
ModelForm = self.get_form(request, obj=obj)
form = ModelForm(instance=obj)
# Create an AdminForm instance
admin_form = AdminForm(
form,
list(self.get_fieldsets(request, obj)),
self.get_prepopulated_fields(request, obj),
self.get_readonly_fields(request, obj),
model_admin=self,
)
media = self.media + form.media
opts = obj._meta
change_form_context = {
**self.admin_site.each_context(request), # Add admin context
"title": f"Add {opts.verbose_name}",
"opts": opts,
"original": obj,
"save_as": self.save_as,
"has_change_permission": self.has_change_permission(request, obj),
"add": True, # Indicate this is an "Add" form
"change": False, # Indicate this is not a "Change" form
"is_popup": False,
"inline_admin_formsets": [],
"save_on_top": self.save_on_top,
"show_delete": self.has_delete_permission(request, obj),
"obj": obj,
"adminform": admin_form, # Pass the AdminForm instance
"media": media,
"errors": None,
}
return self.render_change_form(
request,
context=change_form_context,
add=True,
change=False,
obj=obj,
)
return super().response_add(request, obj, post_url_continue)
class DomainInformationResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1830,10 +1948,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
form = DomainRequestAdminForm
change_form_template = "django/admin/domain_request_change_form.html"
# ------ Filters ------
# Define custom filters
class StatusListFilter(MultipleChoiceListFilter):
"""Custom status filter which is a multiple choice filter"""
title = "Status"
title = "status"
parameter_name = "status__in"
template = "django/admin/multiple_choice_list_filter.html"
@ -1877,7 +1997,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
If we have a portfolio, use the portfolio's federal type. If not, use the
organization in the Domain Request object."""
title = "federal Type"
title = "federal type"
parameter_name = "converted_federal_types"
def lookups(self, request, model_admin):
@ -1965,13 +2085,58 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if self.value() == "0":
return queryset.filter(Q(is_election_board=False) | Q(is_election_board=None))
class PortfolioFilter(admin.SimpleListFilter):
"""Define a custom filter for portfolio"""
title = _("portfolio")
parameter_name = "portfolio__isnull"
def lookups(self, request, model_admin):
return (
("1", _("Yes")),
("0", _("No")),
)
def queryset(self, request, queryset):
if self.value() == "1":
return queryset.filter(Q(portfolio__isnull=False))
if self.value() == "0":
return queryset.filter(Q(portfolio__isnull=True))
# ------ Custom fields ------
def custom_election_board(self, obj):
return "Yes" if obj.is_election_board else "No"
custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
@admin.display(description=_("Requested Domain"))
def custom_requested_domain(self, obj):
# Example: Show different icons based on `status`
url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}"
text = obj.requested_domain
if obj.portfolio:
return format_html('<a href="{}"><img src="/public/admin/img/icon-yes.svg"> {}</a>', url, text)
return format_html('<a href="{}">{}</a>', url, text)
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore
# ------ Converted fields ------
# These fields map to @Property methods and
# require these custom definitions to work properly
@admin.display(description=_("Generic Org Type"))
def converted_generic_org_type(self, obj):
return obj.converted_generic_org_type_display
@admin.display(description=_("Organization Name"))
def converted_organization_name(self, obj):
return obj.converted_organization_name
# Example: Show different icons based on `status`
if obj.portfolio:
url = reverse("admin:registrar_portfolio_change", args=[obj.portfolio.id])
text = obj.converted_organization_name
return format_html('<a href="{}">{}</a>', url, text)
else:
return obj.converted_organization_name
@admin.display(description=_("Federal Agency"))
def converted_federal_agency(self, obj):
@ -1989,34 +2154,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def converted_state_territory(self, obj):
return obj.converted_state_territory
# Columns
list_display = [
"requested_domain",
"first_submitted_date",
"last_submitted_date",
"last_status_update",
"status",
"custom_election_board",
"converted_generic_org_type",
"converted_organization_name",
"converted_federal_agency",
"converted_federal_type",
"converted_city",
"converted_state_territory",
"investigator",
]
orderable_fk_fields = [
("requested_domain", "name"),
("investigator", ["first_name", "last_name"]),
]
def custom_election_board(self, obj):
return "Yes" if obj.is_election_board else "No"
custom_election_board.admin_order_field = "is_election_board" # type: ignore
custom_election_board.short_description = "Election office" # type: ignore
# ------ Portfolio fields ------
# Define methods to display fields from the related portfolio
def portfolio_senior_official(self, obj) -> Optional[SeniorOfficial]:
return obj.portfolio.senior_official if obj.portfolio and obj.portfolio.senior_official else None
@ -2086,10 +2224,33 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
def status_history(self, obj):
return "No changelog to display."
status_history.short_description = "Status History" # type: ignore
status_history.short_description = "Status history" # type: ignore
# Columns
list_display = [
"custom_requested_domain",
"first_submitted_date",
"last_submitted_date",
"last_status_update",
"status",
"custom_election_board",
"converted_generic_org_type",
"converted_organization_name",
"converted_federal_agency",
"converted_federal_type",
"converted_city",
"converted_state_territory",
"investigator",
]
orderable_fk_fields = [
("requested_domain", "name"),
("investigator", ["first_name", "last_name"]),
]
# Filters
list_filter = (
PortfolioFilter,
StatusListFilter,
GenericOrgFilter,
FederalTypeFilter,
@ -2099,13 +2260,14 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
)
# Search
# NOTE: converted fields are included in the override for get_search_results
search_fields = [
"requested_domain__name",
"creator__email",
"creator__first_name",
"creator__last_name",
]
search_help_text = "Search by domain or creator."
search_help_text = "Search by domain, creator, or organization name."
fieldsets = [
(
@ -2271,9 +2433,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
"cisa_representative_first_name",
"cisa_representative_last_name",
"cisa_representative_email",
"requested_suborganization",
"suborganization_city",
"suborganization_state_territory",
]
autocomplete_fields = [
@ -2577,8 +2736,30 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return response
def change_view(self, request, object_id, form_url="", extra_context=None):
"""Display restricted warning,
Setup the auditlog trail and pass it in extra context."""
"""Display restricted warning, setup the auditlog trail and pass it in extra context,
display warning that status cannot be changed from 'Approved' if domain is in Ready state"""
# Fetch the domain request instance
domain_request: models.DomainRequest = models.DomainRequest.objects.get(pk=object_id)
if domain_request.approved_domain and domain_request.approved_domain.state == models.Domain.State.READY:
domain = domain_request.approved_domain
# get change url for domain
app_label = domain_request.approved_domain._meta.app_label
model_name = domain._meta.model_name
obj_id = domain.id
change_url = reverse("admin:%s_%s_change" % (app_label, model_name), args=[obj_id])
message = format_html(
"The status of this domain request cannot be changed because it has been joined to a domain in Ready status: " # noqa: E501
"<a href='{}'>{}</a>",
mark_safe(change_url), # nosec
escape(str(domain)),
)
messages.warning(
request,
message,
)
obj = self.get_object(request, object_id)
self.display_restricted_warning(request, obj)
@ -2692,6 +2873,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
qs = qs.filter(portfolio=portfolio_id)
return qs
def get_search_results(self, request, queryset, search_term):
# Call the parent's method to apply default search logic
base_queryset, use_distinct = super().get_search_results(request, queryset, search_term)
# Add custom search logic for the annotated field
if search_term:
annotated_queryset = queryset.filter(
# converted_organization_name
Q(portfolio__organization_name__icontains=search_term)
| Q(portfolio__isnull=True, organization_name__icontains=search_term)
)
# Combine the two querysets using union
combined_queryset = base_queryset | annotated_queryset
else:
combined_queryset = base_queryset
return combined_queryset, use_distinct
class TransitionDomainAdmin(ListHeaderAdmin):
"""Custom transition domain admin class."""
@ -3746,9 +3946,9 @@ class PortfolioAdmin(ListHeaderAdmin):
"senior_official",
]
analyst_readonly_fields = [
"organization_name",
]
# Even though this is empty, I will leave it as a stub for easy changes in the future
# rather than strip it out of our logic.
analyst_readonly_fields = [] # type: ignore
def get_admin_users(self, obj):
# Filter UserPortfolioPermission objects related to the portfolio

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

@ -0,0 +1,15 @@
import { submitForm } from './helpers.js';
export function initDomainDNSSEC() {
document.addEventListener('DOMContentLoaded', function() {
let domain_dnssec_page = document.getElementById("domain-dnssec");
if (domain_dnssec_page) {
const button = document.getElementById("disable-dnssec-button");
if (button) {
button.addEventListener("click", function () {
submitForm("disable-dnssec-form");
});
}
}
});
}

View file

@ -0,0 +1,27 @@
import { submitForm } from './helpers.js';
export function initDomainDSData() {
document.addEventListener('DOMContentLoaded', function() {
let domain_dsdata_page = document.getElementById("domain-dsdata");
if (domain_dsdata_page) {
const override_button = document.getElementById("disable-override-click-button");
const cancel_button = document.getElementById("btn-cancel-click-button");
const cancel_close_button = document.getElementById("btn-cancel-click-close-button");
if (override_button) {
override_button.addEventListener("click", function () {
submitForm("disable-override-click-form");
});
}
if (cancel_button) {
cancel_button.addEventListener("click", function () {
submitForm("btn-cancel-click-form");
});
}
if (cancel_close_button) {
cancel_close_button.addEventListener("click", function () {
submitForm("btn-cancel-click-form");
});
}
}
});
}

View file

@ -0,0 +1,20 @@
import { submitForm } from './helpers.js';
export function initDomainManagersPage() {
document.addEventListener('DOMContentLoaded', function() {
let domain_managers_page = document.getElementById("domain-managers");
if (domain_managers_page) {
// Add event listeners for all buttons matching user-delete-button-{NUMBER}
const deleteButtons = document.querySelectorAll('[id^="user-delete-button-"]'); // Select buttons with ID starting with "user-delete-button-"
deleteButtons.forEach((button) => {
const buttonId = button.id; // e.g., "user-delete-button-1"
const number = buttonId.split('-').pop(); // Extract the NUMBER part
const formId = `user-delete-form-${number}`; // Generate the corresponding form ID
button.addEventListener("click", function () {
submitForm(formId); // Pass the form ID to submitForm
});
});
}
});
}

View file

@ -0,0 +1,12 @@
import { submitForm } from './helpers.js';
export function initDomainRequestForm() {
document.addEventListener('DOMContentLoaded', function() {
const button = document.getElementById("domain-request-form-submit-button");
if (button) {
button.addEventListener("click", function () {
submitForm("submit-domain-request-form");
});
}
});
}

View file

@ -0,0 +1,19 @@
export function initFormErrorHandling() {
document.addEventListener('DOMContentLoaded', function() {
const errorSummary = document.getElementById('form-errors');
const firstErrorField = document.querySelector('.usa-input--error');
if (firstErrorField) {
// Scroll to the first field in error
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
// Add focus to the first field in error
setTimeout(() => {
firstErrorField.focus();
}, 50);
} else if (errorSummary) {
// Scroll to the error summary
errorSummary.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
});
}

View file

@ -1,9 +1,17 @@
export function hideElement(element) {
element.classList.add('display-none');
if (element) {
element.classList.add('display-none');
} else {
throw new Error('hideElement expected a passed DOM element as an argument, but none was provided.');
}
};
export function showElement(element) {
element.classList.remove('display-none');
if (element) {
element.classList.remove('display-none');
} else {
throw new Error('showElement expected a passed DOM element as an argument, but none was provided.');
}
};
/**
@ -75,3 +83,16 @@ export function debounce(handler, cooldown=600) {
export function getCsrfToken() {
return document.querySelector('input[name="csrfmiddlewaretoken"]').value;
}
/**
* Helper function to submit a form
* @param {} form_id - the id of the form to be submitted
*/
export function submitForm(form_id) {
let form = document.getElementById(form_id);
if (form) {
form.submit();
} else {
console.error("Form '" + form_id + "' not found.");
}
}

View file

@ -10,8 +10,12 @@ import { initDomainRequestsTable } from './table-domain-requests.js';
import { initMembersTable } from './table-members.js';
import { initMemberDomainsTable } from './table-member-domains.js';
import { initEditMemberDomainsTable } from './table-edit-member-domains.js';
import { initPortfolioMemberPageToggle } from './portfolio-member-page.js';
import { initAddNewMemberPageListeners } from './portfolio-member-page.js';
import { initPortfolioNewMemberPageToggle, initAddNewMemberPageListeners, initPortfolioMemberPageRadio } from './portfolio-member-page.js';
import { initDomainRequestForm } from './domain-request-form.js';
import { initDomainManagersPage } from './domain-managers.js';
import { initDomainDSData } from './domain-dsdata.js';
import { initDomainDNSSEC } from './domain-dnssec.js';
import { initFormErrorHandling } from './form-errors.js';
initDomainValidators();
@ -21,13 +25,6 @@ nameserversFormListener();
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
hookupRadioTogglerListener(
'member_access_level',
{
'admin': 'new-member-admin-permissions',
'basic': 'new-member-basic-permissions'
}
);
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
initializeUrbanizationToggle();
@ -44,5 +41,14 @@ initMembersTable();
initMemberDomainsTable();
initEditMemberDomainsTable();
initPortfolioMemberPageToggle();
initDomainRequestForm();
initDomainManagersPage();
initDomainDSData();
initDomainDNSSEC();
initFormErrorHandling();
// Init the portfolio new member page
initPortfolioMemberPageRadio();
initPortfolioNewMemberPageToggle();
initAddNewMemberPageListeners();

View file

@ -2,9 +2,10 @@ import { uswdsInitializeModals } from './helpers-uswds.js';
import { getCsrfToken } from './helpers.js';
import { generateKebabHTML } from './table-base.js';
import { MembersTable } from './table-members.js';
import { hookupRadioTogglerListener } from './radios.js';
// This is specifically for the Member Profile (Manage Member) Page member/invitation removal
export function initPortfolioMemberPageToggle() {
export function initPortfolioNewMemberPageToggle() {
document.addEventListener("DOMContentLoaded", () => {
const wrapperDeleteAction = document.getElementById("wrapper-delete-action")
if (wrapperDeleteAction) {
@ -149,14 +150,14 @@ export function initAddNewMemberPageListeners() {
document.getElementById('modalEmail').textContent = emailValue;
// Get selected radio button for access level
let selectedAccess = document.querySelector('input[name="member_access_level"]:checked');
let selectedAccess = document.querySelector('input[name="role"]:checked');
// Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
// This value does not have the first letter capitalized so let's capitalize it
let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
document.getElementById('modalAccessLevel').textContent = accessText;
// Populate permission details based on access level
if (selectedAccess && selectedAccess.value === 'admin') {
if (selectedAccess && selectedAccess.value === 'organization_admin') {
populatePermissionDetails('new-member-admin-permissions');
} else {
populatePermissionDetails('new-member-basic-permissions');
@ -169,4 +170,29 @@ export function initAddNewMemberPageListeners() {
}
}
}
}
// Initalize the radio for the member pages
export function initPortfolioMemberPageRadio() {
document.addEventListener("DOMContentLoaded", () => {
let memberForm = document.getElementById("member_form");
let newMemberForm = document.getElementById("add_member_form")
if (memberForm) {
hookupRadioTogglerListener(
'role',
{
'organization_admin': 'member-admin-permissions',
'organization_member': 'member-basic-permissions'
}
);
}else if (newMemberForm){
hookupRadioTogglerListener(
'role',
{
'organization_admin': 'new-member-admin-permissions',
'organization_member': 'new-member-basic-permissions'
}
);
}
});
}

View file

@ -38,21 +38,21 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
**/
export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
// Get the radio buttons
let radioButtons = document.querySelectorAll('input[name="'+radioButtonName+'"]');
let radioButtons = document.querySelectorAll(`input[name="${radioButtonName}"]`);
// Extract the list of all element IDs from the valueToElementMap
let allElementIds = Object.values(valueToElementMap);
function handleRadioButtonChange() {
// Find the checked radio button
let radioButtonChecked = document.querySelector('input[name="'+radioButtonName+'"]:checked');
let radioButtonChecked = document.querySelector(`input[name="${radioButtonName}"]:checked`);
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
// Hide all elements by default
allElementIds.forEach(function (elementId) {
let element = document.getElementById(elementId);
if (element) {
hideElement(element);
hideElement(element);
}
});
@ -64,8 +64,8 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
}
}
}
if (radioButtons.length) {
if (radioButtons && radioButtons.length) {
// Add event listener to each radio button
radioButtons.forEach(function (radioButton) {
radioButton.addEventListener('change', handleRadioButtonChange);

View file

@ -143,7 +143,7 @@ export class BaseTable {
this.statusCheckboxes = document.querySelectorAll(`.${this.sectionSelector} input[name="filter-status"]`);
this.statusIndicator = document.getElementById(`${this.sectionSelector}__filter-indicator`);
this.statusToggle = document.getElementById(`${this.sectionSelector}__usa-button--filter`);
this.noTableWrapper = document.getElementById(`${this.sectionSelector}__no-data`);
this.noDataTableWrapper = document.getElementById(`${this.sectionSelector}__no-data`);
this.noSearchResultsWrapper = document.getElementById(`${this.sectionSelector}__no-search-results`);
this.portfolioElement = document.getElementById('portfolio-js-value');
this.portfolioValue = this.portfolioElement ? this.portfolioElement.getAttribute('data-portfolio') : null;
@ -451,7 +451,7 @@ export class BaseTable {
}
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
this.updateDisplay(data, this.tableWrapper, this.noDataTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
// identify the DOM element where the list of results will be inserted into the DOM
const tbody = this.tableWrapper.querySelector('tbody');
tbody.innerHTML = '';
@ -495,7 +495,8 @@ export class BaseTable {
// Add event listeners to table headers for sorting
initializeTableHeaders() {
this.tableHeaders.forEach(header => {
header.addEventListener('click', () => {
header.addEventListener('click', event => {
let button = header.querySelector('.usa-table__header__button')
const sortBy = header.getAttribute('data-sortable');
let order = 'asc';
// sort order will be ascending, unless the currently sorted column is ascending, and the user
@ -505,6 +506,13 @@ export class BaseTable {
}
// load the results with the updated sort
this.loadTable(1, sortBy, order);
// If the click occurs outside of the button, need to simulate a button click in order
// for USWDS listener on the button to execute.
// Check first to see if click occurs outside of the button
if (!button.contains(event.target)) {
// Simulate a button click
button.click();
}
});
});
}

View file

@ -31,6 +31,9 @@ export class DomainsTable extends BaseTable {
</td>
`
}
const isExpiring = domain.state_display === "Expiring soon"
const iconType = isExpiring ? "error_outline" : "info_outline";
const iconColor = isExpiring ? "text-secondary-vivid" : "text-accent-cool"
row.innerHTML = `
<th scope="row" role="rowheader" data-label="Domain name">
${domain.name}
@ -41,14 +44,14 @@ export class DomainsTable extends BaseTable {
<td data-label="Status">
${domain.state_display}
<svg
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 text-accent-cool no-click-outline-and-cursor-help"
data-position="top"
class="usa-icon usa-tooltip usa-tooltip--registrar text-middle margin-bottom-05 ${iconColor} no-click-outline-and-cursor-help"
data-position="top"
title="${domain.get_state_help_text}"
focusable="true"
aria-label="${domain.get_state_help_text}"
role="tooltip"
>
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#${iconType}"></use>
</svg>
</td>
${markupForSuborganizationRow}
@ -77,3 +80,30 @@ export function initDomainsTable() {
}
});
}
// For clicking the "Expiring" checkbox
document.addEventListener('DOMContentLoaded', () => {
const expiringLink = document.getElementById('link-expiring-domains');
if (expiringLink) {
// Grab the selection for the status filter by
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
expiringLink.addEventListener('click', (event) => {
event.preventDefault();
// Loop through all statuses
statusCheckboxes.forEach(checkbox => {
// To find the for checkbox for "Expiring soon"
if (checkbox.value === "expiring") {
// If the checkbox is not already checked, check it
if (!checkbox.checked) {
checkbox.checked = true;
// Do the checkbox action
let event = new Event('change');
checkbox.dispatchEvent(event)
}
}
});
});
}
});

View file

@ -1,5 +1,6 @@
import { BaseTable } from './table-base.js';
import { hideElement, showElement } from './helpers.js';
/**
* EditMemberDomainsTable is used for PortfolioMember and PortfolioInvitedMember
@ -18,8 +19,14 @@ export class EditMemberDomainsTable extends BaseTable {
this.initialDomainAssignmentsOnlyMember = []; // list of initially assigned domains which are readonly
this.addedDomains = []; // list of domains added to member
this.removedDomains = []; // list of domains removed from member
this.editModeContainer = document.getElementById('domain-assignments-edit-view');
this.readonlyModeContainer = document.getElementById('domain-assignments-readonly-view');
this.reviewButton = document.getElementById('review-domain-assignments');
this.backButton = document.getElementById('back-to-edit-domain-assignments');
this.saveButton = document.getElementById('save-domain-assignments');
this.initializeDomainAssignments();
this.initCancelEditDomainAssignmentButton();
this.initEventListeners();
}
getBaseUrl() {
return document.getElementById("get_member_domains_json_url");
@ -55,6 +62,14 @@ export class EditMemberDomainsTable extends BaseTable {
getSearchParams(page, sortBy, order, searchTerm, status, portfolio) {
let searchParams = super.getSearchParams(page, sortBy, order, searchTerm, status, portfolio);
// Add checkedDomains to searchParams
let checkedDomains = this.getCheckedDomains();
// Append updated checkedDomain IDs to searchParams
if (checkedDomains.length > 0) {
searchParams.append("checkedDomainIds", checkedDomains.join(","));
}
return searchParams;
}
getCheckedDomains() {
// Clone the initial domains to avoid mutating them
let checkedDomains = [...this.initialDomainAssignments];
// Add IDs from addedDomains that are not already in checkedDomains
@ -70,11 +85,7 @@ export class EditMemberDomainsTable extends BaseTable {
checkedDomains.splice(index, 1);
}
});
// Append updated checkedDomain IDs to searchParams
if (checkedDomains.length > 0) {
searchParams.append("checkedDomainIds", checkedDomains.join(","));
}
return searchParams;
return checkedDomains
}
addRow(dataObject, tbody, customTableOptions) {
const domain = dataObject;
@ -217,7 +228,123 @@ export class EditMemberDomainsTable extends BaseTable {
}
});
}
updateReadonlyDisplay() {
let totalAssignedDomains = this.getCheckedDomains().length;
// Create unassigned domains list
const unassignedDomainsList = document.createElement('ul');
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
this.removedDomains.forEach(removedDomain => {
const removedDomainListItem = document.createElement('li');
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
unassignedDomainsList.appendChild(removedDomainListItem);
});
// Create assigned domains list
const assignedDomainsList = document.createElement('ul');
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
this.addedDomains.forEach(addedDomain => {
const addedDomainListItem = document.createElement('li');
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
assignedDomainsList.appendChild(addedDomainListItem);
});
// Get the summary container
const domainAssignmentSummary = document.getElementById('domain-assignments-summary');
// Clear existing content
domainAssignmentSummary.innerHTML = '';
// Append unassigned domains section
if (this.removedDomains.length) {
const unassignedHeader = document.createElement('h3');
unassignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
unassignedHeader.textContent = 'Unassigned domains';
domainAssignmentSummary.appendChild(unassignedHeader);
domainAssignmentSummary.appendChild(unassignedDomainsList);
}
// Append assigned domains section
if (this.addedDomains.length) {
const assignedHeader = document.createElement('h3');
assignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
assignedHeader.textContent = 'Assigned domains';
domainAssignmentSummary.appendChild(assignedHeader);
domainAssignmentSummary.appendChild(assignedDomainsList);
}
// Append total assigned domains section
const totalHeader = document.createElement('h3');
totalHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
totalHeader.textContent = 'Total assigned domains';
domainAssignmentSummary.appendChild(totalHeader);
const totalCount = document.createElement('p');
totalCount.classList.add('margin-y-0');
totalCount.textContent = totalAssignedDomains;
domainAssignmentSummary.appendChild(totalCount);
}
showReadonlyMode() {
this.updateReadonlyDisplay();
hideElement(this.editModeContainer);
showElement(this.readonlyModeContainer);
}
showEditMode() {
hideElement(this.readonlyModeContainer);
showElement(this.editModeContainer);
}
submitChanges() {
let memberDomainsEditForm = document.getElementById("member-domains-edit-form");
if (memberDomainsEditForm) {
// Serialize data to send
const addedDomainIds = this.addedDomains.map(domain => domain.id);
const addedDomainsInput = document.createElement('input');
addedDomainsInput.type = 'hidden';
addedDomainsInput.name = 'added_domains'; // Backend will use this key to retrieve data
addedDomainsInput.value = JSON.stringify(addedDomainIds); // Stringify the array
const removedDomainsIds = this.removedDomains.map(domain => domain.id);
const removedDomainsInput = document.createElement('input');
removedDomainsInput.type = 'hidden';
removedDomainsInput.name = 'removed_domains'; // Backend will use this key to retrieve data
removedDomainsInput.value = JSON.stringify(removedDomainsIds); // Stringify the array
// Append input to the form
memberDomainsEditForm.appendChild(addedDomainsInput);
memberDomainsEditForm.appendChild(removedDomainsInput);
memberDomainsEditForm.submit();
}
}
initEventListeners() {
if (this.reviewButton) {
this.reviewButton.addEventListener('click', () => {
this.showReadonlyMode();
});
} else {
console.warn('Missing DOM element. Expected element with id review-domain-assignments');
}
if (this.backButton) {
this.backButton.addEventListener('click', () => {
this.showEditMode();
});
} else {
console.warn('Missing DOM element. Expected element with id back-to-edit-domain-assignments');
}
if (this.saveButton) {
this.saveButton.addEventListener('click', () => {
this.submitChanges();
});
} else {
console.warn('Missing DOM element. Expected element with id save-domain-assignments');
}
}
}
export function initEditMemberDomainsTable() {

View file

@ -1,4 +1,5 @@
import { showElement, hideElement } from './helpers.js';
import { BaseTable } from './table-base.js';
export class MemberDomainsTable extends BaseTable {
@ -24,7 +25,28 @@ export class MemberDomainsTable extends BaseTable {
`;
tbody.appendChild(row);
}
updateDisplay = (data, dataWrapper, noDataWrapper, noSearchResultsWrapper) => {
const { unfiltered_total, total } = data;
const searchSection = document.getElementById('edit-member-domains__search');
if (!searchSection) console.warn('MemberDomainsTable updateDisplay expected an element with id edit-member-domains__search but none was found');
if (unfiltered_total) {
showElement(searchSection);
if (total) {
showElement(dataWrapper);
hideElement(noSearchResultsWrapper);
hideElement(noDataWrapper);
} else {
hideElement(dataWrapper);
showElement(noSearchResultsWrapper);
hideElement(noDataWrapper);
}
} else {
hideElement(searchSection);
hideElement(dataWrapper);
hideElement(noSearchResultsWrapper);
showElement(noDataWrapper);
}
};
}
export function initMemberDomainsTable() {

View file

@ -40,7 +40,11 @@
top: 30px;
}
tr:last-child .usa-accordion--more-actions .usa-accordion__content {
// Special positioning for the kabob menu popup in the last row on a given page
// This won't work on the Members table rows because that table has show-more rows
// Currently, that's not an issue since that Members table is not wrapped in the
// reponsive wrapper.
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
top: auto;
bottom: -10px;
right: 30px;

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;
@ -47,3 +60,8 @@
background-color: color('base-darkest');
}
}
// Override the specificity of USWDS css to enable no max width on admin alerts
.usa-alert__body.maxw-none {
max-width: none;
}

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. */
@ -39,7 +41,8 @@ body {
padding-top: units(5)!important;
}
#wrapper.dashboard--grey-1 {
#wrapper.dashboard--grey-1,
.bg-gray-1 {
background-color: color('gray-1');
}
@ -149,6 +152,11 @@ footer {
color: color('primary');
}
.usa-radio {
margin-top: 1rem;
font-size: 1.06rem;
}
abbr[title] {
// workaround for underlining abbr element
border-bottom: none;
@ -247,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;
}
@ -260,4 +277,8 @@ abbr[title] {
margin: 0;
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

@ -78,3 +78,7 @@ legend.float-left-tablet + button.float-right-tablet {
.read-only-value {
margin-top: units(0);
}
.bg-gray-1 .usa-radio {
background: color('gray-1');
}

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

@ -12,7 +12,7 @@
margin-top: units(1);
}
// register-form-review-header is used on the summary page and
// header--body is used on the summary page and
// should not be styled like the register form headers
.register-form-step h3 {
color: color('primary-dark');
@ -25,15 +25,6 @@
}
}
.register-form-review-header {
color: color('primary-dark');
margin-top: units(2);
margin-bottom: 0;
font-weight: font-weight('semibold');
// The units mixin can only get us close, so it's between
// hardcoding the value and using in markup
font-size: 16.96px;
}
.register-form-step h4 {
margin-bottom: 0;

View file

@ -88,8 +88,14 @@ th {
}
@include at-media(tablet-lg) {
th[data-sortable]:not([aria-sort]) .usa-table__header__button {
th[data-sortable] .usa-table__header__button {
right: auto;
&[aria-sort=ascending],
&[aria-sort=descending],
&:not([aria-sort]) {
right: auto;
}
}
}
}

View file

@ -23,6 +23,14 @@ h2 {
color: color('primary-darker');
}
.header--body {
margin-top: units(2);
font-weight: font-weight('semibold');
// The units mixin can only get us close, so it's between
// hardcoding the value and using in markup
font-size: 16.96px;
}
.h4--sm-05 {
font-size: size('body', 'sm');
font-weight: normal;
@ -34,6 +42,9 @@ h2 {
.usa-form,
.usa-form fieldset {
font-size: 1rem;
.usa-legend {
font-size: 1rem;
}
}
.p--blockquote {

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

@ -146,7 +146,7 @@ urlpatterns = [
# ),
path(
"members/new-member/",
views.NewMemberView.as_view(),
views.PortfolioAddMemberView.as_view(),
name="new-member",
),
path(

View file

@ -69,9 +69,19 @@ def portfolio_permissions(request):
"has_organization_requests_flag": False,
"has_organization_members_flag": False,
"is_portfolio_admin": False,
"has_domain_renewal_flag": False,
}
try:
portfolio = request.session.get("portfolio")
# These feature flags will display and doesn't depend on portfolio
portfolio_context.update(
{
"has_organization_feature_flag": True,
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
}
)
# Linting: line too long
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
@ -90,6 +100,7 @@ def portfolio_permissions(request):
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
"has_organization_members_flag": request.user.has_organization_members_flag(),
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
}
return portfolio_context
@ -98,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

@ -60,7 +60,10 @@ class UserPortfolioPermissionFixture:
user=user,
portfolio=portfolio,
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
additional_permissions=[UserPortfolioPermissionChoices.EDIT_MEMBERS],
additional_permissions=[
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.EDIT_REQUESTS,
],
)
user_portfolio_permissions_to_create.append(user_portfolio_permission)
else:

View file

@ -151,6 +151,27 @@ class UserFixture:
"email": "skey@truss.works",
"title": "Designer",
},
{
"username": "f20b7a53-f40d-48f8-8c12-f42f35eede92",
"first_name": "Kimberly",
"last_name": "Aralar",
"email": "kimberly.aralar@gsa.gov",
"title": "Designer",
},
{
"username": "4aa78480-6272-42f9-ac29-a034ebdd9231",
"first_name": "Kaitlin",
"last_name": "Abbitt",
"email": "kaitlin.abbitt@cisa.dhs.gov",
"title": "Product Manager",
},
{
"username": "5e54fd98-6c11-4cb3-82b6-93ed8be50a61",
"first_name": "Gina",
"last_name": "Summers",
"email": "gina.summers@ecstech.com",
"title": "Scrum Master",
},
]
STAFF = [
@ -257,6 +278,18 @@ class UserFixture:
"last_name": "Key-Analyst",
"email": "skey+1@truss.works",
},
{
"username": "cf2b32fe-280d-4bc0-96c2-99eec09ba4da",
"first_name": "Kimberly-Analyst",
"last_name": "Aralar-Analyst",
"email": "kimberly.aralar+1@gsa.gov",
},
{
"username": "80db923e-ac64-4128-9b6f-e54b2174a09b",
"first_name": "Kaitlin-Analyst",
"last_name": "Abbitt-Analyst",
"email": "kaitlin.abbitt@gwe.cisa.dhs.gov",
},
]
# Additional emails to add to the AllowedEmail whitelist.

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.")
@ -530,7 +545,7 @@ class PurposeForm(RegistrarForm):
widget=forms.Textarea(
attrs={
"aria-label": "What is the purpose of your requested domain? Describe how youll use your .gov domain. \
Will it be used for a website, email, or something else? You can enter up to 2000 characters."
Will it be used for a website, email, or something else?"
}
),
validators=[
@ -736,7 +751,13 @@ class NoOtherContactsForm(BaseDeletableRegistrarForm):
required=True,
# label has to end in a space to get the label_suffix to show
label=("No other employees rationale"),
widget=forms.Textarea(),
widget=forms.Textarea(
attrs={
"aria-label": "You dont need to provide names of other employees now, \
but it may slow down our assessment of your eligibility. Describe \
why there are no other employees who can help verify your request."
}
),
validators=[
MaxLengthValidator(
1000,
@ -784,7 +805,12 @@ class AnythingElseForm(BaseDeletableRegistrarForm):
anything_else = forms.CharField(
required=True,
label="Anything else?",
widget=forms.Textarea(),
widget=forms.Textarea(
attrs={
"aria-label": "Is there anything else youd like us to know about your domain request? \
Provide details below. You can enter up to 2000 characters"
}
),
validators=[
MaxLengthValidator(
2000,

View file

@ -4,6 +4,7 @@ import logging
from django import forms
from django.core.validators import RegexValidator
from django.core.validators import MaxLengthValidator
from django.utils.safestring import mark_safe
from registrar.models import (
PortfolioInvitation,
@ -11,7 +12,6 @@ from registrar.models import (
DomainInformation,
Portfolio,
SeniorOfficial,
User,
)
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
@ -110,102 +110,218 @@ class PortfolioSeniorOfficialForm(forms.ModelForm):
return cleaned_data
class PortfolioMemberForm(forms.ModelForm):
"""
Form for updating a portfolio member.
"""
class BasePortfolioMemberForm(forms.ModelForm):
"""Base form for the PortfolioMemberForm and PortfolioInvitedMemberForm"""
roles = forms.MultipleChoiceField(
choices=UserPortfolioRoleChoices.choices,
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Roles",
)
additional_permissions = forms.MultipleChoiceField(
choices=UserPortfolioPermissionChoices.choices,
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Additional Permissions",
)
class Meta:
model = UserPortfolioPermission
fields = [
"roles",
"additional_permissions",
]
class PortfolioInvitedMemberForm(forms.ModelForm):
"""
Form for updating a portfolio invited member.
"""
roles = forms.MultipleChoiceField(
choices=UserPortfolioRoleChoices.choices,
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Roles",
)
additional_permissions = forms.MultipleChoiceField(
choices=UserPortfolioPermissionChoices.choices,
widget=forms.SelectMultiple(attrs={"class": "usa-select"}),
required=False,
label="Additional Permissions",
)
class Meta:
model = PortfolioInvitation
fields = [
"roles",
"additional_permissions",
]
class NewMemberForm(forms.ModelForm):
member_access_level = forms.ChoiceField(
label="Select permission",
choices=[("admin", "Admin Access"), ("basic", "Basic Access")],
widget=forms.RadioSelect(attrs={"class": "usa-radio__input usa-radio__input--tile"}),
# The label for each of these has a red "required" star. We can just embed that here for simplicity.
required_star = '<abbr class="usa-hint usa-hint--required" title="required">*</abbr>'
role = forms.ChoiceField(
choices=[
# Uses .value because the choice has a different label (on /admin)
(UserPortfolioRoleChoices.ORGANIZATION_ADMIN.value, "Admin access"),
(UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value, "Basic access"),
],
widget=forms.RadioSelect,
required=True,
error_messages={
"required": "Member access level is required",
},
)
admin_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all requests"), ("view_and_create", "View all requests plus create requests")],
domain_request_permission_admin = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
],
widget=forms.RadioSelect,
required=True,
required=False,
error_messages={
"required": "Admin domain request permission is required",
},
)
admin_org_members_permissions = forms.ChoiceField(
label="Select permission",
choices=[("view_only", "View all members"), ("view_and_create", "View all members plus manage members")],
member_permission_admin = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
(UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "View all members plus manage members"),
],
widget=forms.RadioSelect,
required=True,
required=False,
error_messages={
"required": "Admin member permission is required",
},
)
basic_org_domain_request_permissions = forms.ChoiceField(
label="Select permission",
domain_request_permission_member = forms.ChoiceField(
label=mark_safe(f"Select permission {required_star}"), # nosec
choices=[
("view_only", "View all requests"),
("view_and_create", "View all requests plus create requests"),
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
("no_access", "No access"),
],
widget=forms.RadioSelect,
required=True,
required=False,
error_messages={
"required": "Basic member permission is required",
},
)
# Tracks what form elements are required for a given role choice.
# All of the fields included here have "required=False" by default as they are conditionally required.
# see def clean() for more details.
ROLE_REQUIRED_FIELDS = {
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
"domain_request_permission_admin",
"member_permission_admin",
],
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
"domain_request_permission_member",
],
}
class Meta:
model = None
fields = ["roles", "additional_permissions"]
def __init__(self, *args, **kwargs):
"""
Override the form's initialization.
Map existing model values to custom form fields.
Update field descriptions.
"""
super().__init__(*args, **kwargs)
# Adds a <p> description beneath each role option
self.fields["role"].descriptions = {
"organization_admin": UserPortfolioRoleChoices.get_role_description(
UserPortfolioRoleChoices.ORGANIZATION_ADMIN
),
"organization_member": UserPortfolioRoleChoices.get_role_description(
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
),
}
# Map model instance values to custom form fields
if self.instance:
self.map_instance_to_initial()
def clean(self):
"""Validates form data based on selected role and its required fields.
Updates roles and additional_permissions in cleaned_data so they can be properly
mapped to the model.
"""
cleaned_data = super().clean()
role = cleaned_data.get("role")
# Get required fields for the selected role. Then validate all required fields for the role.
required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
for field_name in required_fields:
# Helpful error for if this breaks
if field_name not in self.fields:
raise ValueError(f"ROLE_REQUIRED_FIELDS referenced a non-existent field: {field_name}.")
if not cleaned_data.get(field_name):
self.add_error(field_name, self.fields.get(field_name).error_messages.get("required"))
# Edgecase: Member uses a special form value for None called "no_access".
if cleaned_data.get("domain_request_permission_member") == "no_access":
cleaned_data["domain_request_permission_member"] = None
# Handle roles
cleaned_data["roles"] = [role]
# Handle additional_permissions
valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)}
# Handle EDIT permissions (should be accompanied with a view permission)
if UserPortfolioPermissionChoices.EDIT_MEMBERS in additional_permissions:
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_MEMBERS)
if UserPortfolioPermissionChoices.EDIT_REQUESTS in additional_permissions:
additional_permissions.add(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
# Only set unique permissions not already defined in the base role
role_permissions = UserPortfolioPermission.get_portfolio_permissions(cleaned_data["roles"], [], get_list=False)
cleaned_data["additional_permissions"] = list(additional_permissions - role_permissions)
return cleaned_data
def map_instance_to_initial(self):
"""
Maps self.instance to self.initial, handling roles and permissions.
Updates self.initial dictionary with appropriate permission levels based on user role:
{
"role": "organization_admin" or "organization_member",
"member_permission_admin": permission level if admin,
"domain_request_permission_admin": permission level if admin,
"domain_request_permission_member": permission level if member
}
"""
if self.initial is None:
self.initial = {}
# Function variables
perms = UserPortfolioPermission.get_portfolio_permissions(
self.instance.roles, self.instance.additional_permissions, get_list=False
)
# Get the available options for roles, domains, and member.
roles = [
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
]
domain_perms = [
UserPortfolioPermissionChoices.EDIT_REQUESTS,
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
]
member_perms = [
UserPortfolioPermissionChoices.EDIT_MEMBERS,
UserPortfolioPermissionChoices.VIEW_MEMBERS,
]
# Build form data based on role (which options are available).
# Get which one should be "selected" by assuming that EDIT takes precedence over view,
# and ADMIN takes precedence over MEMBER.
roles = self.instance.roles or []
selected_role = next((role for role in roles if role in roles), None)
self.initial["role"] = selected_role
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
if is_admin:
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None)
selected_member_permission = next((perm for perm in member_perms if perm in perms), None)
self.initial["domain_request_permission_admin"] = selected_domain_permission
self.initial["member_permission_admin"] = selected_member_permission
else:
# Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access")
self.initial["domain_request_permission_member"] = selected_domain_permission
class PortfolioMemberForm(BasePortfolioMemberForm):
"""
Form for updating a portfolio member.
"""
class Meta:
model = UserPortfolioPermission
fields = ["roles", "additional_permissions"]
class PortfolioInvitedMemberForm(BasePortfolioMemberForm):
"""
Form for updating a portfolio invited member.
"""
class Meta:
model = PortfolioInvitation
fields = ["roles", "additional_permissions"]
class PortfolioNewMemberForm(BasePortfolioMemberForm):
"""
Form for adding a portfolio invited member.
"""
email = forms.EmailField(
label="Enter the email of the member you'd like to invite",
max_length=None,
@ -223,51 +339,5 @@ class NewMemberForm(forms.ModelForm):
)
class Meta:
model = User
fields = ["email"]
def clean(self):
cleaned_data = super().clean()
# Lowercase the value of the 'email' field
email_value = cleaned_data.get("email")
if email_value:
cleaned_data["email"] = email_value.lower()
##########################################
# TODO: future ticket
# (invite new member)
##########################################
# Check for an existing user (if there isn't any, send an invite)
# if email_value:
# try:
# existingUser = User.objects.get(email=email_value)
# except User.DoesNotExist:
# raise forms.ValidationError("User with this email does not exist.")
member_access_level = cleaned_data.get("member_access_level")
# Intercept the error messages so that we don't validate hidden inputs
if not member_access_level:
# If no member access level has been selected, delete error messages
# for all hidden inputs (which is everything except the e-mail input
# and member access selection)
for field in self.fields:
if field in self.errors and field != "email" and field != "member_access_level":
del self.errors[field]
return cleaned_data
basic_dom_req_error = "basic_org_domain_request_permissions"
admin_dom_req_error = "admin_org_domain_request_permissions"
admin_member_error = "admin_org_members_permissions"
if member_access_level == "admin" and basic_dom_req_error in self.errors:
# remove the error messages pertaining to basic permission inputs
del self.errors[basic_dom_req_error]
elif member_access_level == "basic":
# remove the error messages pertaining to admin permission inputs
if admin_dom_req_error in self.errors:
del self.errors[admin_dom_req_error]
if admin_member_error in self.errors:
del self.errors[admin_member_error]
return cleaned_data
model = PortfolioInvitation
fields = ["portfolio", "email", "roles", "additional_permissions"]

View file

@ -2,7 +2,7 @@ from itertools import zip_longest
import logging
import ipaddress
import re
from datetime import date
from datetime import date, timedelta
from typing import Optional
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
@ -40,6 +40,7 @@ from .utility.time_stamped_model import TimeStampedModel
from .public_contact import PublicContact
from .user_domain_role import UserDomainRole
from waffle.decorators import flag_is_active
logger = logging.getLogger(__name__)
@ -1152,14 +1153,29 @@ class Domain(TimeStampedModel, DomainHelper):
now = timezone.now().date()
return self.expiration_date < now
def state_display(self):
def is_expiring(self):
"""
Check if the domain's expiration date is within 60 days.
Return True if domain expiration date exists and within 60 days
and otherwise False bc there's no expiration date meaning so not expiring
"""
if self.expiration_date is None:
return False
now = timezone.now().date()
threshold_date = now + timedelta(days=60)
return now < self.expiration_date <= threshold_date
def state_display(self, request=None):
"""Return the display status of the domain."""
if self.is_expired() and self.state != self.State.UNKNOWN:
if self.is_expired() and (self.state != self.State.UNKNOWN):
return "Expired"
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
return "Expiring soon"
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
return "DNS needed"
else:
return self.state.capitalize()
return self.state.capitalize()
def map_epp_contact_to_public_contact(self, contact: eppInfo.InfoContactResultData, contact_id, contact_type):
"""Maps the Epp contact representation to a PublicContact object.

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

@ -14,6 +14,8 @@ from .domain import Domain
from .domain_request import DomainRequest
from registrar.utility.waffle import flag_is_active_for_user
from waffle.decorators import flag_is_active
from django.utils import timezone
from datetime import timedelta
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
@ -163,6 +165,20 @@ class User(AbstractUser):
active_requests_count = self.domain_requests_created.filter(status__in=allowed_states).count()
return active_requests_count
def get_num_expiring_domains(self, request):
"""Return number of expiring domains"""
domain_ids = self.get_user_domain_ids(request)
now = timezone.now().date()
expiration_window = 60
threshold_date = now + timedelta(days=expiration_window)
num_of_expiring_domains = Domain.objects.filter(
id__in=domain_ids,
expiration_date__isnull=False,
expiration_date__lte=threshold_date,
expiration_date__gt=now,
).count()
return num_of_expiring_domains
def get_rejected_requests_count(self):
"""Return count of rejected requests"""
return self.domain_requests_created.filter(status=DomainRequest.DomainRequestStatus.REJECTED).count()
@ -259,6 +275,9 @@ class User(AbstractUser):
def is_portfolio_admin(self, portfolio):
return "Admin" in self.portfolio_role_summary(portfolio)
def has_domain_renewal_flag(self):
return flag_is_active_for_user(self, "domain_renewal")
def get_first_portfolio(self):
permission = self.portfolio_permissions.first()
if permission:

View file

@ -110,8 +110,13 @@ class UserPortfolioPermission(TimeStampedModel):
return self.get_portfolio_permissions(self.roles, self.additional_permissions)
@classmethod
def get_portfolio_permissions(cls, roles, additional_permissions):
"""Class method to return a list of permissions based on roles and addtl permissions"""
def get_portfolio_permissions(cls, roles, additional_permissions, get_list=True):
"""Class method to return a list of permissions based on roles and addtl permissions.
Params:
roles => An array of roles
additional_permissions => An array of additional_permissions
get_list => If true, returns a list of perms. If false, returns a set of perms.
"""
# Use a set to avoid duplicate permissions
portfolio_permissions = set()
if roles:
@ -119,7 +124,7 @@ class UserPortfolioPermission(TimeStampedModel):
portfolio_permissions.update(cls.PORTFOLIO_ROLE_PERMISSIONS.get(role, []))
if additional_permissions:
portfolio_permissions.update(additional_permissions)
return list(portfolio_permissions)
return list(portfolio_permissions) if get_list else portfolio_permissions
@classmethod
def get_domain_request_permission_display(cls, roles, additional_permissions):
@ -166,8 +171,10 @@ class UserPortfolioPermission(TimeStampedModel):
# The solution to this is to only grab what is only COMMONLY "forbidden".
# This will scale if we add more roles in the future.
# This is thes same as applying the `&` operator across all sets for each role.
common_forbidden_perms = set.intersection(
*[set(cls.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) for role in roles]
common_forbidden_perms = (
set.intersection(*[set(cls.FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS.get(role, [])) for role in roles])
if roles
else set()
)
# Check if the users current permissions overlap with any forbidden permissions

View file

@ -4,6 +4,9 @@ from django.apps import apps
from django.forms import ValidationError
from registrar.utility.waffle import flag_is_active_for_user
from django.contrib.auth import get_user_model
import logging
logger = logging.getLogger(__name__)
class UserPortfolioRoleChoices(models.TextChoices):
@ -16,7 +19,28 @@ class UserPortfolioRoleChoices(models.TextChoices):
@classmethod
def get_user_portfolio_role_label(cls, user_portfolio_role):
return cls(user_portfolio_role).label if user_portfolio_role else None
try:
return cls(user_portfolio_role).label if user_portfolio_role else None
except ValueError:
logger.warning(f"Invalid portfolio role: {user_portfolio_role}")
return f"Unknown ({user_portfolio_role})"
@classmethod
def get_role_description(cls, user_portfolio_role):
"""Returns a detailed description for a given role."""
descriptions = {
cls.ORGANIZATION_ADMIN: (
"Grants this member access to the organization-wide information "
"on domains, domain requests, and members. Domain management can be assigned separately."
),
cls.ORGANIZATION_MEMBER: (
"Grants this member access to the organization. They can be given extra permissions to view all "
"organization domain requests and submit domain requests on behalf of the organization. Basic access "
"members cant view all members of an organization or manage them. "
"Domain management can be assigned separately."
),
}
return descriptions.get(user_portfolio_role)
class UserPortfolioPermissionChoices(models.TextChoices):

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

@ -0,0 +1,14 @@
{% extends 'django/admin/email_clipboard_change_form.html' %}
{% load custom_filters %}
{% load i18n static %}
{% block content_subtitle %}
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body margin-left-1 maxw-none">
<p class="usa-alert__text maxw-none">
If you add someone to a portfolio here, it will trigger an invitation email when you click "save." If you don't want to trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">User portfolio permissions table</a> instead.
</p>
</div>
</div>
{{ block.super }}
{% endblock %}

View file

@ -2,15 +2,13 @@
{% load custom_filters %}
{% load i18n static %}
{% block field_sets %}
{% for fieldset in adminform %}
{% comment %}
This is a placeholder for now.
Disclaimer:
When extending the fieldset view consider whether you need to make a new one that extends from detail_table_fieldset.
detail_table_fieldset is used on multiple admin pages, so a change there can have unintended consequences.
{% endcomment %}
{% include "django/admin/includes/user_portfolio_permission_fieldset.html" with original_object=original %}
{% endfor %}
{% endblock %}
{% block content_subtitle %}
<div class="usa-alert usa-alert--info usa-alert--slim">
<div class="usa-alert__body margin-left-1 maxw-none">
<p class="usa-alert__text maxw-none">
If you add someone to a portfolio here, it will not trigger an invitation email. To trigger an email, use the <a class="usa-link" href="{% url 'admin:registrar_portfolioinvitation_changelist' %}">Portfolio invitations table</a> instead.
</p>
</div>
</div>
{{ block.super }}
{% endblock %}

View file

@ -2,15 +2,25 @@
class="{% if label_classes %} {{ label_classes }}{% endif %}{% if label_tag == 'legend' %} {{ legend_classes }}{% endif %}"
{% if not field.use_fieldset %}for="{{ widget.attrs.id }}"{% endif %}
>
{% if span_for_text %}
<span>{{ field.label }}</span>
{% if legend_heading %}
<h2 class="{{ legend_classes }}">{{ legend_heading }} </h2>
{% if widget.attrs.id == 'id_additional_details-has_cisa_representative' %}
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
{% endif %}
{% else %}
{{ field.label }}
{% if span_for_text %}
<span>{{ field.label }}</span>
{% else %}
{{ field.label }}
{% endif %}
{% endif %}
{% if widget.attrs.required %}
<!--Don't add asterisk to one-field forms -->
{% if field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." %}
{% if field.widget_type == 'radioselect' %}
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
<!--Don't add asterisk to one-field forms -->
{% elif field.label == "Is your organization an election office?" or field.label == "What .gov domain do you want?" or field.label == "I read and agree to the requirements for operating a .gov domain." or field.label == "Please explain why there are no other employees from your organization we can contact to help us assess your eligibility for a .gov domain." or field.label == "Has other contacts" %}
{% else %}
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
{% endif %}

View file

@ -1,3 +1,5 @@
{% load static custom_filters %}
<div class="{{ uswds_input_class }}">
{% for group, options, index in widget.optgroups %}
{% if group %}<div><label>{{ group }}</label>{% endif %}
@ -13,7 +15,17 @@
<label
class="{{ uswds_input_class }}__label{% if label_classes %} {{ label_classes }}{% endif %}"
for="{{ option.attrs.id }}"
>{{ option.label }}</label>
>
{{ option.label }}
{% comment %} Add a description on each, if available {% endcomment %}
{% if field and field.field and field.field.descriptions %}
{% with description=field.field.descriptions|get_dict_value:option.value %}
{% if description %}
<p class="margin-0 margin-top-1">{{ description }}</p>
{% endif %}
{% endwith %}
{% endif %}
</label>
{% endfor %}
{% if group %}</div>{% endif %}
{% endfor %}

View file

@ -37,6 +37,9 @@
</nav>
{% endif %}
{% endblock breadcrumb %}
{% include "includes/form_errors.html" with form=form %}
<h1>Add a domain manager</h1>
{% if has_organization_feature_flag %}
<p>

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

@ -35,18 +35,27 @@
Status:
</span>
<span class="text-primary-darker">
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
Expired
{% elif has_domain_renewal_flag and domain.is_expiring %}
Expiring soon
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
DNS needed
{% else %}
{{ domain.state|title }}
{{ domain.state|title }}
{% endif %}
</span>
{% if domain.get_state_help_text %}
<div class="padding-top-1 text-primary-darker">
{{ domain.get_state_help_text }}
{% if has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
This domain will expire soon. <a href="/not-available-yet">Renew to maintain access.</a>
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
{% else %}
{{ domain.get_state_help_text }}
{% endif %}
</div>
{% endif %}
</p>
@ -119,4 +128,4 @@
{% endif %}
</div>
{% endblock %} {# domain_content #}
{% endblock %} {# domain_content #}

View file

@ -27,7 +27,7 @@
{% endif %}
{% endblock breadcrumb %}
<h1>DNSSEC</h1>
<h1 id="domain-dnssec">DNSSEC</h1>
<p>DNSSEC, or DNS Security Extensions, is an additional security layer to protect your website. Enabling DNSSEC ensures that when someone visits your domain, they can be certain that its connecting to the correct server, preventing potential hijacking or tampering with your domain's records.</p>
@ -78,7 +78,11 @@
aria-labelledby="Are you sure you want to continue?"
aria-describedby="Your DNSSEC records will be deleted from the registry."
>
{% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button=modal_button|safe %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to disable DNSSEC?" modal_button_id="disable-dnssec-button" modal_button_text="Confirm" modal_button_class="usa-button--secondary" %}
</div>
<form method="post" id="disable-dnssec-form">
{% csrf_token %}
<input type="hidden" name="disable_dnssec" value="1">
</form>
{% endblock %} {# domain_content #}

View file

@ -42,7 +42,7 @@
{% include "includes/form_errors.html" with form=form %}
{% endfor %}
<h1>DS data</h1>
<h1 id="domain-dsdata">DS data</h1>
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
@ -141,7 +141,15 @@
aria-describedby="Your DNSSEC records will be deleted from the registry."
data-force-action
>
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, youll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button=modal_button|safe %}
{% include 'includes/modal.html' with cancel_button_resets_ds_form=True modal_heading="Warning: You are about to remove all DS records on your domain." modal_description="To fully disable DNSSEC: In addition to removing your DS records here, youll need to delete the DS records at your DNS host. To avoid causing your domain to appear offline, you should wait to delete your DS records at your DNS host until the Time to Live (TTL) expires. This is often less than 24 hours, but confirm with your provider." modal_button_id="disable-override-click-button" modal_button_text="Remove all DS data" modal_button_class="usa-button--secondary" %}
</div>
<form method="post" id="disable-override-click-form">
{% csrf_token %}
<input type="hidden" name="disable-override-click" value="1">
</form>
<form method="post" id="btn-cancel-click-form">
{% csrf_token %}
<input type="hidden" name="btn-cancel-click" value="1">
</form>
{% endblock %} {# domain_content #}

View file

@ -9,15 +9,9 @@
{% block form_fields %}
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Are you working with a CISA regional representative on your domain request?</h2>
<p>.gov is managed by the Cybersecurity and Infrastructure Security Agency. CISA has <a href="https://www.cisa.gov/about/regions" target="_blank">10 regions</a> that some organizations choose to work with. Regional representatives use titles like protective security advisors, cyber security advisors, or election security advisors.</p>
</legend>
<fieldset class="usa-fieldset">
<!-- Toggle -->
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% with add_class="usa-radio__input--tile" add_legend_class="margin-top-0" add_legend_heading="Are you working with a CISA regional representative on your domain request?" %}
{% input_with_errors forms.0.has_cisa_representative %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
@ -31,13 +25,8 @@
</div>
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Is there anything else youd like us to know about your domain request?</h2>
</legend>
<!-- Toggle -->
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
{% with add_class="usa-radio__input--tile" add_legend_heading="Is there anything else youd like us to know about your domain request?" %}
{% input_with_errors forms.2.has_anything_else_text %}
{% endwith %}
{# forms.2 is a small yes/no form that toggles the visibility of "cisa representative" formset #}
@ -45,7 +34,7 @@
<div class="margin-top-3" id="anything-else">
<p>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></p>
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
{% with attr_maxlength=2000 add_label_class="usa-sr-only" add_legend_class="usa-sr-only" add_legend_heading="Is there anything else youd like us to know about your domain request?" add_aria_label="Provide details below. You can enter up to 2000 characters" %}
{% input_with_errors forms.3.anything_else %}
{% endwith %}
{# forms.3 is a form for inputting the e-mail of a cisa representative #}

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>
@ -130,9 +130,17 @@
aria-describedby="Are you sure you want to submit a domain request?"
data-force-action
>
{% include 'includes/modal.html' with is_domain_request_form=True review_form_is_complete=review_form_is_complete modal_heading=modal_heading|safe modal_description=modal_description|safe modal_button=modal_button|safe %}
{% if review_form_is_complete %}
{% include 'includes/modal.html' with modal_heading="You are about to submit a domain request for " domain_name_modal=requested_domain__name modal_description="Once you submit this request, you wont be able to edit it until we review it. Youll only be able to withdraw your request." modal_button_id="domain-request-form-submit-button" modal_button_text="Submit request" %}
{% else %}
{% include 'includes/modal.html' with modal_heading="Your request form is incomplete" modal_description='This request cannot be submitted yet. Return to the request and visit the steps that are marked as "incomplete."' modal_button_text="Return to request" cancel_button_only=True %}
{% endif %}
</div>
<form method="post" id="submit-domain-request-form">
{% csrf_token %}
</form>
{% block after_form_content %}{% endblock %}
</main>

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

@ -9,7 +9,7 @@
<li><strong>We typically dont reach out to these employees</strong>, but if contact is necessary, our practice is to coordinate with you first.</li>
</ul>
</p>
{% include "includes/required_fields.html" %}
{% endblock %}
{% block form_required_fields_help_text %}
@ -17,20 +17,14 @@
{% endblock %}
{% block form_fields %}
<fieldset class="usa-fieldset margin-top-2">
<legend>
<h2>Are there other employees who can help verify your request?</h2>
</legend>
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
<div class="margin-top-2">
{% with add_class="usa-radio__input--tile" add_legend_heading="Are there other employees who can help verify your request?" %}
{% input_with_errors forms.0.has_other_contacts %}
{% endwith %}
{# forms.0 is a small yes/no form that toggles the visibility of "other contact" formset #}
</fieldset>
</div>
<div id="other-employees" class="other-contacts-form">
{% include "includes/required_fields.html" %}
{{ forms.1.management_form }}
{# forms.1 is a formset and this iterates over its forms #}
{% for form in forms.1.forms %}

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

@ -49,7 +49,7 @@
</ul>
{% if domain_manager_roles %}
<section class="section-outlined">
<section class="section-outlined" id="domain-managers">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
<h2 class> Domain managers </h2>
<caption class="sr-only">Domain managers</caption>
@ -89,12 +89,13 @@
aria-describedby="You will be removed from this domain"
data-force-action
>
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
{% with domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button_self|safe %}
{% endwith %}
</form>
{% with domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove myself" modal_button_class="usa-button--secondary" %}
{% endwith %}
</div>
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}" >
{% csrf_token %}
</form>
{% else %}
<div
class="usa-modal"
@ -103,12 +104,13 @@
aria-describedby="{{ item.permission.user.email }} will be removed"
data-force-action
>
<form method="POST" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
{% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button=modal_button|safe %}
{% endwith %}
</form>
{% with email=item.permission.user.email|default:item.permission.user|force_escape domain_name=domain.name|force_escape counter_str=forloop.counter|stringformat:"s" %}
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove domain manager" modal_button_class="usa-button--secondary" %}
{% endwith %}
</div>
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
{% csrf_token %}
</form>
{% endif %}
{% else %}
<input

View file

@ -0,0 +1,34 @@
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
Hi.
{{ requestor_email }} has invited you to {{ portfolio.organization_name }}.
You can view this organization on the .gov registrar <https://manage.get.gov>.
----------------------------------------------------------------
YOU NEED A LOGIN.GOV ACCOUNT
Youll need a Login.gov account to access this .gov organization. That account
needs to be associated with the following email address: {{ email }}
Login.gov provides a simple and secure process for signing in to many government
services with one account. If you dont already have one, follow these steps to
create your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
SOMETHING WRONG?
If youre not affiliated with {{ portfolio.organization_name }} or think you received this
message in error, reply to this email.
THANK YOU
.Gov helps the public identify official, trusted information. Thank you for using a .gov domain.
----------------------------------------------------------------
The .gov team
Contact us: <https://get.gov/contact/>
Learn about .gov <https://get.gov>
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
{% endautoescape %}

View file

@ -0,0 +1 @@
Youve been invited to a .gov organization

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="register-form-review-header">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="register-form-review-header">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 %}
@ -54,11 +54,14 @@
{% if portfolio %}
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
<section aria-label="Domain Requests report component" class="margin-top-205">
<a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
<!----------------------------------------------------------------------
This link is commented out because we intend to add it back in later.
------------------------------------------------------------------------->
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
</svg>Export as CSV
</a>
</a> -->
</section>
</div>
{% endif %}

View file

@ -1,10 +1,30 @@
{% load static %}
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domains_json' as url %}
<span id="get_domains_json_url" class="display-none">{{url}}</span>
<!-- Org model banner (org manager can view, domain manager can edit) -->
{% 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 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>
{% else%}
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
{% endif %}
</p>
</div>
</div>
</section>
{% endif %}
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
<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 %}
@ -13,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 %}
@ -53,7 +73,24 @@
</div>
{% endif %}
</div>
{% if portfolio %}
<!-- Non org model banner -->
{% 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 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>
{% else%}
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
{% endif %}
</p>
</div>
</div>
</section>
{% endif %}
<div class="display-flex flex-align-center">
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
<div class="usa-accordion usa-accordion--select margin-right-2">
@ -135,6 +172,19 @@
>Deleted</label
>
</div>
{% if has_domain_renewal_flag and num_expiring_domains > 0 %}
<div class="usa-checkbox">
<input
class="usa-checkbox__input"
id="filter-status-expiring"
type="checkbox"
name="filter-status"
value="expiring"
/>
<label class="usa-checkbox__label" for="filter-status-expiring"
>Expiring soon</label>
</div>
{% endif %}
</fieldset>
</div>
</div>
@ -149,7 +199,6 @@
</svg>
</button>
</div>
{% endif %}
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your registered domains</caption>
@ -200,4 +249,4 @@
<ul class="usa-pagination__list">
<!-- Pagination links will be dynamically populated by JS -->
</ul>
</nav>
</nav>

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,6 +1,7 @@
{% if form.errors %}
<div id="form-errors">
{% for error in form.non_field_errors %}
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2">
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert">
<div class="usa-alert__body">
{{ error|escape }}
</div>
@ -14,5 +15,6 @@
</div>
</div>
{% endfor %}
{% endfor %}
{% endfor %}
</div>
{% endif %}

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

@ -71,7 +71,11 @@ error messages, if necessary.
{% endif %}
{# this is the input field, itself #}
{% include widget.template_name %}
{% with aria_label=aria_label %}
{% include widget.template_name %}
{% endwith %}
{% if append_gov %}
<span class="padding-top-05 padding-left-2px">.gov </span>

View file

@ -34,7 +34,7 @@
{% endif %}
</h2>
<div class="section-outlined__header margin-bottom-3 grid-row">
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
<!-- ---------- SEARCH ---------- -->
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
<section aria-label="Member domains search component" class="margin-top-2">

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,4 +1,5 @@
{% load static form_helpers url_helpers %}
{% load custom_filters %}
<div class="usa-modal__content">
<div class="usa-modal__main">
@ -24,39 +25,51 @@
<div class="usa-modal__footer">
<ul class="usa-button-group">
<li class="usa-button-group__item">
{% if not_form and modal_button %}
{{ modal_button }}
{% if cancel_button_only %}
<button
type="button"
class="{{ modal_button_class|button_class }}"
data-close-modal
>
{% if modal_button_text %}
{{ modal_button_text }}
{% else %}
Cancel
{% endif %}
</button>
{% elif modal_button_id and modal_button_text %}
{% comment %} Adding button id allows for onclick listeners on button by id,
which execute form submission on form elsewhere on a page outside modal.{% endcomment %}
<button
id="{{ modal_button_id }}"
type="button"
class="{{ modal_button_class|button_class }}"
>
{{ modal_button_text }}
</button>
{% elif modal_button_url and modal_button_text %}
<a
href="{{ modal_button_url }}"
type="button"
class="usa-button"
class="{{ modal_button_class|button_class }}"
>
{{ modal_button_text }}
</a>
{% else %}
<form method="post">
{% csrf_token %}
{{ modal_button }}
</form>
{% endif %}
</li>
<li class="usa-button-group__item">
{% comment %} The cancel button the DS form actually triggers a context change in the view,
in addition to being a close modal hook {% endcomment %}
{% if cancel_button_resets_ds_form %}
<form method="post">
{% csrf_token %}
<button
type="submit"
class="usa-button usa-button--unstyled padding-105 text-center"
name="btn-cancel-click"
data-close-modal
>
Cancel
</button>
</form>
{% elif not is_domain_request_form or review_form_is_complete %}
<button
type="submit"
class="usa-button usa-button--unstyled padding-105 text-center"
id="btn-cancel-click-button"
data-close-modal
>
Cancel
</button>
{% elif not cancel_button_only %}
<button
type="button"
class="usa-button usa-button--unstyled padding-105 text-center"
@ -72,20 +85,17 @@
{% comment %} The cancel button the DS form actually triggers a context change in the view,
in addition to being a close modal hook {% endcomment %}
{% if cancel_button_resets_ds_form %}
<form method="post">
{% csrf_token %}
<button
type="submit"
class="usa-button usa-modal__close"
aria-label="Close this window"
name="btn-cancel-click"
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>
</form>
<button
type="submit"
class="usa-button usa-modal__close"
aria-label="Close this window"
id="btn-cancel-click-close-button"
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>
{% else %}
<button
type="button"

View file

@ -46,7 +46,7 @@
{% endwith %}
{% if domain_request.alternative_domains.all %}
<h3 class="register-form-review-header">Alternative domains</h3>
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% for site in domain_request.alternative_domains.all %}
<li>{{ site.website }}</li>

View file

@ -88,7 +88,7 @@
{% endwith %}
{% if domain_request.alternative_domains.all %}
<h3 class="register-form-review-header">Alternative domains</h3>
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% for site in domain_request.alternative_domains.all %}
<li>{{ site.website }}</li>
@ -132,7 +132,7 @@
{% with title=form_titles|get_item:step %}
{% if domain_request.has_additional_details %}
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
<h3 class="register-form-review-header">CISA Regional Representative</h3>
<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 domain_request.cisa_representative_first_name %}
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
@ -144,7 +144,7 @@
{% endif %}
</ul>
<h3 class="register-form-review-header">Anything else</h3>
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
<ul class="usa-list usa-list--unstyled margin-top-0">
{% if domain_request.anything_else %}
{{domain_request.anything_else}}

View file

@ -1,222 +1,223 @@
{% 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 %}
<h3 class="register-form-review-header">CISA Regional Representative</h3>
<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 }}
@ -224,7 +225,7 @@
No
{% endif %}
</ul>
<h3 class="register-form-review-header">Anything else</h3>
<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}}
@ -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 %}
@ -26,7 +30,6 @@
</form>
{% elif not form.full_name.value and not form.title.value and not form.email.value %}
<p>
Your senior official is a person within your organization who can authorize domain requests.
We don't have information about your organization's senior official. To suggest an update, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
</p>
{% else %}

View file

@ -22,7 +22,7 @@
</h3>
{% endif %}
{% if sub_header_text %}
<h4 class="register-form-review-header">{{ sub_header_text }}</h4>
<h4 class="header--body text-primary-dark margin-bottom-0">{{ sub_header_text }}</h4>
{% endif %}
{% if permissions %}
{% include "includes/member_permissions.html" with permissions=value %}

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%}

Some files were not shown because too many files have changed in this diff Show more