mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-22 18:56:15 +02:00
Merge branch 'main' into documentation-for-cloning-workflow
This commit is contained in:
commit
cd2c1eb404
46 changed files with 604 additions and 221 deletions
|
@ -907,13 +907,14 @@ Example (only requests): `./manage.py create_federal_portfolio --branch "executi
|
||||||
```docker-compose exec app ./manage.py create_federal_portfolio --agency_name "{federal_agency_name}" --both```
|
```docker-compose exec app ./manage.py create_federal_portfolio --agency_name "{federal_agency_name}" --both```
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
| | Parameter | Description |
|
| | Parameter | Description |
|
||||||
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
|:-:|:---------------------------- |:-------------------------------------------------------------------------------------------|
|
||||||
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||||
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
||||||
| 3 | **both** | If True, runs parse_requests and parse_domains. |
|
| 3 | **both** | If True, runs parse_requests and parse_domains. |
|
||||||
| 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
| 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||||
| 5 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
| 5 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
||||||
|
| 6 | **skip_existing_portfolios** | If True, then the script will only create suborganizations, modify DomainRequest, and modify DomainInformation records only when creating a new portfolio. Use this flag when you do not want to modify existing records. |
|
||||||
|
|
||||||
- Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both.
|
- Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both.
|
||||||
- Parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
- Parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
||||||
|
|
|
@ -1678,22 +1678,25 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainInformation.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2031,22 +2034,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainRequest.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_request in DomainRequest.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_request.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2062,24 +2068,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
DomainRequest.objects.annotate(
|
||||||
for domain_request in DomainRequest.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_request.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_request.converted_federal_type_display # Display value
|
portfolio__isnull=False,
|
||||||
|
portfolio__federal_agency__federal_type__isnull=False,
|
||||||
if converted_federal_type:
|
then="portfolio__federal_agency__federal_type",
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
portfolio__isnull=True,
|
||||||
|
federal_agency__federal_type__isnull=False,
|
||||||
|
then="federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__federal_agency__federal_type=self.value())
|
Q(portfolio__federal_agency__federal_type=self.value())
|
||||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||||
|
@ -3226,59 +3247,86 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
Domain.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__organization_type__isnull=False,
|
||||||
|
then="domain_info__portfolio__organization_type",
|
||||||
|
),
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__generic_org_type__isnull=False,
|
||||||
|
then="domain_info__generic_org_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__organization_type=self.value())
|
Q(domain_info__portfolio__organization_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
class FederalTypeFilter(admin.SimpleListFilter):
|
class FederalTypeFilter(admin.SimpleListFilter):
|
||||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
federal type in the Domain Information object."""
|
organization in the Domain Request object."""
|
||||||
|
|
||||||
title = "federal type"
|
title = "federal type"
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
Domain.objects.annotate(
|
||||||
for domain_info in DomainInformation.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_info.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_info.converted_federal_type_display # Display value
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=False,
|
||||||
if converted_federal_type:
|
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__federal_type__isnull=False,
|
||||||
|
then="domain_info__federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__federal_agency__federal_type=self.value())
|
Q(domain_info__portfolio__federal_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -18,11 +18,11 @@ export function initPortfolioNewMemberPageToggle() {
|
||||||
const unique_id = `${member_type}-${member_id}`;
|
const unique_id = `${member_type}-${member_id}`;
|
||||||
|
|
||||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
|
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
|
||||||
|
|
||||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||||
// All prod users will have emails linked to their account
|
// All prod users will have emails linked to their account
|
||||||
MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
MembersTable.addMemberDeleteModal(num_domains, member_email || member_name || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
||||||
|
|
||||||
uswdsInitializeModals();
|
uswdsInitializeModals();
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,6 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
</svg>` : ''}
|
</svg>` : ''}
|
||||||
${modal_button_text}
|
${modal_button_text}
|
||||||
<span class="usa-sr-only">${screen_reader_text}</span>
|
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -107,6 +106,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
||||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="more-actions-${unique_id}"
|
aria-controls="more-actions-${unique_id}"
|
||||||
|
aria-label="${screen_reader_text}"
|
||||||
>
|
>
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||||
|
@ -284,15 +284,18 @@ export class BaseTable {
|
||||||
showElement(dataWrapper);
|
showElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
showElement(noSearchResultsWrapper);
|
showElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noSearchResultsWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
showElement(noDataWrapper);
|
showElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noDataWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -448,6 +451,7 @@ export class BaseTable {
|
||||||
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
||||||
if (!baseUrlValue) return;
|
if (!baseUrlValue) return;
|
||||||
|
|
||||||
|
this.tableAnnouncementRegion.innerHTML = '<p>Loading table.</p>';
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
@ -469,7 +473,6 @@ export class BaseTable {
|
||||||
|
|
||||||
let dataObjects = this.getDataObjects(data);
|
let dataObjects = this.getDataObjects(data);
|
||||||
let customTableOptions = this.customizeTable(data);
|
let customTableOptions = this.customizeTable(data);
|
||||||
|
|
||||||
dataObjects.forEach(dataObject => {
|
dataObjects.forEach(dataObject => {
|
||||||
this.addRow(dataObject, tbody, customTableOptions);
|
this.addRow(dataObject, tbody, customTableOptions);
|
||||||
});
|
});
|
||||||
|
|
|
@ -103,8 +103,9 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
disabled = true;
|
disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uses margin-right-neg-5 as a hack to increase the text-wrapping width on this table
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td data-label="Selection" data-sort-value="0" class="padding-right-105">
|
<th scope="row" role="rowheader" data-label="Selection" data-sort-value="0" class="padding-right-105">
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
class="usa-checkbox__input"
|
class="usa-checkbox__input"
|
||||||
|
@ -112,6 +113,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="${domain.name}"
|
name="${domain.name}"
|
||||||
value="${domain.id}"
|
value="${domain.id}"
|
||||||
|
aria-label="${domain.name}"
|
||||||
${checked ? 'checked' : ''}
|
${checked ? 'checked' : ''}
|
||||||
${disabled ? 'disabled' : ''}
|
${disabled ? 'disabled' : ''}
|
||||||
/>
|
/>
|
||||||
|
@ -119,10 +121,10 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
<span class="sr-only">${domain.id}</span>
|
<span class="sr-only">${domain.id}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</th>
|
||||||
<td data-label="Domain name">
|
<td data-label="Domain name">
|
||||||
${domain.name}
|
${domain.name}
|
||||||
${disabled ? '<span class="display-block margin-top-05 text-gray-50">Domains must have one domain manager. To unassign this member, the domain needs another domain manager.</span>' : ''}
|
${disabled ? '<span class="display-block margin-top-05 text-gray-50 margin-right-neg-5">Domains must have one domain manager. To unassign this member, the domain needs another domain manager.</span>' : ''}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
|
@ -235,7 +237,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Create unassigned domains list
|
// Create unassigned domains list
|
||||||
const unassignedDomainsList = document.createElement('ul');
|
const unassignedDomainsList = document.createElement('ul');
|
||||||
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
this.removedDomains.forEach(removedDomain => {
|
let removedDomainsCopy = [...this.removedDomains].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
removedDomainsCopy.forEach(removedDomain => {
|
||||||
const removedDomainListItem = document.createElement('li');
|
const removedDomainListItem = document.createElement('li');
|
||||||
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
|
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
|
||||||
unassignedDomainsList.appendChild(removedDomainListItem);
|
unassignedDomainsList.appendChild(removedDomainListItem);
|
||||||
|
@ -244,7 +247,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Create assigned domains list
|
// Create assigned domains list
|
||||||
const assignedDomainsList = document.createElement('ul');
|
const assignedDomainsList = document.createElement('ul');
|
||||||
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
this.addedDomains.forEach(addedDomain => {
|
let addedDomainsCopy = [...this.addedDomains].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
addedDomainsCopy.forEach(addedDomain => {
|
||||||
const addedDomainListItem = document.createElement('li');
|
const addedDomainListItem = document.createElement('li');
|
||||||
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
|
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
|
||||||
assignedDomainsList.appendChild(addedDomainListItem);
|
assignedDomainsList.appendChild(addedDomainListItem);
|
||||||
|
@ -259,7 +263,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append unassigned domains section
|
// Append unassigned domains section
|
||||||
if (this.removedDomains.length) {
|
if (this.removedDomains.length) {
|
||||||
const unassignedHeader = document.createElement('h3');
|
const unassignedHeader = document.createElement('h3');
|
||||||
unassignedHeader.classList.add('margin-bottom-1');
|
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
unassignedHeader.textContent = 'Unassigned domains';
|
unassignedHeader.textContent = 'Unassigned domains';
|
||||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||||
|
@ -268,7 +272,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append assigned domains section
|
// Append assigned domains section
|
||||||
if (this.addedDomains.length) {
|
if (this.addedDomains.length) {
|
||||||
const assignedHeader = document.createElement('h3');
|
const assignedHeader = document.createElement('h3');
|
||||||
assignedHeader.classList.add('margin-bottom-1');
|
// Make this h3 look like a h4
|
||||||
|
assignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
assignedHeader.textContent = 'Assigned domains';
|
assignedHeader.textContent = 'Assigned domains';
|
||||||
domainAssignmentSummary.appendChild(assignedHeader);
|
domainAssignmentSummary.appendChild(assignedHeader);
|
||||||
domainAssignmentSummary.appendChild(assignedDomainsList);
|
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||||
|
@ -276,7 +281,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
|
|
||||||
// Append total assigned domains section
|
// Append total assigned domains section
|
||||||
const totalHeader = document.createElement('h3');
|
const totalHeader = document.createElement('h3');
|
||||||
totalHeader.classList.add('margin-bottom-1');
|
// Make this h3 look like a h4
|
||||||
|
totalHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
totalHeader.textContent = 'Total assigned domains';
|
totalHeader.textContent = 'Total assigned domains';
|
||||||
domainAssignmentSummary.appendChild(totalHeader);
|
domainAssignmentSummary.appendChild(totalHeader);
|
||||||
const totalCount = document.createElement('p');
|
const totalCount = document.createElement('p');
|
||||||
|
@ -289,6 +295,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
this.updateReadonlyDisplay();
|
this.updateReadonlyDisplay();
|
||||||
hideElement(this.editModeContainer);
|
hideElement(this.editModeContainer);
|
||||||
showElement(this.readonlyModeContainer);
|
showElement(this.readonlyModeContainer);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditMode() {
|
showEditMode() {
|
||||||
|
|
|
@ -19,9 +19,9 @@ export class MemberDomainsTable extends BaseTable {
|
||||||
const domain = dataObject;
|
const domain = dataObject;
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td scope="row" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
${domain.name}
|
${domain.name}
|
||||||
</td>
|
</th>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,13 +78,12 @@ export class MembersTable extends BaseTable {
|
||||||
const num_domains = member.domain_urls.length;
|
const num_domains = member.domain_urls.length;
|
||||||
const last_active = this.handleLastActive(member.last_active);
|
const last_active = this.handleLastActive(member.last_active);
|
||||||
let cancelInvitationButton = member.type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
let cancelInvitationButton = member.type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||||
const kebabHTML = customTableOptions.hasAdditionalActions ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member.name}`): '';
|
const kebabHTML = customTableOptions.hasAdditionalActions ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `Expand for more options for ${member.name}`): '';
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
let admin_tagHTML = ``;
|
let admin_tagHTML = ``;
|
||||||
if (member.is_admin)
|
if (member.is_admin)
|
||||||
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`
|
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary-dark text-semibold">Admin</span>`
|
||||||
|
|
||||||
// generate html blocks for domains and permissions for the member
|
// generate html blocks for domains and permissions for the member
|
||||||
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
||||||
|
@ -99,7 +98,8 @@ export class MembersTable extends BaseTable {
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
||||||
data-for=${unique_id}
|
data-for=${unique_id}
|
||||||
aria-label="Expand for additional information"
|
aria-label="Expand for additional information for ${member.member_display}"
|
||||||
|
aria-label-placeholder="${member.member_display}"
|
||||||
>
|
>
|
||||||
<span>Expand</span>
|
<span>Expand</span>
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
@ -137,7 +137,7 @@ export class MembersTable extends BaseTable {
|
||||||
}
|
}
|
||||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||||
// All prod users will have emails linked to their account
|
// All prod users will have emails linked to their account
|
||||||
if (customTableOptions.hasAdditionalActions) MembersTable.addMemberDeleteModal(num_domains, member.email || "Samwise Gamgee", member_delete_url, unique_id, row);
|
if (customTableOptions.hasAdditionalActions) MembersTable.addMemberDeleteModal(num_domains, member.email || member.name || "Samwise Gamgee", member_delete_url, unique_id, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,13 +166,27 @@ export class MembersTable extends BaseTable {
|
||||||
spanElement.textContent = 'Close';
|
spanElement.textContent = 'Close';
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||||
buttonParentRow.classList.add('hide-td-borders');
|
buttonParentRow.classList.add('hide-td-borders');
|
||||||
toggleButton.setAttribute('aria-label', 'Close additional information');
|
|
||||||
|
let ariaLabelText = "Close additional information";
|
||||||
|
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||||
|
if (ariaLabelPlaceholder) {
|
||||||
|
ariaLabelText = `Close additional information for ${ariaLabelPlaceholder}`;
|
||||||
|
}
|
||||||
|
toggleButton.setAttribute('aria-label', ariaLabelText);
|
||||||
|
|
||||||
|
// Set tabindex for focusable elements in expanded content
|
||||||
} else {
|
} else {
|
||||||
hideElement(contentDiv);
|
hideElement(contentDiv);
|
||||||
spanElement.textContent = 'Expand';
|
spanElement.textContent = 'Expand';
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||||
buttonParentRow.classList.remove('hide-td-borders');
|
buttonParentRow.classList.remove('hide-td-borders');
|
||||||
toggleButton.setAttribute('aria-label', 'Expand for additional information');
|
|
||||||
|
let ariaLabelText = "Expand for additional information";
|
||||||
|
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||||
|
if (ariaLabelPlaceholder) {
|
||||||
|
ariaLabelText = `Expand for additional information for ${ariaLabelPlaceholder}`;
|
||||||
|
}
|
||||||
|
toggleButton.setAttribute('aria-label', ariaLabelText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,21 +259,19 @@ export class MembersTable extends BaseTable {
|
||||||
// Only generate HTML if the member has one or more assigned domains
|
// Only generate HTML if the member has one or more assigned domains
|
||||||
if (num_domains > 0) {
|
if (num_domains > 0) {
|
||||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||||
domainsHTML += "<h4 class='margin-y-0'>Domains assigned</h4>";
|
domainsHTML += "<h4 class='font-body-xs margin-y-0'>Domains assigned</h4>";
|
||||||
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
domainsHTML += `<p class='font-body-xs text-base-dark margin-y-0'>This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:</p>`;
|
||||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||||
|
|
||||||
// Display up to 6 domains with their URLs
|
// Display up to 6 domains with their URLs
|
||||||
for (let i = 0; i < num_domains && i < 6; i++) {
|
for (let i = 0; i < num_domains && i < 6; i++) {
|
||||||
domainsHTML += `<li><a href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
domainsHTML += `<li><a class="font-body-xs" href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
domainsHTML += "</ul>";
|
domainsHTML += "</ul>";
|
||||||
|
|
||||||
// If there are more than 6 domains, display a "View assigned domains" link
|
// If there are more than 6 domains, display a "View assigned domains" link
|
||||||
if (num_domains >= 6) {
|
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View assigned domains</a></p>`;
|
||||||
domainsHTML += `<p><a href="${action_url}/domains">View assigned domains</a></p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
domainsHTML += "</div>";
|
domainsHTML += "</div>";
|
||||||
}
|
}
|
||||||
|
@ -378,34 +390,37 @@ export class MembersTable extends BaseTable {
|
||||||
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
||||||
let permissionsHTML = '';
|
let permissionsHTML = '';
|
||||||
|
|
||||||
|
// Define shared classes across elements for easier refactoring
|
||||||
|
let sharedParagraphClasses = "font-body-xs text-base-dark margin-top-1 p--blockquote";
|
||||||
|
|
||||||
// Check domain-related permissions
|
// Check domain-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check request-related permissions
|
// Check request-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check member-related permissions
|
// Check member-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific permissions are assigned, display a message indicating no additional permissions
|
// If no specific permissions are assigned, display a message indicating no additional permissions
|
||||||
if (!permissionsHTML) {
|
if (!permissionsHTML) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><b>No additional permissions:</b> There are no additional permissions for this member.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><b>No additional permissions:</b> There are no additional permissions for this member.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a permissions header and wrap the entire output in a container
|
// Add a permissions header and wrap the entire output in a container
|
||||||
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
permissionsHTML = `<div class='desktop:grid-col-7'><h4 class='font-body-xs margin-y-0'>Additional permissions for this member</h4>${permissionsHTML}</div>`;
|
||||||
|
|
||||||
return permissionsHTML;
|
return permissionsHTML;
|
||||||
}
|
}
|
||||||
|
@ -423,7 +438,7 @@ export class MembersTable extends BaseTable {
|
||||||
let modalDescription = ``;
|
let modalDescription = ``;
|
||||||
|
|
||||||
if (num_domains >= 0){
|
if (num_domains >= 0){
|
||||||
modalHeading = `Are you sure you want to delete ${member_email}?`;
|
modalHeading = `Are you sure you want to remove ${member_email} from the organization?`;
|
||||||
modalDescription = `They will no longer be able to access this organization.
|
modalDescription = `They will no longer be able to access this organization.
|
||||||
This action cannot be undone.`;
|
This action cannot be undone.`;
|
||||||
if (num_domains >= 1)
|
if (num_domains >= 1)
|
||||||
|
|
|
@ -246,6 +246,10 @@ a.text-secondary:hover {
|
||||||
color: $theme-color-error;
|
color: $theme-color-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button.usa-button--secondary {
|
||||||
|
background-color: $theme-color-error;
|
||||||
|
}
|
||||||
|
|
||||||
.usa-button--show-more-button {
|
.usa-button--show-more-button {
|
||||||
font-size: size('ui', 'xs');
|
font-size: size('ui', 'xs');
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
5
src/registrar/assets/src/sass/_theme/_modals.scss
Normal file
5
src/registrar/assets/src/sass/_theme/_modals.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-modal__main {
|
||||||
|
padding: 0 2rem 2rem;
|
||||||
|
}
|
|
@ -41,6 +41,13 @@ th {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The member table has an extra "expand" row, which looks like a single row.
|
||||||
|
// But the DOM disagrees - so we basically need to hide the border on both rows.
|
||||||
|
#members__table-wrapper .dotgov-table tr:nth-last-child(2) td,
|
||||||
|
#members__table-wrapper .dotgov-table tr:nth-last-child(2) th {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.dotgov-table {
|
.dotgov-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -56,10 +63,9 @@ th {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:not(.hide-td-borders) {
|
tr:not(.hide-td-borders):not(:last-child) td,
|
||||||
td, th {
|
tr:not(.hide-td-borders):not(:last-child) th {
|
||||||
border-bottom: 1px solid color('base-lighter');
|
border-bottom: 1px solid color('base-lighter');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
thead th {
|
thead th {
|
||||||
|
|
3
src/registrar/assets/src/sass/_theme/_tags.scss
Normal file
3
src/registrar/assets/src/sass/_theme/_tags.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.usa-tag {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
|
@ -68,6 +68,7 @@ in the form $setting: value,
|
||||||
/*---------------------------
|
/*---------------------------
|
||||||
## Font weights
|
## Font weights
|
||||||
----------------------------*/
|
----------------------------*/
|
||||||
|
$theme-font-weight-medium: 400,
|
||||||
$theme-font-weight-semibold: 600,
|
$theme-font-weight-semibold: 600,
|
||||||
|
|
||||||
/*---------------------------
|
/*---------------------------
|
||||||
|
|
|
@ -26,6 +26,8 @@
|
||||||
@forward "header";
|
@forward "header";
|
||||||
@forward "register-form";
|
@forward "register-form";
|
||||||
@forward "containers";
|
@forward "containers";
|
||||||
|
@forward "modals";
|
||||||
|
@forward "tags";
|
||||||
|
|
||||||
/*--------------------------------------------------
|
/*--------------------------------------------------
|
||||||
--- Admin ---------------------------------*/
|
--- Admin ---------------------------------*/
|
||||||
|
|
|
@ -323,22 +323,50 @@ class DomainRequestFixture:
|
||||||
cls._create_domain_requests(users)
|
cls._create_domain_requests(users)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_domain_requests(cls, users):
|
def _create_domain_requests(cls, users): # noqa: C901
|
||||||
"""Creates DomainRequests given a list of users."""
|
"""Creates DomainRequests given a list of users."""
|
||||||
|
total_domain_requests_to_make = len(users) # 100000
|
||||||
|
|
||||||
|
# Check if the database is already populated with the desired
|
||||||
|
# number of entries.
|
||||||
|
# (Prevents re-adding more entries to an already populated database,
|
||||||
|
# which happens when restarting Docker src)
|
||||||
|
domain_requests_already_made = DomainRequest.objects.count()
|
||||||
|
|
||||||
domain_requests_to_create = []
|
domain_requests_to_create = []
|
||||||
for user in users:
|
if domain_requests_already_made < total_domain_requests_to_make:
|
||||||
for request_data in cls.DOMAINREQUESTS:
|
for user in users:
|
||||||
# Prepare DomainRequest objects
|
for request_data in cls.DOMAINREQUESTS:
|
||||||
try:
|
# Prepare DomainRequest objects
|
||||||
domain_request = DomainRequest(
|
try:
|
||||||
creator=user,
|
domain_request = DomainRequest(
|
||||||
organization_name=request_data["organization_name"],
|
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)
|
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||||
domain_requests_to_create.append(domain_request)
|
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||||
except Exception as e:
|
domain_requests_to_create.append(domain_request)
|
||||||
logger.warning(e)
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
num_additional_requests_to_make = (
|
||||||
|
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
||||||
|
)
|
||||||
|
if num_additional_requests_to_make > 0:
|
||||||
|
for _ in range(num_additional_requests_to_make):
|
||||||
|
random_user = random.choice(users) # nosec
|
||||||
|
try:
|
||||||
|
random_request_type = random.choice(cls.DOMAINREQUESTS) # nosec
|
||||||
|
# Prepare DomainRequest objects
|
||||||
|
domain_request = DomainRequest(
|
||||||
|
creator=random_user,
|
||||||
|
organization_name=random_request_type["organization_name"],
|
||||||
|
)
|
||||||
|
cls._set_non_foreign_key_fields(domain_request, random_request_type)
|
||||||
|
cls._set_foreign_key_fields(domain_request, random_request_type, random_user)
|
||||||
|
domain_requests_to_create.append(domain_request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error creating random domain request: {e}")
|
||||||
|
|
||||||
# Bulk create domain requests
|
# Bulk create domain requests
|
||||||
cls._bulk_create_requests(domain_requests_to_create)
|
cls._bulk_create_requests(domain_requests_to_create)
|
||||||
|
|
|
@ -196,6 +196,7 @@ class UserFixture:
|
||||||
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
|
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
|
||||||
"first_name": "Alysia-Analyst",
|
"first_name": "Alysia-Analyst",
|
||||||
"last_name": "Alysia-Analyst",
|
"last_name": "Alysia-Analyst",
|
||||||
|
"email": "abroddrick+1@truss.works",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
||||||
|
@ -361,22 +362,30 @@ class UserFixture:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers):
|
def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers):
|
||||||
return [
|
new_users = []
|
||||||
User(
|
for i, user_data in enumerate(users):
|
||||||
id=user_data.get("id"),
|
username = user_data.get("username")
|
||||||
first_name=user_data.get("first_name"),
|
id = user_data.get("id")
|
||||||
last_name=user_data.get("last_name"),
|
first_name = user_data.get("first_name", "Bob")
|
||||||
username=user_data.get("username"),
|
last_name = user_data.get("last_name", "Builder")
|
||||||
email=user_data.get("email", ""),
|
|
||||||
title=user_data.get("title", "Peon"),
|
default_email = f"placeholder.{first_name.lower()}.{last_name.lower()}+{i}@igorville.gov"
|
||||||
phone=user_data.get("phone", "2022222222"),
|
email = user_data.get("email", default_email)
|
||||||
is_active=user_data.get("is_active", True),
|
if username not in existing_usernames and id not in existing_user_ids:
|
||||||
is_staff=True,
|
user = User(
|
||||||
is_superuser=are_superusers,
|
id=id,
|
||||||
)
|
first_name=first_name,
|
||||||
for user_data in users
|
last_name=last_name,
|
||||||
if user_data.get("username") not in existing_usernames and user_data.get("id") not in existing_user_ids
|
username=username,
|
||||||
]
|
email=email,
|
||||||
|
title=user_data.get("title", "Peon"),
|
||||||
|
phone=user_data.get("phone", "2022222222"),
|
||||||
|
is_active=user_data.get("is_active", True),
|
||||||
|
is_staff=True,
|
||||||
|
is_superuser=are_superusers,
|
||||||
|
)
|
||||||
|
new_users.append(user)
|
||||||
|
return new_users
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_new_users(new_users):
|
def _create_new_users(new_users):
|
||||||
|
|
|
@ -64,6 +64,11 @@ class Command(BaseCommand):
|
||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
help="Adds portfolio to both requests and domains",
|
help="Adds portfolio to both requests and domains",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skip_existing_portfolios",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
help="Only add suborganizations to newly created portfolios, skip existing ones.",
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
agency_name = options.get("agency_name")
|
agency_name = options.get("agency_name")
|
||||||
|
@ -71,6 +76,7 @@ class Command(BaseCommand):
|
||||||
parse_requests = options.get("parse_requests")
|
parse_requests = options.get("parse_requests")
|
||||||
parse_domains = options.get("parse_domains")
|
parse_domains = options.get("parse_domains")
|
||||||
both = options.get("both")
|
both = options.get("both")
|
||||||
|
skip_existing_portfolios = options.get("skip_existing_portfolios")
|
||||||
|
|
||||||
if not both:
|
if not both:
|
||||||
if not parse_requests and not parse_domains:
|
if not parse_requests and not parse_domains:
|
||||||
|
@ -97,7 +103,9 @@ class Command(BaseCommand):
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
try:
|
try:
|
||||||
# C901 'Command.handle' is too complex (12)
|
# C901 'Command.handle' is too complex (12)
|
||||||
portfolio = self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both)
|
portfolio = self.handle_populate_portfolio(
|
||||||
|
federal_agency, parse_domains, parse_requests, both, skip_existing_portfolios
|
||||||
|
)
|
||||||
portfolios.append(portfolio)
|
portfolios.append(portfolio)
|
||||||
except Exception as exec:
|
except Exception as exec:
|
||||||
self.failed_portfolios.add(federal_agency)
|
self.failed_portfolios.add(federal_agency)
|
||||||
|
@ -109,26 +117,33 @@ class Command(BaseCommand):
|
||||||
updated_suborg_count = self.post_process_all_suborganization_fields(agencies)
|
updated_suborg_count = self.post_process_all_suborganization_fields(agencies)
|
||||||
message = f"Added city and state_territory information to {updated_suborg_count} suborgs."
|
message = f"Added city and state_territory information to {updated_suborg_count} suborgs."
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
|
||||||
TerminalHelper.log_script_run_summary(
|
TerminalHelper.log_script_run_summary(
|
||||||
self.updated_portfolios,
|
self.updated_portfolios,
|
||||||
self.failed_portfolios,
|
self.failed_portfolios,
|
||||||
self.skipped_portfolios,
|
self.skipped_portfolios,
|
||||||
debug=False,
|
debug=False,
|
||||||
skipped_header="----- SOME PORTFOLIOS WERENT CREATED -----",
|
log_header="============= FINISHED HANDLE PORTFOLIO STEP ===============",
|
||||||
|
skipped_header="----- SOME PORTFOLIOS WERENT CREATED (BUT OTHER RECORDS ARE STILL PROCESSED) -----",
|
||||||
display_as_str=True,
|
display_as_str=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name.
|
# POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name.
|
||||||
# We only do this for started domain requests.
|
# We only do this for started domain requests.
|
||||||
if parse_requests or both:
|
if parse_requests or both:
|
||||||
|
prompt_message = (
|
||||||
|
"This action will update domain requests even if they aren't on a portfolio."
|
||||||
|
"\nNOTE: This will modify domain requests, even if no portfolios were created."
|
||||||
|
"\nIn the event no portfolios *are* created, then this step will target "
|
||||||
|
"the existing portfolios with your given params."
|
||||||
|
"\nThis step is entirely optional, and is just for extra data cleanup."
|
||||||
|
)
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
prompt_message="This action will update domain requests even if they aren't on a portfolio.",
|
prompt_message=prompt_message,
|
||||||
prompt_title=(
|
prompt_title=(
|
||||||
"POST PROCESS STEP: Do you want to clear federal agency on (related) started domain requests?"
|
"POST PROCESS STEP: Do you want to clear federal agency on (related) started domain requests?"
|
||||||
),
|
),
|
||||||
verify_message=None,
|
verify_message="*** THIS STEP IS OPTIONAL ***",
|
||||||
)
|
)
|
||||||
self.post_process_started_domain_requests(agencies, portfolios)
|
self.post_process_started_domain_requests(agencies, portfolios)
|
||||||
|
|
||||||
|
@ -151,6 +166,11 @@ class Command(BaseCommand):
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
organization_name__isnull=False,
|
organization_name__isnull=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if domain_requests_to_update.count() == 0:
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, "No domain requests to update.")
|
||||||
|
return
|
||||||
|
|
||||||
portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio}
|
portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio}
|
||||||
|
|
||||||
# Update the request, assuming the given agency name matches the portfolio name
|
# Update the request, assuming the given agency name matches the portfolio name
|
||||||
|
@ -173,10 +193,19 @@ class Command(BaseCommand):
|
||||||
DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"])
|
DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"])
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.")
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.")
|
||||||
|
|
||||||
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both):
|
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both, skip_existing_portfolios):
|
||||||
"""Attempts to create a portfolio. If successful, this function will
|
"""Attempts to create a portfolio. If successful, this function will
|
||||||
also create new suborganizations"""
|
also create new suborganizations"""
|
||||||
portfolio, _ = self.create_portfolio(federal_agency)
|
portfolio, created = self.create_portfolio(federal_agency)
|
||||||
|
if skip_existing_portfolios and not created:
|
||||||
|
TerminalHelper.colorful_logger(
|
||||||
|
logger.warning,
|
||||||
|
TerminalColors.YELLOW,
|
||||||
|
"Skipping modifications to suborgs, domain requests, and "
|
||||||
|
"domains due to the --skip_existing_portfolios flag. Portfolio already exists.",
|
||||||
|
)
|
||||||
|
return portfolio
|
||||||
|
|
||||||
self.create_suborganizations(portfolio, federal_agency)
|
self.create_suborganizations(portfolio, federal_agency)
|
||||||
if parse_domains or both:
|
if parse_domains or both:
|
||||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||||
|
@ -283,15 +312,13 @@ class Command(BaseCommand):
|
||||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
]
|
]
|
||||||
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
||||||
status__in=invalid_states
|
|
||||||
)
|
|
||||||
if not domain_requests.exists():
|
if not domain_requests.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolio '{portfolio}' not added to domain requests: no valid records found.
|
Portfolio '{portfolio}' not added to domain requests: no valid records found.
|
||||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||||
Filter info: DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
Filter info: DomainRequest.objects.filter(federal_agency=federal_agency).exclude(
|
||||||
status__in=invalid_states
|
status__in=invalid_states
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
@ -335,12 +362,12 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
Returns a queryset of DomainInformation objects, or None if nothing changed.
|
Returns a queryset of DomainInformation objects, or None if nothing changed.
|
||||||
"""
|
"""
|
||||||
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||||
if not domain_infos.exists():
|
if not domain_infos.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolio '{portfolio}' not added to domains: no valid records found.
|
Portfolio '{portfolio}' not added to domains: no valid records found.
|
||||||
The filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
The filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
Filter info: DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
Filter info: DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||||
"""
|
"""
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
@ -903,6 +904,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
bcc_address="",
|
bcc_address="",
|
||||||
|
cc_addresses: list[str] = [],
|
||||||
context=None,
|
context=None,
|
||||||
send_email=True,
|
send_email=True,
|
||||||
wrap_email=False,
|
wrap_email=False,
|
||||||
|
@ -955,12 +957,20 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
if custom_email_content:
|
if custom_email_content:
|
||||||
context["custom_email_content"] = custom_email_content
|
context["custom_email_content"] = custom_email_content
|
||||||
|
|
||||||
|
if self.requesting_entity_is_portfolio() or self.requesting_entity_is_suborganization():
|
||||||
|
portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( # type: ignore
|
||||||
|
permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], include_admin=True
|
||||||
|
)
|
||||||
|
cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True))
|
||||||
|
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
recipient.email,
|
recipient.email,
|
||||||
context=context,
|
context=context,
|
||||||
bcc_address=bcc_address,
|
bcc_address=bcc_address,
|
||||||
|
cc_addresses=cc_addresses,
|
||||||
wrap_email=wrap_email,
|
wrap_email=wrap_email,
|
||||||
)
|
)
|
||||||
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
||||||
|
|
|
@ -4,6 +4,7 @@ from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
@ -122,6 +123,16 @@ class Portfolio(TimeStampedModel):
|
||||||
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
||||||
self.urbanization = None
|
self.urbanization = None
|
||||||
|
|
||||||
|
# If the org type is federal, and org federal agency is not blank, and is a federal agency
|
||||||
|
# overwrite the organization name with the federal agency's agency
|
||||||
|
if (
|
||||||
|
self.organization_type == self.OrganizationChoices.FEDERAL
|
||||||
|
and self.federal_agency
|
||||||
|
and self.federal_agency != FederalAgency.get_non_federal_agency()
|
||||||
|
and self.federal_agency.agency
|
||||||
|
):
|
||||||
|
self.organization_name = self.federal_agency.agency
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -144,6 +155,25 @@ class Portfolio(TimeStampedModel):
|
||||||
).values_list("user__id", flat=True)
|
).values_list("user__id", flat=True)
|
||||||
return User.objects.filter(id__in=admin_ids)
|
return User.objects.filter(id__in=admin_ids)
|
||||||
|
|
||||||
|
def portfolio_users_with_permissions(self, permissions=[], include_admin=False):
|
||||||
|
"""Gets all users with specified additional permissions for this particular portfolio.
|
||||||
|
Returns a queryset of User."""
|
||||||
|
portfolio_users = self.portfolio_users
|
||||||
|
if permissions:
|
||||||
|
if include_admin:
|
||||||
|
portfolio_users = portfolio_users.filter(
|
||||||
|
Q(additional_permissions__overlap=permissions)
|
||||||
|
| Q(
|
||||||
|
roles__overlap=[
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
portfolio_users = portfolio_users.filter(additional_permissions__overlap=permissions)
|
||||||
|
user_ids = portfolio_users.values_list("user__id", flat=True)
|
||||||
|
return User.objects.filter(id__in=user_ids)
|
||||||
|
|
||||||
# == Getters for domains == #
|
# == Getters for domains == #
|
||||||
def get_domains(self, order_by=None):
|
def get_domains(self, order_by=None):
|
||||||
"""Returns all DomainInformations associated with this portfolio"""
|
"""Returns all DomainInformations associated with this portfolio"""
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<td>{{ member.user.phone }}</td>
|
<td>{{ member.user.phone }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% for role in member.user|portfolio_role_summary:original %}
|
{% for role in member.user|portfolio_role_summary:original %}
|
||||||
<span class="usa-tag">{{ role }}</span>
|
<span class="usa-tag bg-primary-dark text-semibold">{{ role }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="padding-left-1 text-size-small">
|
<td class="padding-left-1 text-size-small">
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
|
<th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
|
||||||
{{ item.permission.user.email }}
|
{{ item.permission.user.email }}
|
||||||
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 primary-dark text-semibold">Admin</span>{% endif %}
|
||||||
</th>
|
</th>
|
||||||
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
||||||
<td>
|
<td>
|
||||||
|
@ -160,7 +160,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
|
<th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
|
||||||
{{ invitation.domain_invitation.email }}
|
{{ invitation.domain_invitation.email }}
|
||||||
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary-dark text-semibold">Admin</span>{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
|
<td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
|
||||||
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Withdrawn
|
STATUS: Withdrawn
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Congratulations! Your .gov domain request has been approved.
|
Congratulations! Your .gov domain request has been approved.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Approved
|
STATUS: Approved
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been rejected.
|
Your .gov domain request has been rejected.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We received your .gov domain request.
|
We received your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Submitted
|
STATUS: Submitted
|
||||||
|
|
||||||
|
@ -11,13 +12,15 @@ STATUS: Submitted
|
||||||
|
|
||||||
NEXT STEPS
|
NEXT STEPS
|
||||||
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
||||||
|
{% if is_org_user %}
|
||||||
|
During our review we’ll verify that your requested domain meets our naming requirements.
|
||||||
|
{% else %}
|
||||||
During our review, we’ll verify that:
|
During our review, we’ll verify that:
|
||||||
- Your organization is eligible for a .gov domain
|
- Your organization is eligible for a .gov domain
|
||||||
- You work at the organization and/or can make requests on its behalf
|
- You work at the organization and/or can make requests on its behalf
|
||||||
- Your requested domain meets our naming requirements
|
- Your requested domain meets our naming requirements
|
||||||
|
{% endif %}
|
||||||
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>
|
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>.
|
||||||
|
|
||||||
|
|
||||||
NEED TO MAKE CHANGES?
|
NEED TO MAKE CHANGES?
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body {% if no_max_width %} maxw-none {% endif %}">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_member_domains_json' as url %}
|
{% url 'get_member_domains_json' as url %}
|
||||||
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined member-domains margin-top-0 section-outlined--border-base-light" id="edit-member-domains">
|
<section class="section-outlined member-domains margin-top-0 padding-bottom-0 section-outlined--border-base-light" id="edit-member-domains">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Edit domains assigned to
|
Edit domains assigned to
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<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-9">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Member domains search component" class="margin-top-2">
|
<section aria-label="Member domains search component">
|
||||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<!-- ---------- MAIN TABLE ---------- -->
|
<!-- ---------- MAIN TABLE ---------- -->
|
||||||
<div class="display-none margin-top-0" id="edit-member-domains__table-wrapper">
|
<div class="display-none margin-top-0" id="edit-member-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 margin-bottom-4">
|
||||||
<caption class="sr-only">member domains</caption>
|
<caption class="sr-only">member domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -99,10 +99,10 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="edit-member-domains__no-data">
|
<div class="display-none margin-bottom-4" id="edit-member-domains__no-data">
|
||||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="edit-member-domains__no-search-results">
|
<div class="display-none margin-bottom-4" id="edit-member-domains__no-search-results">
|
||||||
<p>No results found</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_member_domains_json' as url %}
|
{% url 'get_member_domains_json' as url %}
|
||||||
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined member-domains margin-top-0 section-outlined--border-base-light" id="member-domains">
|
<section class="section-outlined member-domains margin-top-0 padding-bottom-0 section-outlined--border-base-light" id="member-domains">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Domains assigned to
|
Domains assigned to
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
|
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Member domains search component" class="margin-top-2">
|
<section aria-label="Member domains search component">
|
||||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
<!-- ---------- MAIN TABLE ---------- -->
|
<!-- ---------- MAIN TABLE ---------- -->
|
||||||
<div class="display-none margin-top-0" id="member-domains__table-wrapper">
|
<div class="display-none margin-top-0" id="member-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 margin-bottom-4">
|
||||||
<caption class="sr-only">member domains</caption>
|
<caption class="sr-only">member domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -94,10 +94,10 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="member-domains__no-data">
|
<div class="display-none margin-bottom-4" id="member-domains__no-data">
|
||||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="member-domains__no-search-results">
|
<div class="display-none margin-bottom-4" id="member-domains__no-search-results">
|
||||||
<p>No results found</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -54,7 +54,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="member" role="columnheader" id="header-member">Member</th>
|
<th data-sortable="member" role="columnheader" id="header-member">Member</th>
|
||||||
<th data-sortable="last_active" role="columnheader" id="header-last-active">Last Active</th>
|
<th data-sortable="last_active" role="columnheader" id="header-last-active">Last active</th>
|
||||||
<th
|
<th
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
id="header-action"
|
id="header-action"
|
||||||
|
|
|
@ -16,7 +16,7 @@ Organization member
|
||||||
{% endblock messages%}
|
{% endblock messages%}
|
||||||
|
|
||||||
{% url 'members' as url %}
|
{% url 'members' as url %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -41,7 +41,7 @@ Organization member
|
||||||
{% if has_edit_members_portfolio_permission %}
|
{% if has_edit_members_portfolio_permission %}
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div id="wrapper-delete-action"
|
<div id="wrapper-delete-action"
|
||||||
data-member-name="{{ member.email }}"
|
data-member-name="{{ member.get_formatted_name }}"
|
||||||
data-member-type="member"
|
data-member-type="member"
|
||||||
data-member-id="{{ member.id }}"
|
data-member-id="{{ member.id }}"
|
||||||
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
|
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-7">
|
<div class="mobile:grid-col-12 tablet:grid-col-7">
|
||||||
<h1 class="margin-bottom-3">Domain assignments</h1>
|
<h1>Domain assignments</h1>
|
||||||
</div>
|
</div>
|
||||||
{% if has_edit_members_portfolio_permission %}
|
{% if has_edit_members_portfolio_permission %}
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||||
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -39,12 +39,10 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section id="domain-assignments-edit-view">
|
<section id="domain-assignments-edit-view">
|
||||||
<h1 class="margin-bottom-3">Edit domain assignments</h1>
|
<h1>Edit domain assignments</h1>
|
||||||
|
|
||||||
<p class="margin-bottom-0">
|
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||||
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
When you save this form the member will get an email to notify them of any changes.
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@
|
||||||
<section id="domain-assignments-readonly-view" class="display-none">
|
<section id="domain-assignments-readonly-view" class="display-none">
|
||||||
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
||||||
|
|
||||||
<h2>Would you like to continue with the following domain assignment changes for
|
<h2 class="margin-top-0">Would you like to continue with the following domain assignment changes for
|
||||||
{% if member %}
|
{% if member %}
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -84,17 +82,19 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>When you save this form the member will get an email to notify them of any changes.</p>
|
<p class="margin-bottom-4">
|
||||||
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div id="domain-assignments-summary" class="margin-bottom-2">
|
<div id="domain-assignments-summary" class="margin-bottom-5">
|
||||||
<!-- AJAX will populate this summary -->
|
<!-- AJAX will populate this summary -->
|
||||||
<h3 class="margin-bottom-1">Unassigned domains</h3>
|
<h3 class="margin-bottom-1 h4">Unassigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="margin-bottom-0">Assigned domains</h3>
|
<h3 class="margin-bottom-0 h4">Assigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
|
|
|
@ -12,12 +12,12 @@
|
||||||
|
|
||||||
<!-- Form messages -->
|
<!-- Form messages -->
|
||||||
{% block messages %}
|
{% block messages %}
|
||||||
{% include "includes/form_messages.html" %}
|
{% include "includes/form_messages.html" with no_max_width=True %}
|
||||||
{% endblock messages%}
|
{% endblock messages%}
|
||||||
|
|
||||||
<div id="main-content">
|
<div id="main-content">
|
||||||
<div id="toggleable-alert" class="usa-alert usa-alert--slim margin-bottom-2 display-none">
|
<div id="toggleable-alert" class="usa-alert usa-alert--slim margin-bottom-2 display-none">
|
||||||
<div class="usa-alert__body usa-alert__body--widescreen">
|
<div class="usa-alert__body">
|
||||||
<p class="usa-alert__text ">
|
<p class="usa-alert__text ">
|
||||||
<!-- alert message will be conditionally populated by javascript -->
|
<!-- alert message will be conditionally populated by javascript -->
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -779,7 +779,7 @@ class TestDomainAdminWithClient(TestCase):
|
||||||
response = self.client.get("/admin/registrar/domain/")
|
response = self.client.get("/admin/registrar/domain/")
|
||||||
# There are 4 template references to Federal (4) plus four references in the table
|
# There are 4 template references to Federal (4) plus four references in the table
|
||||||
# for our actual domain_request
|
# for our actual domain_request
|
||||||
self.assertContains(response, "Federal", count=57)
|
self.assertContains(response, "Federal", count=56)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
|
|
|
@ -662,7 +662,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
response = self.client.get("/admin/registrar/domainrequest/?generic_org_type__exact=federal")
|
||||||
# There are 2 template references to Federal (4) and two in the results data
|
# There are 2 template references to Federal (4) and two in the results data
|
||||||
# of the request
|
# of the request
|
||||||
self.assertContains(response, "Federal", count=55)
|
self.assertContains(response, "Federal", count=54)
|
||||||
# This may be a bit more robust
|
# This may be a bit more robust
|
||||||
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
self.assertContains(response, '<td class="field-converted_generic_org_type">Federal</td>', count=1)
|
||||||
# Now let's make sure the long description does not exist
|
# Now let's make sure the long description does not exist
|
||||||
|
|
|
@ -35,7 +35,13 @@ import tablib
|
||||||
from unittest.mock import patch, call, MagicMock, mock_open
|
from unittest.mock import patch, call, MagicMock, mock_open
|
||||||
from epplibwrapper import commands, common
|
from epplibwrapper import commands, common
|
||||||
|
|
||||||
from .common import MockEppLib, less_console_noise, completed_domain_request, MockSESClient, MockDbForIndividualTests
|
from .common import (
|
||||||
|
MockEppLib,
|
||||||
|
less_console_noise,
|
||||||
|
completed_domain_request,
|
||||||
|
MockSESClient,
|
||||||
|
MockDbForIndividualTests,
|
||||||
|
)
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
|
||||||
|
|
||||||
|
@ -1825,7 +1831,7 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
self.run_create_federal_portfolio(agency_name="Non-existent Agency", parse_requests=True)
|
self.run_create_federal_portfolio(agency_name="Non-existent Agency", parse_requests=True)
|
||||||
|
|
||||||
def test_does_not_update_existing_portfolio(self):
|
def test_does_not_update_existing_portfolio(self):
|
||||||
"""Tests that an existing portfolio is not updated"""
|
"""Tests that an existing portfolio is not updated when"""
|
||||||
# Create an existing portfolio
|
# Create an existing portfolio
|
||||||
existing_portfolio = Portfolio.objects.create(
|
existing_portfolio = Portfolio.objects.create(
|
||||||
federal_agency=self.federal_agency,
|
federal_agency=self.federal_agency,
|
||||||
|
@ -1848,6 +1854,71 @@ class TestCreateFederalPortfolio(TestCase):
|
||||||
self.assertEqual(existing_portfolio.notes, "Old notes")
|
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||||
self.assertEqual(existing_portfolio.creator, self.user)
|
self.assertEqual(existing_portfolio.creator, self.user)
|
||||||
|
|
||||||
|
def test_skip_existing_portfolios(self):
|
||||||
|
"""Tests the skip_existing_portfolios to ensure that it doesn't add
|
||||||
|
suborgs, domain requests, and domain info."""
|
||||||
|
# Create an existing portfolio with a suborganization
|
||||||
|
existing_portfolio = Portfolio.objects.create(
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
organization_name="Test Federal Agency",
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.CITY,
|
||||||
|
creator=self.user,
|
||||||
|
notes="Old notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
existing_suborg = Suborganization.objects.create(
|
||||||
|
portfolio=existing_portfolio, name="Existing Suborg", city="Old City", state_territory="CA"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a domain request that would normally be associated
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="wackytaco.gov",
|
||||||
|
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||||
|
generic_org_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
user=self.user,
|
||||||
|
organization_name="would_create_suborg",
|
||||||
|
)
|
||||||
|
domain_request.approve()
|
||||||
|
domain = Domain.objects.get(name="wackytaco.gov").domain_info
|
||||||
|
|
||||||
|
# Run the command with skip_existing_portfolios=True
|
||||||
|
self.run_create_federal_portfolio(
|
||||||
|
agency_name="Test Federal Agency", parse_requests=True, skip_existing_portfolios=True
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh objects from database
|
||||||
|
existing_portfolio.refresh_from_db()
|
||||||
|
existing_suborg.refresh_from_db()
|
||||||
|
domain_request.refresh_from_db()
|
||||||
|
domain.refresh_from_db()
|
||||||
|
|
||||||
|
# Verify nothing was changed on the portfolio itself
|
||||||
|
# SANITY CHECK: if the portfolio updates, it will change to FEDERAL.
|
||||||
|
# if this case fails, it means we are overriding data (and not simply just other weirdness)
|
||||||
|
self.assertNotEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||||
|
|
||||||
|
# Notes and creator should be untouched
|
||||||
|
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.CITY)
|
||||||
|
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||||
|
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||||
|
self.assertEqual(existing_portfolio.creator, self.user)
|
||||||
|
|
||||||
|
# Verify suborganization wasn't modified
|
||||||
|
self.assertEqual(existing_suborg.city, "Old City")
|
||||||
|
self.assertEqual(existing_suborg.state_territory, "CA")
|
||||||
|
|
||||||
|
# Verify that the domain request wasn't modified
|
||||||
|
self.assertIsNone(domain_request.portfolio)
|
||||||
|
self.assertIsNone(domain_request.sub_organization)
|
||||||
|
|
||||||
|
# Verify that the domain wasn't modified
|
||||||
|
self.assertIsNone(domain.portfolio)
|
||||||
|
self.assertIsNone(domain.sub_organization)
|
||||||
|
|
||||||
|
# Verify that a new suborg wasn't created
|
||||||
|
self.assertFalse(Suborganization.objects.filter(name="would_create_suborg").exists())
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_post_process_suborganization_fields(self):
|
def test_post_process_suborganization_fields(self):
|
||||||
"""Test suborganization field updates from domain and request data.
|
"""Test suborganization field updates from domain and request data.
|
||||||
|
@ -2217,6 +2288,11 @@ class TestRemovePortfolios(TestCase):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.logger_patcher.stop()
|
self.logger_patcher.stop()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Suborganization.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
User.objects.all().delete()
|
||||||
|
|
||||||
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
@patch("registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no")
|
||||||
def test_delete_unlisted_portfolios(self, mock_query_yes_no):
|
def test_delete_unlisted_portfolios(self, mock_query_yes_no):
|
||||||
|
|
|
@ -2073,13 +2073,18 @@ class TestPortfolio(TestCase):
|
||||||
self.user, _ = User.objects.get_or_create(
|
self.user, _ = User.objects.get_or_create(
|
||||||
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
||||||
)
|
)
|
||||||
|
self.non_federal_agency, _ = FederalAgency.objects.get_or_create(agency="Non-Federal Agency")
|
||||||
|
self.federal_agency, _ = FederalAgency.objects.get_or_create(agency="Federal Agency")
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
|
self.federal_agency.delete()
|
||||||
|
# not deleting non_federal_agency so as not to interfere potentially with other tests
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_urbanization_field_resets_when_not_puetro_rico(self):
|
def test_urbanization_field_resets_when_not_puetro_rico(self):
|
||||||
"""The urbanization field should only be populated when the state is puetro rico.
|
"""The urbanization field should only be populated when the state is puetro rico.
|
||||||
Otherwise, this field should be empty."""
|
Otherwise, this field should be empty."""
|
||||||
|
@ -2100,6 +2105,7 @@ class TestPortfolio(TestCase):
|
||||||
self.assertEqual(portfolio.urbanization, None)
|
self.assertEqual(portfolio.urbanization, None)
|
||||||
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA)
|
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.ALABAMA)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_can_add_urbanization_field(self):
|
def test_can_add_urbanization_field(self):
|
||||||
"""Ensures that you can populate the urbanization field when conditions are right"""
|
"""Ensures that you can populate the urbanization field when conditions are right"""
|
||||||
# Create a portfolio that cannot have this field
|
# Create a portfolio that cannot have this field
|
||||||
|
@ -2121,6 +2127,32 @@ class TestPortfolio(TestCase):
|
||||||
self.assertEqual(portfolio.urbanization, "test123")
|
self.assertEqual(portfolio.urbanization, "test123")
|
||||||
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO)
|
self.assertEqual(portfolio.state_territory, DomainRequest.StateTerritoryChoices.PUERTO_RICO)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_organization_name_updates_for_federal_agency(self):
|
||||||
|
# Create a Portfolio instance with a federal agency
|
||||||
|
portfolio = Portfolio(
|
||||||
|
creator=self.user,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.federal_agency,
|
||||||
|
)
|
||||||
|
portfolio.save()
|
||||||
|
|
||||||
|
# Assert that organization_name is updated to the federal agency's name
|
||||||
|
self.assertEqual(portfolio.organization_name, "Federal Agency")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_organization_name_does_not_update_for_non_federal_agency(self):
|
||||||
|
# Create a Portfolio instance with a non-federal agency
|
||||||
|
portfolio = Portfolio(
|
||||||
|
creator=self.user,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
federal_agency=self.non_federal_agency,
|
||||||
|
)
|
||||||
|
portfolio.save()
|
||||||
|
|
||||||
|
# Assert that organization_name remains None
|
||||||
|
self.assertIsNone(portfolio.organization_name)
|
||||||
|
|
||||||
|
|
||||||
class TestAllowedEmail(TestCase):
|
class TestAllowedEmail(TestCase):
|
||||||
"""Tests our allowed email whitelist"""
|
"""Tests our allowed email whitelist"""
|
||||||
|
|
|
@ -16,7 +16,9 @@ from registrar.models import (
|
||||||
AllowedEmail,
|
AllowedEmail,
|
||||||
Portfolio,
|
Portfolio,
|
||||||
Suborganization,
|
Suborganization,
|
||||||
|
UserPortfolioPermission,
|
||||||
)
|
)
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
|
||||||
import boto3_mocking
|
import boto3_mocking
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
|
@ -46,6 +48,14 @@ class TestDomainRequest(TestCase):
|
||||||
self.dummy_user_2, _ = User.objects.get_or_create(
|
self.dummy_user_2, _ = User.objects.get_or_create(
|
||||||
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
username="intern@igorville.com", email="intern@igorville.com", first_name="Lava", last_name="World"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.dummy_user_3, _ = User.objects.get_or_create(
|
||||||
|
username="portfolioadmin@igorville.com",
|
||||||
|
email="portfolioadmin@igorville.com",
|
||||||
|
first_name="Portfolio",
|
||||||
|
last_name="Admin",
|
||||||
|
)
|
||||||
|
|
||||||
self.started_domain_request = completed_domain_request(
|
self.started_domain_request = completed_domain_request(
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
name="started.gov",
|
name="started.gov",
|
||||||
|
@ -273,7 +283,14 @@ class TestDomainRequest(TestCase):
|
||||||
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
self.assertEqual(domain_request.status, domain_request.DomainRequestStatus.SUBMITTED)
|
||||||
|
|
||||||
def check_email_sent(
|
def check_email_sent(
|
||||||
self, domain_request, msg, action, expected_count, expected_content=None, expected_email="mayor@igorville.com"
|
self,
|
||||||
|
domain_request,
|
||||||
|
msg,
|
||||||
|
action,
|
||||||
|
expected_count,
|
||||||
|
expected_content=None,
|
||||||
|
expected_email="mayor@igorville.com",
|
||||||
|
expected_cc=[],
|
||||||
):
|
):
|
||||||
"""Check if an email was sent after performing an action."""
|
"""Check if an email was sent after performing an action."""
|
||||||
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
|
email_allowed, _ = AllowedEmail.objects.get_or_create(email=expected_email)
|
||||||
|
@ -292,6 +309,11 @@ class TestDomainRequest(TestCase):
|
||||||
]
|
]
|
||||||
self.assertEqual(len(sent_emails), expected_count)
|
self.assertEqual(len(sent_emails), expected_count)
|
||||||
|
|
||||||
|
if expected_cc:
|
||||||
|
sent_cc_adddresses = sent_emails[0]["kwargs"]["Destination"]["CcAddresses"]
|
||||||
|
for cc_address in expected_cc:
|
||||||
|
self.assertIn(cc_address, sent_cc_adddresses)
|
||||||
|
|
||||||
if expected_content:
|
if expected_content:
|
||||||
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
|
email_content = sent_emails[0]["kwargs"]["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||||
self.assertIn(expected_content, email_content)
|
self.assertIn(expected_content, email_content)
|
||||||
|
@ -1074,6 +1096,36 @@ class TestDomainRequest(TestCase):
|
||||||
self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type)
|
self.assertEqual(domain_request2.generic_org_type, domain_request2.converted_generic_org_type)
|
||||||
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency)
|
self.assertEqual(domain_request2.federal_agency, domain_request2.converted_federal_agency)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_portfolio_domain_requests_cc_requests_viewers(self):
|
||||||
|
"""test that portfolio domain request emails cc portfolio members who have read requests access"""
|
||||||
|
fed_agency = FederalAgency.objects.filter(agency="Non-Federal Agency").first()
|
||||||
|
portfolio = Portfolio.objects.create(
|
||||||
|
organization_name="Test Portfolio",
|
||||||
|
creator=self.dummy_user_2,
|
||||||
|
federal_agency=fed_agency,
|
||||||
|
organization_type=DomainRequest.OrganizationChoices.FEDERAL,
|
||||||
|
)
|
||||||
|
user_portfolio_permission = UserPortfolioPermission.objects.create( # noqa: F841
|
||||||
|
user=self.dummy_user_3, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
# Adds cc'ed email in this test's allow list
|
||||||
|
AllowedEmail.objects.create(email="portfolioadmin@igorville.com")
|
||||||
|
|
||||||
|
msg = "Create a domain request and submit it and see if email cc's portfolio admin and members who can view \
|
||||||
|
requests."
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
name="test.gov", user=self.dummy_user_2, portfolio=portfolio, organization_name="Test Portfolio"
|
||||||
|
)
|
||||||
|
self.check_email_sent(
|
||||||
|
domain_request,
|
||||||
|
msg,
|
||||||
|
"submit",
|
||||||
|
1,
|
||||||
|
expected_email="intern@igorville.com",
|
||||||
|
expected_cc=["portfolioadmin@igorville.com"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestDomainRequestSuborganization(TestCase):
|
class TestDomainRequestSuborganization(TestCase):
|
||||||
"""Tests for the suborganization fields on domain requests"""
|
"""Tests for the suborganization fields on domain requests"""
|
||||||
|
|
|
@ -255,10 +255,10 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"Organization name,City,State,SO,SO email,"
|
"Organization name,City,State,SO,SO email,"
|
||||||
"Security contact email,Domain managers,Invited domain managers\n"
|
"Security contact email,Domain managers,Invited domain managers\n"
|
||||||
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,"
|
||||||
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
"meoward@rocks.com,squeaker@rocks.com\n"
|
"meoward@rocks.com,squeaker@rocks.com\n"
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,"
|
||||||
"Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
"cdomain11.gov,Ready,2024-04-02,(blank),Federal - Executive,"
|
||||||
"World War I Centennial Commission,,,, ,,(blank),"
|
"World War I Centennial Commission,,,, ,,(blank),"
|
||||||
|
@ -280,6 +280,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -316,9 +317,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
|
"Domain name,Status,First ready on,Expiration date,Domain type,Agency,Organization name,"
|
||||||
"City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
|
"City,State,SO,SO email,Security contact email,Domain managers,Invited domain managers\n"
|
||||||
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"adomain2.gov,Dns needed,(blank),(blank),Federal - Executive,Portfolio 1 Federal Agency,"
|
||||||
|
"Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
|
'"info@example.com, meoward@rocks.com",squeaker@rocks.com\n'
|
||||||
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,,,, ,,(blank),"
|
"defaultsecurity.gov,Ready,2023-11-01,(blank),Federal - Executive,Portfolio 1 Federal Agency,"
|
||||||
|
"Portfolio 1 Federal Agency,,, ,,(blank),"
|
||||||
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
'"big_lebowski@dude.co, info@example.com, meoward@rocks.com",woofwardthethird@rocks.com\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -326,6 +329,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
# spaces and leading/trailing whitespace
|
# spaces and leading/trailing whitespace
|
||||||
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content = csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -587,7 +591,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
expected_content = (
|
expected_content = (
|
||||||
"Domain name,Domain type,Agency,Organization name,City,"
|
"Domain name,Domain type,Agency,Organization name,City,"
|
||||||
"State,Status,Expiration date, Deleted\n"
|
"State,Status,Expiration date, Deleted\n"
|
||||||
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Ready,(blank)\n"
|
"cdomain1.gov,Federal-Executive,Portfolio1FederalAgency,Portfolio1FederalAgency,Ready,(blank)\n"
|
||||||
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
"adomain10.gov,Federal,ArmedForcesRetirementHome,Ready,(blank)\n"
|
||||||
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
"cdomain11.gov,Federal-Executive,WorldWarICentennialCommission,Ready,(blank)\n"
|
||||||
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
"zdomain12.gov,Interstate,Ready,(blank)\n"
|
||||||
|
@ -601,6 +605,7 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
csv_content.replace(",,", "").replace(",", "").replace(" ", "").replace("\r\n", "\n").strip()
|
||||||
)
|
)
|
||||||
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
expected_content = expected_content.replace(",,", "").replace(",", "").replace(" ", "").strip()
|
||||||
|
self.maxDiff = None
|
||||||
self.assertEqual(csv_content, expected_content)
|
self.assertEqual(csv_content, expected_content)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -780,9 +785,11 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
"city5.gov,Approved,Federal,No,Executive,,Testorg,N/A,,NY,2,requested_suborg,SanFran,CA,,,,,1,0,"
|
||||||
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
"city1.gov,Testy,Tester,testy@town.com,Chief Tester,Purpose of the site,There is more,"
|
||||||
"Testy Tester testy2@town.com,,city.com,\n"
|
"Testy Tester testy2@town.com,,city.com,\n"
|
||||||
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,SubOrg 1,,,,,,,0,"
|
"city2.gov,In review,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||||
"1,city1.gov,,,,,Purpose of the site,There is more,Testy Tester testy2@town.com,,city.com,\n"
|
"N/A,,,2,SubOrg 1,,,,,,,0,1,city1.gov,,,,,Purpose of the site,There is more,"
|
||||||
"city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1,"
|
"Testy Tester testy2@town.com,,city.com,\n"
|
||||||
|
"city3.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,"
|
||||||
|
"N/A,,,2,,,,,,,,0,1,"
|
||||||
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
'"cheeseville.gov, city1.gov, igorville.gov",,,,,Purpose of the site,CISA-first-name CISA-last-name | '
|
||||||
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
'There is more,"Meow Tester24 te2@town.com, Testy1232 Tester24 te2@town.com, '
|
||||||
'Testy Tester testy2@town.com",'
|
'Testy Tester testy2@town.com",'
|
||||||
|
@ -792,9 +799,9 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
"Chief Tester,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"Testy Tester testy2@town.com,"
|
"Testy Tester testy2@town.com,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"cisaRep@igorville.gov,city.com,\n"
|
||||||
"city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,,N/A,,,2,,,,,,,,0,1,city1.gov,"
|
"city6.gov,Submitted,Federal,Yes,Executive,Portfolio 1 Federal Agency,Portfolio 1 Federal Agency,N/A,"
|
||||||
",,,,Purpose of the site,CISA-first-name CISA-last-name | There is more,Testy Tester testy2@town.com,"
|
",,2,,,,,,,,0,1,city1.gov,,,,,Purpose of the site,CISA-first-name CISA-last-name | There is more,"
|
||||||
"cisaRep@igorville.gov,city.com,\n"
|
"Testy Tester testy2@town.com,cisaRep@igorville.gov,city.com,\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize line endings and remove commas,
|
# Normalize line endings and remove commas,
|
||||||
|
|
|
@ -1468,7 +1468,9 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
# Create a member under same portfolio
|
# Create a member under same portfolio
|
||||||
member_email = "a_member@example.com"
|
member_email = "a_member@example.com"
|
||||||
member, _ = User.objects.get_or_create(username="a_member", email=member_email)
|
member, _ = User.objects.get_or_create(
|
||||||
|
username="a_member", email=member_email, first_name="First", last_name="Last"
|
||||||
|
)
|
||||||
|
|
||||||
upp, _ = UserPortfolioPermission.objects.get_or_create(
|
upp, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
user=member,
|
user=member,
|
||||||
|
@ -1485,7 +1487,8 @@ class TestPortfolio(WebTest):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
# Check for email AND member type (which here is just member)
|
# Check for email AND member type (which here is just member)
|
||||||
self.assertContains(response, f'data-member-name="{member_email}"')
|
self.assertContains(response, f'data-member-email="{member_email}"')
|
||||||
|
self.assertContains(response, 'data-member-name="First Last"')
|
||||||
self.assertContains(response, 'data-member-type="member"')
|
self.assertContains(response, 'data-member-type="member"')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@ -1676,8 +1679,9 @@ class TestPortfolio(WebTest):
|
||||||
self.assertEqual(response.status_code, 400) # Bad request due to active requests
|
self.assertEqual(response.status_code, 400) # Bad request due to active requests
|
||||||
support_url = "https://get.gov/contact/"
|
support_url = "https://get.gov/contact/"
|
||||||
expected_error_message = (
|
expected_error_message = (
|
||||||
f"This member has an active domain request and can't be removed from the organization. "
|
"This member can't be removed from the organization because they have an active domain request. "
|
||||||
f"<a href='{support_url}' target='_blank'>Contact the .gov team</a> to remove them."
|
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> "
|
||||||
|
"to remove this member."
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertContains(response, expected_error_message, status_code=400)
|
self.assertContains(response, expected_error_message, status_code=400)
|
||||||
|
@ -1799,8 +1803,9 @@ class TestPortfolio(WebTest):
|
||||||
|
|
||||||
support_url = "https://get.gov/contact/"
|
support_url = "https://get.gov/contact/"
|
||||||
expected_error_message = (
|
expected_error_message = (
|
||||||
f"This member has an active domain request and can't be removed from the organization. "
|
"This member can't be removed from the organization because they have an active domain request. "
|
||||||
f"<a href='{support_url}' target='_blank'>Contact the .gov team</a> to remove them."
|
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> "
|
||||||
|
"to remove this member."
|
||||||
)
|
)
|
||||||
|
|
||||||
args, kwargs = mock_error.call_args
|
args, kwargs = mock_error.call_args
|
||||||
|
|
|
@ -36,7 +36,7 @@ def send_templated_email( # noqa
|
||||||
|
|
||||||
to_address and bcc_address currently only support single addresses.
|
to_address and bcc_address currently only support single addresses.
|
||||||
|
|
||||||
cc_address is a list and can contain many addresses. Emails not in the
|
cc_addresses is a list and can contain many addresses. Emails not in the
|
||||||
whitelist (if applicable) will be filtered out before sending.
|
whitelist (if applicable) will be filtered out before sending.
|
||||||
|
|
||||||
template_name and subject_template_name are relative to the same template
|
template_name and subject_template_name are relative to the same template
|
||||||
|
|
|
@ -1336,6 +1336,8 @@ class DomainDeleteUserView(UserDomainRolePermissionDeleteView):
|
||||||
# Is the user deleting themselves? If so, display a different message
|
# Is the user deleting themselves? If so, display a different message
|
||||||
delete_self = self.request.user == self.object.user
|
delete_self = self.request.user == self.object.user
|
||||||
|
|
||||||
|
# Email domain managers
|
||||||
|
|
||||||
# Add a success message
|
# Add a success message
|
||||||
messages.success(self.request, self.get_success_message(delete_self))
|
messages.success(self.request, self.get_success_message(delete_self))
|
||||||
return redirect(self.get_success_url())
|
return redirect(self.get_success_url())
|
||||||
|
|
|
@ -118,8 +118,8 @@ class PortfolioMemberDeleteView(PortfolioMemberPermission, View):
|
||||||
if active_requests_count > 0:
|
if active_requests_count > 0:
|
||||||
# If they have any in progress requests
|
# If they have any in progress requests
|
||||||
error_message = mark_safe( # nosec
|
error_message = mark_safe( # nosec
|
||||||
f"This member has an active domain request and can't be removed from the organization. "
|
"This member can't be removed from the organization because they have an active domain request. "
|
||||||
f"<a href='{support_url}' target='_blank'>Contact the .gov team</a> to remove them."
|
f"Please <a class='usa-link' href='{support_url}' target='_blank'>contact us</a> to remove this member."
|
||||||
)
|
)
|
||||||
elif member.is_only_admin_of_portfolio(portfolio_member_permission.portfolio):
|
elif member.is_only_admin_of_portfolio(portfolio_member_permission.portfolio):
|
||||||
# If they are the last manager of a domain
|
# If they are the last manager of a domain
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue