merge main

This commit is contained in:
David Kennedy 2025-03-05 15:16:53 -05:00
commit a76779b2c2
No known key found for this signature in database
GPG key ID: 6528A5386E66B96B
29 changed files with 280 additions and 241 deletions

View file

@ -163,6 +163,18 @@ class MyUserAdminForm(UserChangeForm):
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False), "user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
} }
# Loads "tabtitle" for this admin page so that on render the <title>
# element will only have the model name instead of
# the default string loaded by native Django admin code.
# (Eg. instead of "Select contact to change", display "Contacts")
# see "base_site.html" for the <title> code.
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Custom init to modify the user form""" """Custom init to modify the user form"""
super(MyUserAdminForm, self).__init__(*args, **kwargs) super(MyUserAdminForm, self).__init__(*args, **kwargs)
@ -662,6 +674,18 @@ class CustomLogEntryAdmin(LogEntryAdmin):
"user_url", "user_url",
] ]
# Loads "tabtitle" for this admin page so that on render the <title>
# element will only have the model name instead of
# the default string loaded by native Django admin code.
# (Eg. instead of "Select contact to change", display "Contacts")
# see "base_site.html" for the <title> code.
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
# We name the custom prop 'resource' because linter # We name the custom prop 'resource' because linter
# is not allowing a short_description attr on it # is not allowing a short_description attr on it
# This gets around the linter limitation, for now. # This gets around the linter limitation, for now.
@ -681,13 +705,6 @@ class CustomLogEntryAdmin(LogEntryAdmin):
change_form_template = "admin/change_form_no_submit.html" change_form_template = "admin/change_form_no_submit.html"
add_form_template = "admin/change_form_no_submit.html" add_form_template = "admin/change_form_no_submit.html"
# Select log entry to change -> Log entries
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Log entries"
return super().changelist_view(request, extra_context=extra_context)
# #786: Skipping on updating audit log tab titles for now # #786: Skipping on updating audit log tab titles for now
# def change_view(self, request, object_id, form_url="", extra_context=None): # def change_view(self, request, object_id, form_url="", extra_context=None):
# if extra_context is None: # if extra_context is None:
@ -768,6 +785,18 @@ class AdminSortFields:
class AuditedAdmin(admin.ModelAdmin): class AuditedAdmin(admin.ModelAdmin):
"""Custom admin to make auditing easier.""" """Custom admin to make auditing easier."""
# Loads "tabtitle" for this admin page so that on render the <title>
# element will only have the model name instead of
# the default string loaded by native Django admin code.
# (Eg. instead of "Select contact to change", display "Contacts")
# see "base_site.html" for the <title> code.
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def history_view(self, request, object_id, extra_context=None): def history_view(self, request, object_id, extra_context=None):
"""On clicking 'History', take admin to the auditlog view for an object.""" """On clicking 'History', take admin to the auditlog view for an object."""
return HttpResponseRedirect( return HttpResponseRedirect(
@ -1168,6 +1197,18 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios} extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
return super().change_view(request, object_id, form_url, extra_context) return super().change_view(request, object_id, form_url, extra_context)
# Loads "tabtitle" for this admin page so that on render the <title>
# element will only have the model name instead of
# the default string loaded by native Django admin code.
# (Eg. instead of "Select contact to change", display "Contacts")
# see "base_site.html" for the <title> code.
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class HostIPInline(admin.StackedInline): class HostIPInline(admin.StackedInline):
"""Edit an ip address on the host page.""" """Edit an ip address on the host page."""
@ -1192,14 +1233,6 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
search_help_text = "Search by domain or host name." search_help_text = "Search by domain or host name."
inlines = [HostIPInline] inlines = [HostIPInline]
# Select host to change -> Host
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Host"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class HostIpResource(resources.ModelResource): class HostIpResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1215,14 +1248,6 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
resource_classes = [HostIpResource] resource_classes = [HostIpResource]
model = models.HostIP model = models.HostIP
# Select host ip to change -> Host ip
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Host IP"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class ContactResource(resources.ModelResource): class ContactResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -1344,14 +1369,6 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
return super().change_view(request, object_id, form_url, extra_context=extra_context) return super().change_view(request, object_id, form_url, extra_context=extra_context)
# Select contact to change -> Contacts
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Contacts"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
# Clear warning messages before saving # Clear warning messages before saving
storage = messages.get_messages(request) storage = messages.get_messages(request)
@ -1667,14 +1684,6 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
# Override for the delete confirmation page on the domain table (bulk delete action) # Override for the delete confirmation page on the domain table (bulk delete action)
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html" delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
# Select domain invitations to change -> Domain invitations
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain invitations"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
"""Override the change_view to add the invitation obj for the change_form_object_tools template""" """Override the change_view to add the invitation obj for the change_form_object_tools template"""
@ -1819,14 +1828,6 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
get_roles.short_description = "Member access" # type: ignore get_roles.short_description = "Member access" # type: ignore
# Select portfolio invitations to change -> Portfolio invitations
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Portfolio invitations"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def save_model(self, request, obj, form, change): def save_model(self, request, obj, form, change):
""" """
Override the save_model method. Override the save_model method.
@ -2216,14 +2217,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
readonly_fields.extend([field for field in self.analyst_readonly_fields]) readonly_fields.extend([field for field in self.analyst_readonly_fields])
return readonly_fields # Read-only fields for analysts return readonly_fields # Read-only fields for analysts
# Select domain information to change -> Domain information
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain information"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
def formfield_for_foreignkey(self, db_field, request, **kwargs): def formfield_for_foreignkey(self, db_field, request, **kwargs):
"""Customize the behavior of formfields with foreign key relationships. This will customize """Customize the behavior of formfields with foreign key relationships. This will customize
the behavior of selects. Customized behavior includes sorting of objects in list.""" the behavior of selects. Customized behavior includes sorting of objects in list."""
@ -3044,11 +3037,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
if next_char.isdigit(): if next_char.isdigit():
should_apply_default_filter = True should_apply_default_filter = True
# Select domain request to change -> Domain requests
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Domain requests"
if should_apply_default_filter: if should_apply_default_filter:
# modify the GET of the request to set the selected filter # modify the GET of the request to set the selected filter
modified_get = copy.deepcopy(request.GET) modified_get = copy.deepcopy(request.GET)
@ -4105,14 +4093,6 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
# If no redirection is needed, return the original response # If no redirection is needed, return the original response
return response return response
# Select draft domain to change -> Draft domains
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "Draft domains"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class PublicContactResource(resources.ModelResource): class PublicContactResource(resources.ModelResource):
"""defines how each field in the referenced model should be mapped to the corresponding fields in the """defines how each field in the referenced model should be mapped to the corresponding fields in the
@ -4534,14 +4514,6 @@ class UserGroupAdmin(AuditedAdmin):
def user_group(self, obj): def user_group(self, obj):
return obj.name return obj.name
# Select user groups to change -> User groups
def changelist_view(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["tabtitle"] = "User groups"
# Get the filtered values
return super().changelist_view(request, extra_context=extra_context)
class WaffleFlagAdmin(FlagAdmin): class WaffleFlagAdmin(FlagAdmin):
"""Custom admin implementation of django-waffle's Flag class""" """Custom admin implementation of django-waffle's Flag class"""
@ -4558,6 +4530,13 @@ class WaffleFlagAdmin(FlagAdmin):
if extra_context is None: if extra_context is None:
extra_context = {} extra_context = {}
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag") extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
# Loads "tabtitle" for this admin page so that on render the <title>
# element will only have the model name instead of
# the default string loaded by native Django admin code.
# (Eg. instead of "Select waffle flags to change", display "Waffle Flags")
# see "base_site.html" for the <title> code.
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
return super().changelist_view(request, extra_context=extra_context) return super().changelist_view(request, extra_context=extra_context)

View file

@ -5284,7 +5284,10 @@ const setUpModal = baseComponent => {
overlayDiv.classList.add(OVERLAY_CLASSNAME); overlayDiv.classList.add(OVERLAY_CLASSNAME);
// Set attributes // Set attributes
modalWrapper.setAttribute("role", "dialog"); // DOTGOV
// Removes the dialog role as this causes a double readout bug with screenreaders
// modalWrapper.setAttribute("role", "dialog");
// END DOTGOV
modalWrapper.setAttribute("id", modalID); modalWrapper.setAttribute("id", modalID);
if (ariaLabelledBy) { if (ariaLabelledBy) {
modalWrapper.setAttribute("aria-labelledby", ariaLabelledBy); modalWrapper.setAttribute("aria-labelledby", ariaLabelledBy);

View file

@ -1,4 +1,4 @@
import { hideElement, showElement, addOrRemoveSessionBoolean } from './helpers-admin.js'; import { hideElement, showElement, addOrRemoveSessionBoolean, announceForScreenReaders } from './helpers-admin.js';
import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js'; import { handlePortfolioSelection } from './helpers-portfolio-dynamic-fields.js';
function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){ function displayModalOnDropdownClick(linkClickedDisplaysModal, statusDropdown, actionButton, valueToCheck){
@ -684,3 +684,33 @@ export function initDynamicDomainRequestFields(){
handleSuborgFieldsAndButtons(); handleSuborgFieldsAndButtons();
} }
} }
export function initFilterFocusListeners() {
document.addEventListener("DOMContentLoaded", function() {
let filters = document.querySelectorAll("#changelist-filter li a"); // Get list of all filter links
let clickedFilter = false; // Used to determine if we are truly navigating away or not
// Restore focus from localStorage
let lastClickedFilterId = localStorage.getItem("admin_filter_focus_id");
if (lastClickedFilterId) {
let focusedElement = document.getElementById(lastClickedFilterId);
if (focusedElement) {
//Focus the element
focusedElement.setAttribute("tabindex", "0");
focusedElement.focus({ preventScroll: true });
// Announce focus change for screen readers
announceForScreenReaders("Filter refocused on " + focusedElement.textContent);
localStorage.removeItem("admin_filter_focus_id");
}
}
// Capture clicked filter and store its ID
filters.forEach(filter => {
filter.addEventListener("click", function() {
localStorage.setItem("admin_filter_focus_id", this.id);
clickedFilter = true; // Mark that a filter was clicked
});
});
});
}

View file

@ -32,3 +32,22 @@ export function getParameterByName(name, url) {
if (!results[2]) return ''; if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' ')); return decodeURIComponent(results[2].replace(/\+/g, ' '));
} }
/**
* Creates a temporary live region to announce messages for screen readers.
*/
export function announceForScreenReaders(message) {
let liveRegion = document.createElement("div");
liveRegion.setAttribute("aria-live", "assertive");
liveRegion.setAttribute("role", "alert");
liveRegion.setAttribute("class", "usa-sr-only");
document.body.appendChild(liveRegion);
// Delay the update slightly to ensure it's recognized
setTimeout(() => {
liveRegion.textContent = message;
setTimeout(() => {
document.body.removeChild(liveRegion);
}, 1000);
}, 100);
}

View file

@ -10,7 +10,8 @@ import {
initRejectedEmail, initRejectedEmail,
initApprovedDomain, initApprovedDomain,
initCopyRequestSummary, initCopyRequestSummary,
initDynamicDomainRequestFields } from './domain-request-form.js'; initDynamicDomainRequestFields,
initFilterFocusListeners } from './domain-request-form.js';
import { initDomainFormTargetBlankButtons } from './domain-form.js'; import { initDomainFormTargetBlankButtons } from './domain-form.js';
import { initDynamicPortfolioFields } from './portfolio-form.js'; import { initDynamicPortfolioFields } from './portfolio-form.js';
import { initDynamicPortfolioPermissionFields } from './portfolio-permissions-form.js' import { initDynamicPortfolioPermissionFields } from './portfolio-permissions-form.js'
@ -35,6 +36,7 @@ initRejectedEmail();
initApprovedDomain(); initApprovedDomain();
initCopyRequestSummary(); initCopyRequestSummary();
initDynamicDomainRequestFields(); initDynamicDomainRequestFields();
initFilterFocusListeners();
// Domain // Domain
initDomainFormTargetBlankButtons(); initDomainFormTargetBlankButtons();

View file

@ -292,7 +292,18 @@ export function initFormsetsForms() {
// For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden, // For the other contacts form, we need to update the fieldset headers based on what's visible vs hidden,
// since the form on the backend employs Django's DELETE widget. // since the form on the backend employs Django's DELETE widget.
let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length; let totalShownForms = document.querySelectorAll(`.repeatable-form:not([style*="display: none"])`).length;
newForm.innerHTML = newForm.innerHTML.replace(formLabelRegex, `${formLabel} ${totalShownForms + 1}`); let newFormCount = totalShownForms + 1;
// update the header
let header = newForm.querySelector('legend h3');
header.textContent = `${formLabel} ${newFormCount}`;
header.id = `org-contact-${newFormCount}`;
// update accessibility elements on the delete buttons
let deleteDescription = newForm.querySelector('.delete-button-description');
deleteDescription.textContent = 'Delete new contact';
deleteDescription.id = `org-contact-${newFormCount}__name`;
let deleteButton = newForm.querySelector('button');
deleteButton.setAttribute("aria-labelledby", header.id);
deleteButton.setAttribute("aria-describedby", deleteDescription.id);
} else { } else {
// Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional // Nameservers form is cloned from index 2 which has the word optional on init, does not have the word optional
// if indices 0 or 1 have been deleted // if indices 0 or 1 have been deleted

View file

@ -15,6 +15,7 @@ import { initDomainManagersPage } from './domain-managers.js';
import { initDomainDSData } from './domain-dsdata.js'; import { initDomainDSData } from './domain-dsdata.js';
import { initDomainDNSSEC } from './domain-dnssec.js'; import { initDomainDNSSEC } from './domain-dnssec.js';
import { initFormErrorHandling } from './form-errors.js'; import { initFormErrorHandling } from './form-errors.js';
import { initButtonLinks } from '../getgov-admin/button-utils.js';
initDomainValidators(); initDomainValidators();
@ -49,3 +50,5 @@ initFormErrorHandling();
initPortfolioMemberPageRadio(); initPortfolioMemberPageRadio();
initPortfolioNewMemberPageToggle(); initPortfolioNewMemberPageToggle();
initAddNewMemberPageListeners(); initAddNewMemberPageListeners();
initButtonLinks();

View file

@ -25,7 +25,6 @@
// Note, width is determined by a custom width class on one of the children // Note, width is determined by a custom width class on one of the children
position: absolute; position: absolute;
z-index: 1; z-index: 1;
left: 0;
border-radius: 4px; border-radius: 4px;
border: solid 1px color('base-lighter'); border: solid 1px color('base-lighter');
padding: units(2) units(2) units(3) units(2); padding: units(2) units(2) units(3) units(2);
@ -42,6 +41,14 @@
} }
} }
// This will work in responsive tables if we overwrite the overflow value on the table container
// Works with styles in _tables
@include at-media(desktop) {
.usa-accordion--more-actions .usa-accordion__content {
left: 0;
}
}
.usa-accordion--select .usa-accordion__content { .usa-accordion--select .usa-accordion__content {
top: 33.88px; top: 33.88px;
} }
@ -59,10 +66,12 @@
// This won't work on the Members table rows because that table has show-more rows // 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 // Currently, that's not an issue since that Members table is not wrapped in the
// reponsive wrapper. // reponsive wrapper.
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content { @include at-media-max("desktop") {
top: auto; tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
bottom: -10px; top: auto;
right: 30px; bottom: -10px;
right: 30px;
}
} }
// A CSS only show-more/show-less based on usa-accordion // A CSS only show-more/show-less based on usa-accordion

View file

@ -226,11 +226,6 @@ abbr[title] {
} }
} }
// Boost this USWDS utility class for the accordions in the portfolio requests table
.left-auto {
left: auto!important;
}
.usa-banner__inner--widescreen { .usa-banner__inner--widescreen {
max-width: $widescreen-max-width; max-width: $widescreen-max-width;
} }

View file

@ -152,3 +152,12 @@ th {
.usa-table--full-borderless th { .usa-table--full-borderless th {
border: none !important; border: none !important;
} }
// This is an override to overflow on certain tables (note the custom class)
// so that a popup menu can appear and starddle the edge of the table on large
// screen sizes. Works with styles in _accordions
@include at-media(desktop) {
.usa-table-container--scrollable.usa-table-container--override-overflow {
overflow-y: visible;
}
}

View file

@ -315,7 +315,7 @@ class DomainRequestFixture:
cls._create_domain_requests(users) cls._create_domain_requests(users)
@classmethod @classmethod
def _create_domain_requests(cls, users): # noqa: C901 def _create_domain_requests(cls, users, total_requests=None): # noqa: C901
"""Creates DomainRequests given a list of users.""" """Creates DomainRequests given a list of users."""
total_domain_requests_to_make = len(users) # 100000 total_domain_requests_to_make = len(users) # 100000
@ -323,27 +323,33 @@ class DomainRequestFixture:
# number of entries. # number of entries.
# (Prevents re-adding more entries to an already populated database, # (Prevents re-adding more entries to an already populated database,
# which happens when restarting Docker src) # which happens when restarting Docker src)
domain_requests_already_made = DomainRequest.objects.count() total_existing_requests = DomainRequest.objects.count()
domain_requests_to_create = [] domain_requests_to_create = []
if domain_requests_already_made < total_domain_requests_to_make: if total_requests and total_requests <= total_existing_requests:
for user in users: total_domain_requests_to_make = total_requests - total_existing_requests
for request_data in cls.DOMAINREQUESTS: if total_domain_requests_to_make >= 0:
# Prepare DomainRequest objects DomainRequest.objects.filter(
try: id__in=list(DomainRequest.objects.values_list("pk", flat=True)[:total_domain_requests_to_make])
domain_request = DomainRequest( ).delete()
creator=user, if total_domain_requests_to_make == 0:
organization_name=request_data["organization_name"], return
)
cls._set_non_foreign_key_fields(domain_request, request_data)
cls._set_foreign_key_fields(domain_request, request_data, user)
domain_requests_to_create.append(domain_request)
except Exception as e:
logger.warning(e)
num_additional_requests_to_make = ( for user in users:
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create) for request_data in cls.DOMAINREQUESTS:
) # Prepare DomainRequest objects
try:
domain_request = DomainRequest(
creator=user,
organization_name=request_data["organization_name"],
)
cls._set_non_foreign_key_fields(domain_request, request_data)
cls._set_foreign_key_fields(domain_request, request_data, user)
domain_requests_to_create.append(domain_request)
except Exception as e:
logger.warning(e)
num_additional_requests_to_make = total_domain_requests_to_make - len(domain_requests_to_create)
if num_additional_requests_to_make > 0: if num_additional_requests_to_make > 0:
for _ in range(num_additional_requests_to_make): for _ in range(num_additional_requests_to_make):
random_user = random.choice(users) # nosec random_user = random.choice(users) # nosec

View file

@ -2,6 +2,10 @@
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block title %}
Registrar Analytics | Django admin
{% endblock %}
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %} {% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}

View file

@ -34,7 +34,7 @@
{% else %} {% else %}
{{ title }} | {{ title }} |
{% endif %} {% endif %}
{{ site_title|default:_('Django site admin') }} Django admin
{% endblock %} {% endblock %}
{% block extrastyle %}{{ block.super }} {% block extrastyle %}{{ block.super }}

View file

@ -0,0 +1,13 @@
{% comment %} Override of this file: https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/filter.html {% endcomment %}
{% load i18n %}
<details data-filter-title="{{ title }}" open>
<summary>
{% blocktranslate with filter_title=title %} By {{ filter_title }} {% endblocktranslate %}
</summary>
<ul>
{% for choice in choices %}
<li {% if choice.selected %} class="selected"{% endif %}>
<a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}">{{ choice.display }}</a></li>
{% endfor %}
</ul>
</details>

View file

@ -9,16 +9,12 @@
{% for choice in choices %} {% for choice in choices %}
{% if choice.reset %} {% 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> <a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
</li> </li>
{% endif %} {% else %}
{% endfor %} <li{% if choice.selected %} class="selected"{% endif %}>
{% for choice in choices %}
{% if not choice.reset %}
<li{% if choice.selected %} class="selected"{% endif %}">
{% if choice.selected and choice.exclude_query_string %} {% if choice.selected and choice.exclude_query_string %}
<a role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }} <a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" 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"> <svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use> <use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
</svg> </svg>
@ -26,9 +22,8 @@
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use> <use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
</svg> </svg>
</a> </a>
{% endif %} {% elif not choice.selected and choice.include_query_string %}
{% if not choice.selected and choice.include_query_string %} <a id="{{ title|lower|cut:' ' }}-filter-{{ choice.display|slugify }}" role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
<a role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_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"> <svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use> <use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
</svg> </svg>

View file

@ -29,7 +29,10 @@
{% csrf_token %} {% csrf_token %}
{% if domain.domain_info.generic_org_type == 'federal' %} {% if domain.domain_info.generic_org_type == 'federal' %}
{% input_with_errors form.federal_agency %} <h4 class="margin-bottom-05">Federal Agency</h4>
<p class="margin-top-0">
{{ domain.domain_info.federal_agency }}
</p>
{% endif %} {% endif %}
{% input_with_errors form.organization_name %} {% input_with_errors form.organization_name %}

View file

@ -61,7 +61,7 @@
<fieldset class="usa-fieldset margin-top-1 dotgov-domain-form" id="form-container"> <fieldset class="usa-fieldset margin-top-1 dotgov-domain-form" id="form-container">
<legend> <legend>
<h2>Alternative domains (optional)</h2> <h2 id="alternative-domains-title">Alternative domains (optional)</h2>
</legend> </legend>
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains youd like if we cant give <p id="alt_domain_instructions" class="margin-top-05">Are there other domains youd like if we cant give
@ -80,18 +80,22 @@
{% endwith %} {% endwith %}
{% endwith %} {% endwith %}
<button type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form"> <div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div>
<button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use> <use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
</svg><span class="margin-left-05">Add another alternative</span> </svg><span class="margin-left-05">Add another alternative</span>
</button> </button>
<div class="margin-bottom-3"> <div class="margin-bottom-3">
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
<button <button
id="validate-alt-domains-availability" id="validate-alt-domains-availability"
type="button" type="button"
class="usa-button usa-button--outline" class="usa-button usa-button--outline"
validate-for="{{ forms.1.requested_domain.auto_id }}" validate-for="{{ forms.1.requested_domain.auto_id }}"
aria-labelledby="alternative-domains-title"
aria-describedby="alternative-domains__check-availability"
>Check availability</button> >Check availability</button>
</div> </div>

View file

@ -31,10 +31,14 @@
<fieldset class="usa-fieldset repeatable-form padding-y-1"> <fieldset class="usa-fieldset repeatable-form padding-y-1">
<legend class="float-left-tablet"> <legend class="float-left-tablet">
<h3 class="margin-top-05">Organization contact {{ forloop.counter }}</h2> <h3 class="margin-top-05" id="org-contact-{{ forloop.counter }}">Organization contact {{ forloop.counter }}</h2>
</legend> </legend>
{% if form.first_name or form.last_name %}
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon"> <span class="usa-sr-only delete-button-description" id="org-contact-{{ forloop.counter }}__name">Delete {{form.first_name.value }} {{ form.last_name.value }}</span>
{% else %}
<span class="usa-sr-only" id="org-contact-{{ forloop.counter }}__name">Delete new contact</span>
{% endif %}
<button aria-labelledby="org-contact-{{ forloop.counter }}" aria-describedby="org-contact-{{ forloop.counter }}__name" type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use> <use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
</svg>Delete </svg>Delete

View file

@ -18,10 +18,10 @@
<h1>Manage your domains</h1> <h1>Manage your domains</h1>
<p class="margin-top-4"> <p class="margin-top-4">
<a href="{% url 'domain-request:start' %}" class="usa-button" <button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
> >
Start a new domain request Start a new domain request
</a> </button>
</p> </p>
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %} {% include "includes/domains_table.html" with user_domain_count=user_domain_count %}

View file

@ -14,22 +14,15 @@
{% endif %} {% endif %}
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% 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"> <section aria-label="Domain requests search component" id="domain-requests-search-component" class="margin-top-2">
<form class="usa-search usa-search--small" method="POST" role="search"> <form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %} {% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button"> <button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button" aria-labelledby="domain-requests-search-component">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use> <use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg> </svg>
Reset Reset
</button> </button>
<label id="domain-requests__search-label" class="usa-sr-only" for="domain-requests__search-field">
{% if portfolio %}
Search by domain name or creator
{% else %}
Search by domain name
{% endif %}
</label>
<input <input
class="usa-input" class="usa-input"
id="domain-requests__search-field" id="domain-requests__search-field"
@ -40,8 +33,10 @@
{% else %} {% else %}
placeholder="Search by domain name" placeholder="Search by domain name"
{% endif %} {% endif %}
aria-labelledby="domain-requests-search-component"
/> />
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests__search-label"> <div class="usa-sr-only" id="domain-requests-search-button__description">Click to search</div>
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests-search-component" aria-describedby="domain-requests-search-button__description">
<img <img
src="{% static 'img/usa-icons-bg/search--white.svg' %}" src="{% static 'img/usa-icons-bg/search--white.svg' %}"
class="usa-search__submit-icon" class="usa-search__submit-icon"
@ -163,7 +158,7 @@
</div> </div>
{% endif %} {% endif %}
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domain-requests__table-wrapper"> <div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domain-requests__table-wrapper">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your domain requests</caption> <caption class="sr-only">Your domain requests</caption>
<thead> <thead>

View file

@ -34,24 +34,25 @@
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span> <span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
{% endif %} {% endif %}
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% 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"> <section aria-label="Domains search component" class="margin-top-2" id="domains-search-component">
<form class="usa-search usa-search--small" method="POST" role="search"> <form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %} {% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button"> <button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button" aria-labelledby="domains-search-component">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use> <use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg> </svg>
Reset Reset
</button> </button>
<label id="domains__search-label" class="usa-sr-only" for="domains__search-field">Search by domain name</label>
<input <input
class="usa-input" class="usa-input"
id="domains__search-field" id="domains__search-field"
type="search" type="search"
name="domains-search" name="domains-search"
placeholder="Search by domain name" placeholder="Search by domain name"
aria-labelledby="domains-search-component"
/> />
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains__search-label"> <div class="usa-sr-only" id="domains-search-button__description">Click to search</div>
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains-search-component" aria-describedby="domains-search-button__description">
<img <img
src="{% static 'img/usa-icons-bg/search--white.svg' %}" src="{% static 'img/usa-icons-bg/search--white.svg' %}"
class="usa-search__submit-icon" class="usa-search__submit-icon"
@ -63,12 +64,13 @@
</div> </div>
{% if user_domain_count and user_domain_count > 0 %} {% if user_domain_count and user_domain_count > 0 %}
<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 %}"> <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 %}">
<section aria-label="Domains report component" class="margin-top-205"> <section aria-label="Domains report component" class="margin-top-205" id="domains-report-component">
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right"> <div class="usa-sr-only" id="domains-export-button__description">Click to export as csv</div>
<button data-href="{% url 'export_data_type_user' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="domains-report-component" aria-describedby="domains-export-button__description">
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <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> <use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
</svg>Export as CSV </svg>Export as CSV
</a> </button>
</section> </section>
</div> </div>
{% endif %} {% endif %}
@ -198,7 +200,7 @@
</svg> </svg>
</button> </button>
</div> </div>
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper"> <div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domains__table-wrapper">
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked"> <table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
<caption class="sr-only">Your registered domains</caption> <caption class="sr-only">Your registered domains</caption>
<thead> <thead>

View file

@ -1,16 +1,18 @@
{% if form.errors %} {% if form.errors %}
<div id="form-errors"> <div id="form-errors">
{% for error in form.non_field_errors %} {% for error in form.non_field_errors %}
<div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert"> <div class="usa-alert usa-alert--error usa-alert--slim margin-bottom-2" role="alert" tabindex="0">
<div class="usa-alert__body"> <div class="usa-alert__body">
{{ error|escape }} <span class="usa-sr-only">Error:</span>
{{ error|escape }}
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
{% for field in form %} {% for field in form %}
{% for error in field.errors %} {% for error in 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" tabindex="0">
<div class="usa-alert__body"> <div class="usa-alert__body">
<span class="usa-sr-only">Error:</span>
{{ error|escape }} {{ error|escape }}
</div> </div>
</div> </div>

View file

@ -9,24 +9,25 @@
<div class="section-outlined__header margin-bottom-3 grid-row"> <div class="section-outlined__header margin-bottom-3 grid-row">
<!-- ---------- SEARCH ---------- --> <!-- ---------- SEARCH ---------- -->
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen"> <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"> <section aria-label="Members search component" class="margin-top-2" id="members-search-component">
<form class="usa-search usa-search--small" method="POST" role="search"> <form class="usa-search usa-search--small" method="POST" role="search">
{% csrf_token %} {% csrf_token %}
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button"> <button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button" aria-labelledby="members-search-component">
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24"> <svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use> <use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
</svg> </svg>
Reset Reset
</button> </button>
<label class="usa-sr-only" for="members__search-field">Search by member name</label>
<input <input
class="usa-input" class="usa-input"
id="members__search-field" id="members__search-field"
type="search" type="search"
name="members-search" name="members-search"
placeholder="Search by member name" placeholder="Search by member name"
aria-labelledby="members-search-component"
/> />
<button class="usa-button" type="submit" id="members__search-field-submit"> <div class="usa-sr-only" id="members-search-button__description">Click to search</div>
<button class="usa-button" type="submit" id="members__search-field-submit" aria-labelledby="members-search-component" aria-describedby="members-search-button__description">
<img <img
src="{% static 'img/usa-icons-bg/search--white.svg' %}" src="{% static 'img/usa-icons-bg/search--white.svg' %}"
class="usa-search__submit-icon" class="usa-search__submit-icon"
@ -37,12 +38,13 @@
</section> </section>
</div> </div>
<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 %}"> <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 %}">
<section aria-label="Domains report component" class="margin-top-205"> <section aria-label="Members report component" class="margin-top-205" id="members-report-component">
<a href="{% url 'export_members_portfolio' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right"> <div class="usa-sr-only" id="members-export-button__description">Click to export as csv</div>
<button href="{% url 'export_members_portfolio' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="members-report-component" aria-describedby="members-export-button__description">
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24"> <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> <use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
</svg>Export as CSV </svg>Export as CSV
</a> </button>
</section> </section>
</div> </div>
</div> </div>

View file

@ -26,10 +26,10 @@
<div class="mobile:grid-col-12 tablet:grid-col-6"> <div class="mobile:grid-col-12 tablet:grid-col-6">
<p class="float-right-tablet tablet:margin-y-0"> <p class="float-right-tablet tablet:margin-y-0">
<a href="{% url 'domain-request:start' %}" class="usa-button" <button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
> >
Start a new domain request Start a new domain request
</a> </button>
</p> </p>
</div> </div>
{% else %} {% else %}

View file

@ -219,12 +219,12 @@ class TestDomainInvitationAdmin(WebTest):
# Assert that the filters are added # Assert that the filters are added
self.assertContains(response, "invited", count=4) self.assertContains(response, "invited", count=4)
self.assertContains(response, "Invited", count=2) self.assertContains(response, "Invited", count=2)
self.assertContains(response, "retrieved", count=2) self.assertContains(response, "retrieved", count=3)
self.assertContains(response, "Retrieved", count=2) self.assertContains(response, "Retrieved", count=2)
# Check for the HTML context specificially # Check for the HTML context specificially
invited_html = '<a href="?status__exact=invited">Invited</a>' invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>' retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'
self.assertContains(response, invited_html, count=1) self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1) self.assertContains(response, retrieved_html, count=1)
@ -1271,14 +1271,14 @@ class TestPortfolioInvitationAdmin(TestCase):
) )
# Assert that the filters are added # Assert that the filters are added
self.assertContains(response, "invited", count=4) self.assertContains(response, "invited", count=5)
self.assertContains(response, "Invited", count=2) self.assertContains(response, "Invited", count=2)
self.assertContains(response, "retrieved", count=2) self.assertContains(response, "retrieved", count=3)
self.assertContains(response, "Retrieved", count=2) self.assertContains(response, "Retrieved", count=2)
# Check for the HTML context specificially # Check for the HTML context specificially
invited_html = '<a href="?status__exact=invited">Invited</a>' invited_html = '<a id="status-filter-invited" href="?status__exact=invited">Invited</a>'
retrieved_html = '<a href="?status__exact=retrieved">Retrieved</a>' retrieved_html = '<a id="status-filter-retrieved" href="?status__exact=retrieved">Retrieved</a>'
self.assertContains(response, invited_html, count=1) self.assertContains(response, invited_html, count=1)
self.assertContains(response, retrieved_html, count=1) self.assertContains(response, retrieved_html, count=1)

View file

@ -888,8 +888,8 @@ class MemberExportTest(MockDbForIndividualTests, MockEppLib):
csv_content = csv_file.read() csv_content = csv_file.read()
expected_content = ( expected_content = (
# Header # Header
"Email,Organization admin,Invited by,Joined date,Last active,Domain requests," "Email,Member access,Invited by,Joined date,Last active,Domain requests,"
"Member management,Domain management,Number of domains,Domains\n" "Members,Domains,Number domains assigned,Domain assignments\n"
# Content # Content
"big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None," "big_lebowski@dude.co,False,help@get.gov,2022-04-01,Invalid date,None,"
"Viewer,True,1,cdomain1.gov\n" "Viewer,True,1,cdomain1.gov\n"

View file

@ -712,7 +712,7 @@ class TestDomainDetailDomainRenewal(TestDomainOverview):
self.assertRedirects(response, reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id})) self.assertRedirects(response, reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}))
# Check for the updated expiration # Check for the updated expiration
formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%b. %-d, %Y") formatted_new_expiration_date = self.expiration_date_one_year_out().strftime("%B %-d, %Y")
redirect_response = self.client.get( redirect_response = self.client.get(
reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}), follow=True reverse("domain", kwargs={"domain_pk": self.domain_with_ip.id}), follow=True
) )
@ -2088,62 +2088,6 @@ class TestDomainOrganization(TestDomainOverview):
# Check for the value we want to update # Check for the value we want to update
self.assertContains(success_result_page, "Faketown") self.assertContains(success_result_page, "Faketown")
@less_console_noise_decorator
def test_domain_org_name_address_form_federal(self):
"""
Submitting a change to federal_agency is blocked for federal domains
"""
fed_org_type = DomainInformation.OrganizationChoices.FEDERAL
self.domain_information.generic_org_type = fed_org_type
self.domain_information.save()
try:
federal_agency, _ = FederalAgency.objects.get_or_create(agency="AMTRAK")
self.domain_information.federal_agency = federal_agency
self.domain_information.save()
except ValueError as err:
self.fail(f"A ValueError was caught during the test: {err}")
self.assertEqual(self.domain_information.generic_org_type, fed_org_type)
org_name_page = self.app.get(reverse("domain-org-name-address", kwargs={"domain_pk": self.domain.id}))
form = org_name_page.forms[0]
# Check the value of the input field
agency_input = form.fields["federal_agency"][0]
self.assertEqual(agency_input.value, str(federal_agency.id))
# Check if the input field is disabled
self.assertTrue("disabled" in agency_input.attrs)
self.assertEqual(agency_input.attrs.get("disabled"), "")
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
org_name_page.form["federal_agency"] = FederalAgency.objects.filter(agency="Department of State").get().id
org_name_page.form["city"] = "Faketown"
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
# Make the change. The agency should be unchanged, but city should be modifiable.
success_result_page = org_name_page.form.submit()
self.assertEqual(success_result_page.status_code, 200)
# Check that the agency has not changed
self.assertEqual(self.domain_information.federal_agency.agency, "AMTRAK")
# Do another check on the form itself
form = success_result_page.forms[0]
# Check the value of the input field
organization_name_input = form.fields["federal_agency"][0]
self.assertEqual(organization_name_input.value, str(federal_agency.id))
# Check if the input field is disabled
self.assertTrue("disabled" in organization_name_input.attrs)
self.assertEqual(organization_name_input.attrs.get("disabled"), "")
# Check for the value we want to update
self.assertContains(success_result_page, "Faketown")
@less_console_noise_decorator @less_console_noise_decorator
def test_federal_agency_submit_blocked(self): def test_federal_agency_submit_blocked(self):
""" """

View file

@ -38,10 +38,15 @@ from django.contrib.admin.models import LogEntry, ADDITION
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from registrar.models.utility.generic_helper import convert_queryset_to_dict from registrar.models.utility.generic_helper import convert_queryset_to_dict
from registrar.models.utility.orm_helper import ArrayRemoveNull from registrar.models.utility.orm_helper import ArrayRemoveNull
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
from registrar.templatetags.custom_filters import get_region from registrar.templatetags.custom_filters import get_region
from registrar.utility.constants import BranchChoices from registrar.utility.constants import BranchChoices
from registrar.utility.enums import DefaultEmail, DefaultUserValues from registrar.utility.enums import DefaultEmail, DefaultUserValues
from registrar.models.utility.portfolio_helper import (
get_role_display,
get_domain_requests_display,
get_domains_display,
get_members_display,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -479,15 +484,15 @@ class MemberExport(BaseExport):
""" """
return [ return [
"Email", "Email",
"Organization admin", "Member access",
"Invited by", "Invited by",
"Joined date", "Joined date",
"Last active", "Last active",
"Domain requests", "Domain requests",
"Member management", "Members",
"Domain management",
"Number of domains",
"Domains", "Domains",
"Number domains assigned",
"Domain assignments",
] ]
@classmethod @classmethod
@ -503,15 +508,15 @@ class MemberExport(BaseExport):
length_user_managed_domains = len(user_managed_domains) length_user_managed_domains = len(user_managed_domains)
FIELDS = { FIELDS = {
"Email": model.get("email_display"), "Email": model.get("email_display"),
"Organization admin": bool(UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles), "Member access": get_role_display(roles),
"Invited by": model.get("invited_by"), "Invited by": model.get("invited_by"),
"Joined date": model.get("joined_date"), "Joined date": model.get("joined_date"),
"Last active": model.get("last_active"), "Last active": model.get("last_active"),
"Domain requests": UserPortfolioPermission.get_domain_request_permission_display(roles, permissions), "Domain requests": f"{get_domain_requests_display(roles, permissions)}",
"Member management": UserPortfolioPermission.get_member_permission_display(roles, permissions), "Members": f"{get_members_display(roles, permissions)}",
"Domain management": bool(length_user_managed_domains > 0), "Domains": f"{get_domains_display(roles, permissions)}",
"Number of domains": length_user_managed_domains, "Number domains assigned": length_user_managed_domains,
"Domains": ",".join(user_managed_domains), "Domain assignments": ", ".join(user_managed_domains),
} }
return [FIELDS.get(column, "") for column in columns] return [FIELDS.get(column, "") for column in columns]

View file