mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-08-04 08:52:16 +02:00
Resolved merge conflicts
This commit is contained in:
commit
3e6fcaa67f
72 changed files with 2146 additions and 1056 deletions
|
@ -1,7 +1,7 @@
|
|||
import os
|
||||
import tempfile
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf import settings # type: ignore
|
||||
|
||||
|
||||
class Cert:
|
||||
|
@ -12,7 +12,7 @@ class Cert:
|
|||
variable but Python's ssl library requires a file.
|
||||
"""
|
||||
|
||||
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None:
|
||||
def __init__(self, data=settings.SECRET_REGISTRY_CERT) -> None: # type: ignore
|
||||
self.filename = self._write(data)
|
||||
|
||||
def __del__(self):
|
||||
|
@ -31,4 +31,4 @@ class Key(Cert):
|
|||
"""Location of private key as written to disk."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(data=settings.SECRET_REGISTRY_KEY)
|
||||
super().__init__(data=settings.SECRET_REGISTRY_KEY) # type: ignore
|
||||
|
|
|
@ -536,7 +536,6 @@ class AdminSortFields:
|
|||
sort_mapping = {
|
||||
# == Contact == #
|
||||
"other_contacts": (Contact, _name_sort),
|
||||
"submitter": (Contact, _name_sort),
|
||||
# == Senior Official == #
|
||||
"senior_official": (SeniorOfficial, _name_sort),
|
||||
# == User == #
|
||||
|
@ -963,7 +962,9 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||
domains = Domain.objects.filter(id__in=domain_ids).exclude(state=Domain.State.DELETED)
|
||||
|
||||
extra_context = {"domain_requests": domain_requests, "domains": domains}
|
||||
portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
|
||||
portfolios = models.Portfolio.objects.filter(id__in=portfolio_ids)
|
||||
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
|
||||
|
@ -1441,13 +1442,9 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"domain",
|
||||
"generic_org_type",
|
||||
"created_at",
|
||||
"submitter",
|
||||
]
|
||||
|
||||
orderable_fk_fields = [
|
||||
("domain", "name"),
|
||||
("submitter", ["first_name", "last_name"]),
|
||||
]
|
||||
orderable_fk_fields = [("domain", "name")]
|
||||
|
||||
# Filters
|
||||
list_filter = ["generic_org_type"]
|
||||
|
@ -1459,7 +1456,7 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
search_help_text = "Search by domain."
|
||||
|
||||
fieldsets = [
|
||||
(None, {"fields": ["portfolio", "sub_organization", "creator", "submitter", "domain_request", "notes"]}),
|
||||
(None, {"fields": ["portfolio", "sub_organization", "creator", "domain_request", "notes"]}),
|
||||
(".gov domain", {"fields": ["domain"]}),
|
||||
("Contacts", {"fields": ["senior_official", "other_contacts", "no_other_contacts_rationale"]}),
|
||||
("Background info", {"fields": ["anything_else"]}),
|
||||
|
@ -1523,7 +1520,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"more_organization_information",
|
||||
"domain",
|
||||
"domain_request",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
|
@ -1538,7 +1534,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"domain_request",
|
||||
"senior_official",
|
||||
"domain",
|
||||
"submitter",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
]
|
||||
|
@ -1711,13 +1706,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"custom_election_board",
|
||||
"city",
|
||||
"state_territory",
|
||||
"submitter",
|
||||
"investigator",
|
||||
]
|
||||
|
||||
orderable_fk_fields = [
|
||||
("requested_domain", "name"),
|
||||
("submitter", ["first_name", "last_name"]),
|
||||
("investigator", ["first_name", "last_name"]),
|
||||
]
|
||||
|
||||
|
@ -1747,11 +1740,11 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Search
|
||||
search_fields = [
|
||||
"requested_domain__name",
|
||||
"submitter__email",
|
||||
"submitter__first_name",
|
||||
"submitter__last_name",
|
||||
"creator__email",
|
||||
"creator__first_name",
|
||||
"creator__last_name",
|
||||
]
|
||||
search_help_text = "Search by domain or submitter."
|
||||
search_help_text = "Search by domain or creator."
|
||||
|
||||
fieldsets = [
|
||||
(
|
||||
|
@ -1767,7 +1760,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"action_needed_reason_email",
|
||||
"investigator",
|
||||
"creator",
|
||||
"submitter",
|
||||
"approved_domain",
|
||||
"notes",
|
||||
]
|
||||
|
@ -1855,7 +1847,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
|
@ -1866,7 +1857,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
autocomplete_fields = [
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
"submitter",
|
||||
"creator",
|
||||
"senior_official",
|
||||
"investigator",
|
||||
|
@ -1988,12 +1978,8 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
"""
|
||||
|
||||
# TODO 2574: remove lines 1977-1978 (refactor as needed)
|
||||
profile_flag = flag_is_active(request, "profile_feature")
|
||||
if profile_flag and hasattr(obj, "creator"):
|
||||
if hasattr(obj, "creator"):
|
||||
recipient = obj.creator
|
||||
elif not profile_flag and hasattr(obj, "submitter"):
|
||||
recipient = obj.submitter
|
||||
else:
|
||||
recipient = None
|
||||
|
||||
|
@ -2229,10 +2215,7 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
||||
return None
|
||||
|
||||
if flag_is_active(None, "profile_feature"): # type: ignore
|
||||
recipient = domain_request.creator
|
||||
else:
|
||||
recipient = domain_request.submitter
|
||||
recipient = domain_request.creator
|
||||
|
||||
# Return the context of the rendered views
|
||||
context = {"domain_request": domain_request, "recipient": recipient}
|
||||
|
|
|
@ -748,7 +748,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
|
||||
//------ Requested Domains
|
||||
const requestedDomainElement = document.getElementById('id_requested_domain');
|
||||
const requestedDomain = requestedDomainElement.options[requestedDomainElement.selectedIndex].text;
|
||||
// We have to account for different superuser and analyst markups
|
||||
const requestedDomain = requestedDomainElement.options
|
||||
? requestedDomainElement.options[requestedDomainElement.selectedIndex].text
|
||||
: requestedDomainElement.text;
|
||||
|
||||
//------ Submitter
|
||||
// Function to extract text by ID and handle missing elements
|
||||
|
@ -762,7 +765,10 @@ function initializeWidgetOnList(list, parentId) {
|
|||
// Extract the submitter name, title, email, and phone number
|
||||
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
||||
const submitterNameElement = document.getElementById('id_submitter');
|
||||
const submitterName = submitterNameElement.options[submitterNameElement.selectedIndex].text;
|
||||
// We have to account for different superuser and analyst markups
|
||||
const submitterName = submitterNameElement
|
||||
? submitterNameElement.options[submitterNameElement.selectedIndex].text
|
||||
: submitterDiv.querySelector('a').text;
|
||||
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
||||
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
||||
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
||||
|
|
|
@ -1168,7 +1168,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const statusCheckboxes = document.querySelectorAll('input[name="filter-status"]');
|
||||
const statusIndicator = document.querySelector('.domain__filter-indicator');
|
||||
const statusToggle = document.querySelector('.usa-button--filter');
|
||||
const noPortfolioFlag = document.getElementById('no-portfolio-js-flag');
|
||||
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||
|
||||
|
@ -1226,7 +1225,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
let markupForSuborganizationRow = '';
|
||||
|
||||
if (!noPortfolioFlag) {
|
||||
if (portfolioValue) {
|
||||
markupForSuborganizationRow = `
|
||||
<td>
|
||||
<span class="text-wrap" aria-label="${domain.suborganization ? suborganization : 'No suborganization'}">${suborganization}</span>
|
||||
|
@ -1427,9 +1426,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('focusin', function(event) {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionIsOpen && !accordion.contains(event.target)) {
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
closeFilters();
|
||||
}
|
||||
});
|
||||
|
@ -1438,9 +1437,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// NOTE: We may need to evolve this as we add more filters.
|
||||
document.addEventListener('click', function(event) {
|
||||
const accordion = document.querySelector('.usa-accordion--select');
|
||||
const accordionIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
const accordionThatIsOpen = document.querySelector('.usa-button--filter[aria-expanded="true"]');
|
||||
|
||||
if (accordionIsOpen && !accordion.contains(event.target)) {
|
||||
if (accordionThatIsOpen && !accordion.contains(event.target)) {
|
||||
closeFilters();
|
||||
}
|
||||
});
|
||||
|
@ -1485,6 +1484,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const tableHeaders = document.querySelectorAll('.domain-requests__table th[data-sortable]');
|
||||
const tableAnnouncementRegion = document.querySelector('.domain-requests__table-wrapper .usa-table__announcement-region');
|
||||
const resetSearchButton = document.querySelector('.domain-requests__reset-search');
|
||||
const portfolioElement = document.getElementById('portfolio-js-value');
|
||||
const portfolioValue = portfolioElement ? portfolioElement.getAttribute('data-portfolio') : null;
|
||||
|
||||
/**
|
||||
* Delete is actually a POST API that requires a csrf token. The token will be waiting for us in the template as a hidden input.
|
||||
|
@ -1533,7 +1534,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} searchTerm - the search term
|
||||
*/
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm) {
|
||||
function loadDomainRequests(page, sortBy = currentSortBy, order = currentOrder, scroll = scrollToTable, searchTerm = currentSearchTerm, portfolio = portfolioValue) {
|
||||
// fetch json of page of domain requests, given params
|
||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||
if (!baseUrl) {
|
||||
|
@ -1545,7 +1546,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
return;
|
||||
}
|
||||
|
||||
fetch(`${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`)
|
||||
// fetch json of page of requests, given params
|
||||
let url = `${baseUrlValue}?page=${page}&sort_by=${sortBy}&order=${order}&search_term=${searchTerm}`
|
||||
if (portfolio)
|
||||
url += `&portfolio=${portfolio}`
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
|
@ -1601,10 +1607,21 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const actionLabel = request.action_label;
|
||||
const submissionDate = request.last_submitted_date ? new Date(request.last_submitted_date).toLocaleDateString('en-US', options) : `<span class="text-base">Not submitted</span>`;
|
||||
|
||||
// Even if the request is not deletable, we may need this empty string for the td if the deletable column is displayed
|
||||
// The markup for the delete function either be a simple trigger or a 3 dots menu with a hidden trigger (in the case of portfolio requests page)
|
||||
// Even if the request is not deletable, we may need these empty strings for the td if the deletable column is displayed
|
||||
let modalTrigger = '';
|
||||
|
||||
// If the request is deletable, create modal body and insert it
|
||||
let markupCreatorRow = '';
|
||||
|
||||
if (portfolioValue) {
|
||||
markupCreatorRow = `
|
||||
<td>
|
||||
<span class="text-wrap break-word">${request.creator ? request.creator : ''}</span>
|
||||
</td>
|
||||
`
|
||||
}
|
||||
|
||||
// If the request is deletable, create modal body and insert it. This is true for both requests and portfolio requests pages
|
||||
if (request.is_deletable) {
|
||||
let modalHeading = '';
|
||||
let modalDescription = '';
|
||||
|
@ -1627,7 +1644,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
role="button"
|
||||
id="button-toggle-delete-domain-alert-${request.id}"
|
||||
href="#toggle-delete-domain-alert-${request.id}"
|
||||
class="usa-button--unstyled text-no-underline late-loading-modal-trigger"
|
||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger line-height-sans-5"
|
||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||
data-open-modal
|
||||
>
|
||||
|
@ -1692,8 +1709,57 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
`
|
||||
|
||||
domainRequestsSectionWrapper.appendChild(modal);
|
||||
|
||||
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
||||
if (portfolioValue) {
|
||||
modalTrigger = `
|
||||
<a
|
||||
role="button"
|
||||
id="button-toggle-delete-domain-alert-${request.id}"
|
||||
href="#toggle-delete-domain-alert-${request.id}"
|
||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 visible-mobile-flex line-height-sans-5"
|
||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||
data-open-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||
</a>
|
||||
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||
aria-expanded="false"
|
||||
aria-controls="more-actions-${request.id}"
|
||||
>
|
||||
<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>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-${request.id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0" hidden>
|
||||
<h2>More options</h2>
|
||||
<a
|
||||
role="button"
|
||||
id="button-toggle-delete-domain-alert-${request.id}"
|
||||
href="#toggle-delete-domain-alert-${request.id}"
|
||||
class="usa-button text-secondary usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5"
|
||||
aria-controls="toggle-delete-domain-alert-${request.id}"
|
||||
data-open-modal
|
||||
>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||
</svg> Delete <span class="usa-sr-only">${domainName}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<th scope="row" role="rowheader" data-label="Domain name">
|
||||
|
@ -1702,6 +1768,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
<td data-sort-value="${new Date(request.last_submitted_date).getTime()}" data-label="Date submitted">
|
||||
${submissionDate}
|
||||
</td>
|
||||
${markupCreatorRow}
|
||||
<td data-label="Status">
|
||||
${request.status}
|
||||
</td>
|
||||
|
@ -1817,6 +1884,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
});
|
||||
}
|
||||
|
||||
function closeMoreActionMenu(accordionThatIsOpen) {
|
||||
if (accordionThatIsOpen.getAttribute("aria-expanded") === "true") {
|
||||
accordionThatIsOpen.click();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('focusin', function(event) {
|
||||
closeOpenAccordions(event);
|
||||
});
|
||||
|
||||
document.addEventListener('click', function(event) {
|
||||
closeOpenAccordions(event);
|
||||
});
|
||||
|
||||
function closeOpenAccordions(event) {
|
||||
const openAccordions = document.querySelectorAll('.usa-button--more-actions[aria-expanded="true"]');
|
||||
openAccordions.forEach((openAccordionButton) => {
|
||||
// Find the corresponding accordion
|
||||
const accordion = openAccordionButton.closest('.usa-accordion--more-actions');
|
||||
if (accordion && !accordion.contains(event.target)) {
|
||||
// Close the accordion if the click is outside
|
||||
closeMoreActionMenu(openAccordionButton);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadDomainRequests(1);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
@use "uswds-core" as *;
|
||||
|
||||
.usa-accordion--select {
|
||||
.usa-accordion--select,
|
||||
.usa-accordion--more-actions {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
position: relative;
|
||||
|
@ -14,7 +15,6 @@
|
|||
// Note, width is determined by a custom width class on one of the children
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 33.88px;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
border: solid 1px color('base-lighter');
|
||||
|
@ -31,3 +31,17 @@
|
|||
margin-top: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-accordion--select .usa-accordion__content {
|
||||
top: 33.88px;
|
||||
}
|
||||
|
||||
.usa-accordion--more-actions .usa-accordion__content {
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
tr:last-child .usa-accordion--more-actions .usa-accordion__content {
|
||||
top: auto;
|
||||
bottom: -10px;
|
||||
right: 30px;
|
||||
}
|
||||
|
|
|
@ -163,6 +163,23 @@ abbr[title] {
|
|||
}
|
||||
}
|
||||
|
||||
.hidden-mobile-flex {
|
||||
display: none!important;
|
||||
}
|
||||
.visible-mobile-flex {
|
||||
display: flex!important;
|
||||
}
|
||||
|
||||
@include at-media(tablet) {
|
||||
.hidden-mobile-flex {
|
||||
display: flex!important;
|
||||
}
|
||||
.visible-mobile-flex {
|
||||
display: none!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.flex-end {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
@ -204,6 +221,9 @@ abbr[title] {
|
|||
}
|
||||
}
|
||||
|
||||
// Boost this USWDS utility class for the accordions in the portfolio requests table
|
||||
.left-auto {
|
||||
left: auto!important;
|
||||
.usa-banner__inner--widescreen {
|
||||
max-width: var(--widescreen-max-width);
|
||||
}
|
||||
|
@ -211,3 +231,7 @@ abbr[title] {
|
|||
.margin-right-neg-4px {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
|
@ -223,6 +223,7 @@ a.usa-button--unstyled:visited {
|
|||
}
|
||||
}
|
||||
|
||||
.dotgov-table a
|
||||
a .usa-icon,
|
||||
.usa-button--with-icon .usa-icon {
|
||||
height: 1.3em;
|
||||
|
@ -234,3 +235,9 @@ a .usa-icon,
|
|||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
}
|
||||
|
||||
button.text-secondary,
|
||||
button.text-secondary:hover,
|
||||
.dotgov-table a.text-secondary {
|
||||
color: $theme-color-error;
|
||||
}
|
||||
|
|
|
@ -89,16 +89,24 @@
|
|||
.usa-nav__primary {
|
||||
.usa-nav-link,
|
||||
.usa-nav-link:hover,
|
||||
.usa-nav-link:active {
|
||||
.usa-nav-link:active,
|
||||
button {
|
||||
color: color('primary');
|
||||
font-weight: font-weight('normal');
|
||||
font-size: 16px;
|
||||
}
|
||||
.usa-current,
|
||||
.usa-current:hover,
|
||||
.usa-current:active {
|
||||
.usa-current:active,
|
||||
button.usa-current {
|
||||
font-weight: font-weight('bold');
|
||||
}
|
||||
button[aria-expanded="true"] {
|
||||
color: color('white');
|
||||
}
|
||||
button:not(.usa-current):hover::after {
|
||||
display: none!important;
|
||||
}
|
||||
}
|
||||
.usa-nav__secondary {
|
||||
// I don't know why USWDS has this at 2 rem, which puts it out of alignment
|
||||
|
|
|
@ -23,6 +23,9 @@ from cfenv import AppEnv # type: ignore
|
|||
from pathlib import Path
|
||||
from typing import Final
|
||||
from botocore.config import Config
|
||||
import json
|
||||
import logging
|
||||
from django.utils.log import ServerFormatter
|
||||
|
||||
# # # ###
|
||||
# Setup code goes here #
|
||||
|
@ -57,7 +60,7 @@ env_db_url = env.dj_db_url("DATABASE_URL")
|
|||
env_debug = env.bool("DJANGO_DEBUG", default=False)
|
||||
env_is_production = env.bool("IS_PRODUCTION", default=False)
|
||||
env_log_level = env.str("DJANGO_LOG_LEVEL", "DEBUG")
|
||||
env_base_url = env.str("DJANGO_BASE_URL")
|
||||
env_base_url: str = env.str("DJANGO_BASE_URL")
|
||||
env_getgov_public_site_url = env.str("GETGOV_PUBLIC_SITE_URL", "")
|
||||
env_oidc_active_provider = env.str("OIDC_ACTIVE_PROVIDER", "identity sandbox")
|
||||
|
||||
|
@ -192,7 +195,7 @@ MIDDLEWARE = [
|
|||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django’s built-in servers (e.g. `runserver`)
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
WSGI_APPLICATION = "registrar.config.wsgi.application"
|
||||
|
||||
# endregion
|
||||
|
@ -416,7 +419,7 @@ LANGUAGE_COOKIE_SECURE = True
|
|||
# and to interpret datetimes entered in forms
|
||||
TIME_ZONE = "UTC"
|
||||
|
||||
# enable Django’s translation system
|
||||
# enable Django's translation system
|
||||
USE_I18N = True
|
||||
|
||||
# enable localized formatting of numbers and dates
|
||||
|
@ -451,6 +454,40 @@ PHONENUMBER_DEFAULT_REGION = "US"
|
|||
# logger.error("Can't do this important task. Something is very wrong.")
|
||||
# logger.critical("Going to crash now.")
|
||||
|
||||
|
||||
class JsonFormatter(logging.Formatter):
|
||||
"""Formats logs into JSON for better parsing"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(datefmt="%d/%b/%Y %H:%M:%S")
|
||||
|
||||
def format(self, record):
|
||||
log_record = {
|
||||
"timestamp": self.formatTime(record, self.datefmt),
|
||||
"level": record.levelname,
|
||||
"name": record.name,
|
||||
"lineno": record.lineno,
|
||||
"message": record.getMessage(),
|
||||
}
|
||||
return json.dumps(log_record)
|
||||
|
||||
|
||||
class JsonServerFormatter(ServerFormatter):
|
||||
"""Formats server logs into JSON for better parsing"""
|
||||
|
||||
def format(self, record):
|
||||
formatted_record = super().format(record)
|
||||
log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record}
|
||||
return json.dumps(log_entry)
|
||||
|
||||
|
||||
# default to json formatted logs
|
||||
server_formatter, console_formatter = "json.server", "json"
|
||||
|
||||
# don't use json format locally, it makes logs hard to read in console
|
||||
if "localhost" in env_base_url:
|
||||
server_formatter, console_formatter = "django.server", "verbose"
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
# Don't import Django's existing loggers
|
||||
|
@ -470,6 +507,12 @@ LOGGING = {
|
|||
"format": "[{server_time}] {message}",
|
||||
"style": "{",
|
||||
},
|
||||
"json.server": {
|
||||
"()": JsonServerFormatter,
|
||||
},
|
||||
"json": {
|
||||
"()": JsonFormatter,
|
||||
},
|
||||
},
|
||||
# define where log messages will be sent;
|
||||
# each logger can have one or more handlers
|
||||
|
@ -477,12 +520,12 @@ LOGGING = {
|
|||
"console": {
|
||||
"level": env_log_level,
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "verbose",
|
||||
"formatter": console_formatter,
|
||||
},
|
||||
"django.server": {
|
||||
"level": "INFO",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "django.server",
|
||||
"formatter": server_formatter,
|
||||
},
|
||||
# No file logger is configured,
|
||||
# because containerized apps
|
||||
|
|
|
@ -53,7 +53,6 @@ for step, view in [
|
|||
(Step.CURRENT_SITES, views.CurrentSites),
|
||||
(Step.DOTGOV_DOMAIN, views.DotgovDomain),
|
||||
(Step.PURPOSE, views.Purpose),
|
||||
(Step.YOUR_CONTACT, views.YourContact),
|
||||
(Step.OTHER_CONTACTS, views.OtherContacts),
|
||||
(Step.ADDITIONAL_DETAILS, views.AdditionalDetails),
|
||||
(Step.REQUIREMENTS, views.Requirements),
|
||||
|
@ -79,6 +78,11 @@ urlpatterns = [
|
|||
views.PortfolioDomainRequestsView.as_view(),
|
||||
name="domain-requests",
|
||||
),
|
||||
path(
|
||||
"no-organization-requests/",
|
||||
views.PortfolioNoDomainRequestsView.as_view(),
|
||||
name="no-portfolio-requests",
|
||||
),
|
||||
path(
|
||||
"organization/",
|
||||
views.PortfolioOrganizationView.as_view(),
|
||||
|
@ -208,11 +212,6 @@ urlpatterns = [
|
|||
views.DomainDsDataView.as_view(),
|
||||
name="domain-dns-dnssec-dsdata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/your-contact-information",
|
||||
views.DomainYourContactInformationView.as_view(),
|
||||
name="domain-your-contact-information",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/org-name-address",
|
||||
views.DomainOrgNameAddressView.as_view(),
|
||||
|
|
|
@ -60,38 +60,45 @@ def add_has_profile_feature_flag_to_context(request):
|
|||
|
||||
def portfolio_permissions(request):
|
||||
"""Make portfolio permissions for the request user available in global context"""
|
||||
context = {
|
||||
portfolio_context = {
|
||||
"has_base_portfolio_permission": False,
|
||||
"has_domains_portfolio_permission": False,
|
||||
"has_domain_requests_portfolio_permission": False,
|
||||
"has_any_domains_portfolio_permission": False,
|
||||
"has_any_requests_portfolio_permission": False,
|
||||
"has_edit_request_portfolio_permission": False,
|
||||
"has_view_suborganization_portfolio_permission": False,
|
||||
"has_edit_suborganization_portfolio_permission": False,
|
||||
"has_view_members_portfolio_permission": False,
|
||||
"has_edit_members_portfolio_permission": False,
|
||||
"has_view_suborganization": False,
|
||||
"has_edit_suborganization": False,
|
||||
"portfolio": None,
|
||||
"has_organization_feature_flag": False,
|
||||
"has_organization_requests_flag": False,
|
||||
"has_organization_members_flag": False,
|
||||
}
|
||||
try:
|
||||
portfolio = request.session.get("portfolio")
|
||||
# Linting: line too long
|
||||
view_suborg = request.user.has_view_suborganization_portfolio_permission(portfolio)
|
||||
edit_suborg = request.user.has_edit_suborganization_portfolio_permission(portfolio)
|
||||
if portfolio:
|
||||
return {
|
||||
"has_base_portfolio_permission": request.user.has_base_portfolio_permission(portfolio),
|
||||
"has_domains_portfolio_permission": request.user.has_domains_portfolio_permission(portfolio),
|
||||
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
|
||||
portfolio
|
||||
),
|
||||
"has_edit_request_portfolio_permission": request.user.has_edit_request_portfolio_permission(portfolio),
|
||||
"has_view_suborganization_portfolio_permission": view_suborg,
|
||||
"has_edit_suborganization_portfolio_permission": edit_suborg,
|
||||
"has_any_domains_portfolio_permission": request.user.has_any_domains_portfolio_permission(portfolio),
|
||||
"has_any_requests_portfolio_permission": request.user.has_any_requests_portfolio_permission(portfolio),
|
||||
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
||||
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
||||
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
|
||||
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
|
||||
"portfolio": portfolio,
|
||||
"has_organization_feature_flag": True,
|
||||
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
||||
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
||||
}
|
||||
return context
|
||||
return portfolio_context
|
||||
|
||||
except AttributeError:
|
||||
# Handles cases where request.user might not exist
|
||||
return context
|
||||
return portfolio_context
|
||||
|
||||
|
||||
def is_widescreen_mode(request):
|
||||
|
|
|
@ -37,7 +37,6 @@ class DomainRequestFixture:
|
|||
# "anything_else": None,
|
||||
# "is_policy_acknowledged": None,
|
||||
# "senior_official": None,
|
||||
# "submitter": None,
|
||||
# "other_contacts": [],
|
||||
# "current_websites": [],
|
||||
# "alternative_domains": [],
|
||||
|
@ -123,12 +122,6 @@ class DomainRequestFixture:
|
|||
else:
|
||||
da.senior_official = Contact.objects.create(**cls.fake_contact())
|
||||
|
||||
if not da.submitter:
|
||||
if "submitter" in app and app["submitter"] is not None:
|
||||
da.submitter, _ = Contact.objects.get_or_create(**app["submitter"])
|
||||
else:
|
||||
da.submitter = Contact.objects.create(**cls.fake_contact())
|
||||
|
||||
if not da.requested_domain:
|
||||
if "requested_domain" in app and app["requested_domain"] is not None:
|
||||
da.requested_domain, _ = DraftDomain.objects.get_or_create(name=app["requested_domain"])
|
||||
|
|
|
@ -56,6 +56,7 @@ class UserFixture:
|
|||
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
|
||||
"first_name": "Katherine",
|
||||
"last_name": "Osos",
|
||||
"email": "kosos@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
|
||||
|
@ -171,7 +172,7 @@ class UserFixture:
|
|||
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
||||
"first_name": "Katherine-Analyst",
|
||||
"last_name": "Osos-Analyst",
|
||||
"email": "kosos@truss.works",
|
||||
"email": "kosos+1@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "2cc0cde8-8313-4a50-99d8-5882e71443e8",
|
||||
|
|
|
@ -386,64 +386,6 @@ class PurposeForm(RegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class YourContactForm(RegistrarForm):
|
||||
JOIN = "submitter"
|
||||
|
||||
def to_database(self, obj):
|
||||
if not self.is_valid():
|
||||
return
|
||||
contact = getattr(obj, "submitter", None)
|
||||
if contact is not None and not contact.has_more_than_one_join("submitted_domain_requests"):
|
||||
# if contact exists in the database and is not joined to other entities
|
||||
super().to_database(contact)
|
||||
else:
|
||||
# no contact exists OR contact exists which is joined also to other entities;
|
||||
# in either case, create a new contact and update it
|
||||
contact = Contact()
|
||||
super().to_database(contact)
|
||||
obj.submitter = contact
|
||||
obj.save()
|
||||
|
||||
@classmethod
|
||||
def from_database(cls, obj):
|
||||
contact = getattr(obj, "submitter", None)
|
||||
return super().from_database(contact)
|
||||
|
||||
first_name = forms.CharField(
|
||||
label="First name / given name",
|
||||
error_messages={"required": "Enter your first name / given name."},
|
||||
)
|
||||
middle_name = forms.CharField(
|
||||
required=False,
|
||||
label="Middle name (optional)",
|
||||
)
|
||||
last_name = forms.CharField(
|
||||
label="Last name / family name",
|
||||
error_messages={"required": "Enter your last name / family name."},
|
||||
)
|
||||
title = forms.CharField(
|
||||
label="Title or role in your organization",
|
||||
error_messages={
|
||||
"required": ("Enter your title or role in your organization (e.g., Chief Information Officer).")
|
||||
},
|
||||
)
|
||||
email = forms.EmailField(
|
||||
label="Email",
|
||||
max_length=None,
|
||||
error_messages={"invalid": ("Enter your email address in the required format, like name@example.com.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
320,
|
||||
message="Response must be less than 320 characters.",
|
||||
)
|
||||
],
|
||||
)
|
||||
phone = PhoneNumberField(
|
||||
label="Phone",
|
||||
error_messages={"invalid": "Enter a valid 10-digit phone number.", "required": "Enter your phone number."},
|
||||
)
|
||||
|
||||
|
||||
class OtherContactsYesNoForm(BaseYesNoForm):
|
||||
"""The yes/no field for the OtherContacts form."""
|
||||
|
||||
|
|
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
255
src/registrar/management/commands/create_federal_portfolio.py
Normal file
|
@ -0,0 +1,255 @@
|
|||
"""Loads files from /tmp into our sandboxes"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from registrar.management.commands.utility.terminal_helper import TerminalColors, TerminalHelper
|
||||
from registrar.models import DomainInformation, DomainRequest, FederalAgency, Suborganization, Portfolio, User
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Creates a federal portfolio given a FederalAgency name"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
"""Add three arguments:
|
||||
1. agency_name => the value of FederalAgency.agency
|
||||
2. --parse_requests => if true, adds the given portfolio to each related DomainRequest
|
||||
3. --parse_domains => if true, adds the given portfolio to each related DomainInformation
|
||||
"""
|
||||
parser.add_argument(
|
||||
"agency_name",
|
||||
help="The name of the FederalAgency to add",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parse_requests",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to DomainRequests",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--parse_domains",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to DomainInformation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--both",
|
||||
action=argparse.BooleanOptionalAction,
|
||||
help="Adds portfolio to both requests and domains",
|
||||
)
|
||||
|
||||
def handle(self, agency_name, **options):
|
||||
parse_requests = options.get("parse_requests")
|
||||
parse_domains = options.get("parse_domains")
|
||||
both = options.get("both")
|
||||
|
||||
if not both:
|
||||
if not parse_requests and not parse_domains:
|
||||
raise CommandError("You must specify at least one of --parse_requests or --parse_domains.")
|
||||
else:
|
||||
if parse_requests or parse_domains:
|
||||
raise CommandError("You cannot pass --parse_requests or --parse_domains when passing --both.")
|
||||
|
||||
federal_agency = FederalAgency.objects.filter(agency__iexact=agency_name).first()
|
||||
if not federal_agency:
|
||||
raise ValueError(
|
||||
f"Cannot find the federal agency '{agency_name}' in our database. "
|
||||
"The value you enter for `agency_name` must be "
|
||||
"prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
|
||||
portfolio = self.create_or_modify_portfolio(federal_agency)
|
||||
self.create_suborganizations(portfolio, federal_agency)
|
||||
|
||||
if parse_requests or both:
|
||||
self.handle_portfolio_requests(portfolio, federal_agency)
|
||||
|
||||
if parse_domains or both:
|
||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||
|
||||
def create_or_modify_portfolio(self, federal_agency):
|
||||
"""Creates or modifies a portfolio record based on a federal agency."""
|
||||
portfolio_args = {
|
||||
"federal_agency": federal_agency,
|
||||
"organization_name": federal_agency.agency,
|
||||
"organization_type": DomainRequest.OrganizationChoices.FEDERAL,
|
||||
"creator": User.get_default_user(),
|
||||
"notes": "Auto-generated record",
|
||||
}
|
||||
|
||||
if federal_agency.so_federal_agency.exists():
|
||||
portfolio_args["senior_official"] = federal_agency.so_federal_agency.first()
|
||||
|
||||
portfolio, created = Portfolio.objects.get_or_create(
|
||||
organization_name=portfolio_args.get("organization_name"), defaults=portfolio_args
|
||||
)
|
||||
|
||||
if created:
|
||||
message = f"Created portfolio '{portfolio}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
else:
|
||||
message = (
|
||||
f"No senior official added to portfolio '{portfolio}'. "
|
||||
"None was returned for the reverse relation `FederalAgency.so_federal_agency.first()`"
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
else:
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""
|
||||
The given portfolio '{federal_agency.agency}' already exists in our DB.
|
||||
If you cancel, the rest of the script will still execute but this record will not update.
|
||||
""",
|
||||
prompt_title="Do you wish to modify this record?",
|
||||
)
|
||||
if proceed:
|
||||
|
||||
# Don't override the creator and notes fields
|
||||
if portfolio.creator:
|
||||
portfolio_args.pop("creator")
|
||||
|
||||
if portfolio.notes:
|
||||
portfolio_args.pop("notes")
|
||||
|
||||
# Update everything else
|
||||
for key, value in portfolio_args.items():
|
||||
setattr(portfolio, key, value)
|
||||
|
||||
portfolio.save()
|
||||
message = f"Modified portfolio '{portfolio}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
if portfolio_args.get("senior_official"):
|
||||
message = f"Added/modified senior official '{portfolio_args['senior_official']}'"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
return portfolio
|
||||
|
||||
def create_suborganizations(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""Create Suborganizations tied to the given portfolio based on DomainInformation objects"""
|
||||
valid_agencies = DomainInformation.objects.filter(
|
||||
federal_agency=federal_agency, organization_name__isnull=False
|
||||
)
|
||||
org_names = set(valid_agencies.values_list("organization_name", flat=True))
|
||||
|
||||
if not org_names:
|
||||
message = (
|
||||
"Could not add any suborganizations."
|
||||
f"\nNo suborganizations were found for '{federal_agency}' when filtering on this name, "
|
||||
"and excluding null organization_name records."
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.FAIL, message)
|
||||
return
|
||||
|
||||
# Check if we need to update any existing suborgs first. This step is optional.
|
||||
existing_suborgs = Suborganization.objects.filter(name__in=org_names)
|
||||
if existing_suborgs.exists():
|
||||
self._update_existing_suborganizations(portfolio, existing_suborgs)
|
||||
|
||||
# Create new suborgs, as long as they don't exist in the db already
|
||||
new_suborgs = []
|
||||
for name in org_names - set(existing_suborgs.values_list("name", flat=True)):
|
||||
# Stored in variables due to linter wanting type information here.
|
||||
portfolio_name: str = portfolio.organization_name if portfolio.organization_name is not None else ""
|
||||
if name is not None and name.lower() == portfolio_name.lower():
|
||||
# You can use this to populate location information, when this occurs.
|
||||
# However, this isn't needed for now so we can skip it.
|
||||
message = (
|
||||
f"Skipping suborganization create on record '{name}'. "
|
||||
"The federal agency name is the same as the portfolio name."
|
||||
)
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, message)
|
||||
else:
|
||||
new_suborgs.append(Suborganization(name=name, portfolio=portfolio)) # type: ignore
|
||||
|
||||
if new_suborgs:
|
||||
Suborganization.objects.bulk_create(new_suborgs)
|
||||
TerminalHelper.colorful_logger(
|
||||
logger.info, TerminalColors.OKGREEN, f"Added {len(new_suborgs)} suborganizations"
|
||||
)
|
||||
else:
|
||||
TerminalHelper.colorful_logger(logger.warning, TerminalColors.YELLOW, "No suborganizations added")
|
||||
|
||||
def _update_existing_suborganizations(self, portfolio, orgs_to_update):
|
||||
"""
|
||||
Update existing suborganizations with new portfolio.
|
||||
Prompts for user confirmation before proceeding.
|
||||
"""
|
||||
proceed = TerminalHelper.prompt_for_execution(
|
||||
system_exit_on_terminate=False,
|
||||
prompt_message=f"""Some suborganizations already exist in our DB.
|
||||
If you cancel, the rest of the script will still execute but these records will not update.
|
||||
|
||||
==Proposed Changes==
|
||||
The following suborgs will be updated: {[org.name for org in orgs_to_update]}
|
||||
""",
|
||||
prompt_title="Do you wish to modify existing suborganizations?",
|
||||
)
|
||||
if proceed:
|
||||
for org in orgs_to_update:
|
||||
org.portfolio = portfolio
|
||||
|
||||
Suborganization.objects.bulk_update(orgs_to_update, ["portfolio"])
|
||||
message = f"Updated {len(orgs_to_update)} suborganizations."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||
|
||||
def handle_portfolio_requests(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""
|
||||
Associate portfolio with domain requests for a federal agency.
|
||||
Updates all relevant domain request records.
|
||||
"""
|
||||
invalid_states = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
]
|
||||
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
||||
if not domain_requests.exists():
|
||||
message = f"""
|
||||
Portfolios 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.
|
||||
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||
"""
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
return None
|
||||
|
||||
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||
for domain_request in domain_requests:
|
||||
domain_request.portfolio = portfolio
|
||||
if domain_request.organization_name in suborgs:
|
||||
domain_request.sub_organization = suborgs.get(domain_request.organization_name)
|
||||
|
||||
DomainRequest.objects.bulk_update(domain_requests, ["portfolio", "sub_organization"])
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_requests)} domain requests."
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
||||
|
||||
def handle_portfolio_domains(self, portfolio: Portfolio, federal_agency: FederalAgency):
|
||||
"""
|
||||
Associate portfolio with domains for a federal agency.
|
||||
Updates all relevant domain information records.
|
||||
"""
|
||||
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||
if not domain_infos.exists():
|
||||
message = f"""
|
||||
Portfolios not added to domains: no valid records found.
|
||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||
"""
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||
return None
|
||||
|
||||
# Get all suborg information and store it in a dict to avoid doing a db call
|
||||
suborgs = Suborganization.objects.filter(portfolio=portfolio).in_bulk(field_name="name")
|
||||
for domain_info in domain_infos:
|
||||
domain_info.portfolio = portfolio
|
||||
if domain_info.organization_name in suborgs:
|
||||
domain_info.sub_organization = suborgs.get(domain_info.organization_name)
|
||||
|
||||
DomainInformation.objects.bulk_update(domain_infos, ["portfolio", "sub_organization"])
|
||||
message = f"Added portfolio '{portfolio}' to {len(domain_infos)} domains"
|
||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKGREEN, message)
|
|
@ -423,7 +423,7 @@ class Command(BaseCommand):
|
|||
valid_fed_type = fed_type in fed_choices
|
||||
valid_fed_agency = fed_agency in agency_choices
|
||||
|
||||
default_creator, _ = User.objects.get_or_create(username="System")
|
||||
default_creator = User.get_default_user()
|
||||
|
||||
new_domain_info_data = {
|
||||
"domain": domain,
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
# Generated by Django 4.2.10 on 2024-09-09 14:48
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0123_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="portfolioinvitation",
|
||||
name="portfolio_additional_permissions",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("view_all_domains", "View all domains and domain reports"),
|
||||
("view_managed_domains", "View managed domains"),
|
||||
("view_members", "View members"),
|
||||
("edit_members", "Create and edit members"),
|
||||
("view_all_requests", "View all requests"),
|
||||
("edit_requests", "Create and edit requests"),
|
||||
("view_portfolio", "View organization"),
|
||||
("edit_portfolio", "Edit organization"),
|
||||
("view_suborganization", "View suborganization"),
|
||||
("edit_suborganization", "Edit suborganization"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userportfoliopermission",
|
||||
name="additional_permissions",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[
|
||||
("view_all_domains", "View all domains and domain reports"),
|
||||
("view_managed_domains", "View managed domains"),
|
||||
("view_members", "View members"),
|
||||
("edit_members", "Create and edit members"),
|
||||
("view_all_requests", "View all requests"),
|
||||
("edit_requests", "Create and edit requests"),
|
||||
("view_portfolio", "View organization"),
|
||||
("edit_portfolio", "Edit organization"),
|
||||
("view_suborganization", "View suborganization"),
|
||||
("edit_suborganization", "Edit suborganization"),
|
||||
],
|
||||
max_length=50,
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more additional permissions.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
# Generated by Django 4.2.10 on 2024-08-29 23:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0124_alter_portfolioinvitation_portfolio_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Person listed under "your contact information" in the request form',
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="submitted_domain_requests_information",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="submitter",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Person listed under "your contact information" in the request form; will receive email updates',
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
related_name="submitted_domain_requests",
|
||||
to="registrar.contact",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,27 @@
|
|||
from django.db import migrations
|
||||
from django.db.models import Q
|
||||
from typing import Any
|
||||
|
||||
|
||||
# Deletes Contact objects associated with a submitter which we are deprecating
|
||||
def cascade_delete_submitter_contacts(apps, schema_editor) -> Any:
|
||||
contacts_model = apps.get_model("registrar", "Contact")
|
||||
submitter_contacts = contacts_model.objects.filter(
|
||||
Q(submitted_domain_requests__isnull=False) | Q(submitted_domain_requests_information__isnull=False)
|
||||
).filter(
|
||||
information_senior_official__isnull=True,
|
||||
senior_official__isnull=True,
|
||||
contact_domain_requests_information__isnull=True,
|
||||
contact_domain_requests__isnull=True,
|
||||
)
|
||||
submitter_contacts.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0125_alter_domaininformation_submitter_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(cascade_delete_submitter_contacts, reverse_code=migrations.RunPython.noop, atomic=True),
|
||||
]
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 4.2.10 on 2024-08-29 24:13
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0126_delete_cascade_submitter_contacts"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="domaininformation",
|
||||
name="submitter",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="domainrequest",
|
||||
name="submitter",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="creator",
|
||||
field=models.ForeignKey(
|
||||
help_text="Person who submitted the domain request. Will receive email updates.",
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="domain_requests_created",
|
||||
to=settings.AUTH_USER_MODEL,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -48,8 +48,7 @@ class DomainInformation(TimeStampedModel):
|
|||
null=True,
|
||||
)
|
||||
|
||||
# This is the domain request user who created this domain request. The contact
|
||||
# information that they gave is in the `submitter` field
|
||||
# This is the domain request user who created this domain request.
|
||||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
on_delete=models.PROTECT,
|
||||
|
@ -197,17 +196,6 @@ class DomainInformation(TimeStampedModel):
|
|||
related_name="domain_info",
|
||||
)
|
||||
|
||||
# This is the contact information provided by the domain requestor. The
|
||||
# user who created the domain request is in the `creator` field.
|
||||
submitter = models.ForeignKey(
|
||||
"registrar.Contact",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="submitted_domain_requests_information",
|
||||
on_delete=models.PROTECT,
|
||||
help_text='Person listed under "your contact information" in the request form',
|
||||
)
|
||||
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
|
|
@ -6,7 +6,6 @@ from django.conf import settings
|
|||
from django.db import models
|
||||
from django_fsm import FSMField, transition # type: ignore
|
||||
from django.utils import timezone
|
||||
from waffle import flag_is_active
|
||||
from registrar.models.domain import Domain
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||
|
@ -339,13 +338,12 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="The suborganization that this domain request is included under",
|
||||
)
|
||||
|
||||
# This is the domain request user who created this domain request. The contact
|
||||
# information that they gave is in the `submitter` field
|
||||
# This is the domain request user who created this domain request.
|
||||
creator = models.ForeignKey(
|
||||
"registrar.User",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="domain_requests_created",
|
||||
help_text="Person who submitted the domain request; will not receive email updates",
|
||||
help_text="Person who submitted the domain request. Will receive email updates.",
|
||||
)
|
||||
|
||||
investigator = models.ForeignKey(
|
||||
|
@ -483,17 +481,6 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Other domain names the creator provided for consideration",
|
||||
)
|
||||
|
||||
# This is the contact information provided by the domain requestor. The
|
||||
# user who created the domain request is in the `creator` field.
|
||||
submitter = models.ForeignKey(
|
||||
"registrar.Contact",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="submitted_domain_requests",
|
||||
on_delete=models.PROTECT,
|
||||
help_text='Person listed under "your contact information" in the request form; will receive email updates',
|
||||
)
|
||||
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
|
@ -741,9 +728,6 @@ class DomainRequest(TimeStampedModel):
|
|||
contact information. If there is not creator information, then do
|
||||
nothing.
|
||||
|
||||
If the waffle flag "profile_feature" is active, then this email will be sent to the
|
||||
domain request creator rather than the submitter
|
||||
|
||||
Optional args:
|
||||
bcc_address: str -> the address to bcc to
|
||||
|
||||
|
@ -758,7 +742,7 @@ class DomainRequest(TimeStampedModel):
|
|||
custom_email_content: str -> Renders an email with the content of this string as its body text.
|
||||
"""
|
||||
|
||||
recipient = self.creator if flag_is_active(None, "profile_feature") else self.submitter
|
||||
recipient = self.creator
|
||||
if recipient is None or recipient.email is None:
|
||||
logger.warning(
|
||||
f"Cannot send {new_status} email, no creator email address for domain request with pk: {self.pk}."
|
||||
|
@ -1182,6 +1166,10 @@ class DomainRequest(TimeStampedModel):
|
|||
# Special District -> "Election office" and "About your organization" page can't be empty
|
||||
return self.is_election_board is not None and self.about_your_organization is not None
|
||||
|
||||
# Do we still want to test this after creator is autogenerated? Currently it went back to being selectable
|
||||
def _is_creator_complete(self):
|
||||
return self.creator is not None
|
||||
|
||||
def _is_organization_name_and_address_complete(self):
|
||||
return not (
|
||||
self.organization_name is None
|
||||
|
@ -1200,9 +1188,6 @@ class DomainRequest(TimeStampedModel):
|
|||
def _is_purpose_complete(self):
|
||||
return self.purpose is not None
|
||||
|
||||
def _is_submitter_complete(self):
|
||||
return self.submitter is not None
|
||||
|
||||
def _has_other_contacts_and_filled(self):
|
||||
# Other Contacts Radio button is Yes and if all required fields are filled
|
||||
return (
|
||||
|
@ -1251,14 +1236,12 @@ class DomainRequest(TimeStampedModel):
|
|||
return self.is_policy_acknowledged is not None
|
||||
|
||||
def _is_general_form_complete(self, request):
|
||||
has_profile_feature_flag = flag_is_active(request, "profile_feature")
|
||||
return (
|
||||
self._is_organization_name_and_address_complete()
|
||||
self._is_creator_complete()
|
||||
and self._is_organization_name_and_address_complete()
|
||||
and self._is_senior_official_complete()
|
||||
and self._is_requested_domain_complete()
|
||||
and self._is_purpose_complete()
|
||||
# NOTE: This flag leaves submitter as empty (request wont submit) hence set to True
|
||||
and (self._is_submitter_complete() if not has_profile_feature_flag else True)
|
||||
and self._is_other_contacts_complete()
|
||||
and self._is_additional_details_complete()
|
||||
and self._is_policy_acknowledgement_complete()
|
||||
|
|
|
@ -131,6 +131,12 @@ class User(AbstractUser):
|
|||
else:
|
||||
return self.username
|
||||
|
||||
@classmethod
|
||||
def get_default_user(cls):
|
||||
"""Returns the default "system" user"""
|
||||
default_creator, _ = User.objects.get_or_create(username="System")
|
||||
return default_creator
|
||||
|
||||
def restrict_user(self):
|
||||
self.status = self.RESTRICTED
|
||||
self.save()
|
||||
|
@ -192,31 +198,25 @@ class User(AbstractUser):
|
|||
def has_edit_org_portfolio_permission(self, portfolio):
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||
|
||||
def has_domains_portfolio_permission(self, portfolio):
|
||||
def has_any_domains_portfolio_permission(self, portfolio):
|
||||
return self._has_portfolio_permission(
|
||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS
|
||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||
|
||||
def has_domain_requests_portfolio_permission(self, portfolio):
|
||||
# BEGIN
|
||||
# Note code below is to add organization_request feature
|
||||
def has_organization_requests_flag(self):
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
has_organization_requests_flag = flag_is_active(request, "organization_requests")
|
||||
if not has_organization_requests_flag:
|
||||
return False
|
||||
# END
|
||||
return self._has_portfolio_permission(
|
||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
||||
return flag_is_active(request, "organization_requests")
|
||||
|
||||
def has_organization_members_flag(self):
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
return flag_is_active(request, "organization_members")
|
||||
|
||||
def has_view_members_portfolio_permission(self, portfolio):
|
||||
# BEGIN
|
||||
# Note code below is to add organization_request feature
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
||||
if not has_organization_members_flag:
|
||||
if not self.has_organization_members_flag():
|
||||
return False
|
||||
# END
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||
|
@ -224,23 +224,37 @@ class User(AbstractUser):
|
|||
def has_edit_members_portfolio_permission(self, portfolio):
|
||||
# BEGIN
|
||||
# Note code below is to add organization_request feature
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
has_organization_members_flag = flag_is_active(request, "organization_members")
|
||||
if not has_organization_members_flag:
|
||||
if not self.has_organization_members_flag():
|
||||
return False
|
||||
# END
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
||||
|
||||
def has_view_all_domains_permission(self, portfolio):
|
||||
def has_view_all_domains_portfolio_permission(self, portfolio):
|
||||
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||
|
||||
def has_any_requests_portfolio_permission(self, portfolio):
|
||||
# BEGIN
|
||||
# Note code below is to add organization_request feature
|
||||
if not self.has_organization_requests_flag():
|
||||
return False
|
||||
# END
|
||||
return self._has_portfolio_permission(
|
||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
def has_view_all_requests_portfolio_permission(self, portfolio):
|
||||
"""Determines if the current user can view all available domain requests in a given portfolio"""
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
|
||||
def has_edit_request_portfolio_permission(self, portfolio):
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
# Field specific permission checks
|
||||
def has_view_suborganization(self, portfolio):
|
||||
def has_view_suborganization_portfolio_permission(self, portfolio):
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||
|
||||
def has_edit_suborganization(self, portfolio):
|
||||
def has_edit_suborganization_portfolio_permission(self, portfolio):
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||
|
||||
def get_first_portfolio(self):
|
||||
|
@ -249,36 +263,36 @@ class User(AbstractUser):
|
|||
return permission.portfolio
|
||||
return None
|
||||
|
||||
def has_edit_requests(self, portfolio):
|
||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
def portfolio_role_summary(self, portfolio):
|
||||
"""Returns a list of roles based on the user's permissions."""
|
||||
roles = []
|
||||
|
||||
# Define the conditions and their corresponding roles
|
||||
conditions_roles = [
|
||||
(self.has_edit_suborganization(portfolio), ["Admin"]),
|
||||
(self.has_edit_suborganization_portfolio_permission(portfolio), ["Admin"]),
|
||||
(
|
||||
self.has_view_all_domains_permission(portfolio)
|
||||
and self.has_domain_requests_portfolio_permission(portfolio)
|
||||
and self.has_edit_requests(portfolio),
|
||||
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and self.has_any_requests_portfolio_permission(portfolio)
|
||||
and self.has_edit_request_portfolio_permission(portfolio),
|
||||
["View-only admin", "Domain requestor"],
|
||||
),
|
||||
(
|
||||
self.has_view_all_domains_permission(portfolio)
|
||||
and self.has_domain_requests_portfolio_permission(portfolio),
|
||||
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and self.has_any_requests_portfolio_permission(portfolio),
|
||||
["View-only admin"],
|
||||
),
|
||||
(
|
||||
self.has_base_portfolio_permission(portfolio)
|
||||
and self.has_edit_requests(portfolio)
|
||||
and self.has_domains_portfolio_permission(portfolio),
|
||||
and self.has_edit_request_portfolio_permission(portfolio)
|
||||
and self.has_any_domains_portfolio_permission(portfolio),
|
||||
["Domain requestor", "Domain manager"],
|
||||
),
|
||||
(self.has_base_portfolio_permission(portfolio) and self.has_edit_requests(portfolio), ["Domain requestor"]),
|
||||
(
|
||||
self.has_base_portfolio_permission(portfolio) and self.has_domains_portfolio_permission(portfolio),
|
||||
self.has_base_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio),
|
||||
["Domain requestor"],
|
||||
),
|
||||
(
|
||||
self.has_base_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio),
|
||||
["Domain manager"],
|
||||
),
|
||||
(self.has_base_portfolio_permission(portfolio), ["Member"]),
|
||||
|
@ -292,6 +306,9 @@ class User(AbstractUser):
|
|||
|
||||
return roles
|
||||
|
||||
def get_portfolios(self):
|
||||
return self.portfolio_permissions.all()
|
||||
|
||||
@classmethod
|
||||
def needs_identity_verification(cls, email, uuid):
|
||||
"""A method used by our oidc classes to test whether a user needs email/uuid verification
|
||||
|
@ -437,8 +454,6 @@ class User(AbstractUser):
|
|||
self.check_domain_invitations_on_login()
|
||||
self.check_portfolio_invitations_on_login()
|
||||
|
||||
# NOTE TO DAVE: I'd simply suggest that we move these functions outside of the user object,
|
||||
# and move them to some sort of utility file. That way we aren't calling request inside here.
|
||||
def is_org_user(self, request):
|
||||
has_organization_feature_flag = flag_is_active(request, "organization_feature")
|
||||
portfolio = request.session.get("portfolio")
|
||||
|
@ -447,7 +462,7 @@ class User(AbstractUser):
|
|||
def get_user_domain_ids(self, request):
|
||||
"""Returns either the domains ids associated with this user on UserDomainRole or Portfolio"""
|
||||
portfolio = request.session.get("portfolio")
|
||||
if self.is_org_user(request) and self.has_view_all_domains_permission(portfolio):
|
||||
if self.is_org_user(request) and self.has_view_all_domains_portfolio_permission(portfolio):
|
||||
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||
else:
|
||||
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
||||
|
|
|
@ -21,7 +21,6 @@ class UserPortfolioPermissionChoices(models.TextChoices):
|
|||
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||
|
||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
||||
EDIT_REQUESTS = "edit_requests", "Create and edit requests"
|
||||
|
||||
VIEW_PORTFOLIO = "view_portfolio", "View organization"
|
||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
|||
from urllib.parse import parse_qs
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from registrar.models.user import User
|
||||
from registrar.models import User
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
from registrar.models.utility.generic_helper import replace_url_queryparams
|
||||
|
@ -144,25 +144,30 @@ class CheckPortfolioMiddleware:
|
|||
if not request.user.is_authenticated:
|
||||
return None
|
||||
|
||||
# set the portfolio in the session if it is not set
|
||||
if "portfolio" not in request.session or request.session["portfolio"] is None:
|
||||
# if multiple portfolios are allowed for this user
|
||||
if flag_is_active(request, "multiple_portfolios"):
|
||||
# NOTE: we will want to change later to have a workflow for selecting
|
||||
# portfolio and another for switching portfolio; for now, select first
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
elif flag_is_active(request, "organization_feature"):
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
else:
|
||||
request.session["portfolio"] = None
|
||||
# if multiple portfolios are allowed for this user
|
||||
if flag_is_active(request, "organization_feature"):
|
||||
self.set_portfolio_in_session(request)
|
||||
elif request.session.get("portfolio"):
|
||||
# Edge case: User disables flag while already logged in
|
||||
request.session["portfolio"] = None
|
||||
elif "portfolio" not in request.session:
|
||||
# Set the portfolio in the session if its not already in it
|
||||
request.session["portfolio"] = None
|
||||
|
||||
if request.session["portfolio"] is not None and current_path == self.home:
|
||||
if request.user.is_org_user(request):
|
||||
if request.user.has_domains_portfolio_permission(request.session["portfolio"]):
|
||||
if request.user.is_org_user(request):
|
||||
if current_path == self.home:
|
||||
if request.user.has_any_domains_portfolio_permission(request.session["portfolio"]):
|
||||
portfolio_redirect = reverse("domains")
|
||||
else:
|
||||
portfolio_redirect = reverse("no-portfolio-domains")
|
||||
|
||||
return HttpResponseRedirect(portfolio_redirect)
|
||||
|
||||
return None
|
||||
|
||||
def set_portfolio_in_session(self, request):
|
||||
# NOTE: we will want to change later to have a workflow for selecting
|
||||
# portfolio and another for switching portfolio; for now, select first
|
||||
if flag_is_active(request, "multiple_portfolios"):
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
else:
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
|
|
|
@ -70,7 +70,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
<div class="readonly textarea-wrapper">
|
||||
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
||||
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||
<strong>Sent to {% if has_profile_feature_flag %}creator{%else%}submitter{%endif%}</strong>
|
||||
<strong>Sent to creator</strong>
|
||||
</label>
|
||||
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||
{{ original_object.action_needed_reason_email }}
|
||||
|
@ -107,7 +107,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endif %}
|
||||
{% elif field.field.name == "requested_domain" %}
|
||||
{% with current_path=request.get_full_path %}
|
||||
<a class="margin-top-05 padding-top-05" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
||||
<a class="margin-top-05 padding-top-05" id="id_requested_domain" href="{% url 'admin:registrar_draftdomain_change' original.requested_domain.id %}?{{ 'return_path='|add:current_path }}">{{ original.requested_domain }}</a>
|
||||
{% endwith%}
|
||||
{% elif field.field.name == "current_websites" %}
|
||||
{% comment %}
|
||||
|
@ -287,11 +287,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% if not skip_additional_contact_info %}
|
||||
{% include "django/admin/includes/user_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly %}
|
||||
{% endif%}
|
||||
{% elif field.field.name == "submitter" %}
|
||||
<div class="flex-container tablet:margin-top-2">
|
||||
<label aria-label="Submitter contact details"></label>
|
||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.submitter no_title_top_padding=field.is_readonly %}
|
||||
</div>
|
||||
{% elif field.field.name == "senior_official" %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Senior official contact details"></label>
|
||||
|
|
|
@ -17,6 +17,26 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
{% if portfolios %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Portfolio information</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Portfolios</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for portfolio in portfolios %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
|
||||
{{ portfolio }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated requests and domains</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
|
|
|
@ -72,9 +72,9 @@
|
|||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio and has_domains_portfolio_permission and has_view_suborganization %}
|
||||
{% if portfolio and has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_suborganization_portfolio_permission %}
|
||||
{% else %}
|
||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Organization name and mailing address' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||
|
|
|
@ -63,7 +63,7 @@
|
|||
|
||||
<div class="grid-row margin-top-1">
|
||||
<div class="grid-col">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon float-right-tablet delete-record text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
{% endwith %}
|
||||
</div>
|
||||
<div class="tablet:grid-col-2">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075">
|
||||
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon delete-record margin-bottom-075 text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg>Delete
|
||||
|
|
|
@ -16,6 +16,26 @@
|
|||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||
</svg><span class="margin-left-05">Previous step</span>
|
||||
</a>
|
||||
{% comment %}
|
||||
TODO: uncomment in #2596
|
||||
{% else %}
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url_2 %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{{ url_2 }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
{% if requested_domain__name %}
|
||||
<span>{{ requested_domain__name }}</span>
|
||||
{% else %}
|
||||
<span>Start a new domain request</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %} {% endcomment %}
|
||||
{% endif %}
|
||||
|
||||
{% block form_messages %}
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
||||
</legend>
|
||||
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2">
|
||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2 text-secondary line-height-sans-5">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||
</svg><span class="margin-left-05">Delete</span>
|
||||
|
|
|
@ -130,8 +130,8 @@
|
|||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.submitter is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.submitter %}
|
||||
{% if domain_request.creator is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.creator %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
|
|
|
@ -8,15 +8,33 @@
|
|||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
<a href="{% url 'home' %}" class="breadcrumb__back">
|
||||
{% comment %}
|
||||
TODO: Uncomment in #2596
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span
|
||||
>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% else %}{% endcomment %}
|
||||
{% url 'home' as url %}
|
||||
<a href="{{ url }}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#arrow_back"></use>
|
||||
</svg>
|
||||
|
||||
<p class="margin-left-05 margin-top-0 margin-bottom-0 line-height-sans-1">
|
||||
Back to manage your domains
|
||||
Back to manage your domains
|
||||
</p>
|
||||
</a>
|
||||
{% comment %} {% endif %}{% endcomment %}
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
|
@ -109,8 +127,8 @@
|
|||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.submitter and not has_profile_feature_flag %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.submitter contact='true' heading_level=heading_level %}
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
|
||||
{% if portfolio %}
|
||||
{% comment %} Only show this menu option if the user has the perms to do so {% endcomment %}
|
||||
{% if has_domains_portfolio_permission and has_view_suborganization %}
|
||||
{% if has_any_domains_portfolio_permission and has_view_suborganization_portfolio_permission %}
|
||||
{% with url_name="domain-suborganization" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Suborganization" %}
|
||||
{% endwith %}
|
||||
|
@ -72,13 +72,6 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if not has_profile_feature_flag %}
|
||||
{# Conditionally display profile link in main nav #}
|
||||
{% with url_name="domain-your-contact-information" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Your contact information" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% with url_name="domain-security-email" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Security email" %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
If you believe there is an error please contact <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
|
||||
{% if has_domains_portfolio_permission and has_edit_suborganization %}
|
||||
{% if has_any_domains_portfolio_permission and has_edit_suborganization_portfolio_permission %}
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
{% input_with_errors form.sub_organization %}
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
{% extends "domain_base.html" %}
|
||||
{% load static field_helpers %}
|
||||
|
||||
{% block title %}Your contact information | {{ domain.name }} | {% endblock %}
|
||||
|
||||
{% block domain_content %}
|
||||
{% include "includes/form_errors.html" with form=form %}
|
||||
|
||||
<h1>Your contact information</h1>
|
||||
|
||||
<p>If you’d like us to use a different name, email, or phone number you can make those changes below. <strong>Updating your contact information here will update the contact information for all domains in your account.</strong> Changing your information here won’t affect your Login.gov account information. The contact information you provide here won’t be made public and will only be used for the .gov program.
|
||||
</p>
|
||||
|
||||
{% include "includes/required_fields.html" %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate id="form-container">
|
||||
{% csrf_token %}
|
||||
|
||||
{% input_with_errors form.first_name %}
|
||||
|
||||
{% input_with_errors form.middle_name %}
|
||||
|
||||
{% input_with_errors form.last_name %}
|
||||
|
||||
{% input_with_errors form.title %}
|
||||
|
||||
{% input_with_errors form.email %}
|
||||
|
||||
{% input_with_errors form.phone %}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
>Save</button>
|
||||
</form>
|
||||
|
||||
{% endblock %} {# domain_content #}
|
|
@ -5,10 +5,13 @@
|
|||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||
<div class="grid-row">
|
||||
{% if not has_domain_requests_portfolio_permission %}
|
||||
{% if not portfolio %}
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
||||
|
@ -45,7 +48,10 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th data-sortable="requested_domain__name" scope="col" role="columnheader">Domain name</th>
|
||||
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Date submitted</th>
|
||||
<th data-sortable="last_submitted_date" scope="col" role="columnheader">Submitted</th>
|
||||
{% if portfolio %}
|
||||
<th data-sortable="creator" scope="col" role="columnheader">Created by</th>
|
||||
{% endif %}
|
||||
<th data-sortable="status" scope="col" role="columnheader">Status</th>
|
||||
<th scope="col" role="columnheader"><span class="usa-sr-only">Action</span></th>
|
||||
<!-- AJAX will conditionally add a th for delete actions -->
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||
<span class="display-none" id="no-portfolio-js-flag"></span>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
|
@ -157,7 +156,7 @@
|
|||
<th data-sortable="name" scope="col" role="columnheader">Domain name</th>
|
||||
<th data-sortable="expiration_date" scope="col" role="columnheader">Expires</th>
|
||||
<th data-sortable="state_display" scope="col" role="columnheader">Status</th>
|
||||
{% if portfolio and has_view_suborganization %}
|
||||
{% if portfolio and has_view_suborganization_portfolio_permission %}
|
||||
<th data-sortable="domain_info__sub_organization" scope="col" role="columnheader">Suborganization</th>
|
||||
{% endif %}
|
||||
<th
|
||||
|
|
|
@ -37,9 +37,9 @@
|
|||
</div>
|
||||
<ul class="usa-nav__primary usa-accordion">
|
||||
<li class="usa-nav__primary-item">
|
||||
{% if has_domains_portfolio_permission %}
|
||||
{% if has_any_domains_portfolio_permission %}
|
||||
{% url 'domains' as url %}
|
||||
{%else %}
|
||||
{% else %}
|
||||
{% url 'no-portfolio-domains' as url %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'domain'|in_path:request.path %} usa-current{% endif %}">
|
||||
|
@ -51,22 +51,56 @@
|
|||
Domain groups
|
||||
</a>
|
||||
</li> -->
|
||||
|
||||
{% if has_domain_requests_portfolio_permission %}
|
||||
<li class="usa-nav__primary-item">
|
||||
|
||||
{% if has_organization_requests_flag %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<!-- user has one of the view permissions plus the edit permission, show the dropdown -->
|
||||
{% if has_edit_request_portfolio_permission %}
|
||||
{% url 'domain-requests' as url %}
|
||||
<button
|
||||
type="button"
|
||||
class="usa-accordion__button usa-nav__link{% if 'request'|in_path:request.path %} usa-current{% endif %}"
|
||||
aria-expanded="false"
|
||||
aria-controls="basic-nav-section-two"
|
||||
>
|
||||
<span>Domain requests</span>
|
||||
</button>
|
||||
<ul id="basic-nav-section-two" class="usa-nav__submenu">
|
||||
<li class="usa-nav__submenu-item">
|
||||
<a href="{{ url }}"
|
||||
><span>Domain requests</span></a
|
||||
>
|
||||
</li>
|
||||
<li class="usa-nav__submenu-item">
|
||||
<a href="{% url 'domain-request:' %}"
|
||||
><span>Start a new domain request</span></a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- user has view but no edit permissions -->
|
||||
{% elif has_any_requests_portfolio_permission %}
|
||||
{% url 'domain-requests' as url %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||
Domain requests
|
||||
</a>
|
||||
<!-- user does not have permissions -->
|
||||
{% else %}
|
||||
{% url 'no-portfolio-requests' as url %}
|
||||
<a href="{{ url }}" class="usa-nav-link{% if 'request'|in_path:request.path %} usa-current{% endif %}">
|
||||
Domain requests
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if has_view_members_portfolio_permission %}
|
||||
|
||||
{% if has_organization_members_flag %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="#" class="usa-nav-link">
|
||||
Members
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="usa-nav__primary-item">
|
||||
{% url 'organization' as url %}
|
||||
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
||||
|
|
30
src/registrar/templates/portfolio_no_requests.html
Normal file
30
src/registrar/templates/portfolio_no_requests.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{% extends 'portfolio_base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Domain Requests | {% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
<h1 id="domains-header">Current domain requests</h1>
|
||||
<section class="section-outlined">
|
||||
<div class="section-outlined__header margin-bottom-3">
|
||||
<h2 id="domains-header" class="display-inline-block">You don’t have access to domain requests.</h2>
|
||||
{% if portfolio_administrators %}
|
||||
<p>If you believe you should have access to a request, reach out to your organization’s administrators.</p>
|
||||
<p>Your organizations administrators:</p>
|
||||
<ul class="margin-top-0">
|
||||
{% for administrator in portfolio_administrators %}
|
||||
{% if administrator.email %}
|
||||
<li>{{ administrator.email }}</li>
|
||||
{% else %}
|
||||
<li>{{ administrator }}</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p><strong>No administrators were found on your organization.</strong></p>
|
||||
<p>If you believe you should have access to a request, email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
|
@ -11,18 +11,27 @@
|
|||
{% block portfolio_content %}
|
||||
<div id="main-content">
|
||||
<h1 id="domain-requests-header">Domain requests</h1>
|
||||
|
||||
{% comment %}
|
||||
IMPORTANT:
|
||||
If this button is added on any other page, make sure to update the
|
||||
relevant view to reset request.session["new_request"] = True
|
||||
{% endcomment %}
|
||||
<p class="margin-top-4">
|
||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||
>
|
||||
Start a new domain request
|
||||
</a>
|
||||
</p>
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
</div>
|
||||
{% if has_edit_request_portfolio_permission %}
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
{% comment %}
|
||||
IMPORTANT:
|
||||
If this button is added on any other page, make sure to update the
|
||||
relevant view to reset request.session["new_request"] = True
|
||||
{% endcomment %}
|
||||
<p class="float-right-tablet tablet:margin-y-0">
|
||||
<a href="{% url 'domain-request:' %}" class="usa-button"
|
||||
>
|
||||
Start a new domain request
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||
</div>
|
||||
|
|
|
@ -394,7 +394,6 @@ class AuditedAdminMockData:
|
|||
about_your_organization: str = "e-Government",
|
||||
anything_else: str = "There is more",
|
||||
senior_official: Contact = self.dummy_contact(item_name, "senior_official"),
|
||||
submitter: Contact = self.dummy_contact(item_name, "submitter"),
|
||||
creator: User = self.dummy_user(item_name, "creator"),
|
||||
}
|
||||
""" # noqa
|
||||
|
@ -412,7 +411,6 @@ class AuditedAdminMockData:
|
|||
about_your_organization="e-Government",
|
||||
anything_else="There is more",
|
||||
senior_official=self.dummy_contact(item_name, "senior_official"),
|
||||
submitter=self.dummy_contact(item_name, "submitter"),
|
||||
creator=creator,
|
||||
)
|
||||
return common_args
|
||||
|
@ -901,7 +899,6 @@ def completed_domain_request( # noqa
|
|||
has_cisa_representative=True,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=False,
|
||||
submitter=False,
|
||||
name="city.gov",
|
||||
investigator=None,
|
||||
generic_org_type="federal",
|
||||
|
@ -911,6 +908,7 @@ def completed_domain_request( # noqa
|
|||
federal_type=None,
|
||||
action_needed_reason=None,
|
||||
portfolio=None,
|
||||
organization_name=None,
|
||||
):
|
||||
"""A completed domain request."""
|
||||
if not user:
|
||||
|
@ -925,14 +923,6 @@ def completed_domain_request( # noqa
|
|||
domain, _ = DraftDomain.objects.get_or_create(name=name)
|
||||
alt, _ = Website.objects.get_or_create(website="city1.gov")
|
||||
current, _ = Website.objects.get_or_create(website="city.com")
|
||||
if not submitter:
|
||||
submitter, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy2",
|
||||
last_name="Tester2",
|
||||
title="Admin Tester",
|
||||
email="mayor@igorville.gov",
|
||||
phone="(555) 555 5556",
|
||||
)
|
||||
other, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
|
@ -954,14 +944,13 @@ def completed_domain_request( # noqa
|
|||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
organization_name=organization_name if organization_name else "Testorg",
|
||||
address_line1="address 1",
|
||||
address_line2="address 2",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
requested_domain=domain,
|
||||
submitter=submitter,
|
||||
creator=user,
|
||||
status=status,
|
||||
investigator=investigator,
|
||||
|
|
|
@ -2,6 +2,7 @@ from datetime import datetime
|
|||
from django.utils import timezone
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.contrib.admin.sites import AdminSite
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from django.urls import reverse
|
||||
from registrar.admin import (
|
||||
|
@ -41,13 +42,12 @@ from registrar.models import (
|
|||
TransitionDomain,
|
||||
Portfolio,
|
||||
Suborganization,
|
||||
UserPortfolioPermission,
|
||||
UserDomainRole,
|
||||
SeniorOfficial,
|
||||
PortfolioInvitation,
|
||||
VerifiedByStaff,
|
||||
)
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.senior_official import SeniorOfficial
|
||||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||
from .common import (
|
||||
MockDbForSharedTests,
|
||||
AuditedAdminMockData,
|
||||
|
@ -60,10 +60,12 @@ from .common import (
|
|||
multiple_unalphabetical_domain_objects,
|
||||
GenericTestHelper,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from unittest.mock import ANY, patch, Mock
|
||||
from django_webtest import WebTest # type: ignore
|
||||
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -383,7 +385,7 @@ class TestDomainInformationAdmin(TestCase):
|
|||
|
||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||
domain_request = completed_domain_request(
|
||||
submitter=contact, name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
name="city1244.gov", status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
)
|
||||
domain_request.approve()
|
||||
|
||||
|
@ -508,7 +510,6 @@ class TestDomainInformationAdmin(TestCase):
|
|||
# These should exist in the response
|
||||
expected_values = [
|
||||
("creator", "Person who submitted the domain request"),
|
||||
("submitter", 'Person listed under "your contact information" in the request form'),
|
||||
("domain_request", "Request associated with this domain"),
|
||||
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||
("urbanization", "Required for Puerto Rico only"),
|
||||
|
@ -632,16 +633,6 @@ class TestDomainInformationAdmin(TestCase):
|
|||
# Check for the field itself
|
||||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the submitter == #
|
||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
||||
expected_submitter_fields = [
|
||||
# Field, expected value
|
||||
("title", "Admin Tester"),
|
||||
("phone", "(555) 555 5556"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
|
||||
# == Check for the senior_official == #
|
||||
self.assertContains(response, "testy@town.com", count=2)
|
||||
expected_so_fields = [
|
||||
|
@ -663,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
|
|||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||
|
||||
# Test for the copy link
|
||||
self.assertContains(response, "button--clipboard", count=4)
|
||||
self.assertContains(response, "button--clipboard", count=3)
|
||||
|
||||
# cleanup this test
|
||||
domain_info.delete()
|
||||
|
@ -687,7 +678,6 @@ class TestDomainInformationAdmin(TestCase):
|
|||
"more_organization_information",
|
||||
"domain",
|
||||
"domain_request",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
|
@ -706,19 +696,19 @@ class TestDomainInformationAdmin(TestCase):
|
|||
# Assert that sorting in reverse works correctly
|
||||
self.test_helper.assert_table_sorted("-1", ("-domain__name",))
|
||||
|
||||
def test_submitter_sortable(self):
|
||||
"""Tests if DomainInformation sorts by submitter correctly"""
|
||||
def test_creator_sortable(self):
|
||||
"""Tests if DomainInformation sorts by creator correctly"""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Assert that our sort works correctly
|
||||
self.test_helper.assert_table_sorted(
|
||||
"4",
|
||||
("submitter__first_name", "submitter__last_name"),
|
||||
("creator__first_name", "creator__last_name"),
|
||||
)
|
||||
|
||||
# Assert that sorting in reverse works correctly
|
||||
self.test_helper.assert_table_sorted("-4", ("-submitter__first_name", "-submitter__last_name"))
|
||||
self.test_helper.assert_table_sorted("-4", ("-creator__first_name", "-creator__last_name"))
|
||||
|
||||
|
||||
class TestUserDomainRoleAdmin(TestCase):
|
||||
|
@ -973,7 +963,7 @@ class TestListHeaderAdmin(TestCase):
|
|||
)
|
||||
|
||||
|
||||
class TestMyUserAdmin(MockDbForSharedTests):
|
||||
class TestMyUserAdmin(MockDbForSharedTests, WebTest):
|
||||
"""Tests for the MyUserAdmin class as super or staff user
|
||||
|
||||
Notes:
|
||||
|
@ -993,6 +983,7 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
|||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.app.set_user(self.superuser.username)
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
|
||||
def tearDown(self):
|
||||
|
@ -1227,6 +1218,20 @@ class TestMyUserAdmin(MockDbForSharedTests):
|
|||
self.assertNotContains(response, "Portfolio roles:")
|
||||
self.assertNotContains(response, "Portfolio additional permissions:")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_user_can_see_related_portfolios(self):
|
||||
"""Tests if a user can see the portfolios they are associated with on the user page"""
|
||||
portfolio, _ = Portfolio.objects.get_or_create(organization_name="test", creator=self.superuser)
|
||||
permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.superuser, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
response = self.app.get(reverse("admin:registrar_user_change", args=[self.superuser.pk]))
|
||||
expected_href = reverse("admin:registrar_portfolio_change", args=[portfolio.pk])
|
||||
self.assertContains(response, expected_href)
|
||||
self.assertContains(response, str(portfolio))
|
||||
permission.delete()
|
||||
portfolio.delete()
|
||||
|
||||
|
||||
class AuditedAdminTest(TestCase):
|
||||
|
||||
|
@ -1301,7 +1306,6 @@ class AuditedAdminTest(TestCase):
|
|||
# Senior offical is commented out for now - this is alphabetized
|
||||
# and this test does not accurately reflect that.
|
||||
# DomainRequest.senior_official.field,
|
||||
DomainRequest.submitter.field,
|
||||
# DomainRequest.investigator.field,
|
||||
DomainRequest.creator.field,
|
||||
DomainRequest.requested_domain.field,
|
||||
|
@ -1361,7 +1365,6 @@ class AuditedAdminTest(TestCase):
|
|||
# Senior offical is commented out for now - this is alphabetized
|
||||
# and this test does not accurately reflect that.
|
||||
# DomainInformation.senior_official.field,
|
||||
DomainInformation.submitter.field,
|
||||
# DomainInformation.creator.field,
|
||||
(DomainInformation.domain.field, ["name"]),
|
||||
(DomainInformation.domain_request.field, ["requested_domain__name"]),
|
||||
|
@ -1670,91 +1673,6 @@ class TestContactAdmin(TestCase):
|
|||
|
||||
self.assertEqual(readonly_fields, expected_fields)
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_less(self):
|
||||
"""Create a contact, join it to 4 domain requests.
|
||||
Assert that the warning on the contact form lists 4 joins."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Create an instance of the model
|
||||
contact, _ = Contact.objects.get_or_create(
|
||||
first_name="Henry",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
|
||||
# join it to 4 domain requests.
|
||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
||||
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 5th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
||||
"</ul>",
|
||||
)
|
||||
|
||||
# cleanup this test
|
||||
DomainRequest.objects.all().delete()
|
||||
contact.delete()
|
||||
|
||||
def test_change_view_for_joined_contact_five_or_more(self):
|
||||
"""Create a contact, join it to 6 domain requests.
|
||||
Assert that the warning on the contact form lists 5 joins and a '1 more' ellispsis."""
|
||||
with less_console_noise():
|
||||
self.client.force_login(self.superuser)
|
||||
# Create an instance of the model
|
||||
# join it to 6 domain requests.
|
||||
contact, _ = Contact.objects.get_or_create(
|
||||
first_name="Henry",
|
||||
last_name="McFakerson",
|
||||
)
|
||||
domain_request1 = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request2 = completed_domain_request(submitter=contact, name="city2.gov")
|
||||
domain_request3 = completed_domain_request(submitter=contact, name="city3.gov")
|
||||
domain_request4 = completed_domain_request(submitter=contact, name="city4.gov")
|
||||
domain_request5 = completed_domain_request(submitter=contact, name="city5.gov")
|
||||
completed_domain_request(submitter=contact, name="city6.gov")
|
||||
with patch("django.contrib.messages.warning") as mock_warning:
|
||||
# Use the test client to simulate the request
|
||||
response = self.client.get(reverse("admin:registrar_contact_change", args=[contact.pk]))
|
||||
logger.debug(mock_warning)
|
||||
# Assert that the error message was called with the correct argument
|
||||
# Note: The 6th join will be a user.
|
||||
mock_warning.assert_called_once_with(
|
||||
response.wsgi_request,
|
||||
"<ul class='messagelist_content-list--unstyled'>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request1.pk}/change/'>city1.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request2.pk}/change/'>city2.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request3.pk}/change/'>city3.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request4.pk}/change/'>city4.gov</a></li>"
|
||||
"<li>Joined to DomainRequest: <a href='/admin/registrar/"
|
||||
f"domainrequest/{domain_request5.pk}/change/'>city5.gov</a></li>"
|
||||
"</ul>"
|
||||
"<p class='font-sans-3xs'>And 1 more...</p>",
|
||||
)
|
||||
# cleanup this test
|
||||
DomainRequest.objects.all().delete()
|
||||
contact.delete()
|
||||
|
||||
|
||||
class TestVerifiedByStaffAdmin(TestCase):
|
||||
|
||||
|
|
|
@ -427,13 +427,6 @@ class TestDomainAdminWithClient(TestCase):
|
|||
# Check for the field itself
|
||||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the submitter == #
|
||||
self.assertContains(response, "mayor@igorville.gov")
|
||||
|
||||
self.assertContains(response, "Admin Tester")
|
||||
self.assertContains(response, "(555) 555 5556")
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
|
||||
# == Check for the senior_official == #
|
||||
self.assertContains(response, "testy@town.com")
|
||||
self.assertContains(response, "Chief Tester")
|
||||
|
|
|
@ -37,6 +37,7 @@ from .common import (
|
|||
GenericTestHelper,
|
||||
)
|
||||
from unittest.mock import patch
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from django.conf import settings
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -100,7 +101,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
SeniorOfficial.objects.get_or_create(first_name="Zoup", last_name="Soup", title="title")
|
||||
|
||||
contact, _ = Contact.objects.get_or_create(first_name="Henry", last_name="McFakerson")
|
||||
domain_request = completed_domain_request(submitter=contact, name="city1.gov")
|
||||
domain_request = completed_domain_request(name="city1.gov")
|
||||
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(domain_request.pk))
|
||||
model_admin = AuditedAdmin(DomainRequest, self.site)
|
||||
|
||||
|
@ -155,11 +156,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
|
||||
# These should exist in the response
|
||||
expected_values = [
|
||||
("creator", "Person who submitted the domain request; will not receive email updates"),
|
||||
(
|
||||
"submitter",
|
||||
'Person listed under "your contact information" in the request form; will receive email updates',
|
||||
),
|
||||
("creator", "Person who submitted the domain request. Will receive email updates"),
|
||||
("approved_domain", "Domain associated with this request; will be blank until request is approved"),
|
||||
("no_other_contacts_rationale", "Required if creator does not list other employees"),
|
||||
("alternative_domains", "Other domain names the creator provided for consideration"),
|
||||
|
@ -442,8 +439,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.test_helper.assert_table_sorted("-1", ("-requested_domain__name",))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_submitter_sortable(self):
|
||||
"""Tests if the DomainRequest sorts by submitter correctly"""
|
||||
def test_creator_sortable(self):
|
||||
"""Tests if the DomainRequest sorts by creator correctly"""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
multiple_unalphabetical_domain_objects("domain_request")
|
||||
|
@ -457,8 +454,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.test_helper.assert_table_sorted(
|
||||
"13",
|
||||
(
|
||||
"submitter__first_name",
|
||||
"submitter__last_name",
|
||||
"creator__first_name",
|
||||
"creator__last_name",
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -466,8 +463,8 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.test_helper.assert_table_sorted(
|
||||
"-13",
|
||||
(
|
||||
"-submitter__first_name",
|
||||
"-submitter__last_name",
|
||||
"-creator__first_name",
|
||||
"-creator__last_name",
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -665,15 +662,24 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
def test_action_needed_sends_reason_email_prod_bcc(self):
|
||||
"""When an action needed reason is set, an email is sent out and help@get.gov
|
||||
is BCC'd in production"""
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
# Create fake creator
|
||||
EMAIL = "meoward.jones@igorville.gov"
|
||||
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email=EMAIL,
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
in_review = DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
action_needed = DomainRequest.DomainRequestStatus.ACTION_NEEDED
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=in_review)
|
||||
domain_request = completed_domain_request(status=in_review, user=_creator)
|
||||
|
||||
# Test the email sent out for already_has_domains
|
||||
already_has_domains = DomainRequest.ActionNeededReasons.ALREADY_HAS_DOMAINS
|
||||
|
@ -702,7 +708,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
questionable_so = DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL
|
||||
self.transition_state_and_send_email(domain_request, action_needed, action_needed_reason=questionable_so)
|
||||
self.assert_email_is_accurate(
|
||||
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, EMAIL, bcc_email_address=BCC_EMAIL
|
||||
"SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", 3, _creator.email, bcc_email_address=BCC_EMAIL
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 4)
|
||||
|
||||
|
@ -723,7 +729,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
)
|
||||
|
||||
domain_request.refresh_from_db()
|
||||
self.assert_email_is_accurate("custom email content", 4, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assert_email_is_accurate("custom email content", 4, _creator.email, bcc_email_address=BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 5)
|
||||
|
||||
# Tests if a new email gets sent when just the email is changed.
|
||||
|
@ -747,7 +753,9 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
action_needed_reason=eligibility_unclear,
|
||||
action_needed_reason_email="custom content when starting anew",
|
||||
)
|
||||
self.assert_email_is_accurate("custom content when starting anew", 5, EMAIL, bcc_email_address=BCC_EMAIL)
|
||||
self.assert_email_is_accurate(
|
||||
"custom content when starting anew", 5, _creator.email, bcc_email_address=BCC_EMAIL
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 6)
|
||||
|
||||
# def test_action_needed_sends_reason_email_prod_bcc(self):
|
||||
|
@ -811,22 +819,31 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
Also test that the default email set in settings is NOT BCCd on non-prod whenever
|
||||
an email does go out."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
EMAIL = "meoward.jones@igorville.gov"
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email=EMAIL,
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Test Submitted Status from started
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, True)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, True)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||
self.assert_email_is_accurate(
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL, True
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email, True
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
|
@ -885,30 +902,37 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
Also test that the default email set in settings IS BCCd on prod whenever
|
||||
an email does go out."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
BCC_EMAIL = settings.DEFAULT_FROM_EMAIL
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request()
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Test Submitted Status from started
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||
self.assert_email_is_accurate(
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, EMAIL
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 1, _creator.email
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
# Test Submitted Status Again (from withdrawn)
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, EMAIL, False, BCC_EMAIL)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 0, _creator.email, False, BCC_EMAIL)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
# Move it to IN_REVIEW
|
||||
|
@ -933,21 +957,29 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
@override_flag("profile_feature", True)
|
||||
@less_console_noise_decorator
|
||||
def test_save_model_sends_approved_email(self):
|
||||
"""When transitioning to approved on a domain request,
|
||||
an email is sent out every time."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Test Submitted Status
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 0, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
|
@ -956,7 +988,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.RejectionReasons.DOMAIN_PURPOSE,
|
||||
)
|
||||
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Your .gov domain request has been rejected.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
# Test Submitted Status Again (No new email should be sent)
|
||||
|
@ -968,12 +1000,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is domain purpose."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason DOMAIN_PURPOSE and test email
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -984,13 +1023,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.assert_email_is_accurate(
|
||||
"Your domain request was rejected because the purpose you provided did not meet our \nrequirements.",
|
||||
0,
|
||||
EMAIL,
|
||||
_creator.email,
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -998,12 +1037,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is requestor."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason REQUESTOR and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1013,13 +1059,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"Your domain request was rejected because we don’t believe you’re eligible to request a \n.gov "
|
||||
"domain on behalf of Testorg",
|
||||
0,
|
||||
EMAIL,
|
||||
_creator.email,
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1027,12 +1073,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is second domain."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason SECOND_DOMAIN_REASONING and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1040,25 +1093,35 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.RejectionReasons.SECOND_DOMAIN_REASONING,
|
||||
)
|
||||
self.assert_email_is_accurate("Your domain request was rejected because Testorg has a .gov domain.", 0, EMAIL)
|
||||
self.assert_email_is_accurate(
|
||||
"Your domain request was rejected because Testorg has a .gov domain.", 0, _creator.email
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_save_model_sends_rejected_email_contacts_or_org_legitimacy(self):
|
||||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is contacts or org legitimacy."""
|
||||
# Create fake creator
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
EMAIL = "meoward.jones@igorville.gov"
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email=EMAIL,
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason CONTACTS_OR_ORGANIZATION_LEGITIMACY and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1070,13 +1133,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"Your domain request was rejected because we could not verify the organizational \n"
|
||||
"contacts you provided. If you have questions or comments, reply to this email.",
|
||||
0,
|
||||
EMAIL,
|
||||
_creator.email,
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1084,12 +1147,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is org eligibility."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason ORGANIZATION_ELIGIBILITY and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1101,26 +1171,32 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"Your domain request was rejected because we determined that Testorg is not \neligible for "
|
||||
"a .gov domain.",
|
||||
0,
|
||||
EMAIL,
|
||||
_creator.email,
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_save_model_sends_rejected_email_naming(self):
|
||||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is naming."""
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1129,13 +1205,13 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
DomainRequest.RejectionReasons.NAMING_REQUIREMENTS,
|
||||
)
|
||||
self.assert_email_is_accurate(
|
||||
"Your domain request was rejected because it does not meet our naming requirements.", 0, EMAIL
|
||||
"Your domain request was rejected because it does not meet our naming requirements.", 0, _creator.email
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1143,12 +1219,19 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to rejected on a domain request, an email is sent
|
||||
explaining why when the reason is other."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelist user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Reject for reason NAMING_REQUIREMENTS and test email including dynamic organization name
|
||||
self.transition_state_and_send_email(
|
||||
|
@ -1156,12 +1239,12 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.RejectionReasons.OTHER,
|
||||
)
|
||||
self.assert_email_is_accurate("Choosing a .gov domain name", 0, EMAIL)
|
||||
self.assert_email_is_accurate("Choosing a .gov domain name", 0, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Approve
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.APPROVED)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("Congratulations! Your .gov domain request has been approved.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1225,23 +1308,30 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"""When transitioning to withdrawn on a domain request,
|
||||
an email is sent out every time."""
|
||||
|
||||
# Ensure there is no user with this email
|
||||
EMAIL = "mayor@igorville.gov"
|
||||
User.objects.filter(email=EMAIL).delete()
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward",
|
||||
first_name="Meoward",
|
||||
last_name="Jones",
|
||||
email="meoward.jones@igorville.gov",
|
||||
phone="(555) 123 12345",
|
||||
title="Treat inspector",
|
||||
)
|
||||
|
||||
# Create a sample domain request
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW)
|
||||
# Create a sample domain request and whitelists user email
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=_creator)
|
||||
AllowedEmail.objects.get_or_create(email=_creator.email)
|
||||
|
||||
# Test Submitted Status
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.WITHDRAWN)
|
||||
self.assert_email_is_accurate(
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, EMAIL
|
||||
"Your .gov domain request has been withdrawn and will not be reviewed by our team.", 0, _creator.email
|
||||
)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 1)
|
||||
|
||||
# Test Withdrawn Status
|
||||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 1, EMAIL)
|
||||
self.assert_email_is_accurate("We received your .gov domain request.", 1, _creator.email)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 2)
|
||||
|
||||
# Test Submitted Status Again (No new email should be sent)
|
||||
|
@ -1420,16 +1510,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
# Check for the field itself
|
||||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the submitter == #
|
||||
self.assertContains(response, "mayor@igorville.gov", count=2)
|
||||
expected_submitter_fields = [
|
||||
# Field, expected value
|
||||
("title", "Admin Tester"),
|
||||
("phone", "(555) 555 5556"),
|
||||
]
|
||||
self.test_helper.assert_response_contains_distinct_values(response, expected_submitter_fields)
|
||||
self.assertContains(response, "Testy2 Tester2")
|
||||
|
||||
# == Check for the senior_official == #
|
||||
self.assertContains(response, "testy@town.com", count=2)
|
||||
expected_so_fields = [
|
||||
|
@ -1450,7 +1530,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||
|
||||
# Test for the copy link
|
||||
self.assertContains(response, "button--clipboard", count=5)
|
||||
self.assertContains(response, "button--clipboard", count=4)
|
||||
|
||||
# Test that Creator counts display properly
|
||||
self.assertNotContains(response, "Approved domains")
|
||||
|
@ -1585,7 +1665,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"senior_official",
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
"submitter",
|
||||
"purpose",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
|
@ -1624,7 +1703,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"approved_domain",
|
||||
"alternative_domains",
|
||||
"purpose",
|
||||
"submitter",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"is_policy_acknowledged",
|
||||
|
|
|
@ -7,7 +7,7 @@ from waffle.testutils import override_flag
|
|||
from registrar.utility import email
|
||||
from registrar.utility.email import send_templated_email
|
||||
from .common import completed_domain_request
|
||||
from registrar.models import AllowedEmail
|
||||
from registrar.models import AllowedEmail, User
|
||||
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from datetime import datetime
|
||||
|
@ -93,7 +93,6 @@ class TestEmails(TestCase):
|
|||
|
||||
# check for optional things
|
||||
self.assertIn("Other employees from your organization:", body)
|
||||
self.assertIn("Testy2 Tester2", body)
|
||||
self.assertIn("Current websites:", body)
|
||||
self.assertIn("city.com", body)
|
||||
self.assertIn("About your organization:", body)
|
||||
|
@ -130,14 +129,20 @@ class TestEmails(TestCase):
|
|||
@less_console_noise_decorator
|
||||
def test_submission_confirmation_other_contacts_spacing(self):
|
||||
"""Test line spacing with other contacts."""
|
||||
domain_request = completed_domain_request(has_other_contacts=True)
|
||||
|
||||
# Create fake creator
|
||||
_creator = User.objects.create(
|
||||
username="MrMeoward", first_name="Meoward", last_name="Jones", phone="(888) 888 8888"
|
||||
)
|
||||
|
||||
# Create a fake domain request
|
||||
domain_request = completed_domain_request(has_other_contacts=True, user=_creator)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client_class):
|
||||
domain_request.submit()
|
||||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
self.assertIn("Other employees from your organization:", body)
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5556\n\nOther employees")
|
||||
self.assertRegex(body, r"8888\n\nOther employees")
|
||||
self.assertRegex(body, r"5557\n\nAnything else")
|
||||
|
||||
@boto3_mocking.patching
|
||||
|
@ -150,7 +155,6 @@ class TestEmails(TestCase):
|
|||
_, kwargs = self.mock_client.send_email.call_args
|
||||
body = kwargs["Content"]["Simple"]["Body"]["Text"]["Data"]
|
||||
# spacing should be right between adjacent elements
|
||||
self.assertRegex(body, r"5556\n\nOther employees")
|
||||
self.assertRegex(body, r"None\n\nAnything else")
|
||||
|
||||
@boto3_mocking.patching
|
||||
|
|
|
@ -10,7 +10,6 @@ from registrar.forms.domain_request_wizard import (
|
|||
DotGovDomainForm,
|
||||
SeniorOfficialForm,
|
||||
OrganizationContactForm,
|
||||
YourContactForm,
|
||||
OtherContactsForm,
|
||||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
|
@ -366,19 +365,6 @@ class TestFormValidation(MockEppLib):
|
|||
["Response must be less than 2000 characters."],
|
||||
)
|
||||
|
||||
def test_your_contact_email_invalid(self):
|
||||
"""must be a valid email address."""
|
||||
form = YourContactForm(data={"email": "boss@boss"})
|
||||
self.assertEqual(
|
||||
form.errors["email"],
|
||||
["Enter your email address in the required format, like name@example.com."],
|
||||
)
|
||||
|
||||
def test_your_contact_phone_invalid(self):
|
||||
"""Must be a valid phone number."""
|
||||
form = YourContactForm(data={"phone": "boss@boss"})
|
||||
self.assertTrue(form.errors["phone"][0].startswith("Enter a valid 10-digit phone number."))
|
||||
|
||||
def test_other_contact_email_invalid(self):
|
||||
"""must be a valid email address."""
|
||||
form = OtherContactsForm(data={"email": "splendid@boss"})
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import copy
|
||||
import boto3_mocking # type: ignore
|
||||
from datetime import date, datetime, time
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase, override_settings
|
||||
|
@ -8,6 +9,7 @@ from django.utils import timezone
|
|||
from django.utils.module_loading import import_string
|
||||
import logging
|
||||
import pyzipper
|
||||
from django.core.management.base import CommandError
|
||||
from registrar.management.commands.clean_tables import Command as CleanTablesCommand
|
||||
from registrar.management.commands.export_tables import Command as ExportTablesCommand
|
||||
from registrar.models import (
|
||||
|
@ -23,14 +25,17 @@ from registrar.models import (
|
|||
VerifiedByStaff,
|
||||
PublicContact,
|
||||
FederalAgency,
|
||||
Portfolio,
|
||||
Suborganization,
|
||||
)
|
||||
import tablib
|
||||
from unittest.mock import patch, call, MagicMock, mock_open
|
||||
from epplibwrapper import commands, common
|
||||
|
||||
from .common import MockEppLib, less_console_noise, completed_domain_request
|
||||
from .common import MockEppLib, less_console_noise, completed_domain_request, MockSESClient
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -1408,3 +1413,137 @@ class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
|
|||
missing_agency.refresh_from_db()
|
||||
self.assertIsNone(missing_agency.initials)
|
||||
self.assertIsNone(missing_agency.is_fceb)
|
||||
|
||||
|
||||
class TestCreateFederalPortfolio(TestCase):
|
||||
|
||||
@less_console_noise_decorator
|
||||
def setUp(self):
|
||||
self.mock_client = MockSESClient()
|
||||
self.user = User.objects.create(username="testuser")
|
||||
self.federal_agency = FederalAgency.objects.create(agency="Test Federal Agency")
|
||||
self.senior_official = SeniorOfficial.objects.create(
|
||||
first_name="first", last_name="last", email="testuser@igorville.gov", federal_agency=self.federal_agency
|
||||
)
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
self.domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
federal_agency=self.federal_agency,
|
||||
user=self.user,
|
||||
)
|
||||
self.domain_request.approve()
|
||||
self.domain_info = DomainInformation.objects.filter(domain_request=self.domain_request).get()
|
||||
|
||||
self.domain_request_2 = completed_domain_request(
|
||||
name="sock@igorville.org",
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
generic_org_type=DomainRequest.OrganizationChoices.CITY,
|
||||
federal_agency=self.federal_agency,
|
||||
user=self.user,
|
||||
organization_name="Test Federal Agency",
|
||||
)
|
||||
self.domain_request_2.approve()
|
||||
self.domain_info_2 = DomainInformation.objects.filter(domain_request=self.domain_request_2).get()
|
||||
|
||||
def tearDown(self):
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
Suborganization.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
SeniorOfficial.objects.all().delete()
|
||||
FederalAgency.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def run_create_federal_portfolio(self, agency_name, parse_requests=False, parse_domains=False):
|
||||
with patch(
|
||||
"registrar.management.commands.utility.terminal_helper.TerminalHelper.query_yes_no_exit",
|
||||
return_value=True,
|
||||
):
|
||||
call_command(
|
||||
"create_federal_portfolio", agency_name, parse_requests=parse_requests, parse_domains=parse_domains
|
||||
)
|
||||
|
||||
def test_create_or_modify_portfolio(self):
|
||||
"""Test portfolio creation and modification with suborg and senior official."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
portfolio = Portfolio.objects.get(federal_agency=self.federal_agency)
|
||||
self.assertEqual(portfolio.organization_name, self.federal_agency.agency)
|
||||
self.assertEqual(portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
self.assertEqual(portfolio.creator, User.get_default_user())
|
||||
self.assertEqual(portfolio.notes, "Auto-generated record")
|
||||
|
||||
# Test the suborgs
|
||||
suborganizations = Suborganization.objects.filter(portfolio__federal_agency=self.federal_agency)
|
||||
self.assertEqual(suborganizations.count(), 1)
|
||||
self.assertEqual(suborganizations.first().name, "Testorg")
|
||||
|
||||
# Test the senior official
|
||||
self.assertEqual(portfolio.senior_official, self.senior_official)
|
||||
|
||||
def test_handle_portfolio_requests(self):
|
||||
"""Verify portfolio association with domain requests."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
self.domain_request.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_request.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_request.sub_organization.name, "Testorg")
|
||||
|
||||
def test_handle_portfolio_domains(self):
|
||||
"""Check portfolio association with domain information."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_domains=True)
|
||||
|
||||
self.domain_info.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_info.portfolio.federal_agency, self.federal_agency)
|
||||
self.assertEqual(self.domain_info.sub_organization.name, "Testorg")
|
||||
|
||||
def test_handle_parse_both(self):
|
||||
"""Ensure correct parsing of both requests and domains."""
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True, parse_domains=True)
|
||||
|
||||
self.domain_request.refresh_from_db()
|
||||
self.domain_info.refresh_from_db()
|
||||
self.assertIsNotNone(self.domain_request.portfolio)
|
||||
self.assertIsNotNone(self.domain_info.portfolio)
|
||||
self.assertEqual(self.domain_request.portfolio, self.domain_info.portfolio)
|
||||
|
||||
def test_command_error_no_parse_options(self):
|
||||
"""Verify error when no parse options are provided."""
|
||||
with self.assertRaisesRegex(
|
||||
CommandError, "You must specify at least one of --parse_requests or --parse_domains."
|
||||
):
|
||||
self.run_create_federal_portfolio("Test Federal Agency")
|
||||
|
||||
def test_command_error_agency_not_found(self):
|
||||
"""Check error handling for non-existent agency."""
|
||||
expected_message = (
|
||||
"Cannot find the federal agency 'Non-existent Agency' in our database. "
|
||||
"The value you enter for `agency_name` must be prepopulated in the FederalAgency table before proceeding."
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, expected_message):
|
||||
self.run_create_federal_portfolio("Non-existent Agency", parse_requests=True)
|
||||
|
||||
def test_update_existing_portfolio(self):
|
||||
"""Test updating an existing portfolio."""
|
||||
# Create an existing portfolio
|
||||
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",
|
||||
)
|
||||
|
||||
self.run_create_federal_portfolio("Test Federal Agency", parse_requests=True)
|
||||
|
||||
existing_portfolio.refresh_from_db()
|
||||
self.assertEqual(existing_portfolio.organization_name, self.federal_agency.agency)
|
||||
self.assertEqual(existing_portfolio.organization_type, DomainRequest.OrganizationChoices.FEDERAL)
|
||||
|
||||
# Notes and creator should be untouched
|
||||
self.assertEqual(existing_portfolio.notes, "Old notes")
|
||||
self.assertEqual(existing_portfolio.creator, self.user)
|
||||
|
|
|
@ -170,7 +170,6 @@ class TestDomainRequest(TestCase):
|
|||
zipcode="12345-6789",
|
||||
senior_official=contact,
|
||||
requested_domain=domain,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
|
@ -197,7 +196,6 @@ class TestDomainRequest(TestCase):
|
|||
state_territory="CA",
|
||||
zipcode="12345-6789",
|
||||
senior_official=contact,
|
||||
submitter=contact,
|
||||
purpose="Igorville rules!",
|
||||
anything_else="All of Igorville loves the dotgov program.",
|
||||
is_policy_acknowledged=True,
|
||||
|
@ -225,7 +223,7 @@ class TestDomainRequest(TestCase):
|
|||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(creator=user, requested_domain=site)
|
||||
|
||||
# no submitter email so this emits a log warning
|
||||
# no email sent to creator so this emits a log warning
|
||||
|
||||
with boto3_mocking.clients.handler_for("sesv2", self.mock_client):
|
||||
with less_console_noise():
|
||||
|
@ -262,15 +260,17 @@ class TestDomainRequest(TestCase):
|
|||
@less_console_noise_decorator
|
||||
def test_submit_from_started_sends_email(self):
|
||||
msg = "Create a domain request and submit it and see if email was sent."
|
||||
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
|
||||
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
|
||||
domain_request = completed_domain_request(user=self.dummy_user_2)
|
||||
self.check_email_sent(
|
||||
domain_request, msg, "submit", 1, expected_content="Lava", expected_email=self.dummy_user_2.email
|
||||
)
|
||||
|
||||
@override_flag("profile_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_submit_from_started_sends_email_to_creator(self):
|
||||
"""Tests if, when the profile feature flag is on, we send an email to the creator"""
|
||||
msg = "Create a domain request and submit it and see if email was sent when the feature flag is on."
|
||||
domain_request = completed_domain_request(submitter=self.dummy_user, user=self.dummy_user_2)
|
||||
domain_request = completed_domain_request(user=self.dummy_user_2)
|
||||
self.check_email_sent(
|
||||
domain_request, msg, "submit", 1, expected_content="Lava", expected_email="intern@igorville.com"
|
||||
)
|
||||
|
@ -278,10 +278,9 @@ class TestDomainRequest(TestCase):
|
|||
@less_console_noise_decorator
|
||||
def test_submit_from_withdrawn_sends_email(self):
|
||||
msg = "Create a withdrawn domain request and submit it and see if email was sent."
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN, submitter=self.dummy_user
|
||||
)
|
||||
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hello")
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.WITHDRAWN, user=user)
|
||||
self.check_email_sent(domain_request, msg, "submit", 1, expected_content="Hi", expected_email=user.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_submit_from_action_needed_does_not_send_email(self):
|
||||
|
@ -298,26 +297,25 @@ class TestDomainRequest(TestCase):
|
|||
@less_console_noise_decorator
|
||||
def test_approve_sends_email(self):
|
||||
msg = "Create a domain request and approve it and see if email was sent."
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
|
||||
)
|
||||
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="Hello")
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
|
||||
self.check_email_sent(domain_request, msg, "approve", 1, expected_content="approved", expected_email=user.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_withdraw_sends_email(self):
|
||||
msg = "Create a domain request and withdraw it and see if email was sent."
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, submitter=self.dummy_user
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=user)
|
||||
self.check_email_sent(
|
||||
domain_request, msg, "withdraw", 1, expected_content="withdrawn", expected_email=user.email
|
||||
)
|
||||
self.check_email_sent(domain_request, msg, "withdraw", 1, expected_content="Hello")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_reject_sends_email(self):
|
||||
msg = "Create a domain request and reject it and see if email was sent."
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.APPROVED, submitter=self.dummy_user
|
||||
)
|
||||
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hello")
|
||||
user, _ = User.objects.get_or_create(username="testy")
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.APPROVED, user=user)
|
||||
self.check_email_sent(domain_request, msg, "reject", 1, expected_content="Hi", expected_email=user.email)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_reject_with_prejudice_does_not_send_email(self):
|
||||
|
@ -1135,7 +1133,7 @@ class TestPortfolioInvitations(TestCase):
|
|||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user2, organization_name="Hotel California")
|
||||
self.portfolio_role_base = UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||
self.portfolio_role_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
||||
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS
|
||||
self.portfolio_permission_1 = UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||
self.portfolio_permission_2 = UserPortfolioPermissionChoices.EDIT_REQUESTS
|
||||
self.invitation, _ = PortfolioInvitation.objects.get_or_create(
|
||||
email=self.email,
|
||||
|
@ -1326,16 +1324,16 @@ class TestUser(TestCase):
|
|||
User.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
|
||||
@patch.object(User, "has_edit_suborganization", return_value=True)
|
||||
@patch.object(User, "has_edit_suborganization_portfolio_permission", return_value=True)
|
||||
def test_portfolio_role_summary_admin(self, mock_edit_suborganization):
|
||||
# Test if the user is recognized as an Admin
|
||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Admin"])
|
||||
|
||||
@patch.multiple(
|
||||
User,
|
||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
||||
has_edit_requests=lambda self, portfolio: True,
|
||||
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||
)
|
||||
def test_portfolio_role_summary_view_only_admin_and_domain_requestor(self):
|
||||
# Test if the user has both 'View-only admin' and 'Domain requestor' roles
|
||||
|
@ -1343,8 +1341,8 @@ class TestUser(TestCase):
|
|||
|
||||
@patch.multiple(
|
||||
User,
|
||||
has_view_all_domains_permission=lambda self, portfolio: True,
|
||||
has_domain_requests_portfolio_permission=lambda self, portfolio: True,
|
||||
has_view_all_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
has_any_requests_portfolio_permission=lambda self, portfolio: True,
|
||||
)
|
||||
def test_portfolio_role_summary_view_only_admin(self):
|
||||
# Test if the user is recognized as a View-only admin
|
||||
|
@ -1353,15 +1351,17 @@ class TestUser(TestCase):
|
|||
@patch.multiple(
|
||||
User,
|
||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||
has_edit_requests=lambda self, portfolio: True,
|
||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
)
|
||||
def test_portfolio_role_summary_member_domain_requestor_domain_manager(self):
|
||||
# Test if the user has 'Member', 'Domain requestor', and 'Domain manager' roles
|
||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), ["Domain requestor", "Domain manager"])
|
||||
|
||||
@patch.multiple(
|
||||
User, has_base_portfolio_permission=lambda self, portfolio: True, has_edit_requests=lambda self, portfolio: True
|
||||
User,
|
||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||
has_edit_request_portfolio_permission=lambda self, portfolio: True,
|
||||
)
|
||||
def test_portfolio_role_summary_member_domain_requestor(self):
|
||||
# Test if the user has 'Member' and 'Domain requestor' roles
|
||||
|
@ -1370,7 +1370,7 @@ class TestUser(TestCase):
|
|||
@patch.multiple(
|
||||
User,
|
||||
has_base_portfolio_permission=lambda self, portfolio: True,
|
||||
has_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
has_any_domains_portfolio_permission=lambda self, portfolio: True,
|
||||
)
|
||||
def test_portfolio_role_summary_member_domain_manager(self):
|
||||
# Test if the user has 'Member' and 'Domain manager' roles
|
||||
|
@ -1385,6 +1385,74 @@ class TestUser(TestCase):
|
|||
# Test if the user has no roles
|
||||
self.assertEqual(self.user.portfolio_role_summary(self.portfolio), [])
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_base_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_base_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_PORTFOLIO)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_edit_org_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_edit_org_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_PORTFOLIO)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_any_domains_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||
|
||||
self.assertTrue(self.user.has_any_domains_portfolio_permission(self.portfolio))
|
||||
self.assertEqual(mock_has_permission.call_count, 2)
|
||||
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_view_all_domains_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_view_all_domains_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_has_any_requests_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.side_effect = [False, True] # First permission false, second permission true
|
||||
|
||||
self.assertTrue(self.user.has_any_requests_portfolio_permission(self.portfolio))
|
||||
self.assertEqual(mock_has_permission.call_count, 2)
|
||||
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
mock_has_permission.assert_any_call(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_view_all_requests_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_view_all_requests_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_edit_request_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_edit_request_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_view_suborganization_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_view_suborganization_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION)
|
||||
|
||||
@patch("registrar.models.User._has_portfolio_permission")
|
||||
def test_has_edit_suborganization_portfolio_permission(self, mock_has_permission):
|
||||
mock_has_permission.return_value = True
|
||||
|
||||
self.assertTrue(self.user.has_edit_suborganization_portfolio_permission(self.portfolio))
|
||||
mock_has_permission.assert_called_once_with(self.portfolio, UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_check_transition_domains_without_domains_on_login(self):
|
||||
"""A user's on_each_login callback does not check transition domains.
|
||||
|
@ -1547,8 +1615,8 @@ class TestUser(TestCase):
|
|||
|
||||
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||
|
||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
||||
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||
|
||||
self.assertFalse(user_can_view_all_domains)
|
||||
self.assertFalse(user_can_view_all_requests)
|
||||
|
@ -1562,8 +1630,8 @@ class TestUser(TestCase):
|
|||
],
|
||||
)
|
||||
|
||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
||||
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||
|
||||
self.assertTrue(user_can_view_all_domains)
|
||||
self.assertFalse(user_can_view_all_requests)
|
||||
|
@ -1572,16 +1640,16 @@ class TestUser(TestCase):
|
|||
portfolio_permission.save()
|
||||
portfolio_permission.refresh_from_db()
|
||||
|
||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
||||
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||
|
||||
self.assertTrue(user_can_view_all_domains)
|
||||
self.assertTrue(user_can_view_all_requests)
|
||||
|
||||
UserDomainRole.objects.get_or_create(user=self.user, domain=self.domain, role=UserDomainRole.Roles.MANAGER)
|
||||
|
||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_domain_requests_portfolio_permission(portfolio)
|
||||
user_can_view_all_domains = self.user.has_any_domains_portfolio_permission(portfolio)
|
||||
user_can_view_all_requests = self.user.has_any_requests_portfolio_permission(portfolio)
|
||||
|
||||
self.assertTrue(user_can_view_all_domains)
|
||||
self.assertTrue(user_can_view_all_requests)
|
||||
|
@ -2069,7 +2137,6 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
senior_official=so,
|
||||
requested_domain=draft_domain,
|
||||
purpose="Some purpose",
|
||||
submitter=you,
|
||||
no_other_contacts_rationale=None,
|
||||
has_cisa_representative=True,
|
||||
cisa_representative_email="somerep@cisa.com",
|
||||
|
@ -2198,13 +2265,6 @@ class TestDomainRequestIncomplete(TestCase):
|
|||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_purpose_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_is_submitter_complete(self):
|
||||
self.assertTrue(self.domain_request._is_submitter_complete())
|
||||
self.domain_request.submitter = None
|
||||
self.domain_request.save()
|
||||
self.assertFalse(self.domain_request._is_submitter_complete())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_is_other_contacts_complete_missing_one_field(self):
|
||||
self.assertTrue(self.domain_request._is_other_contacts_complete())
|
||||
|
|
|
@ -369,19 +369,12 @@ class HomeTests(TestWithUser):
|
|||
last_name="Mars",
|
||||
)
|
||||
|
||||
# Attach a user object to a contact (should not be deleted)
|
||||
contact_user, _ = Contact.objects.get_or_create(
|
||||
first_name="Hank",
|
||||
last_name="McFakey",
|
||||
)
|
||||
|
||||
site = DraftDomain.objects.create(name="igorville.gov")
|
||||
domain_request = DomainRequest.objects.create(
|
||||
creator=self.user,
|
||||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
senior_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
domain_request.other_contacts.set([contact_2])
|
||||
|
||||
|
@ -392,7 +385,6 @@ class HomeTests(TestWithUser):
|
|||
requested_domain=site_2,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
senior_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
domain_request_2.other_contacts.set([contact_shared])
|
||||
|
||||
|
@ -409,8 +401,6 @@ class HomeTests(TestWithUser):
|
|||
# Check if the orphaned contacts were deleted
|
||||
orphan = Contact.objects.filter(id=contact.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
orphan = Contact.objects.filter(id=contact_user.id)
|
||||
self.assertFalse(orphan.exists())
|
||||
|
||||
try:
|
||||
edge_case = Contact.objects.filter(id=contact_2.id).get()
|
||||
|
@ -455,7 +445,6 @@ class HomeTests(TestWithUser):
|
|||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
senior_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
domain_request.other_contacts.set([contact_2])
|
||||
|
||||
|
@ -466,7 +455,6 @@ class HomeTests(TestWithUser):
|
|||
requested_domain=site_2,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
senior_official=contact_2,
|
||||
submitter=contact_shared,
|
||||
)
|
||||
domain_request_2.other_contacts.set([contact_shared])
|
||||
|
||||
|
@ -1010,20 +998,6 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
self.assertContains(response, "Your profile")
|
||||
self.assertNotContains(response, "Your contact information")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_your_contact_information_when_profile_feature_off(self):
|
||||
"""test that Your contact information is accessible when profile_feature is off"""
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||
self.assertContains(response, "Your contact information")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_your_contact_information_when_profile_feature_on(self):
|
||||
"""test that Your contact information is not accessible when profile feature is on"""
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get(f"/domain/{self.domain.id}/your-contact-information", follow=True)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_request_when_profile_feature_on(self):
|
||||
"""test that Your profile is in request page when profile feature is on"""
|
||||
|
@ -1038,7 +1012,6 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
senior_official=contact_user,
|
||||
submitter=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=True):
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
|
@ -1060,7 +1033,6 @@ class UserProfileTests(TestWithUser, WebTest):
|
|||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
senior_official=contact_user,
|
||||
submitter=contact_user,
|
||||
)
|
||||
with override_flag("profile_feature", active=False):
|
||||
response = self.client.get(f"/domain-request/{domain_request.id}", follow=True)
|
||||
|
|
|
@ -161,7 +161,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain-dns-nameservers",
|
||||
"domain-org-name-address",
|
||||
"domain-senior-official",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
|
@ -181,7 +180,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain-dns-nameservers",
|
||||
"domain-org-name-address",
|
||||
"domain-senior-official",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
with self.subTest(view_name=view_name):
|
||||
|
@ -202,7 +200,6 @@ class TestDomainPermissions(TestWithDomainPermissions):
|
|||
"domain-dns-dnssec-dsdata",
|
||||
"domain-org-name-address",
|
||||
"domain-senior-official",
|
||||
"domain-your-contact-information",
|
||||
"domain-security-email",
|
||||
]:
|
||||
for domain in [
|
||||
|
@ -1624,22 +1621,6 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
portfolio.delete()
|
||||
|
||||
|
||||
class TestDomainContactInformation(TestDomainOverview):
|
||||
@less_console_noise_decorator
|
||||
def test_domain_your_contact_information(self):
|
||||
"""Can load domain's your contact information page."""
|
||||
page = self.client.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(page, "Your contact information")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_your_contact_information_content(self):
|
||||
"""Logged-in user's contact information appears on the page."""
|
||||
self.user.first_name = "Testy"
|
||||
self.user.save()
|
||||
page = self.app.get(reverse("domain-your-contact-information", kwargs={"pk": self.domain.id}))
|
||||
self.assertContains(page, "Testy")
|
||||
|
||||
|
||||
class TestDomainSecurityEmail(TestDomainOverview):
|
||||
def test_domain_security_email_existing_security_contact(self):
|
||||
"""Can load domain's security email page."""
|
||||
|
|
|
@ -12,10 +12,11 @@ from registrar.models import (
|
|||
)
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from .common import create_test_user
|
||||
from .common import MockSESClient, completed_domain_request, create_test_user
|
||||
from waffle.testutils import override_flag
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
|
||||
import boto3_mocking # type: ignore
|
||||
from django.test import Client
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -24,6 +25,7 @@ logger = logging.getLogger(__name__)
|
|||
class TestPortfolio(WebTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.user = create_test_user()
|
||||
self.domain, _ = Domain.objects.get_or_create(name="igorville.gov")
|
||||
self.portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Hotel California")
|
||||
|
@ -76,7 +78,7 @@ class TestPortfolio(WebTest):
|
|||
def test_middleware_does_not_redirect_if_no_permission(self):
|
||||
"""Test that user with no portfolio permission is not redirected when attempting to access home"""
|
||||
self.app.set_user(self.user.username)
|
||||
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=self.portfolio, additional_permissions=[]
|
||||
)
|
||||
self.user.portfolio = self.portfolio
|
||||
|
@ -504,7 +506,7 @@ class TestPortfolio(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
self.assertFalse(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertFalse(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "You aren")
|
||||
|
||||
|
@ -519,7 +521,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Test the domains page - this user should have access
|
||||
response = self.client.get(reverse("domains"))
|
||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Domain name")
|
||||
|
||||
|
@ -530,7 +532,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Test the domains page - this user should have access
|
||||
response = self.client.get(reverse("domains"))
|
||||
self.assertTrue(self.user.has_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertTrue(self.user.has_any_domains_portfolio_permission(response.wsgi_request.session.get("portfolio")))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertContains(response, "Domain name")
|
||||
permission.delete()
|
||||
|
@ -569,7 +571,7 @@ class TestPortfolio(WebTest):
|
|||
portfolio=self.portfolio,
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
],
|
||||
|
@ -595,7 +597,7 @@ class TestPortfolio(WebTest):
|
|||
portfolio=self.portfolio,
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
],
|
||||
|
@ -621,7 +623,7 @@ class TestPortfolio(WebTest):
|
|||
portfolio=self.portfolio,
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
],
|
||||
|
@ -652,3 +654,208 @@ class TestPortfolio(WebTest):
|
|||
|
||||
self.assertContains(home, "Hotel California")
|
||||
self.assertContains(home, "Members")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_portfolio_domain_requests_page_when_user_has_no_permissions(self):
|
||||
"""Test the no requests page"""
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
# create and submit a domain request
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
domain_request.submit()
|
||||
domain_request.save()
|
||||
|
||||
requests_page = self.client.get(reverse("no-portfolio-requests"), follow=True)
|
||||
|
||||
self.assertContains(requests_page, "You don’t have access to domain requests.")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_main_nav_when_user_has_no_permissions(self):
|
||||
"""Test the nav contains a link to the no requests page"""
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
# create and submit a domain request
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
domain_request.submit()
|
||||
domain_request.save()
|
||||
|
||||
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
# link to no requests
|
||||
self.assertContains(portfolio_landing_page, "no-organization-requests/")
|
||||
# dropdown
|
||||
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||
# link to requests
|
||||
self.assertNotContains(portfolio_landing_page, 'href="/requests/')
|
||||
# link to create
|
||||
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_main_nav_when_user_has_all_permissions(self):
|
||||
"""Test the nav contains a dropdown with a link to create and another link to view requests
|
||||
Also test for the existence of the Create a new request btn on the requests page"""
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
# create and submit a domain request
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
domain_request.submit()
|
||||
domain_request.save()
|
||||
|
||||
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
# link to no requests
|
||||
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||
# dropdown
|
||||
self.assertContains(portfolio_landing_page, "basic-nav-section-two")
|
||||
# link to requests
|
||||
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||
# link to create
|
||||
self.assertContains(portfolio_landing_page, 'href="/request/')
|
||||
|
||||
requests_page = self.client.get(reverse("domain-requests"))
|
||||
|
||||
# create new request btn
|
||||
self.assertContains(requests_page, "Start a new domain request")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_main_nav_when_user_has_view_but_not_edit_permissions(self):
|
||||
"""Test the nav contains a simple link to view requests
|
||||
Also test for the existence of the Create a new request btn on the requests page"""
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
],
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
# create and submit a domain request
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
domain_request.submit()
|
||||
domain_request.save()
|
||||
|
||||
portfolio_landing_page = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
# link to no requests
|
||||
self.assertNotContains(portfolio_landing_page, "no-organization-requests/")
|
||||
# dropdown
|
||||
self.assertNotContains(portfolio_landing_page, "basic-nav-section-two")
|
||||
# link to requests
|
||||
self.assertContains(portfolio_landing_page, 'href="/requests/')
|
||||
# link to create
|
||||
self.assertNotContains(portfolio_landing_page, 'href="/request/')
|
||||
|
||||
requests_page = self.client.get(reverse("domain-requests"))
|
||||
|
||||
# create new request btn
|
||||
self.assertNotContains(requests_page, "Start a new domain request")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
def test_organization_requests_additional_column(self):
|
||||
"""The requests table has a column for created at"""
|
||||
self.app.set_user(self.user.username)
|
||||
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
additional_permissions=[
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
],
|
||||
)
|
||||
|
||||
home = self.app.get(reverse("home")).follow()
|
||||
|
||||
self.assertContains(home, "Hotel California")
|
||||
self.assertContains(home, "Domain requests")
|
||||
|
||||
domain_requests = self.app.get(reverse("domain-requests"))
|
||||
self.assertEqual(domain_requests.status_code, 200)
|
||||
|
||||
self.assertContains(domain_requests, "Created by")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_no_org_requests_no_additional_column(self):
|
||||
"""The requests table does not have a column for created at"""
|
||||
self.app.set_user(self.user.username)
|
||||
|
||||
home = self.app.get(reverse("home"))
|
||||
|
||||
self.assertContains(home, "Domain requests")
|
||||
self.assertNotContains(home, "Created by")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_portfolio_cache_updates_when_modified(self):
|
||||
"""Test that the portfolio in session updates when the portfolio is modified"""
|
||||
self.client.force_login(self.user)
|
||||
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||
|
||||
with override_flag("organization_feature", active=True):
|
||||
# Initial request to set the portfolio in session
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
portfolio = self.client.session.get("portfolio")
|
||||
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||
self.assertContains(response, "Hotel California")
|
||||
|
||||
# Modify the portfolio
|
||||
self.portfolio.organization_name = "Updated Hotel California"
|
||||
self.portfolio.save()
|
||||
|
||||
# Make another request
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
|
||||
# Check if the updated portfolio name is in the response
|
||||
self.assertContains(response, "Updated Hotel California")
|
||||
|
||||
# Verify that the session contains the updated portfolio
|
||||
portfolio = self.client.session.get("portfolio")
|
||||
self.assertEqual(portfolio.organization_name, "Updated Hotel California")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_portfolio_cache_updates_when_flag_disabled_while_logged_in(self):
|
||||
"""Test that the portfolio in session is set to None when the organization_feature flag is disabled"""
|
||||
self.client.force_login(self.user)
|
||||
portfolio_roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
UserPortfolioPermission.objects.get_or_create(user=self.user, portfolio=self.portfolio, roles=portfolio_roles)
|
||||
|
||||
with override_flag("organization_feature", active=True):
|
||||
# Initial request to set the portfolio in session
|
||||
response = self.client.get(reverse("home"), follow=True)
|
||||
portfolio = self.client.session.get("portfolio")
|
||||
self.assertEqual(portfolio.organization_name, "Hotel California")
|
||||
self.assertContains(response, "Hotel California")
|
||||
|
||||
# Disable the organization_feature flag
|
||||
with override_flag("organization_feature", active=False):
|
||||
# Make another request
|
||||
response = self.client.get(reverse("home"))
|
||||
self.assertIsNone(self.client.session.get("portfolio"))
|
||||
self.assertNotContains(response, "Hotel California")
|
||||
|
|
|
@ -7,6 +7,7 @@ from api.tests.common import less_console_noise_decorator
|
|||
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||
from django_webtest import WebTest # type: ignore
|
||||
import boto3_mocking # type: ignore
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from registrar.models import (
|
||||
DomainRequest,
|
||||
|
@ -17,12 +18,14 @@ from registrar.models import (
|
|||
User,
|
||||
Website,
|
||||
FederalAgency,
|
||||
Portfolio,
|
||||
UserPortfolioPermission,
|
||||
)
|
||||
from registrar.views.domain_request import DomainRequestWizard, Step
|
||||
|
||||
from .common import less_console_noise
|
||||
from .test_views import TestWithUser
|
||||
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -348,41 +351,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(purpose_result.status_code, 302)
|
||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- YOUR CONTACT INFO PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_page = purpose_result.follow()
|
||||
your_contact_form = your_contact_page.forms[0]
|
||||
|
||||
your_contact_form["your_contact-first_name"] = "Testy you"
|
||||
your_contact_form["your_contact-last_name"] = "Tester you"
|
||||
your_contact_form["your_contact-title"] = "Admin Tester"
|
||||
your_contact_form["your_contact-email"] = "testy-admin@town.com"
|
||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_result = your_contact_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.submitter.first_name, "Testy you")
|
||||
self.assertEqual(domain_request.submitter.last_name, "Tester you")
|
||||
self.assertEqual(domain_request.submitter.title, "Admin Tester")
|
||||
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
|
||||
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(your_contact_result.status_code, 302)
|
||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
||||
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- OTHER CONTACTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
other_contacts_page = your_contact_result.follow()
|
||||
other_contacts_page = purpose_result.follow()
|
||||
|
||||
# This page has 3 forms in 1.
|
||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||
|
@ -492,11 +467,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertContains(review_page, "city.gov")
|
||||
self.assertContains(review_page, "city1.gov")
|
||||
self.assertContains(review_page, "For all kinds of things.")
|
||||
self.assertContains(review_page, "Testy you")
|
||||
self.assertContains(review_page, "Tester you")
|
||||
self.assertContains(review_page, "Admin Tester")
|
||||
self.assertContains(review_page, "testy-admin@town.com")
|
||||
self.assertContains(review_page, "(201) 555-5556")
|
||||
self.assertContains(review_page, "Testy2")
|
||||
self.assertContains(review_page, "Tester2")
|
||||
self.assertContains(review_page, "Another Tester")
|
||||
|
@ -704,41 +674,13 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(purpose_result.status_code, 302)
|
||||
self.assertEqual(purpose_result["Location"], "/request/your_contact/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- YOUR CONTACT INFO PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_page = purpose_result.follow()
|
||||
your_contact_form = your_contact_page.forms[0]
|
||||
|
||||
your_contact_form["your_contact-first_name"] = "Testy you"
|
||||
your_contact_form["your_contact-last_name"] = "Tester you"
|
||||
your_contact_form["your_contact-title"] = "Admin Tester"
|
||||
your_contact_form["your_contact-email"] = "testy-admin@town.com"
|
||||
your_contact_form["your_contact-phone"] = "(201) 555 5556"
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
your_contact_result = your_contact_form.submit()
|
||||
# validate that data from this step are being saved
|
||||
domain_request = DomainRequest.objects.get() # there's only one
|
||||
self.assertEqual(domain_request.submitter.first_name, "Testy you")
|
||||
self.assertEqual(domain_request.submitter.last_name, "Tester you")
|
||||
self.assertEqual(domain_request.submitter.title, "Admin Tester")
|
||||
self.assertEqual(domain_request.submitter.email, "testy-admin@town.com")
|
||||
self.assertEqual(domain_request.submitter.phone, "(201) 555 5556")
|
||||
# the post request should return a redirect to the next form in
|
||||
# the domain request page
|
||||
self.assertEqual(your_contact_result.status_code, 302)
|
||||
self.assertEqual(your_contact_result["Location"], "/request/other_contacts/")
|
||||
self.assertEqual(purpose_result["Location"], "/request/other_contacts/")
|
||||
num_pages_tested += 1
|
||||
|
||||
# ---- OTHER CONTACTS PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
other_contacts_page = your_contact_result.follow()
|
||||
other_contacts_page = purpose_result.follow()
|
||||
|
||||
# This page has 3 forms in 1.
|
||||
# Let's set the yes/no radios to enable the other contacts fieldsets
|
||||
|
@ -1643,7 +1585,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -1780,7 +1721,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -1855,7 +1795,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -1933,7 +1872,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -2010,7 +1948,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -2086,7 +2023,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
senior_official=so,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
@ -2270,23 +2206,15 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
senior_official = domain_request.senior_official
|
||||
self.assertEquals("Testy2", senior_official.first_name)
|
||||
|
||||
@override_flag("profile_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_edit_submitter_in_place(self):
|
||||
def test_edit_creator_in_place(self):
|
||||
"""When you:
|
||||
1. edit a submitter (your contact) which is not joined to another model,
|
||||
1. edit a your user profile information,
|
||||
2. then submit,
|
||||
the domain request is linked to the existing submitter, and the submitter updated."""
|
||||
the domain request also updates its creator data to reflect user profile changes."""
|
||||
|
||||
# Populate the database with a domain request that
|
||||
# has a submitter
|
||||
# We'll do it from scratch
|
||||
you, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(201) 555 5555",
|
||||
)
|
||||
# Populate the database with a domain request
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(
|
||||
generic_org_type="federal",
|
||||
federal_type="executive",
|
||||
|
@ -2297,14 +2225,11 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
address_line1="address 1",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
|
||||
# submitter_pk is the initial pk of the submitter. set it before update
|
||||
# to be able to verify after update that the same contact object is in place
|
||||
submitter_pk = you.id
|
||||
creator_pk = self.user.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
|
@ -2315,98 +2240,25 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
|
||||
profile_page = self.app.get("/user-profile")
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
your_contact_form = your_contact_page.forms[0]
|
||||
profile_form = profile_page.forms[0]
|
||||
|
||||
# Minimal check to ensure the form is loaded
|
||||
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
|
||||
self.assertEqual(profile_form["first_name"].value, self.user.first_name)
|
||||
|
||||
# update the first name of the contact
|
||||
your_contact_form["your_contact-first_name"] = "Testy2"
|
||||
profile_form["first_name"] = "Testy2"
|
||||
|
||||
# Submit the updated form
|
||||
your_contact_form.submit()
|
||||
profile_form.submit()
|
||||
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
updated_submitter = domain_request.submitter
|
||||
self.assertEquals(submitter_pk, updated_submitter.id)
|
||||
self.assertEquals("Testy2", updated_submitter.first_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_edit_submitter_creates_new(self):
|
||||
"""When you:
|
||||
1. edit an existing your contact which IS joined to another model,
|
||||
2. then submit,
|
||||
the domain request is linked to a new Contact, and the new Contact is updated."""
|
||||
|
||||
# Populate the database with a domain request that
|
||||
# has submitter assigned to it, the submitter is also
|
||||
# an other contact initially
|
||||
# We'll do it from scratch
|
||||
submitter, _ = Contact.objects.get_or_create(
|
||||
first_name="Testy",
|
||||
last_name="Tester",
|
||||
title="Chief Tester",
|
||||
email="testy@town.com",
|
||||
phone="(201) 555 5555",
|
||||
)
|
||||
domain_request, _ = DomainRequest.objects.get_or_create(
|
||||
generic_org_type="federal",
|
||||
federal_type="executive",
|
||||
purpose="Purpose of the site",
|
||||
anything_else="No",
|
||||
is_policy_acknowledged=True,
|
||||
organization_name="Testorg",
|
||||
address_line1="address 1",
|
||||
state_territory="NY",
|
||||
zipcode="10002",
|
||||
submitter=submitter,
|
||||
creator=self.user,
|
||||
status="started",
|
||||
)
|
||||
domain_request.other_contacts.add(submitter)
|
||||
|
||||
# submitter_pk is the initial pk of the your contact. set it before update
|
||||
# to be able to verify after update that the other contact is still in place
|
||||
# and not updated, and that the new submitter has a new id
|
||||
submitter_pk = submitter.id
|
||||
|
||||
# prime the form by visiting /edit
|
||||
self.app.get(reverse("edit-domain-request", kwargs={"id": domain_request.pk}))
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
# of a "session". We are going to do it manually, saving the session ID here
|
||||
# and then setting the cookie on each request.
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
your_contact_page = self.app.get(reverse("domain-request:your_contact"))
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
your_contact_form = your_contact_page.forms[0]
|
||||
|
||||
# Minimal check to ensure the form is loaded
|
||||
self.assertEqual(your_contact_form["your_contact-first_name"].value, "Testy")
|
||||
|
||||
# update the first name of the contact
|
||||
your_contact_form["your_contact-first_name"] = "Testy2"
|
||||
|
||||
# Submit the updated form
|
||||
your_contact_form.submit()
|
||||
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
# assert that the other contact is not updated
|
||||
other_contacts = domain_request.other_contacts.all()
|
||||
other_contact = other_contacts[0]
|
||||
self.assertEquals(submitter_pk, other_contact.id)
|
||||
self.assertEquals("Testy", other_contact.first_name)
|
||||
# assert that the submitter is updated
|
||||
submitter = domain_request.submitter
|
||||
self.assertEquals("Testy2", submitter.first_name)
|
||||
updated_creator = domain_request.creator
|
||||
self.assertEquals(creator_pk, updated_creator.id)
|
||||
self.assertEquals("Testy2", updated_creator.first_name)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_about_your_organiztion_interstate(self):
|
||||
|
@ -2729,7 +2581,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
zipcode="10002",
|
||||
senior_official=so,
|
||||
requested_domain=domain,
|
||||
submitter=you,
|
||||
creator=self.user,
|
||||
)
|
||||
domain_request.other_contacts.add(other)
|
||||
|
@ -2874,7 +2725,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2891,7 +2741,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -2905,7 +2754,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
# click the "Withdraw request" button
|
||||
mock_client = MockSESClient()
|
||||
|
@ -2925,6 +2773,38 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
response = self.client.get("/get-domain-requests-json/")
|
||||
self.assertContains(response, "Withdrawn")
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_domain_request_withdraw_portfolio_redirects_correctly(self):
|
||||
"""Tests that the withdraw button on portfolio redirects to the portfolio domain requests page"""
|
||||
portfolio, _ = Portfolio.objects.get_or_create(creator=self.user, organization_name="Test Portfolio")
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||
domain_request.save()
|
||||
|
||||
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
self.assertContains(detail_page, "city.gov")
|
||||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
# click the "Withdraw request" button
|
||||
mock_client = MockSESClient()
|
||||
with boto3_mocking.clients.handler_for("sesv2", mock_client):
|
||||
with less_console_noise():
|
||||
withdraw_page = detail_page.click("Withdraw request")
|
||||
self.assertContains(withdraw_page, "Withdraw request for")
|
||||
home_page = withdraw_page.click("Withdraw request")
|
||||
|
||||
# Assert that it redirects to the portfolio requests page and the status has been updated to withdrawn
|
||||
self.assertEqual(home_page.status_code, 302)
|
||||
self.assertEqual(home_page.location, reverse("domain-requests"))
|
||||
|
||||
response = self.client.get("/get-domain-requests-json/")
|
||||
self.assertContains(response, "Withdrawn")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_withdraw_no_permissions(self):
|
||||
"""Can't withdraw domain requests as a restricted user."""
|
||||
|
@ -2938,7 +2818,6 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
self.assertContains(detail_page, "city1.gov")
|
||||
self.assertContains(detail_page, "Chief Tester")
|
||||
self.assertContains(detail_page, "testy@town.com")
|
||||
self.assertContains(detail_page, "Admin Tester")
|
||||
self.assertContains(detail_page, "Status:")
|
||||
# Restricted user trying to withdraw results in 403 error
|
||||
with less_console_noise():
|
||||
|
@ -3037,10 +2916,10 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
|||
self.assertEqual(detail_page.status_code, 200)
|
||||
|
||||
# 10 unlocked steps, one active step, the review step will have link_usa but not check_circle
|
||||
self.assertContains(detail_page, "#check_circle", count=10)
|
||||
self.assertContains(detail_page, "#check_circle", count=9)
|
||||
# Type of organization
|
||||
self.assertContains(detail_page, "usa-current", count=1)
|
||||
self.assertContains(detail_page, "link_usa-checked", count=11)
|
||||
self.assertContains(detail_page, "link_usa-checked", count=10)
|
||||
|
||||
else:
|
||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||
|
@ -3072,7 +2951,6 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
|||
requested_domain=site,
|
||||
status=DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
senior_official=contact,
|
||||
submitter=contact_user,
|
||||
)
|
||||
domain_request.other_contacts.set([contact_2])
|
||||
|
||||
|
@ -3098,12 +2976,12 @@ class TestWizardUnlockingSteps(TestWithUser, WebTest):
|
|||
# Now 'detail_page' contains the response after following the redirect
|
||||
self.assertEqual(detail_page.status_code, 200)
|
||||
|
||||
# 5 unlocked steps (so, domain, submitter, other contacts, and current sites
|
||||
# 5 unlocked steps (so, domain, other contacts, and current sites
|
||||
# which unlocks if domain exists), one active step, the review step is locked
|
||||
self.assertContains(detail_page, "#check_circle", count=5)
|
||||
self.assertContains(detail_page, "#check_circle", count=4)
|
||||
# Type of organization
|
||||
self.assertContains(detail_page, "usa-current", count=1)
|
||||
self.assertContains(detail_page, "link_usa-checked", count=5)
|
||||
self.assertContains(detail_page, "link_usa-checked", count=4)
|
||||
|
||||
else:
|
||||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||
|
|
|
@ -2,9 +2,14 @@ from registrar.models import DomainRequest
|
|||
from django.urls import reverse
|
||||
|
||||
from registrar.models.draft_domain import DraftDomain
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.user import User
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from .test_views import TestWithUser
|
||||
from django_webtest import WebTest # type: ignore
|
||||
from django.utils.dateparse import parse_datetime
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
|
||||
class GetRequestsJsonTest(TestWithUser, WebTest):
|
||||
|
@ -20,6 +25,19 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
beef_chuck, _ = DraftDomain.objects.get_or_create(name="beef-chuck.gov")
|
||||
stew_beef, _ = DraftDomain.objects.get_or_create(name="stew-beef.gov")
|
||||
|
||||
# Create Portfolio
|
||||
cls.portfolio = Portfolio.objects.create(creator=cls.user, organization_name="Example org")
|
||||
|
||||
# create a second user to assign requests to
|
||||
cls.user2 = User.objects.create(
|
||||
username="test_user2",
|
||||
first_name="Second",
|
||||
last_name="last",
|
||||
email="info2@example.com",
|
||||
phone="8003111234",
|
||||
title="title",
|
||||
)
|
||||
|
||||
# Create domain requests for the user
|
||||
cls.domain_requests = [
|
||||
DomainRequest.objects.create(
|
||||
|
@ -28,6 +46,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
last_submitted_date="2024-01-01",
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
created_at="2024-01-01",
|
||||
portfolio=cls.portfolio,
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=cls.user,
|
||||
|
@ -42,6 +61,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
last_submitted_date="2024-03-01",
|
||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||
created_at="2024-03-01",
|
||||
portfolio=cls.portfolio,
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=cls.user,
|
||||
|
@ -113,6 +133,14 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
status=DomainRequest.DomainRequestStatus.APPROVED,
|
||||
created_at="2024-12-01",
|
||||
),
|
||||
DomainRequest.objects.create(
|
||||
creator=cls.user2,
|
||||
requested_domain=None,
|
||||
last_submitted_date="2024-12-01",
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
created_at="2024-12-01",
|
||||
portfolio=cls.portfolio,
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
|
@ -120,6 +148,7 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
super().tearDownClass()
|
||||
DomainRequest.objects.all().delete()
|
||||
DraftDomain.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
|
||||
def test_get_domain_requests_json_authenticated(self):
|
||||
"""Test that domain requests are returned properly for an authenticated user."""
|
||||
|
@ -262,6 +291,118 @@ class GetRequestsJsonTest(TestWithUser, WebTest):
|
|||
for expected_value, actual_value in zip(expected_domain_values, requested_domains):
|
||||
self.assertEqual(expected_value, actual_value)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_get_domain_requests_json_with_portfolio_view_all_requests(self):
|
||||
"""Test that an authenticated user gets the list of 3 requests for portfolio. The 3 requests
|
||||
are the requests that are associated with the portfolio."""
|
||||
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
|
||||
)
|
||||
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
|
||||
# Check pagination info
|
||||
self.assertEqual(data["page"], 1)
|
||||
self.assertFalse(data["has_next"])
|
||||
self.assertFalse(data["has_previous"])
|
||||
self.assertEqual(data["num_pages"], 1)
|
||||
|
||||
# Check the number of requests
|
||||
self.assertEqual(len(data["domain_requests"]), 3)
|
||||
|
||||
# Expected domain requests
|
||||
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2], self.domain_requests[13]]
|
||||
|
||||
# Extract fields from response
|
||||
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||
creator = [domain_request["creator"] for domain_request in data["domain_requests"]]
|
||||
status = [domain_request["status"] for domain_request in data["domain_requests"]]
|
||||
action_urls = [domain_request["action_url"] for domain_request in data["domain_requests"]]
|
||||
action_labels = [domain_request["action_label"] for domain_request in data["domain_requests"]]
|
||||
svg_icons = [domain_request["svg_icon"] for domain_request in data["domain_requests"]]
|
||||
|
||||
# Check fields for each domain_request
|
||||
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||
if expected_domain_request.requested_domain:
|
||||
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||
else:
|
||||
self.assertIsNone(requested_domain[i])
|
||||
self.assertEqual(expected_domain_request.creator.email, creator[i])
|
||||
# Check action url, action label and svg icon
|
||||
# Example domain requests will test each of below three scenarios
|
||||
if creator[i] != self.user.email:
|
||||
# Test case where action is View
|
||||
self.assertEqual("View", action_labels[i])
|
||||
self.assertEqual("#", action_urls[i])
|
||||
self.assertEqual("visibility", svg_icons[i])
|
||||
elif status[i] in [
|
||||
DomainRequest.DomainRequestStatus.STARTED.label,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED.label,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN.label,
|
||||
]:
|
||||
# Test case where action is Edit
|
||||
self.assertEqual("Edit", action_labels[i])
|
||||
self.assertEqual(
|
||||
reverse("edit-domain-request", kwargs={"id": expected_domain_request.id}), action_urls[i]
|
||||
)
|
||||
self.assertEqual("edit", svg_icons[i])
|
||||
else:
|
||||
# Test case where action is Manage
|
||||
self.assertEqual("Manage", action_labels[i])
|
||||
self.assertEqual(
|
||||
reverse("domain-request-status", kwargs={"pk": expected_domain_request.id}), action_urls[i]
|
||||
)
|
||||
self.assertEqual("settings", svg_icons[i])
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_get_domain_requests_json_with_portfolio_edit_requests(self):
|
||||
"""Test that an authenticated user gets the list of 2 requests for portfolio. The 2 requests
|
||||
are the requests that are associated with the portfolio and owned by self.user."""
|
||||
|
||||
UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||
)
|
||||
|
||||
response = self.app.get(reverse("get_domain_requests_json"), {"portfolio": self.portfolio.id})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json
|
||||
|
||||
# Check pagination info
|
||||
self.assertEqual(data["page"], 1)
|
||||
self.assertFalse(data["has_next"])
|
||||
self.assertFalse(data["has_previous"])
|
||||
self.assertEqual(data["num_pages"], 1)
|
||||
|
||||
# Check the number of requests
|
||||
self.assertEqual(len(data["domain_requests"]), 2)
|
||||
|
||||
# Expected domain requests
|
||||
expected_domain_requests = [self.domain_requests[0], self.domain_requests[2]]
|
||||
|
||||
# Extract fields from response, since other tests test all fields, only ids and requested
|
||||
# domains tested in this test
|
||||
domain_request_ids = [domain_request["id"] for domain_request in data["domain_requests"]]
|
||||
requested_domain = [domain_request["requested_domain"] for domain_request in data["domain_requests"]]
|
||||
|
||||
# Check fields for each domain_request
|
||||
for i, expected_domain_request in enumerate(expected_domain_requests):
|
||||
self.assertEqual(expected_domain_request.id, domain_request_ids[i])
|
||||
if expected_domain_request.requested_domain:
|
||||
self.assertEqual(expected_domain_request.requested_domain.name, requested_domain[i])
|
||||
else:
|
||||
self.assertIsNone(requested_domain[i])
|
||||
|
||||
def test_pagination(self):
|
||||
"""Test that pagination works properly. There are 11 total non-approved requests and
|
||||
a page size of 10"""
|
||||
|
|
|
@ -77,17 +77,15 @@ class FSMErrorCodes(IntEnum):
|
|||
- 1 APPROVE_DOMAIN_IN_USE The domain is already in use
|
||||
- 2 NO_INVESTIGATOR No investigator is assigned
|
||||
- 3 INVESTIGATOR_NOT_STAFF Investigator is a non-staff user
|
||||
- 4 INVESTIGATOR_NOT_SUBMITTER The form submitter is not the investigator
|
||||
- 5 NO_REJECTION_REASON No rejection reason is specified
|
||||
- 6 NO_ACTION_NEEDED_REASON No action needed reason is specified
|
||||
- 4 NO_REJECTION_REASON No rejection reason is specified
|
||||
- 5 NO_ACTION_NEEDED_REASON No action needed reason is specified
|
||||
"""
|
||||
|
||||
APPROVE_DOMAIN_IN_USE = 1
|
||||
NO_INVESTIGATOR = 2
|
||||
INVESTIGATOR_NOT_STAFF = 3
|
||||
INVESTIGATOR_NOT_SUBMITTER = 4
|
||||
NO_REJECTION_REASON = 5
|
||||
NO_ACTION_NEEDED_REASON = 6
|
||||
NO_REJECTION_REASON = 4
|
||||
NO_ACTION_NEEDED_REASON = 5
|
||||
|
||||
|
||||
class FSMDomainRequestError(Exception):
|
||||
|
@ -100,7 +98,6 @@ class FSMDomainRequestError(Exception):
|
|||
FSMErrorCodes.APPROVE_DOMAIN_IN_USE: ("Cannot approve. Requested domain is already in use."),
|
||||
FSMErrorCodes.NO_INVESTIGATOR: ("Investigator is required for this status."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_STAFF: ("Investigator is not a staff user."),
|
||||
FSMErrorCodes.INVESTIGATOR_NOT_SUBMITTER: ("Only the assigned investigator can make this change."),
|
||||
FSMErrorCodes.NO_REJECTION_REASON: ("A reason is required for this status."),
|
||||
FSMErrorCodes.NO_ACTION_NEEDED_REASON: ("A reason is required for this status."),
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ from .domain import (
|
|||
DomainNameserversView,
|
||||
DomainDNSSECView,
|
||||
DomainDsDataView,
|
||||
DomainYourContactInformationView,
|
||||
DomainSecurityEmailView,
|
||||
DomainUsersView,
|
||||
DomainAddUserView,
|
||||
|
|
|
@ -40,7 +40,6 @@ from registrar.models.utility.contact_error import ContactError
|
|||
from registrar.views.utility.permission_views import UserDomainRolePermissionDeleteView
|
||||
|
||||
from ..forms import (
|
||||
UserForm,
|
||||
SeniorOfficialContactForm,
|
||||
DomainOrgNameAddressForm,
|
||||
DomainAddUserForm,
|
||||
|
@ -59,7 +58,6 @@ from epplibwrapper import (
|
|||
|
||||
from ..utility.email import send_templated_email, EmailSendingError
|
||||
from .utility import DomainPermissionView, DomainInvitationPermissionDeleteView
|
||||
from waffle.decorators import waffle_flag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -175,7 +173,7 @@ class DomainView(DomainBaseView):
|
|||
If particular views allow permissions, they will need to override
|
||||
this function."""
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if self.request.user.has_domains_portfolio_permission(portfolio):
|
||||
if self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||
if Domain.objects.filter(id=pk).exists():
|
||||
domain = Domain.objects.get(id=pk)
|
||||
if domain.domain_info.portfolio == portfolio:
|
||||
|
@ -641,38 +639,6 @@ class DomainDsDataView(DomainFormBaseView):
|
|||
return super().form_valid(formset)
|
||||
|
||||
|
||||
class DomainYourContactInformationView(DomainFormBaseView):
|
||||
"""Domain your contact information editing view."""
|
||||
|
||||
template_name = "domain_your_contact_information.html"
|
||||
form_class = UserForm
|
||||
|
||||
@waffle_flag("!profile_feature") # type: ignore
|
||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_form_kwargs(self, *args, **kwargs):
|
||||
"""Add domain_info.submitter instance to make a bound form."""
|
||||
form_kwargs = super().get_form_kwargs(*args, **kwargs)
|
||||
form_kwargs["instance"] = self.request.user
|
||||
return form_kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
"""Redirect to the your contact information for the domain."""
|
||||
return reverse("domain-your-contact-information", kwargs={"pk": self.object.pk})
|
||||
|
||||
def form_valid(self, form):
|
||||
"""The form is valid, call setter in model."""
|
||||
|
||||
# Post to DB using values from the form
|
||||
form.save()
|
||||
|
||||
messages.success(self.request, "Your contact information for all your domains has been updated.")
|
||||
|
||||
# superclass has the redirect
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class DomainSecurityEmailView(DomainFormBaseView):
|
||||
"""Domain security email editing view."""
|
||||
|
||||
|
@ -837,6 +803,23 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
)
|
||||
return None
|
||||
|
||||
# Check to see if an invite has already been sent
|
||||
try:
|
||||
invite = DomainInvitation.objects.get(email=email, domain=self.object)
|
||||
# check if the invite has already been accepted
|
||||
if invite.status == DomainInvitation.DomainInvitationStatus.RETRIEVED:
|
||||
add_success = False
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email} is already a manager for this domain.",
|
||||
)
|
||||
else:
|
||||
add_success = False
|
||||
# else if it has been sent but not accepted
|
||||
messages.warning(self.request, f"{email} has already been invited to this domain")
|
||||
except Exception:
|
||||
logger.error("An error occured")
|
||||
|
||||
try:
|
||||
send_templated_email(
|
||||
"emails/domain_invitation.txt",
|
||||
|
@ -862,24 +845,13 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
|
||||
def _make_invitation(self, email_address: str, requestor: User):
|
||||
"""Make a Domain invitation for this email and redirect with a message."""
|
||||
# Check to see if an invite has already been sent (NOTE: we do not want to create an invite just yet.)
|
||||
try:
|
||||
invite = DomainInvitation.objects.get(email=email_address, domain=self.object)
|
||||
# that invitation already existed
|
||||
if invite is not None:
|
||||
messages.warning(
|
||||
self.request,
|
||||
f"{email_address} has already been invited to this domain.",
|
||||
)
|
||||
except DomainInvitation.DoesNotExist:
|
||||
# Try to send the invitation. If it succeeds, add it to the DomainInvitation table.
|
||||
try:
|
||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
||||
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
self._send_domain_invitation_email(email=email_address, requestor=requestor)
|
||||
except EmailSendingError:
|
||||
messages.warning(self.request, "Could not send email invitation.")
|
||||
else:
|
||||
# (NOTE: only create a domainInvitation if the e-mail sends correctly)
|
||||
DomainInvitation.objects.get_or_create(email=email_address, domain=self.object)
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def form_valid(self, form):
|
||||
|
@ -919,11 +891,9 @@ class DomainAddUserView(DomainFormBaseView):
|
|||
role=UserDomainRole.Roles.MANAGER,
|
||||
)
|
||||
except IntegrityError:
|
||||
# User already has the desired role! Do nothing??
|
||||
pass
|
||||
|
||||
messages.success(self.request, f"Added user {requested_email}.")
|
||||
|
||||
messages.warning(self.request, f"{requested_email} is already a manager for this domain")
|
||||
else:
|
||||
messages.success(self.request, f"Added user {requested_email}.")
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
|
||||
|
|
|
@ -22,8 +22,6 @@ from .utility import (
|
|||
DomainRequestWizardPermissionView,
|
||||
)
|
||||
|
||||
from waffle.decorators import flag_is_active, waffle_flag
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -45,7 +43,6 @@ class Step(StrEnum):
|
|||
CURRENT_SITES = "current_sites"
|
||||
DOTGOV_DOMAIN = "dotgov_domain"
|
||||
PURPOSE = "purpose"
|
||||
YOUR_CONTACT = "your_contact"
|
||||
OTHER_CONTACTS = "other_contacts"
|
||||
ADDITIONAL_DETAILS = "additional_details"
|
||||
REQUIREMENTS = "requirements"
|
||||
|
@ -91,7 +88,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
Step.CURRENT_SITES: _("Current websites"),
|
||||
Step.DOTGOV_DOMAIN: _(".gov domain"),
|
||||
Step.PURPOSE: _("Purpose of your domain"),
|
||||
Step.YOUR_CONTACT: _("Your contact information"),
|
||||
Step.OTHER_CONTACTS: _("Other employees from your organization"),
|
||||
Step.ADDITIONAL_DETAILS: _("Additional details"),
|
||||
Step.REQUIREMENTS: _("Requirements for operating a .gov domain"),
|
||||
|
@ -152,7 +148,14 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
except DomainRequest.DoesNotExist:
|
||||
logger.debug("DomainRequest id %s did not have a DomainRequest" % id)
|
||||
|
||||
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
||||
# If a user is creating a request, we assume that perms are handled upstream
|
||||
if self.request.user.is_org_user(self.request):
|
||||
self._domain_request = DomainRequest.objects.create(
|
||||
creator=self.request.user,
|
||||
portfolio=self.request.session.get("portfolio"),
|
||||
)
|
||||
else:
|
||||
self._domain_request = DomainRequest.objects.create(creator=self.request.user)
|
||||
|
||||
self.storage["domain_request_id"] = self._domain_request.id
|
||||
return self._domain_request
|
||||
|
@ -375,7 +378,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
),
|
||||
"dotgov_domain": self.domain_request.requested_domain is not None,
|
||||
"purpose": self.domain_request.purpose is not None,
|
||||
"your_contact": self.domain_request.submitter is not None,
|
||||
"other_contacts": (
|
||||
self.domain_request.other_contacts.exists()
|
||||
or self.domain_request.no_other_contacts_rationale is not None
|
||||
|
@ -395,6 +397,10 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
def get_context_data(self):
|
||||
"""Define context for access on all wizard pages."""
|
||||
|
||||
requested_domain_name = None
|
||||
if self.domain_request.requested_domain is not None:
|
||||
requested_domain_name = self.domain_request.requested_domain.name
|
||||
|
||||
context_stuff = {}
|
||||
if DomainRequest._form_complete(self.domain_request, self.request):
|
||||
modal_button = '<button type="submit" ' 'class="usa-button" ' ">Submit request</button>"
|
||||
|
@ -411,6 +417,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
You’ll only be able to withdraw your request.",
|
||||
"review_form_is_complete": True,
|
||||
"user": self.request.user,
|
||||
"requested_domain__name": requested_domain_name,
|
||||
}
|
||||
else: # form is not complete
|
||||
modal_button = '<button type="button" class="usa-button" data-close-modal>Return to request</button>'
|
||||
|
@ -426,6 +433,7 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
Return to the request and visit the steps that are marked as "incomplete."',
|
||||
"review_form_is_complete": False,
|
||||
"user": self.request.user,
|
||||
"requested_domain__name": requested_domain_name,
|
||||
}
|
||||
return context_stuff
|
||||
|
||||
|
@ -439,9 +447,6 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
if condition:
|
||||
step_list.append(step)
|
||||
|
||||
if flag_is_active(self.request, "profile_feature"):
|
||||
step_list.remove(Step.YOUR_CONTACT)
|
||||
|
||||
return step_list
|
||||
|
||||
def goto(self, step):
|
||||
|
@ -505,7 +510,11 @@ class DomainRequestWizard(DomainRequestWizardPermissionView, TemplateView):
|
|||
# if user opted to save progress and return,
|
||||
# return them to the home page
|
||||
if button == "save_and_return":
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
if request.user.is_org_user(request):
|
||||
return HttpResponseRedirect(reverse("domain-requests"))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
# otherwise, proceed as normal
|
||||
return self.goto_next_step()
|
||||
|
||||
|
@ -582,15 +591,6 @@ class Purpose(DomainRequestWizard):
|
|||
forms = [forms.PurposeForm]
|
||||
|
||||
|
||||
class YourContact(DomainRequestWizard):
|
||||
template_name = "domain_request_your_contact.html"
|
||||
forms = [forms.YourContactForm]
|
||||
|
||||
@waffle_flag("!profile_feature") # type: ignore
|
||||
def dispatch(self, request, *args, **kwargs): # type: ignore
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class OtherContacts(DomainRequestWizard):
|
||||
template_name = "domain_request_other_contacts.html"
|
||||
forms = [forms.OtherContactsYesNoForm, forms.OtherContactsFormSet, forms.NoOtherContactsForm]
|
||||
|
@ -774,7 +774,10 @@ class DomainRequestWithdrawn(DomainRequestPermissionWithdrawView):
|
|||
domain_request = DomainRequest.objects.get(id=self.kwargs["pk"])
|
||||
domain_request.withdraw()
|
||||
domain_request.save()
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
if self.request.user.is_org_user(self.request):
|
||||
return HttpResponseRedirect(reverse("domain-requests"))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse("home"))
|
||||
|
||||
|
||||
class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
||||
|
@ -813,7 +816,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
|
||||
# After a delete occurs, do a second sweep on any returned duplicates.
|
||||
# This determines if any of these three fields share a contact, which is used for
|
||||
# the edge case where the same user may be an SO, and a submitter, for example.
|
||||
# the edge case where the same user may be an SO, and a creator, for example.
|
||||
if len(duplicates) > 0:
|
||||
duplicates_to_delete, _ = self._get_orphaned_contacts(domain_request, check_db=True)
|
||||
Contact.objects.filter(id__in=duplicates_to_delete).delete()
|
||||
|
@ -826,7 +829,7 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
Collects all orphaned contacts associated with a given DomainRequest object.
|
||||
|
||||
An orphaned contact is defined as a contact that is associated with the domain request,
|
||||
but not with any other domain_request. This includes the senior official, the submitter,
|
||||
but not with any other domain_request. This includes the senior official, the creator,
|
||||
and any other contacts linked to the domain_request.
|
||||
|
||||
Parameters:
|
||||
|
@ -842,18 +845,16 @@ class DomainRequestDeleteView(DomainRequestPermissionDeleteView):
|
|||
|
||||
# Get each contact object on the DomainRequest object
|
||||
so = domain_request.senior_official
|
||||
submitter = domain_request.submitter
|
||||
other_contacts = list(domain_request.other_contacts.all())
|
||||
other_contact_ids = domain_request.other_contacts.all().values_list("id", flat=True)
|
||||
|
||||
# Check if the desired item still exists in the DB
|
||||
if check_db:
|
||||
so = self._get_contacts_by_id([so.id]).first() if so is not None else None
|
||||
submitter = self._get_contacts_by_id([submitter.id]).first() if submitter is not None else None
|
||||
other_contacts = self._get_contacts_by_id(other_contact_ids)
|
||||
|
||||
# Pair each contact with its db related name for use in checking if it has joins
|
||||
checked_contacts = [(so, "senior_official"), (submitter, "submitted_domain_requests")]
|
||||
checked_contacts = [(so, "senior_official")]
|
||||
checked_contacts.extend((contact, "contact_domain_requests") for contact in other_contacts)
|
||||
|
||||
for contact, related_name in checked_contacts:
|
||||
|
|
|
@ -10,16 +10,59 @@ from django.db.models import Q
|
|||
@login_required
|
||||
def get_domain_requests_json(request):
|
||||
"""Given the current request,
|
||||
get all domain requests that are associated with the request user and exclude the APPROVED ones"""
|
||||
get all domain requests that are associated with the request user and exclude the APPROVED ones.
|
||||
If we are on the portfolio requests page, limit the response to only those requests associated with
|
||||
the given portfolio."""
|
||||
|
||||
domain_requests = DomainRequest.objects.filter(creator=request.user).exclude(
|
||||
domain_request_ids = get_domain_request_ids_from_request(request)
|
||||
|
||||
objects = DomainRequest.objects.filter(id__in=domain_request_ids)
|
||||
unfiltered_total = objects.count()
|
||||
|
||||
objects = apply_search(objects, request)
|
||||
objects = apply_sorting(objects, request)
|
||||
|
||||
paginator = Paginator(objects, 10)
|
||||
page_number = request.GET.get("page", 1)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
domain_requests = [
|
||||
serialize_domain_request(domain_request, request.user) for domain_request in page_obj.object_list
|
||||
]
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"domain_requests": domain_requests,
|
||||
"has_next": page_obj.has_next(),
|
||||
"has_previous": page_obj.has_previous(),
|
||||
"page": page_obj.number,
|
||||
"num_pages": paginator.num_pages,
|
||||
"total": paginator.count,
|
||||
"unfiltered_total": unfiltered_total,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_domain_request_ids_from_request(request):
|
||||
"""Get domain request ids from request.
|
||||
|
||||
If portfolio specified, return domain request ids associated with portfolio.
|
||||
Otherwise, return domain request ids associated with request.user.
|
||||
"""
|
||||
portfolio = request.GET.get("portfolio")
|
||||
filter_condition = Q(creator=request.user)
|
||||
if portfolio:
|
||||
if request.user.is_org_user(request) and request.user.has_view_all_requests_portfolio_permission(portfolio):
|
||||
filter_condition = Q(portfolio=portfolio)
|
||||
else:
|
||||
filter_condition = Q(portfolio=portfolio, creator=request.user)
|
||||
domain_requests = DomainRequest.objects.filter(filter_condition).exclude(
|
||||
status=DomainRequest.DomainRequestStatus.APPROVED
|
||||
)
|
||||
unfiltered_total = domain_requests.count()
|
||||
return domain_requests.values_list("id", flat=True)
|
||||
|
||||
# Handle sorting
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
|
||||
def apply_search(queryset, request):
|
||||
search_term = request.GET.get("search_term")
|
||||
|
||||
if search_term:
|
||||
|
@ -30,70 +73,60 @@ def get_domain_requests_json(request):
|
|||
# If yes, we should return domain requests that do not have a
|
||||
# requested_domain (those display as New domain request in the UI)
|
||||
if search_term_lower in new_domain_request_text:
|
||||
domain_requests = domain_requests.filter(
|
||||
queryset = queryset.filter(
|
||||
Q(requested_domain__name__icontains=search_term) | Q(requested_domain__isnull=True)
|
||||
)
|
||||
else:
|
||||
domain_requests = domain_requests.filter(Q(requested_domain__name__icontains=search_term))
|
||||
queryset = queryset.filter(Q(requested_domain__name__icontains=search_term))
|
||||
return queryset
|
||||
|
||||
|
||||
def apply_sorting(queryset, request):
|
||||
sort_by = request.GET.get("sort_by", "id") # Default to 'id'
|
||||
order = request.GET.get("order", "asc") # Default to 'asc'
|
||||
|
||||
if order == "desc":
|
||||
sort_by = f"-{sort_by}"
|
||||
domain_requests = domain_requests.order_by(sort_by)
|
||||
page_number = request.GET.get("page", 1)
|
||||
paginator = Paginator(domain_requests, 10)
|
||||
page_obj = paginator.get_page(page_number)
|
||||
return queryset.order_by(sort_by)
|
||||
|
||||
domain_requests_data = [
|
||||
{
|
||||
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
||||
"last_submitted_date": domain_request.last_submitted_date,
|
||||
"status": domain_request.get_status_display(),
|
||||
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
||||
"id": domain_request.id,
|
||||
"is_deletable": domain_request.status
|
||||
in [DomainRequest.DomainRequestStatus.STARTED, DomainRequest.DomainRequestStatus.WITHDRAWN],
|
||||
"action_url": (
|
||||
reverse("edit-domain-request", kwargs={"id": domain_request.id})
|
||||
if domain_request.status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else reverse("domain-request-status", kwargs={"pk": domain_request.id})
|
||||
),
|
||||
"action_label": (
|
||||
"Edit"
|
||||
if domain_request.status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else "Manage"
|
||||
),
|
||||
"svg_icon": (
|
||||
"edit"
|
||||
if domain_request.status
|
||||
in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
else "settings"
|
||||
),
|
||||
}
|
||||
for domain_request in page_obj
|
||||
|
||||
def serialize_domain_request(domain_request, user):
|
||||
# Determine if the request is deletable
|
||||
is_deletable = domain_request.status in [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
"domain_requests": domain_requests_data,
|
||||
"has_next": page_obj.has_next(),
|
||||
"has_previous": page_obj.has_previous(),
|
||||
"page": page_obj.number,
|
||||
"num_pages": paginator.num_pages,
|
||||
"total": paginator.count,
|
||||
"unfiltered_total": unfiltered_total,
|
||||
}
|
||||
)
|
||||
# Determine action label based on user permissions and request status
|
||||
editable_statuses = [
|
||||
DomainRequest.DomainRequestStatus.STARTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
]
|
||||
|
||||
if user.has_edit_request_portfolio_permission and domain_request.creator == user:
|
||||
action_label = "Edit" if domain_request.status in editable_statuses else "Manage"
|
||||
else:
|
||||
action_label = "View"
|
||||
|
||||
# Map the action label to corresponding URLs and icons
|
||||
action_url_map = {
|
||||
"Edit": reverse("edit-domain-request", kwargs={"id": domain_request.id}),
|
||||
"Manage": reverse("domain-request-status", kwargs={"pk": domain_request.id}),
|
||||
"View": "#",
|
||||
}
|
||||
|
||||
svg_icon_map = {"Edit": "edit", "Manage": "settings", "View": "visibility"}
|
||||
|
||||
return {
|
||||
"requested_domain": domain_request.requested_domain.name if domain_request.requested_domain else None,
|
||||
"last_submitted_date": domain_request.last_submitted_date,
|
||||
"status": domain_request.get_status_display(),
|
||||
"created_at": format(domain_request.created_at, "c"), # Serialize to ISO 8601
|
||||
"creator": domain_request.creator.email,
|
||||
"id": domain_request.id,
|
||||
"is_deletable": is_deletable,
|
||||
"action_url": action_url_map.get(action_label),
|
||||
"action_label": action_label,
|
||||
"svg_icon": svg_icon_map.get(action_label),
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging
|
||||
from django.http import JsonResponse
|
||||
from django.core.paginator import Paginator
|
||||
from registrar.models import UserDomainRole, Domain, DomainInformation
|
||||
from registrar.models import UserDomainRole, Domain, DomainInformation, User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.urls import reverse
|
||||
from django.db.models import Q
|
||||
|
@ -50,7 +50,8 @@ def get_domain_ids_from_request(request):
|
|||
"""
|
||||
portfolio = request.GET.get("portfolio")
|
||||
if portfolio:
|
||||
if request.user.is_org_user(request) and request.user.has_view_all_domains_permission(portfolio):
|
||||
current_user: User = request.user
|
||||
if current_user.is_org_user(request) and current_user.has_view_all_domains_portfolio_permission(portfolio):
|
||||
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
||||
return domain_infos.values_list("domain_id", flat=True)
|
||||
else:
|
||||
|
|
|
@ -42,12 +42,41 @@ class PortfolioDomainRequestsView(PortfolioDomainRequestsPermissionView, View):
|
|||
|
||||
|
||||
class PortfolioNoDomainsView(NoPortfolioDomainsPermissionView, View):
|
||||
"""Some users have access to the underlying portfolio, but not any domains.
|
||||
"""Some users have access to the underlying portfolio, but not any domains.
|
||||
This is a custom view which explains that to the user - and denotes who to contact.
|
||||
"""
|
||||
|
||||
model = Portfolio
|
||||
template_name = "no_portfolio_domains.html"
|
||||
template_name = "portfolio_no_domains.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, context=self.get_context_data())
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""Add additional context data to the template."""
|
||||
# We can override the base class. This view only needs this item.
|
||||
context = {}
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if portfolio:
|
||||
admin_ids = UserPortfolioPermission.objects.filter(
|
||||
portfolio=portfolio,
|
||||
roles__overlap=[
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
],
|
||||
).values_list("user__id", flat=True)
|
||||
|
||||
admin_users = User.objects.filter(id__in=admin_ids)
|
||||
context["portfolio_administrators"] = admin_users
|
||||
return context
|
||||
|
||||
|
||||
class PortfolioNoDomainRequestsView(NoPortfolioDomainsPermissionView, View):
|
||||
"""Some users have access to the underlying portfolio, but not any domain requests.
|
||||
This is a custom view which explains that to the user - and denotes who to contact.
|
||||
"""
|
||||
|
||||
model = Portfolio
|
||||
template_name = "portfolio_no_requests.html"
|
||||
|
||||
def get(self, request):
|
||||
return render(request, self.template_name, context=self.get_context_data())
|
||||
|
|
|
@ -81,12 +81,12 @@ class OrderableFieldsMixin:
|
|||
Or for fields with multiple order_fields:
|
||||
|
||||
```
|
||||
def get_sortable_submitter(self, obj):
|
||||
return obj.submitter
|
||||
def get_sortable_creator(self, obj):
|
||||
return obj.creator
|
||||
# Allows column order sorting
|
||||
get_sortable_submitter.admin_order_field = ["submitter__first_name", "submitter__last_name"]
|
||||
get_sortable_creator.admin_order_field = ["creator__first_name", "creator__last_name"]
|
||||
# Sets column's header
|
||||
get_sortable_submitter.short_description = "submitter"
|
||||
get_sortable_creator.short_description = "creator"
|
||||
```
|
||||
|
||||
Parameters:
|
||||
|
@ -114,8 +114,8 @@ class OrderableFieldsMixin:
|
|||
|
||||
Returns (example):
|
||||
```
|
||||
def get_submitter(self, obj):
|
||||
return obj.submitter
|
||||
def get_creator(self, obj):
|
||||
return obj.creator
|
||||
```
|
||||
"""
|
||||
attr = getattr(obj, field)
|
||||
|
@ -433,7 +433,7 @@ class PortfolioDomainsPermission(PortfolioBasePermission):
|
|||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_domains_portfolio_permission(portfolio):
|
||||
if not self.request.user.has_any_domains_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
@ -450,7 +450,7 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
|||
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||
|
||||
portfolio = self.request.session.get("portfolio")
|
||||
if not self.request.user.has_domain_requests_portfolio_permission(portfolio):
|
||||
if not self.request.user.has_any_requests_portfolio_permission(portfolio):
|
||||
return False
|
||||
|
||||
return super().has_permission()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue