mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-13 16:17:01 +02:00
Merge branch 'main' into rjm/doc-uswds-updates
This commit is contained in:
commit
5ab67d4b65
44 changed files with 1667 additions and 402 deletions
|
@ -207,6 +207,17 @@ Linters:
|
|||
docker-compose exec app ./manage.py lint
|
||||
```
|
||||
|
||||
### Get availability for domain requests to work locally
|
||||
|
||||
If you're on local (localhost:8080) and want to submit a domain request, and keep getting the "We’re experiencing a system error. Please wait a few minutes and try again. If you continue to get this error, contact help@get.gov." error, you can get past the availability check by updating the available() function in registrar/models/domain.py to return True and comment everything else out - see below for reference!
|
||||
|
||||
```
|
||||
@classmethod
|
||||
def available(cls, domain: str) -> bool:
|
||||
# Comment everything else out in the function
|
||||
return True
|
||||
```
|
||||
|
||||
### Testing behind logged in pages
|
||||
|
||||
To test behind logged in pages with external tools, like `pa11y-ci` or `OWASP Zap`, add
|
||||
|
|
|
@ -1846,7 +1846,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
requested_user = get_requested_user(requested_email)
|
||||
|
||||
permission_exists = UserPortfolioPermission.objects.filter(
|
||||
user__email=requested_email, portfolio=portfolio, user__email__isnull=False
|
||||
user__email__iexact=requested_email, portfolio=portfolio, user__email__isnull=False
|
||||
).exists()
|
||||
if not permission_exists:
|
||||
# if permission does not exist for a user with requested_email, send email
|
||||
|
@ -1856,9 +1856,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
portfolio=portfolio,
|
||||
is_admin_invitation=is_admin_invitation,
|
||||
):
|
||||
messages.warning(
|
||||
self.request, "Could not send email notification to existing organization admins."
|
||||
)
|
||||
messages.warning(request, "Could not send email notification to existing organization admins.")
|
||||
# if user exists for email, immediately retrieve portfolio invitation upon creation
|
||||
if requested_user is not None:
|
||||
obj.retrieve()
|
||||
|
@ -2758,17 +2756,16 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"investigator",
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
"senior_official",
|
||||
]
|
||||
|
||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||
|
||||
# Table ordering
|
||||
# NOTE: This impacts the select2 dropdowns (combobox)
|
||||
# Currentl, there's only one for requests on DomainInfo
|
||||
# Currently, there's only one for requests on DomainInfo
|
||||
ordering = ["-last_submitted_date", "requested_domain__name"]
|
||||
|
||||
change_form_template = "django/admin/domain_request_change_form.html"
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = super().get_fieldsets(request, obj)
|
||||
|
||||
|
|
60
src/registrar/assets/src/js/getgov-admin/andi.js
Normal file
60
src/registrar/assets/src/js/getgov-admin/andi.js
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
This function intercepts all select2 dropdowns and adds aria content.
|
||||
It relies on an override in detail_table_fieldset.html that provides
|
||||
a span with a corresponding id for aria-describedby content.
|
||||
|
||||
This allows us to avoid overriding aria-label, which is used by select2
|
||||
to send the current dropdown selection to ANDI.
|
||||
*/
|
||||
export function initAriaInjectionsForSelect2Dropdowns() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Find all spans with "--aria-description" in their id
|
||||
const descriptionSpans = document.querySelectorAll('span[id*="--aria-description"]');
|
||||
|
||||
descriptionSpans.forEach(function (span) {
|
||||
// Extract the base ID from the span's id (remove "--aria-description")
|
||||
const fieldId = span.id.replace('--aria-description', '');
|
||||
const field = document.getElementById(fieldId);
|
||||
|
||||
if (field) {
|
||||
// If Select2 is already initialized, apply aria-describedby immediately
|
||||
if (field.classList.contains('select2-hidden-accessible')) {
|
||||
applyAriaDescribedBy(field, span.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use MutationObserver to detect Select2 initialization
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (document.getElementById(fieldId)?.classList.contains("select2-hidden-accessible")) {
|
||||
applyAriaDescribedBy(field, span.id);
|
||||
observer.disconnect(); // Stop observing after applying attributes
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Function to apply aria-describedby to Select2 UI
|
||||
function applyAriaDescribedBy(field, descriptionId) {
|
||||
let select2ElementDetected = false;
|
||||
const select2Id = "select2-" + field.id + "-container";
|
||||
|
||||
// Find the Select2 selection box
|
||||
const select2SpanThatTriggersAria = document.querySelector(`span[aria-labelledby='${select2Id}']`);
|
||||
|
||||
if (select2SpanThatTriggersAria) {
|
||||
select2SpanThatTriggersAria.setAttribute('aria-describedby', descriptionId);
|
||||
select2ElementDetected = true;
|
||||
}
|
||||
|
||||
// If no Select2 component was detected, apply aria-describedby directly to the field
|
||||
if (!select2ElementDetected) {
|
||||
field.setAttribute('aria-describedby', descriptionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -19,6 +19,7 @@ import { initDynamicDomainInformationFields } from './domain-information-form.js
|
|||
import { initDynamicDomainFields } from './domain-form.js';
|
||||
import { initAnalyticsDashboard } from './analytics.js';
|
||||
import { initButtonLinks } from './button-utils.js';
|
||||
import { initAriaInjectionsForSelect2Dropdowns } from './andi.js'
|
||||
|
||||
// General
|
||||
initModals();
|
||||
|
@ -26,6 +27,7 @@ initCopyToClipboard();
|
|||
initFilterHorizontalWidget();
|
||||
initDescriptions();
|
||||
initSubmitBar();
|
||||
initAriaInjectionsForSelect2Dropdowns();
|
||||
initButtonLinks();
|
||||
|
||||
// Domain request
|
||||
|
|
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal file
41
src/registrar/assets/src/js/getgov/domain-purpose-form.js
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { showElement } from './helpers.js';
|
||||
|
||||
export const domain_purpose_choice_callbacks = {
|
||||
'new': {
|
||||
callback: function(value, element) {
|
||||
//show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Explain why a new domain is required and why a ' +
|
||||
'subdomain of an existing domain doesn\'t meet your needs.' +
|
||||
'<br><br>' + // Adding double line break for spacing
|
||||
'Include any data that supports a clear public benefit or ' +
|
||||
'evidence user need for this new domain. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
},
|
||||
'redirect': {
|
||||
callback: function(value, element) {
|
||||
// show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Explain why a redirect is necessary. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
},
|
||||
'other': {
|
||||
callback: function(value, element) {
|
||||
// Show the purpose details container
|
||||
showElement(element);
|
||||
// change just the text inside the em tag
|
||||
const labelElement = element.querySelector('.usa-label em');
|
||||
labelElement.innerHTML = 'Describe how this domain will be used. ' +
|
||||
'<span class="usa-label--required">*</span>';
|
||||
},
|
||||
element: document.getElementById('purpose-details-container')
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { hookupYesNoListener } from './radios.js';
|
||||
import { hookupYesNoListener, hookupCallbacksToRadioToggler } from './radios.js';
|
||||
import { initDomainValidators } from './domain-validators.js';
|
||||
import { initFormsetsForms, triggerModalOnDsDataForm } from './formset-forms.js';
|
||||
import { initFormNameservers } from './form-nameservers'
|
||||
|
@ -16,6 +16,7 @@ import { initDomainManagersPage } from './domain-managers.js';
|
|||
import { initDomainDSData } from './domain-dsdata.js';
|
||||
import { initDomainDNSSEC } from './domain-dnssec.js';
|
||||
import { initFormErrorHandling } from './form-errors.js';
|
||||
import { domain_purpose_choice_callbacks } from './domain-purpose-form.js';
|
||||
import { initButtonLinks } from '../getgov-admin/button-utils.js';
|
||||
|
||||
initDomainValidators();
|
||||
|
@ -27,6 +28,14 @@ initFormNameservers();
|
|||
hookupYesNoListener("other_contacts-has_other_contacts",'other-employees', 'no-other-employees');
|
||||
hookupYesNoListener("additional_details-has_anything_else_text",'anything-else', null);
|
||||
hookupYesNoListener("additional_details-has_cisa_representative",'cisa-representative', null);
|
||||
hookupYesNoListener("dotgov_domain-feb_naming_requirements", null, "domain-naming-requirements-details-container");
|
||||
|
||||
hookupCallbacksToRadioToggler("purpose-feb_purpose_choice", domain_purpose_choice_callbacks);
|
||||
|
||||
hookupYesNoListener("purpose-has_timeframe", "purpose-timeframe-details-container", null);
|
||||
hookupYesNoListener("purpose-is_interagency_initiative", "purpose-interagency-initaitive-details-container", null);
|
||||
|
||||
|
||||
initializeUrbanizationToggle();
|
||||
|
||||
userProfileListener();
|
||||
|
|
|
@ -17,7 +17,7 @@ export function hookupYesNoListener(radioButtonName, elementIdToShowIfYes, eleme
|
|||
'False': elementIdToShowIfNo
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
|
@ -75,3 +75,57 @@ export function hookupRadioTogglerListener(radioButtonName, valueToElementMap) {
|
|||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookup listeners for radio togglers in form fields.
|
||||
*
|
||||
* Parameters:
|
||||
* - radioButtonName: The "name=" value for the radio buttons being used as togglers
|
||||
* - valueToCallbackMap: An object where keys are the values of the radio buttons,
|
||||
* and values are dictionaries containing a 'callback' key and an optional 'element' key.
|
||||
* If provided, the element will be passed in as the second argument to the callback function.
|
||||
*
|
||||
* Usage Example:
|
||||
* Assuming you have radio buttons with values 'option1', 'option2', and 'option3',
|
||||
* and corresponding callback functions 'function1', 'function2', 'function3' that will
|
||||
* apply to elements 'element1', 'element2', 'element3' respectively.
|
||||
*
|
||||
* hookupCallbacksToRadioToggler('exampleRadioGroup', {
|
||||
* 'option1': {callback: function1, element: element1},
|
||||
* 'option2': {callback: function2, element: element2},
|
||||
* 'option3': {callback: function3} // No element provided
|
||||
* });
|
||||
*
|
||||
* Picking the 'option1' radio button will call function1('option1', element1).
|
||||
* Picking the 'option3' radio button will call function3('option3') without a second parameter.
|
||||
**/
|
||||
export function hookupCallbacksToRadioToggler(radioButtonName, valueToCallbackMap) {
|
||||
// Get the radio buttons
|
||||
let radioButtons = document.querySelectorAll(`input[name="${radioButtonName}"]`);
|
||||
|
||||
function handleRadioButtonChange() {
|
||||
// Find the checked radio button
|
||||
let radioButtonChecked = document.querySelector(`input[name="${radioButtonName}"]:checked`);
|
||||
let selectedValue = radioButtonChecked ? radioButtonChecked.value : null;
|
||||
|
||||
// Execute the callback function for the selected value
|
||||
if (selectedValue && valueToCallbackMap[selectedValue]) {
|
||||
const entry = valueToCallbackMap[selectedValue];
|
||||
if ('element' in entry) {
|
||||
entry.callback(selectedValue, entry.element);
|
||||
} else {
|
||||
entry.callback(selectedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (radioButtons && radioButtons.length) {
|
||||
// Add event listener to each radio button
|
||||
radioButtons.forEach(function (radioButton) {
|
||||
radioButton.addEventListener('change', handleRadioButtonChange);
|
||||
});
|
||||
|
||||
// Initialize by checking the current state
|
||||
handleRadioButtonChange();
|
||||
}
|
||||
}
|
|
@ -99,9 +99,7 @@ body {
|
|||
}
|
||||
.section-outlined__search {
|
||||
flex-grow: 4;
|
||||
// Align right
|
||||
max-width: 383px;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,52 +89,52 @@ urlpatterns = [
|
|||
name="members",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>",
|
||||
"member/<int:member_pk>",
|
||||
views.PortfolioMemberView.as_view(),
|
||||
name="member",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/delete",
|
||||
"member/<int:member_pk>/delete",
|
||||
views.PortfolioMemberDeleteView.as_view(),
|
||||
name="member-delete",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/permissions",
|
||||
"member/<int:member_pk>/permissions",
|
||||
views.PortfolioMemberEditView.as_view(),
|
||||
name="member-permissions",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/domains",
|
||||
"member/<int:member_pk>/domains",
|
||||
views.PortfolioMemberDomainsView.as_view(),
|
||||
name="member-domains",
|
||||
),
|
||||
path(
|
||||
"member/<int:pk>/domains/edit",
|
||||
"member/<int:member_pk>/domains/edit",
|
||||
views.PortfolioMemberDomainsEditView.as_view(),
|
||||
name="member-domains-edit",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>",
|
||||
"invitedmember/<int:invitedmember_pk>",
|
||||
views.PortfolioInvitedMemberView.as_view(),
|
||||
name="invitedmember",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/delete",
|
||||
"invitedmember/<int:invitedmember_pk>/delete",
|
||||
views.PortfolioInvitedMemberDeleteView.as_view(),
|
||||
name="invitedmember-delete",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/permissions",
|
||||
"invitedmember/<int:invitedmember_pk>/permissions",
|
||||
views.PortfolioInvitedMemberEditView.as_view(),
|
||||
name="invitedmember-permissions",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/domains",
|
||||
"invitedmember/<int:invitedmember_pk>/domains",
|
||||
views.PortfolioInvitedMemberDomainsView.as_view(),
|
||||
name="invitedmember-domains",
|
||||
),
|
||||
path(
|
||||
"invitedmember/<int:pk>/domains/edit",
|
||||
"invitedmember/<int:invitedmember_pk>/domains/edit",
|
||||
views.PortfolioInvitedMemberDomainsEditView.as_view(),
|
||||
name="invitedmember-domains-edit",
|
||||
),
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import logging
|
||||
import functools
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.decorators import method_decorator
|
||||
from registrar.models import Domain, DomainInformation, DomainInvitation, DomainRequest, UserDomainRole
|
||||
from registrar.models.portfolio_invitation import PortfolioInvitation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Constants for clarity
|
||||
ALL = "all"
|
||||
|
@ -98,24 +104,38 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
if not user.is_authenticated or user.is_restricted():
|
||||
return False
|
||||
|
||||
portfolio = request.session.get("portfolio")
|
||||
# Define permission checks
|
||||
permission_checks = [
|
||||
(IS_STAFF, lambda: user.is_staff),
|
||||
(IS_DOMAIN_MANAGER, lambda: _is_domain_manager(user, **kwargs)),
|
||||
(
|
||||
IS_DOMAIN_MANAGER,
|
||||
lambda: (not user.is_org_user(request) and _is_domain_manager(user, **kwargs))
|
||||
or (
|
||||
user.is_org_user(request)
|
||||
and _is_domain_manager(user, **kwargs)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk"))
|
||||
),
|
||||
),
|
||||
(IS_STAFF_MANAGING_DOMAIN, lambda: _is_staff_managing_domain(request, **kwargs)),
|
||||
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")),
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_domains_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_any_domains_portfolio_permission(portfolio)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and _is_portfolio_member(request),
|
||||
lambda: _is_domain_manager(user, **kwargs)
|
||||
and _is_portfolio_member(request)
|
||||
and _domain_exists_under_portfolio(portfolio, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
|
||||
|
@ -129,34 +149,55 @@ def _user_has_permission(user, request, rules, **kwargs):
|
|||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_any_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domain_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_view_all_domain_requests_portfolio_permission(portfolio)
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk")),
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk"))
|
||||
and _domain_request_exists_under_portfolio(portfolio, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and (
|
||||
user.has_view_members_portfolio_permission(request.session.get("portfolio"))
|
||||
or user.has_edit_members_portfolio_permission(request.session.get("portfolio"))
|
||||
user.has_view_members_portfolio_permission(portfolio)
|
||||
or user.has_edit_members_portfolio_permission(portfolio)
|
||||
)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_edit_members_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_edit_members_portfolio_permission(portfolio)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_members_portfolio_permission(request.session.get("portfolio")),
|
||||
and user.has_view_members_portfolio_permission(portfolio)
|
||||
and (
|
||||
# AND rather than OR because these functions return true if the PK is not found.
|
||||
# This adds support for if the view simply doesn't have said PK.
|
||||
_member_exists_under_portfolio(portfolio, kwargs.get("member_pk"))
|
||||
and _member_invitation_exists_under_portfolio(portfolio, kwargs.get("invitedmember_pk"))
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
|
@ -191,6 +232,70 @@ def _is_domain_manager(user, **kwargs):
|
|||
return False
|
||||
|
||||
|
||||
def _domain_exists_under_portfolio(portfolio, domain_pk):
|
||||
"""Checks to see if the given domain exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not domain_pk:
|
||||
logger.warning(
|
||||
"_domain_exists_under_portfolio => Could not find domain_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return Domain.objects.filter(domain_info__portfolio=portfolio, id=domain_pk).exists()
|
||||
|
||||
|
||||
def _domain_request_exists_under_portfolio(portfolio, domain_request_pk):
|
||||
"""Checks to see if the given domain request exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not domain_request_pk:
|
||||
logger.warning(
|
||||
"_domain_request_exists_under_portfolio => Could not find domain_request_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return DomainRequest.objects.filter(portfolio=portfolio, id=domain_request_pk).exists()
|
||||
|
||||
|
||||
def _member_exists_under_portfolio(portfolio, member_pk):
|
||||
"""Checks to see if the given UserPortfolioPermission exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not member_pk:
|
||||
logger.warning(
|
||||
"_member_exists_under_portfolio => Could not find member_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return UserPortfolioPermission.objects.filter(portfolio=portfolio, id=member_pk).exists()
|
||||
|
||||
|
||||
def _member_invitation_exists_under_portfolio(portfolio, invitedmember_pk):
|
||||
"""Checks to see if the given PortfolioInvitation exists under the provided portfolio.
|
||||
HELPFUL REMINDER: Watch for typos! Verify that the kwarg key exists before using this function.
|
||||
Returns True if the pk is falsy. Otherwise, returns a bool if said object exists.
|
||||
"""
|
||||
# The view expects this, and the page will throw an error without this if it needs it.
|
||||
# Thus, if it is none, we are not checking on a specific record and therefore there is nothing to check.
|
||||
if not invitedmember_pk:
|
||||
logger.warning(
|
||||
"_member_invitation_exists_under_portfolio => Could not find invitedmember_pk. "
|
||||
"This is a non-issue if called from the right context."
|
||||
)
|
||||
return True
|
||||
return PortfolioInvitation.objects.filter(portfolio=portfolio, id=invitedmember_pk).exists()
|
||||
|
||||
|
||||
def _is_domain_request_creator(user, domain_request_pk):
|
||||
"""Checks to see if the user is the creator of a domain request
|
||||
with domain_request_pk."""
|
||||
|
@ -286,15 +391,3 @@ def _is_staff_managing_domain(request, **kwargs):
|
|||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
def _has_portfolio_view_all_domains(request, domain_pk):
|
||||
"""Returns whether the user in the request can access the domain
|
||||
via portfolio view all domains permission."""
|
||||
portfolio = request.session.get("portfolio")
|
||||
if request.user.has_view_all_domains_portfolio_permission(portfolio):
|
||||
if Domain.objects.filter(id=domain_pk).exists():
|
||||
domain = Domain.objects.get(id=domain_pk)
|
||||
if domain.domain_info.portfolio == portfolio:
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -86,7 +86,6 @@ class RequestingEntityForm(RegistrarForm):
|
|||
return {}
|
||||
# get the domain request as a dict, per usual method
|
||||
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||
|
||||
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||
domain_request_dict["sub_organization"] = "other"
|
||||
|
@ -348,7 +347,7 @@ class OrganizationContactForm(RegistrarForm):
|
|||
error_messages={
|
||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||
},
|
||||
widget=ComboboxWidget,
|
||||
widget=ComboboxWidget(attrs={"required": True}),
|
||||
)
|
||||
zipcode = forms.CharField(
|
||||
label="Zip code",
|
||||
|
@ -608,7 +607,10 @@ class DotGovDomainForm(RegistrarForm):
|
|||
)
|
||||
|
||||
|
||||
class PurposeForm(RegistrarForm):
|
||||
class PurposeDetailsForm(BaseDeletableRegistrarForm):
|
||||
|
||||
field_name = "purpose"
|
||||
|
||||
purpose = forms.CharField(
|
||||
label="Purpose",
|
||||
widget=forms.Textarea(
|
||||
|
|
123
src/registrar/forms/feb.py
Normal file
123
src/registrar/forms/feb.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
from django import forms
|
||||
from django.core.validators import MaxLengthValidator
|
||||
from registrar.forms.utility.wizard_form_helper import BaseDeletableRegistrarForm, BaseYesNoForm
|
||||
|
||||
|
||||
class ExecutiveNamingRequirementsYesNoForm(BaseYesNoForm, BaseDeletableRegistrarForm):
|
||||
"""
|
||||
Form for verifying if the domain request meets the Federal Executive Branch domain naming requirements.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "feb_naming_requirements"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.feb_naming_requirements
|
||||
|
||||
|
||||
class ExecutiveNamingRequirementsDetailsForm(BaseDeletableRegistrarForm):
|
||||
# Text area for additional details; rendered conditionally when "no" is selected.
|
||||
feb_naming_requirements_details = forms.CharField(
|
||||
widget=forms.Textarea(attrs={"maxlength": "2000"}),
|
||||
max_length=2000,
|
||||
required=True,
|
||||
error_messages={"required": ("This field is required.")},
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
label="",
|
||||
help_text="Maximum 2000 characters allowed.",
|
||||
)
|
||||
|
||||
|
||||
class FEBPurposeOptionsForm(BaseDeletableRegistrarForm):
|
||||
|
||||
field_name = "feb_purpose_choice"
|
||||
|
||||
form_choices = (
|
||||
("new", "Used for a new website"),
|
||||
("redirect", "Used as a redirect for an existing website"),
|
||||
("other", "Not for a website"),
|
||||
)
|
||||
|
||||
feb_purpose_choice = forms.ChoiceField(
|
||||
required=True,
|
||||
choices=form_choices,
|
||||
widget=forms.RadioSelect,
|
||||
error_messages={
|
||||
"required": "This question is required.",
|
||||
},
|
||||
label="Select one",
|
||||
)
|
||||
|
||||
|
||||
class FEBTimeFrameYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
|
||||
"""
|
||||
Form for determining whether the domain request comes with a target timeframe for launch.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "has_timeframe"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.has_timeframe
|
||||
|
||||
|
||||
class FEBTimeFrameDetailsForm(BaseDeletableRegistrarForm):
|
||||
time_frame_details = forms.CharField(
|
||||
label="time_frame_details",
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"aria-label": "Provide details on your target timeframe. \
|
||||
Is there a special significance to this date (legal requirement, announcement, event, etc)?"
|
||||
}
|
||||
),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Provide details on your target timeframe."},
|
||||
)
|
||||
|
||||
|
||||
class FEBInteragencyInitiativeYesNoForm(BaseDeletableRegistrarForm, BaseYesNoForm):
|
||||
"""
|
||||
Form for determining whether the domain request is part of an interagency initative.
|
||||
If the "no" option is selected, details must be provided via the separate details form.
|
||||
"""
|
||||
|
||||
field_name = "is_interagency_initiative"
|
||||
|
||||
@property
|
||||
def form_is_checked(self):
|
||||
"""
|
||||
Determines the initial checked state of the form based on the domain_request's attributes.
|
||||
"""
|
||||
return self.domain_request.is_interagency_initiative
|
||||
|
||||
|
||||
class FEBInteragencyInitiativeDetailsForm(BaseDeletableRegistrarForm):
|
||||
interagency_initiative_details = forms.CharField(
|
||||
label="interagency_initiative_details",
|
||||
widget=forms.Textarea(attrs={"aria-label": "Name the agencies that will be involved in this initiative."}),
|
||||
validators=[
|
||||
MaxLengthValidator(
|
||||
2000,
|
||||
message="Response must be less than 2000 characters.",
|
||||
)
|
||||
],
|
||||
error_messages={"required": "Name the agencies that will be involved in this initiative."},
|
||||
)
|
|
@ -22,6 +22,7 @@ from registrar.models.utility.portfolio_helper import (
|
|||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_portfolio_invitation_associations,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -459,7 +460,14 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
|||
if hasattr(e, "code"):
|
||||
field = "email" if "email" in self.fields else None
|
||||
if e.code == "has_existing_permissions":
|
||||
self.add_error(field, f"{self.instance.email} is already a member of another .gov organization.")
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(self.instance)
|
||||
|
||||
same_portfolio_for_permissions = existing_permissions.exclude(portfolio=self.instance.portfolio)
|
||||
same_portfolio_for_invitations = existing_invitations.exclude(portfolio=self.instance.portfolio)
|
||||
if same_portfolio_for_permissions.exists() or same_portfolio_for_invitations.exists():
|
||||
self.add_error(
|
||||
field, f"{self.instance.email} is already a member of another .gov organization."
|
||||
)
|
||||
override_error = True
|
||||
elif e.code == "has_existing_invitations":
|
||||
self.add_error(
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# Generated by Django 4.2.17 on 2025-03-10 19:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0141_alter_portfolioinvitation_additional_permissions_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_naming_requirements",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_naming_requirements_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="feb_purpose_choice",
|
||||
field=models.CharField(
|
||||
blank=True, choices=[("website", "Website"), ("redirect", "Redirect"), ("other", "Other")], null=True
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="has_timeframe",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="interagency_initiative_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="is_interagency_initiative",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="domainrequest",
|
||||
name="time_frame_details",
|
||||
field=models.TextField(blank=True, null=True),
|
||||
),
|
||||
]
|
|
@ -245,6 +245,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
is called in the validate function on the request/domain page
|
||||
|
||||
throws- RegistryError or InvalidDomainError"""
|
||||
|
||||
if not cls.string_could_be_domain(domain):
|
||||
logger.warning("Not a valid domain: %s" % str(domain))
|
||||
# throw invalid domain error so that it can be caught in
|
||||
|
|
|
@ -54,6 +54,11 @@ class DomainRequest(TimeStampedModel):
|
|||
"""Returns the associated label for a given status name"""
|
||||
return cls(status_name).label if status_name else None
|
||||
|
||||
class FEBPurposeChoices(models.TextChoices):
|
||||
WEBSITE = "website"
|
||||
REDIRECT = "redirect"
|
||||
OTHER = "other"
|
||||
|
||||
class StateTerritoryChoices(models.TextChoices):
|
||||
ALABAMA = "AL", "Alabama (AL)"
|
||||
ALASKA = "AK", "Alaska (AK)"
|
||||
|
@ -501,6 +506,51 @@ class DomainRequest(TimeStampedModel):
|
|||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
# Fields specific to Federal Executive Branch agencies, used by OMB for reviewing requests
|
||||
feb_naming_requirements = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
feb_naming_requirements_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
feb_purpose_choice = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
choices=FEBPurposeChoices.choices,
|
||||
)
|
||||
|
||||
# This field is alternately used for generic domain purpose explanations
|
||||
# and for explanations of the specific purpose chosen with feb_purpose_choice
|
||||
# by a Federal Executive Branch agency.
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
has_timeframe = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
time_frame_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
is_interagency_initiative = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
interagency_initiative_details = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
alternative_domains = models.ManyToManyField(
|
||||
"registrar.Website",
|
||||
blank=True,
|
||||
|
@ -508,11 +558,6 @@ class DomainRequest(TimeStampedModel):
|
|||
help_text="Other domain names the creator provided for consideration",
|
||||
)
|
||||
|
||||
purpose = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
other_contacts = models.ManyToManyField(
|
||||
"registrar.Contact",
|
||||
blank=True,
|
||||
|
@ -1389,6 +1434,12 @@ class DomainRequest(TimeStampedModel):
|
|||
has_details = False
|
||||
return has_details
|
||||
|
||||
def is_feb(self) -> bool:
|
||||
"""Is this domain request for a Federal Executive Branch agency?"""
|
||||
if self.portfolio:
|
||||
return self.portfolio.federal_type == BranchChoices.EXECUTIVE
|
||||
return False
|
||||
|
||||
def is_federal(self) -> Union[bool, None]:
|
||||
"""Is this domain request for a federal agency?
|
||||
|
||||
|
|
|
@ -257,9 +257,6 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
|
||||
has_portfolio = bool(user_portfolio_permission.portfolio_id)
|
||||
portfolio_permissions = set(user_portfolio_permission._get_portfolio_permissions())
|
||||
|
||||
|
@ -286,8 +283,8 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
if not flag_is_active_for_user(user_portfolio_permission.user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
existing_permissions, existing_invitations = get_user_portfolio_permission_associations(
|
||||
user_portfolio_permission
|
||||
)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
|
@ -296,10 +293,6 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
code="has_existing_permissions",
|
||||
)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=user_portfolio_permission.user.email).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
if existing_invitations.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio invitation. "
|
||||
|
@ -308,6 +301,32 @@ def validate_user_portfolio_permission(user_portfolio_permission):
|
|||
)
|
||||
|
||||
|
||||
def get_user_portfolio_permission_associations(user_portfolio_permission):
|
||||
"""
|
||||
Retrieves the associations for a user portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects excluding the current permission.
|
||||
- existing_invitations: PortfolioInvitation objects for the user email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.exclude(id=user_portfolio_permission.id).filter(
|
||||
user=user_portfolio_permission.user
|
||||
)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(
|
||||
email__iexact=user_portfolio_permission.user.email
|
||||
).exclude(
|
||||
Q(portfolio=user_portfolio_permission.portfolio)
|
||||
| Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def validate_portfolio_invitation(portfolio_invitation):
|
||||
"""
|
||||
Validates a PortfolioInvitation instance. Located in portfolio_helper to avoid circular imports
|
||||
|
@ -324,7 +343,6 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
Raises:
|
||||
ValidationError: If any of the validation rules are violated.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -351,17 +369,12 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
)
|
||||
|
||||
# == Validate the multiple_porfolios flag. == #
|
||||
user = User.objects.filter(email=portfolio_invitation.email).first()
|
||||
user = User.objects.filter(email__iexact=portfolio_invitation.email).first()
|
||||
|
||||
# If user returns None, then we check for global assignment of multiple_portfolios.
|
||||
# Otherwise we just check on the user.
|
||||
if not flag_is_active_for_user(user, "multiple_portfolios"):
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user=user)
|
||||
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
|
||||
existing_permissions, existing_invitations = get_portfolio_invitation_associations(portfolio_invitation)
|
||||
if existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"This user is already assigned to a portfolio. "
|
||||
|
@ -377,6 +390,27 @@ def validate_portfolio_invitation(portfolio_invitation):
|
|||
)
|
||||
|
||||
|
||||
def get_portfolio_invitation_associations(portfolio_invitation):
|
||||
"""
|
||||
Retrieves the associations for a portfolio invitation.
|
||||
|
||||
Returns:
|
||||
A tuple:
|
||||
(existing_permissions, existing_invitations)
|
||||
where:
|
||||
- existing_permissions: UserPortfolioPermission objects matching the email.
|
||||
- existing_invitations: PortfolioInvitation objects for the email excluding
|
||||
the current invitation and those with status RETRIEVED.
|
||||
"""
|
||||
PortfolioInvitation = apps.get_model("registrar.PortfolioInvitation")
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user__email__iexact=portfolio_invitation.email)
|
||||
existing_invitations = PortfolioInvitation.objects.filter(email__iexact=portfolio_invitation.email).exclude(
|
||||
Q(id=portfolio_invitation.id) | Q(status=PortfolioInvitation.PortfolioInvitationStatus.RETRIEVED)
|
||||
)
|
||||
return (existing_permissions, existing_invitations)
|
||||
|
||||
|
||||
def cleanup_after_portfolio_member_deletion(portfolio, email, user=None):
|
||||
"""
|
||||
Cleans up after removing a portfolio member or a portfolio invitation.
|
||||
|
|
|
@ -160,6 +160,16 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endblock field_readonly %}
|
||||
|
||||
{% block field_other %}
|
||||
{% comment %}
|
||||
.gov override - add Aria messages for select2 dropdowns. These messages are hooked-up to their respective DOM
|
||||
elements via javascript (see andi.js)
|
||||
{% endcomment %}
|
||||
{% if "related_widget_wrapper" in field.field.field.widget.template_name %}
|
||||
<span id="{{ field.field.id_for_label }}--aria-description" class="visually-hidden admin-select--aria-description">
|
||||
{{ field.field.label }}, edit, has autocomplete. To set the value, use the arrow keys or type the text.
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
{{ field.field }}
|
||||
|
||||
|
@ -251,7 +261,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
|
||||
{% elif field.field.name == "rejection_reason_email" %}
|
||||
{{ field.field }}
|
||||
|
||||
<div class="margin-top-05 text-faded custom-email-placeholder">
|
||||
–
|
||||
</div>
|
||||
|
@ -331,7 +340,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if original_object.rejection_reason_email %}
|
||||
<input id="last-sent-rejection-email-content" class="display-none" value="{{original_object.rejection_reason_email}}">
|
||||
{% else %}
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
{% if request.path|endswith:"renewal"%}
|
||||
<h1>Renew {{domain.name}} </h1>
|
||||
{%else%}
|
||||
<h1 class="break-word">Domain Overview</h1>
|
||||
<h1 class="break-word">Domain overview</h1>
|
||||
{% endif%}
|
||||
|
||||
{% endblock %} {# domain_content #}
|
||||
|
|
|
@ -99,7 +99,7 @@
|
|||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not Enabled' edit_link=url editable=is_editable %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Not enabled' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
{% load static field_helpers url_helpers %}
|
||||
|
||||
{% block form_instructions %}
|
||||
<p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/choosing' %}">our naming requirements</a>. Your domain name must:
|
||||
<p>Before requesting a .gov domain, please make sure it meets <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% if requires_feb_questions %}https://get.gov/domains/executive-branch-guidance/{% else %}{% public_site_url 'domains/choosing' %}{% endif %}">our naming requirements</a>. Your domain name must:
|
||||
<ul class="usa-list">
|
||||
<li>Be available </li>
|
||||
<li>Relate to your organization’s name, location, and/or services </li>
|
||||
<li>Relate to your organization's name, location, and/or services </li>
|
||||
<li>Be unlikely to mislead or confuse the general public (even if your domain is only intended for a specific audience) </li>
|
||||
</ul>
|
||||
</p>
|
||||
|
||||
<p>Names that <em>uniquely apply to your organization</em> are likely to be approved over names that could also apply to other organizations.
|
||||
{% if not is_federal %}In most instances, this requires including your state’s two-letter abbreviation.{% endif %}</p>
|
||||
{% if not is_federal %}In most instances, this requires including your state's two-letter abbreviation.{% endif %}</p>
|
||||
|
||||
{% if not portfolio %}
|
||||
<p>Requests for your organization’s initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
<p>Requests for your organization's initials or an abbreviated name might not be approved, but we encourage you to request the name you want.</p>
|
||||
{% endif %}
|
||||
|
||||
<p>Note that <strong>only federal agencies can request generic terms</strong> like
|
||||
|
@ -41,9 +41,10 @@
|
|||
<legend>
|
||||
<h2>What .gov domain do you want?</h2>
|
||||
</legend>
|
||||
|
||||
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
||||
|
||||
<p id="domain_instructions" class="margin-top-05">
|
||||
After you enter your domain, we'll make sure it's available and that it meets some of our naming requirements.
|
||||
If your domain passes these initial checks, we'll verify that it meets all our requirements after you complete the rest of this form.
|
||||
</p>
|
||||
{% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %}
|
||||
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||
|
@ -63,10 +64,9 @@
|
|||
<legend>
|
||||
<h2 id="alternative-domains-title">Alternative domains (optional)</h2>
|
||||
</legend>
|
||||
|
||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||
you your first choice?</p>
|
||||
|
||||
<p id="alt_domain_instructions" class="margin-top-05">
|
||||
Are there other domains you'd like if we can't give you your first choice?
|
||||
</p>
|
||||
{% with attr_aria_labelledby="alt_domain_instructions" %}
|
||||
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
||||
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
||||
|
@ -83,10 +83,10 @@
|
|||
<div class="usa-sr-only" id="alternative-domains__add-another-alternative">Add another alternative domain</div>
|
||||
<button aria-labelledby="alternative-domains-title" aria-describedby="alternative-domains__add-another-alternative" type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another alternative</span>
|
||||
<use xlink:href="{% static 'img/sprite.svg' %}#add_circle"></use>
|
||||
</svg>
|
||||
<span class="margin-left-05">Add another alternative</span>
|
||||
</button>
|
||||
|
||||
<div class="margin-bottom-3">
|
||||
<div class="usa-sr-only" id="alternative-domains__check-availability">Check domain availability</div>
|
||||
<button
|
||||
|
@ -98,10 +98,41 @@
|
|||
aria-describedby="alternative-domains__check-availability"
|
||||
>Check availability</button>
|
||||
</div>
|
||||
|
||||
<p class="margin-top-05">
|
||||
If you're not sure this is the domain you want, that's ok. You can change the domain later.
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<p class="margin-top-05">If you’re not sure this is the domain you want, that’s ok. You can change the domain later. </p>
|
||||
{{ forms.2.management_form }}
|
||||
{{ forms.3.management_form }}
|
||||
|
||||
</fieldset>
|
||||
{% if requires_feb_questions %}
|
||||
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
|
||||
<legend>
|
||||
<h2>Does this submission meet each domain naming requirement?</h2>
|
||||
</legend>
|
||||
<p id="dotgov-domain-naming-requirements" class="margin-top-05">
|
||||
OMB will review each request against the domain
|
||||
<a class="usa-link" rel="noopener noreferrer" target="_blank" href="https://get.gov/domains/executive-branch-guidance/">
|
||||
naming requirements for executive branch agencies
|
||||
</a>.
|
||||
Agency submissions are expected to meet each requirement.
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.feb_naming_requirements %}
|
||||
{% endwith %}
|
||||
|
||||
{# Conditional Details Field – only shown when the executive naming requirements radio is "False" #}
|
||||
<div id="domain-naming-requirements-details-container" class="conditional-panel" style="display: none;">
|
||||
<p class="usa-label">
|
||||
Provide details below <span class="usa-label--required">*</span>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" maxlength="2000" %}
|
||||
{% input_with_errors forms.3.feb_naming_requirements_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -3,17 +3,84 @@
|
|||
|
||||
{% block form_instructions %}
|
||||
<p>.Gov domains are intended for public use. Domains will not be given to organizations that only want to reserve a domain name (defensive registration) or that only intend to use the domain internally (as for an intranet).</p>
|
||||
<p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p>Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?</p>
|
||||
<p>Read about <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'domains/requirements/' %}">activities that are prohibited on .gov domains.</a></p>
|
||||
{% endblock %}
|
||||
|
||||
{% block form_required_fields_help_text %}
|
||||
{# commented out so it does not appear on this page #}
|
||||
{# empty this block so it doesn't show on this page #}
|
||||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% if requires_feb_questions %}
|
||||
<fieldset class="usa-fieldset margin-top-0 dotgov-domain-form">
|
||||
{{forms.0.management_form}}
|
||||
{{forms.1.management_form}}
|
||||
{{forms.2.management_form}}
|
||||
{{forms.3.management_form}}
|
||||
{{forms.4.management_form}}
|
||||
{{forms.5.management_form}}
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.feb_purpose_choice %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-details-container" class="conditional-panel display-none">
|
||||
<p class="usa-label">
|
||||
<em>Provide details below <span class="usa-label--required">*</span></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.1.purpose %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
|
||||
<h2>Do you have a target time frame for launching this domain?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.2.has_timeframe %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-timeframe-details-container" class="conditional-panel">
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.3.time_frame_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
|
||||
<h2>Will the domain name be used for an interagency initiative?</h2>
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Select one. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_class="usa-radio__input--tile" add_legend_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.4.is_interagency_initiative %}
|
||||
{% endwith %}
|
||||
|
||||
<div id="purpose-interagency-initaitive-details-container" class="conditional-panel">
|
||||
<p class="margin-bottom-0 margin-top-1">
|
||||
<em>Provide details below. <abbr class="usa-hint usa-hint--required" title="required">*</abbr></em>
|
||||
</p>
|
||||
{% with add_label_class="usa-sr-only" attr_required="required" attr_maxlength="2000" %}
|
||||
{% input_with_errors forms.5.interagency_initiative_details %}
|
||||
{% endwith %}
|
||||
<p class="usa-hint margin-top-0">Maximum 2000 characters allowed.</p>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% else %}
|
||||
<h2>What is the purpose of your requested domain?</h2>
|
||||
<p>Describe how you’ll use your .gov domain. Will it be used for a website, email, or something else?</p>
|
||||
|
||||
{% with attr_maxlength=2000 add_label_class="usa-sr-only" %}
|
||||
{% input_with_errors forms.0.purpose %}
|
||||
{% input_with_errors forms.1.purpose %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
|
|
@ -57,7 +57,7 @@ THANK YOU
|
|||
The .gov team
|
||||
|
||||
.Gov blog <https://get.gov/updates/>
|
||||
Domain management <{{ manage_url }}}>
|
||||
Domain management <{{ manage_url }}>
|
||||
Get.gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
|
|
|
@ -15,14 +15,14 @@
|
|||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<h1>Manage your domains</h1>
|
||||
|
||||
<p class="margin-top-4">
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||
>
|
||||
Start a new domain request
|
||||
</button>
|
||||
</p>
|
||||
<div class="grid-row margin-bottom-3">
|
||||
<h1 class="flex-fill">Manage your domains</h1>
|
||||
<div>
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link">
|
||||
Start a new domain request
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "includes/domains_table.html" with user_domain_count=user_domain_count %}
|
||||
{% include "includes/domain_requests_table.html" %}
|
||||
|
|
|
@ -4,49 +4,20 @@
|
|||
{% url 'get_domain_requests_json' as url %}
|
||||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||
|
||||
<section class="section-outlined domain-requests {% if portfolio %}section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %}section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
<section class="section-outlined domain-requests {% if portfolio %}margin-top-0 section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domain-requests-header" class="display-inline-block">Domain requests</h2>
|
||||
<div class="grid-row grid-col-12">
|
||||
<h2 id="domain-requests-header" class="display-inline-block flex-fill">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="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<section aria-label="Domain requests search component" id="domain-requests-search-component" class="margin-top-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domain-requests__reset-search" type="button" aria-labelledby="domain-requests-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domain-requests__search-field"
|
||||
type="search"
|
||||
name="domain-requests-search"
|
||||
{% if portfolio %}
|
||||
placeholder="Search by domain name or creator"
|
||||
{% else %}
|
||||
placeholder="Search by domain name"
|
||||
{% endif %}
|
||||
aria-labelledby="domain-requests-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="domain-requests-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="domain-requests__search-field-submit" aria-labelledby="domain-requests-search-component" aria-describedby="domain-requests-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text=portfolio|yesno:"Search by domain name or creator,Search by domain name" item_name="domain-requests" aria_label_text="Domain requests search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
{% if portfolio %}
|
||||
|
|
|
@ -26,56 +26,32 @@
|
|||
{% endif %}
|
||||
|
||||
<section class="section-outlined domains margin-top-0{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domains">
|
||||
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
{% if not portfolio %}
|
||||
<h2 id="domains-header" class="display-inline-block">Domains</h2>
|
||||
<div class="grid-row grid-col-12">
|
||||
<h2 id="domains-header" class="display-inline-block flex-fill">Domains</h2>
|
||||
<!-- ---------- EXPORT (non-org placement) ---------- -->
|
||||
{% if user_domain_count and user_domain_count > 0 %}
|
||||
{% with export_aria="Domains report component" export_url='export_data_type_user' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
<div class="section-outlined__search section-outlined__search--widescreen {% if portfolio %}mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<section aria-label="Domains search component" class="margin-top-2" id="domains-search-component">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="domains__reset-search" type="button" aria-labelledby="domains-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="domains__search-field"
|
||||
type="search"
|
||||
name="domains-search"
|
||||
placeholder="Search by domain name"
|
||||
aria-labelledby="domains-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="domains-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="domains__search-field-submit" aria-labelledby="domains-search-component" aria-describedby="domains-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% if user_domain_count and user_domain_count > 0 %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Domains report component" class="margin-top-205" id="domains-report-component">
|
||||
<div class="usa-sr-only" id="domains-export-button__description">Click to export as csv</div>
|
||||
<button data-href="{% url 'export_data_type_user' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="domains-report-component" aria-describedby="domains-export-button__description">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text="Search by domain name" item_name="domains" aria_label_text="Domains search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
<!-- ---------- EXPORT (org placement) ---------- -->
|
||||
{% if user_domain_count and user_domain_count > 0 and portfolio%}
|
||||
{% with export_aria="Domains report component" export_url='export_data_type_user' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Non org model banner -->
|
||||
{% if num_expiring_domains > 0 and not portfolio %}
|
||||
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
|
|
12
src/registrar/templates/includes/export.html
Normal file
12
src/registrar/templates/includes/export.html
Normal file
|
@ -0,0 +1,12 @@
|
|||
{% load static %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} flex-auto desktop:padding-left-3{% endif %} margin-top-0">
|
||||
<section aria-label="{{export_aria}}" class="margin-top-205">
|
||||
<button data-href="{% url export_url %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right use-button-as-link">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
|
|
@ -91,9 +91,9 @@
|
|||
aria-describedby="You have unsaved changes that will be lost."
|
||||
>
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member-domains' pk=portfolio_permission.id as url %}
|
||||
{% url 'member-domains' member_pk=portfolio_permission.id as url %}
|
||||
{% else %}
|
||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url %}
|
||||
{% url 'invitedmember-domains' invitedmember_pk=portfolio_invitation.id as url %}
|
||||
{% endif %}
|
||||
|
||||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to continue?" modal_description="You have unsaved changes that will be lost." modal_button_url=url modal_button_text="Continue without saving" %}
|
||||
|
|
|
@ -7,47 +7,14 @@
|
|||
<span id="get_members_json_url" class="display-none">{{url}}</span>
|
||||
<section class="section-outlined members margin-top-0 section-outlined--border-base-light" id="members">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6 section-outlined__search--widescreen">
|
||||
<section aria-label="Members search component" class="margin-top-2" id="members-search-component">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="members__reset-search" type="button" aria-labelledby="members-search-component">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="members__search-field"
|
||||
type="search"
|
||||
name="members-search"
|
||||
placeholder="Search by member name"
|
||||
aria-labelledby="members-search-component"
|
||||
/>
|
||||
<div class="usa-sr-only" id="members-search-button__description">Click to search</div>
|
||||
<button class="usa-button" type="submit" id="members__search-field-submit" aria-labelledby="members-search-component" aria-describedby="members-search-button__description">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Members report component" class="margin-top-205" id="members-report-component">
|
||||
<div class="usa-sr-only" id="members-export-button__description">Click to export as csv</div>
|
||||
<button href="{% url 'export_members_portfolio' %}" class="use-button-as-link usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" aria-labelledby="members-report-component" aria-describedby="members-export-button__description">
|
||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||
</svg>Export as CSV
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
{% with label_text="Search by member name" item_name="members" aria_label_text="Members search component"%}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
{% with export_aria="Members report component" export_url='export_members_portfolio' %}
|
||||
{% include "includes/export.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
<div class="display-none margin-top-0" id="members__table-wrapper">
|
||||
|
|
|
@ -1,26 +1,28 @@
|
|||
{% load static %}
|
||||
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||
<div class="section-outlined__search tablet:grid-col">
|
||||
<section aria-label="{{aria_label_text}}">
|
||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||
<form class="usa-search {% if use_search_icon %} usa-search--small {% else %} usa-search--default {% endif %}usa-search--show-label" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<label class="usa-label display-block margin-bottom-05 maxw-none" for="{{item_name}}__search-field">
|
||||
<label id="{{item_name}}__search-label" class="usa-label display-block maxw-none margin-top-2 margin-bottom-1" for="{{item_name}}__search-field">
|
||||
{{ label_text }}
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper flex-align-self-end">
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper">
|
||||
<input
|
||||
class="usa-input minw-15"
|
||||
id="{{item_name}}__search-field"
|
||||
type="search"
|
||||
name="{{item_name}}-search"
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="{{item_name}}__search-field-submit">
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
<button class="usa-button" type="submit" id="{{item_name}}__search-field-submit" aria-labelledby="{{item_name}}__search-label">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
{% if not use_search_icon %}
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
{% endif %}
|
||||
</button>
|
||||
<button class="usa-button usa-button--unstyled margin-left-3 display-none flex-1" id="{{item_name}}__reset-search" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains-edit' pk=portfolio_permission.id as url3 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains-edit' member_pk=portfolio_permission.id as url3 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
||||
{% url 'invitedmember' invitedmember_pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains-edit' invitedmember_pk=portfolio_invitation.id as url3 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -15,11 +15,11 @@
|
|||
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains' pk=portfolio_permission.id as url3 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member-domains' member_pk=portfolio_permission.id as url3 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||
{% url 'invitedmember' invitedmember_pk=portfolio_invitation.id as url2 %}
|
||||
{% url 'invitedmember-domains' invitedmember_pk=portfolio_invitation.id as url3 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -20,9 +20,9 @@
|
|||
<!-- Navigation breadcrumbs -->
|
||||
{% url 'members' as url %}
|
||||
{% if portfolio_permission %}
|
||||
{% url 'member' pk=portfolio_permission.id as url2 %}
|
||||
{% url 'member' member_pk=portfolio_permission.id as url2 %}
|
||||
{% else %}
|
||||
{% url 'invitedmember' pk=invitation.id as url2 %}
|
||||
{% url 'invitedmember' invitedmember_pk=invitation.id as url2 %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0 bg-gray-1" aria-label="Portfolio member breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
|
|
|
@ -16,15 +16,14 @@
|
|||
{% endblock messages%}
|
||||
|
||||
<div id="main-content">
|
||||
<h1 id="domain-requests-header" class="margin-bottom-1">Domain requests</h1>
|
||||
<div class="grid-row grid-gap">
|
||||
|
||||
<div class="grid-row grid-gap margin-bottom-3">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<h1 id="domain-requests-header" class="margin-bottom-1">Domain requests</h1>
|
||||
<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">
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
|
||||
<p class="float-right-tablet tablet:margin-y-0">
|
||||
<button data-href="{% url 'domain-request:start' %}" class="usa-button use-button-as-link"
|
||||
>
|
||||
|
@ -32,10 +31,9 @@
|
|||
</button>
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="margin-y-0">Domain requests can only be modified by the person who created the request.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% include "includes/domain_requests_table.html" with portfolio=portfolio %}
|
||||
|
|
|
@ -1981,7 +1981,14 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
"senior_official",
|
||||
"approved_domain",
|
||||
"requested_domain",
|
||||
"feb_naming_requirements",
|
||||
"feb_naming_requirements_details",
|
||||
"feb_purpose_choice",
|
||||
"purpose",
|
||||
"has_timeframe",
|
||||
"time_frame_details",
|
||||
"is_interagency_initiative",
|
||||
"interagency_initiative_details",
|
||||
"no_other_contacts_rationale",
|
||||
"anything_else",
|
||||
"has_anything_else_text",
|
||||
|
|
|
@ -14,10 +14,11 @@ from registrar.forms.domain_request_wizard import (
|
|||
OtherContactsForm,
|
||||
RequirementsForm,
|
||||
TribalGovernmentForm,
|
||||
PurposeForm,
|
||||
AnythingElseForm,
|
||||
AboutYourOrganizationForm,
|
||||
)
|
||||
from registrar.forms import PurposeDetailsForm
|
||||
|
||||
from registrar.forms.domain import ContactForm
|
||||
from registrar.forms.portfolio import (
|
||||
PortfolioInvitedMemberForm,
|
||||
|
@ -257,7 +258,7 @@ class TestFormValidation(MockEppLib):
|
|||
@less_console_noise_decorator
|
||||
def test_purpose_form_character_count_invalid(self):
|
||||
"""Response must be less than 2000 characters."""
|
||||
form = PurposeForm(
|
||||
form = PurposeDetailsForm(
|
||||
data={
|
||||
"purpose": "Bacon ipsum dolor amet fatback strip steak pastrami"
|
||||
"shankle, drumstick doner chicken landjaeger turkey andouille."
|
||||
|
|
355
src/registrar/tests/test_resource_access.py
Normal file
355
src/registrar/tests/test_resource_access.py
Normal file
|
@ -0,0 +1,355 @@
|
|||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
from waffle.testutils import override_flag
|
||||
|
||||
from registrar.tests.common import (
|
||||
MockDbForIndividualTests,
|
||||
less_console_noise_decorator,
|
||||
completed_domain_request,
|
||||
)
|
||||
from registrar.models import (
|
||||
DomainRequest,
|
||||
Portfolio,
|
||||
UserPortfolioPermission,
|
||||
PortfolioInvitation,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import (
|
||||
UserPortfolioRoleChoices,
|
||||
UserPortfolioPermissionChoices,
|
||||
)
|
||||
from registrar.decorators import (
|
||||
_domain_exists_under_portfolio,
|
||||
_domain_request_exists_under_portfolio,
|
||||
_member_exists_under_portfolio,
|
||||
_member_invitation_exists_under_portfolio,
|
||||
)
|
||||
|
||||
|
||||
class TestPortfolioResourceAccess(MockDbForIndividualTests):
|
||||
"""Test functions that verify resources belong to a portfolio.
|
||||
More specifically, this function tests our helper utilities in decorators.py"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
# Create portfolios
|
||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||
self.other_portfolio = Portfolio.objects.create(
|
||||
creator=self.custom_staffuser, organization_name="Other Portfolio"
|
||||
)
|
||||
|
||||
# Create domain requests
|
||||
self.domain_request = completed_domain_request(name="eggnog.gov", user=self.user, portfolio=self.portfolio)
|
||||
|
||||
self.other_domain_request = completed_domain_request(
|
||||
name="christmas.gov", user=self.tired_user, portfolio=self.other_portfolio
|
||||
)
|
||||
|
||||
# Create domains
|
||||
self.approved_domain_request_1 = completed_domain_request(
|
||||
name="done_1.gov",
|
||||
user=self.tired_user,
|
||||
portfolio=self.portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
)
|
||||
self.approved_domain_request_2 = completed_domain_request(
|
||||
name="done_2.gov",
|
||||
user=self.tired_user,
|
||||
portfolio=self.other_portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
)
|
||||
self.approved_domain_request_1.approve()
|
||||
self.approved_domain_request_2.approve()
|
||||
self.domain = self.approved_domain_request_1.approved_domain
|
||||
self.other_domain = self.approved_domain_request_2.approved_domain
|
||||
|
||||
# Create portfolio permissions
|
||||
self.user_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
|
||||
self.other_user_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.tired_user, portfolio=self.other_portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
|
||||
# Create portfolio invitations
|
||||
self.portfolio_invitation = PortfolioInvitation.objects.create(
|
||||
email="invited@example.com",
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
status=PortfolioInvitation.PortfolioInvitationStatus.INVITED,
|
||||
)
|
||||
|
||||
self.other_portfolio_invitation = PortfolioInvitation.objects.create(
|
||||
email="other-invited@example.com",
|
||||
portfolio=self.other_portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
status=PortfolioInvitation.PortfolioInvitationStatus.INVITED,
|
||||
)
|
||||
|
||||
# Domain request tests
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_exists_under_portfolio_when_pk_is_none(self):
|
||||
"""Check behavior when the PK is None."""
|
||||
self.assertTrue(_domain_request_exists_under_portfolio(self.portfolio, None))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_exists_under_portfolio_when_exists(self):
|
||||
"""Verify returns True when the domain request exists under the portfolio."""
|
||||
self.assertTrue(_domain_request_exists_under_portfolio(self.portfolio, self.domain_request.id))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_exists_under_portfolio_when_not_exists(self):
|
||||
"""Verify returns False when the domain request does not exist under the portfolio."""
|
||||
self.assertFalse(_domain_request_exists_under_portfolio(self.portfolio, self.other_domain_request.id))
|
||||
|
||||
# Domain tests
|
||||
@less_console_noise_decorator
|
||||
def test_domain_exists_under_portfolio_when_pk_is_none(self):
|
||||
"""Check behavior when the PK is None."""
|
||||
self.assertTrue(_domain_exists_under_portfolio(self.portfolio, None))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_exists_under_portfolio_when_exists(self):
|
||||
"""Verify returns True when the domain exists under the portfolio."""
|
||||
self.assertTrue(_domain_exists_under_portfolio(self.portfolio, self.domain.id))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_exists_under_portfolio_when_not_exists(self):
|
||||
"""Verify returns False when the domain does not exist under the portfolio."""
|
||||
self.assertFalse(_domain_exists_under_portfolio(self.portfolio, self.other_domain.id))
|
||||
|
||||
# Member tests
|
||||
@less_console_noise_decorator
|
||||
def test_member_exists_under_portfolio_when_pk_is_none(self):
|
||||
"""Check behavior when the PK is None."""
|
||||
self.assertTrue(_member_exists_under_portfolio(self.portfolio, None))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_member_exists_under_portfolio_when_exists(self):
|
||||
"""Verify returns True when the member exists under the portfolio."""
|
||||
self.assertTrue(_member_exists_under_portfolio(self.portfolio, self.user_permission.id))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_member_exists_under_portfolio_when_not_exists(self):
|
||||
"""Verify returns False when the member does not exist under the portfolio."""
|
||||
self.assertFalse(_member_exists_under_portfolio(self.portfolio, self.other_user_permission.id))
|
||||
|
||||
# Member invitation tests
|
||||
@less_console_noise_decorator
|
||||
def test_member_invitation_exists_under_portfolio_when_pk_is_none(self):
|
||||
"""Check behavior when the PK is None."""
|
||||
self.assertTrue(_member_invitation_exists_under_portfolio(self.portfolio, None))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_member_invitation_exists_under_portfolio_when_exists(self):
|
||||
"""Verify returns True when the member invitation exists under the portfolio."""
|
||||
self.assertTrue(_member_invitation_exists_under_portfolio(self.portfolio, self.portfolio_invitation.id))
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_member_invitation_exists_under_portfolio_when_not_exists(self):
|
||||
"""Verify returns False when the member invitation does not exist under the portfolio."""
|
||||
self.assertFalse(_member_invitation_exists_under_portfolio(self.portfolio, self.other_portfolio_invitation.id))
|
||||
|
||||
|
||||
class TestPortfolioDomainRequestViewAccess(MockDbForIndividualTests):
|
||||
"""Tests for domain request views to ensure users can only access domain requests in their portfolio."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Create portfolios
|
||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||
self.other_portfolio = Portfolio.objects.create(creator=self.tired_user, organization_name="Other Portfolio")
|
||||
|
||||
# Create domain requests
|
||||
self.domain_request = completed_domain_request(
|
||||
name="test-domain.gov",
|
||||
portfolio=self.portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=self.user,
|
||||
)
|
||||
|
||||
self.other_domain_request = completed_domain_request(
|
||||
name="other-domain.gov",
|
||||
portfolio=self.other_portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=self.tired_user,
|
||||
)
|
||||
|
||||
# Give user permission to view all requests
|
||||
self.user_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS],
|
||||
)
|
||||
|
||||
# Setup session for portfolio views
|
||||
session = self.client.session
|
||||
session["portfolio"] = self.portfolio
|
||||
session.save()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_view_same_portfolio(self):
|
||||
"""Test that user can access domain requests in their portfolio."""
|
||||
# With just the view all permission, access should be denied
|
||||
response = self.client.get(reverse("edit-domain-request", kwargs={"domain_request_pk": self.domain_request.pk}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# But with the edit permission, the user should be able to access this domain request
|
||||
self.user_permission.additional_permissions = [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
]
|
||||
self.user_permission.save()
|
||||
self.user_permission.refresh_from_db()
|
||||
response = self.client.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": self.domain_request.pk}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_view_different_portfolio(self):
|
||||
"""Test that user cannot access domain request not in their portfolio."""
|
||||
response = self.client.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": self.other_domain_request.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_viewonly_same_portfolio(self):
|
||||
"""Test that user can access view-only domain request in their portfolio."""
|
||||
response = self.client.get(
|
||||
reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": self.domain_request.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_requests", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_viewonly_different_portfolio(self):
|
||||
"""Test that user cannot access view-only domain request not in their portfolio."""
|
||||
response = self.client.get(
|
||||
reverse("domain-request-status-viewonly", kwargs={"domain_request_pk": self.other_domain_request.pk})
|
||||
)
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestPortfolioDomainViewAccess(MockDbForIndividualTests):
|
||||
"""Tests for domain views to ensure users can only access domains in their portfolio."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Create portfolios
|
||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||
self.other_portfolio = Portfolio.objects.create(creator=self.tired_user, organization_name="Other Portfolio")
|
||||
|
||||
# Create domains through domain requests
|
||||
self.domain_request = completed_domain_request(
|
||||
name="test-domain.gov",
|
||||
portfolio=self.portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
user=self.user,
|
||||
)
|
||||
self.domain_request.approve()
|
||||
self.domain = self.domain_request.approved_domain
|
||||
|
||||
self.other_domain_request = completed_domain_request(
|
||||
name="other-domain.gov",
|
||||
portfolio=self.other_portfolio,
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
user=self.user,
|
||||
)
|
||||
self.other_domain_request.approve()
|
||||
self.other_domain = self.other_domain_request.approved_domain
|
||||
|
||||
# Give user permission to view all domains
|
||||
self.user_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
|
||||
)
|
||||
|
||||
# Setup session for portfolio views
|
||||
session = self.client.session
|
||||
session["portfolio"] = self.portfolio
|
||||
session.save()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_view_same_portfolio(self):
|
||||
"""Test that user can access domain in their portfolio."""
|
||||
response = self.client.get(reverse("domain", kwargs={"domain_pk": self.domain.pk}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_domain_view_different_portfolio(self):
|
||||
"""Test that user cannot access domain not in their portfolio."""
|
||||
response = self.client.get(reverse("domain", kwargs={"domain_pk": self.other_domain.pk}))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
||||
class TestPortfolioMemberViewAccess(MockDbForIndividualTests):
|
||||
"""Tests for member views to ensure users can only access members in their portfolio."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.client = Client()
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Create portfolios
|
||||
self.portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||
self.other_portfolio = Portfolio.objects.create(creator=self.tired_user, organization_name="Other Portfolio")
|
||||
|
||||
# Create portfolio permissions
|
||||
self.member_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.meoward_user, portfolio=self.portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
)
|
||||
|
||||
self.other_member_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.lebowski_user,
|
||||
portfolio=self.other_portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||
)
|
||||
|
||||
# Give user permission to view/edit members
|
||||
self.user_permission = UserPortfolioPermission.objects.create(
|
||||
user=self.user,
|
||||
portfolio=self.portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
)
|
||||
|
||||
# Setup session for portfolio views
|
||||
session = self.client.session
|
||||
session["portfolio"] = self.portfolio
|
||||
session.save()
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_member_view_same_portfolio(self):
|
||||
"""Test that user can access member in their portfolio."""
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": self.member_permission.pk}))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@less_console_noise_decorator
|
||||
def test_member_view_different_portfolio(self):
|
||||
"""Test that user cannot access member not in their portfolio."""
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": self.other_member_permission.pk}))
|
||||
self.assertEqual(response.status_code, 403)
|
|
@ -29,6 +29,8 @@ SAMPLE_KWARGS = {
|
|||
"user_pk": "1",
|
||||
"portfolio_id": "1",
|
||||
"user_id": "1",
|
||||
"member_pk": "1",
|
||||
"invitedmember_pk": "1",
|
||||
}
|
||||
|
||||
# Our test suite will ignore some namespaces.
|
||||
|
|
|
@ -867,7 +867,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify that the user cannot access the member page
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("member", kwargs={"pk": 1}), follow=True)
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": 1}), follow=True)
|
||||
# Make sure the page is denied
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
@ -886,7 +886,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify that the user cannot access the member page
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("member", kwargs={"pk": 1}), follow=True)
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": 1}), follow=True)
|
||||
# Make sure the page is denied
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
@ -909,7 +909,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify the page can be accessed
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("member", kwargs={"pk": permission_obj.pk}), follow=True)
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": permission_obj.pk}), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert text within the page is correct
|
||||
|
@ -942,7 +942,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify the page can be accessed
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("member", kwargs={"pk": permission_obj.pk}), follow=True)
|
||||
response = self.client.get(reverse("member", kwargs={"member_pk": permission_obj.pk}), follow=True)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert text within the page is correct
|
||||
|
@ -966,7 +966,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify that the user cannot access the member page
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"pk": 1}), follow=True)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"invitedmember_pk": 1}), follow=True)
|
||||
# Make sure the page is denied
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
@ -985,7 +985,7 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify that the user cannot access the member page
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"pk": 1}), follow=True)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"invitedmember_pk": 1}), follow=True)
|
||||
# Make sure the page is denied
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
|
@ -1016,7 +1016,9 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify the page can be accessed
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"pk": portfolio_invitation.pk}), follow=True)
|
||||
response = self.client.get(
|
||||
reverse("invitedmember", kwargs={"invitedmember_pk": portfolio_invitation.pk}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert text within the page is correct
|
||||
|
@ -1054,7 +1056,9 @@ class TestPortfolio(WebTest):
|
|||
|
||||
# Verify the page can be accessed
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse("invitedmember", kwargs={"pk": portfolio_invitation.pk}), follow=True)
|
||||
response = self.client.get(
|
||||
reverse("invitedmember", kwargs={"invitedmember_pk": portfolio_invitation.pk}), follow=True
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Assert text within the page is correct
|
||||
|
@ -1697,7 +1701,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
# We check X_REQUESTED_WITH bc those return JSON responses
|
||||
response = self.client.post(
|
||||
reverse("member-delete", kwargs={"pk": upp.pk}), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
reverse("member-delete", kwargs={"member_pk": upp.pk}), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400) # Bad request due to active requests
|
||||
|
@ -1738,7 +1742,8 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
# We check X_REQUESTED_WITH bc those return JSON responses
|
||||
response = self.client.post(
|
||||
reverse("member-delete", kwargs={"pk": admin_perm_user.pk}), HTTP_X_REQUESTED_WITH="XMLHttpRequest"
|
||||
reverse("member-delete", kwargs={"member_pk": admin_perm_user.pk}),
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
@ -1795,7 +1800,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
# We check X_REQUESTED_WITH bc those return JSON responses
|
||||
reverse("member-delete", kwargs={"pk": upp.pk}),
|
||||
reverse("member-delete", kwargs={"member_pk": upp.pk}),
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
|
||||
|
@ -1862,7 +1867,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
# We check X_REQUESTED_WITH bc those return JSON responses
|
||||
reverse("member-delete", kwargs={"pk": upp.pk}),
|
||||
reverse("member-delete", kwargs={"member_pk": upp.pk}),
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
|
||||
|
@ -1939,7 +1944,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
# We check X_REQUESTED_WITH bc those return JSON responses
|
||||
reverse("member-delete", kwargs={"pk": upp.pk}),
|
||||
reverse("member-delete", kwargs={"member_pk": upp.pk}),
|
||||
HTTP_X_REQUESTED_WITH="XMLHttpRequest",
|
||||
)
|
||||
|
||||
|
@ -2000,7 +2005,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
with patch("django.contrib.messages.error") as mock_error:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("member-delete", kwargs={"pk": upp.pk}),
|
||||
reverse("member-delete", kwargs={"member_pk": upp.pk}),
|
||||
)
|
||||
# We don't want to do follow=True in response bc that does automatic redirection
|
||||
|
||||
|
@ -2023,7 +2028,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
|
||||
# and then confirm that we're still on the Manage Members page
|
||||
self.assertEqual(response.headers["Location"], reverse("member", kwargs={"pk": upp.pk}))
|
||||
self.assertEqual(response.headers["Location"], reverse("member", kwargs={"member_pk": upp.pk}))
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -2047,7 +2052,7 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
with patch("django.contrib.messages.error") as mock_error:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("member-delete", kwargs={"pk": admin_perm_user.pk}),
|
||||
reverse("member-delete", kwargs={"member_pk": admin_perm_user.pk}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
@ -2066,7 +2071,9 @@ class TestPortfolioMemberDeleteView(WebTest):
|
|||
|
||||
# Location is used for a 3xx HTTP status code to indicate that the URL was redirected
|
||||
# and then confirm that we're still on the Manage Members page
|
||||
self.assertEqual(response.headers["Location"], reverse("member", kwargs={"pk": admin_perm_user.pk}))
|
||||
self.assertEqual(
|
||||
response.headers["Location"], reverse("member", kwargs={"member_pk": admin_perm_user.pk})
|
||||
)
|
||||
|
||||
|
||||
class TestPortfolioInvitedMemberDeleteView(WebTest):
|
||||
|
@ -2125,7 +2132,7 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
with patch("django.contrib.messages.success") as mock_success:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
|
||||
reverse("invitedmember-delete", kwargs={"invitedmember_pk": invitation.pk}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
@ -2190,7 +2197,7 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
with patch("django.contrib.messages.success") as mock_success:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
|
||||
reverse("invitedmember-delete", kwargs={"invitedmember_pk": invitation.pk}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
@ -2263,7 +2270,7 @@ class TestPortfolioInvitedMemberDeleteView(WebTest):
|
|||
with patch("django.contrib.messages.success") as mock_success:
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-delete", kwargs={"pk": invitation.pk}),
|
||||
reverse("invitedmember-delete", kwargs={"invitedmember_pk": invitation.pk}),
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
@ -2365,7 +2372,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains view is accessible."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the page loaded, and that we're on the right page
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -2378,7 +2385,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains view is not accessible to user with no perms."""
|
||||
self.client.force_login(self.user_no_perms)
|
||||
|
||||
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the request returns forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -2390,7 +2397,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains view is not accessible when no authenticated user."""
|
||||
self.client.logout()
|
||||
|
||||
response = self.client.get(reverse("member-domains", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the request returns redirect to openid login
|
||||
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||
|
@ -2403,7 +2410,7 @@ class TestPortfolioMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains view returns not found if user portfolio permission not found."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("member-domains", kwargs={"pk": "0"}))
|
||||
response = self.client.get(reverse("member-domains", kwargs={"member_pk": "0"}))
|
||||
|
||||
# Make sure the response is not found
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -2463,7 +2470,7 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains view is accessible."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.id}))
|
||||
|
||||
# Make sure the page loaded, and that we're on the right page
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -2476,7 +2483,7 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains view is not accessible to user with no perms."""
|
||||
self.client.force_login(self.user_no_perms)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.id}))
|
||||
|
||||
# Make sure the request returns forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -2488,7 +2495,7 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains view is not accessible when no authenticated user."""
|
||||
self.client.logout()
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.id}))
|
||||
|
||||
# Make sure the request returns redirect to openid login
|
||||
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||
|
@ -2501,7 +2508,7 @@ class TestPortfolioInvitedMemberDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains view returns not found if user is not a member."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"pk": "0"}))
|
||||
response = self.client.get(reverse("invitedmember-domains", kwargs={"invitedmember_pk": "0"}))
|
||||
|
||||
# Make sure the response is not found
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -2566,7 +2573,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
],
|
||||
)
|
||||
# Create url to be used in all tests
|
||||
self.url = reverse("member-domains-edit", kwargs={"pk": self.portfolio_permission.pk})
|
||||
self.url = reverse("member-domains-edit", kwargs={"member_pk": self.portfolio_permission.pk})
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
@ -2584,7 +2591,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains edit view is accessible."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the page loaded, and that we're on the right page
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -2597,7 +2604,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains edit view is not accessible to user with no perms."""
|
||||
self.client.force_login(self.user_no_perms)
|
||||
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the request returns forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -2609,7 +2616,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio member domains edit view is not accessible when no authenticated user."""
|
||||
self.client.logout()
|
||||
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": self.permission.id}))
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"member_pk": self.permission.id}))
|
||||
|
||||
# Make sure the request returns redirect to openid login
|
||||
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||
|
@ -2623,7 +2630,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
portfolio permission not found."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"pk": "0"}))
|
||||
response = self.client.get(reverse("member-domains-edit", kwargs={"member_pk": "0"}))
|
||||
|
||||
# Make sure the response is not found
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -2645,7 +2652,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(user=self.user, role=UserDomainRole.Roles.MANAGER).count(), 3)
|
||||
|
||||
# Check for a success message and a redirect
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"member_pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
@ -2681,7 +2688,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(domain=self.domain3, user=self.user).count(), 1)
|
||||
|
||||
# Check for a success message and a redirect
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"member_pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
@ -2706,7 +2713,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||
|
||||
# Check for an error message and a redirect
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"member_pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -2729,7 +2736,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||
|
||||
# Check for an error message and a redirect
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"member_pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -2749,7 +2756,7 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(user=self.user).count(), 0)
|
||||
|
||||
# Check for an info message and a redirect
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(response, reverse("member-domains", kwargs={"member_pk": self.portfolio_permission.pk}))
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
@ -2772,7 +2779,9 @@ class TestPortfolioMemberDomainsEditView(TestWithUser, WebTest):
|
|||
self.assertEqual(UserDomainRole.objects.filter(user=self.user, role=UserDomainRole.Roles.MANAGER).count(), 0)
|
||||
|
||||
# Check for an error message and a redirect to edit form
|
||||
self.assertRedirects(response, reverse("member-domains-edit", kwargs={"pk": self.portfolio_permission.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("member-domains-edit", kwargs={"member_pk": self.portfolio_permission.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -2831,7 +2840,7 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
],
|
||||
)
|
||||
self.url = reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.pk})
|
||||
self.url = reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
|
@ -2849,7 +2858,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains edit view is accessible."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(
|
||||
reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": self.invitation.id})
|
||||
)
|
||||
|
||||
# Make sure the page loaded, and that we're on the right page
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
@ -2862,7 +2873,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains edit view is not accessible to user with no perms."""
|
||||
self.client.force_login(self.user_no_perms)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(
|
||||
reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": self.invitation.id})
|
||||
)
|
||||
|
||||
# Make sure the request returns forbidden
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
@ -2874,7 +2887,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains edit view is not accessible when no authenticated user."""
|
||||
self.client.logout()
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.id}))
|
||||
response = self.client.get(
|
||||
reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": self.invitation.id})
|
||||
)
|
||||
|
||||
# Make sure the request returns redirect to openid login
|
||||
self.assertEqual(response.status_code, 302) # Redirect to openid login
|
||||
|
@ -2887,7 +2902,7 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
"""Tests that the portfolio invited member domains edit view returns not found if user is not a member."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"pk": "0"}))
|
||||
response = self.client.get(reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": "0"}))
|
||||
|
||||
# Make sure the response is not found
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -2914,7 +2929,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# Check for a success message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
@ -2971,7 +2988,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# Check for a success message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
|
@ -3015,7 +3034,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# Check for a success message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
# assert that send_domain_invitation_email is not called
|
||||
mock_send_domain_email.assert_not_called()
|
||||
|
||||
|
@ -3035,7 +3056,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||
|
||||
# Check for an error message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -3058,7 +3081,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||
|
||||
# Check for an error message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -3078,7 +3103,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
self.assertEqual(DomainInvitation.objects.count(), 0)
|
||||
|
||||
# Check for an info message and a redirect
|
||||
self.assertRedirects(response, reverse("invitedmember-domains", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(str(messages[0]), "The domain assignment changes have been saved.")
|
||||
|
@ -3106,7 +3133,9 @@ class TestPortfolioInvitedMemberEditDomainsView(TestWithUser, WebTest):
|
|||
)
|
||||
|
||||
# Check for an error message and a redirect to edit form
|
||||
self.assertRedirects(response, reverse("invitedmember-domains-edit", kwargs={"pk": self.invitation.pk}))
|
||||
self.assertRedirects(
|
||||
response, reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": self.invitation.pk})
|
||||
)
|
||||
messages = list(response.wsgi_request._messages)
|
||||
self.assertEqual(len(messages), 1)
|
||||
self.assertEqual(
|
||||
|
@ -3901,17 +3930,59 @@ class TestPortfolioInviteNewMemberView(MockEppLib, WebTest):
|
|||
response = self.client.post(
|
||||
reverse("new-member"),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER.value,
|
||||
"domain_request_permission_member": UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
"email": self.user.email,
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
with open("debug_response.html", "w") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
|
||||
# Verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
f"{self.user.email} is already a member of another .gov organization.",
|
||||
"User is already a member of this portfolio.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
invite_count_after = PortfolioInvitation.objects.count()
|
||||
self.assertEqual(invite_count_after, invite_count_before)
|
||||
|
||||
# assert that send_portfolio_invitation_email is not called
|
||||
mock_send_email.assert_not_called()
|
||||
|
||||
@less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
@override_flag("organization_members", active=True)
|
||||
@patch("registrar.views.portfolios.send_portfolio_invitation_email")
|
||||
def test_member_invite_for_existing_member_uppercase(self, mock_send_email):
|
||||
"""Tests the member invitation flow for existing portfolio member with a different case."""
|
||||
self.client.force_login(self.user)
|
||||
|
||||
# Simulate a session to ensure continuity
|
||||
session_id = self.client.session.session_key
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
invite_count_before = PortfolioInvitation.objects.count()
|
||||
|
||||
# Simulate submission of member invite for user who has already been invited
|
||||
response = self.client.post(
|
||||
reverse("new-member"),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
"email": self.user.email.upper(),
|
||||
},
|
||||
follow=True,
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
with open("debug_response.html", "w") as f:
|
||||
f.write(response.content.decode("utf-8"))
|
||||
|
||||
# Verify messages
|
||||
self.assertContains(
|
||||
response,
|
||||
"User is already a member of this portfolio.",
|
||||
)
|
||||
|
||||
# Validate Database has not changed
|
||||
|
@ -4081,7 +4152,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
mock_send_update_email.return_value = True
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": basic_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
@ -4144,7 +4215,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
mock_send_update_email.return_value = False
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": basic_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
@ -4211,7 +4282,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": admin_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
@ -4249,7 +4320,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
mock_send_update_email.return_value = True
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": basic_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": basic_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4298,7 +4369,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
mock_send_update_email.return_value = True
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": admin_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4361,7 +4432,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
mock_send_update_email.return_value = False
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": admin_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4421,7 +4492,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
|
||||
# Test missing required admin permissions
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
# Missing required admin fields
|
||||
|
@ -4453,7 +4524,7 @@ class TestPortfolioMemberEditView(WebTest):
|
|||
admin_permission = UserPortfolioPermission.objects.get(user=self.user, portfolio=self.portfolio)
|
||||
|
||||
response = self.client.post(
|
||||
reverse("member-permissions", kwargs={"pk": admin_permission.id}),
|
||||
reverse("member-permissions", kwargs={"member_pk": admin_permission.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4526,7 +4597,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
@ -4569,7 +4640,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
@ -4615,7 +4686,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.admin_invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4661,7 +4732,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.admin_invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4707,7 +4778,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||
"domain_permissions": UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||
|
@ -4734,7 +4805,7 @@ class TestPortfolioInvitedMemberEditView(WebTest):
|
|||
|
||||
# Test updating invitation permissions
|
||||
response = self.client.post(
|
||||
reverse("invitedmember-permissions", kwargs={"pk": self.admin_invitation.id}),
|
||||
reverse("invitedmember-permissions", kwargs={"invitedmember_pk": self.admin_invitation.id}),
|
||||
{
|
||||
"role": UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ from django.utils import timezone
|
|||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from .common import MockSESClient, completed_domain_request # type: ignore
|
||||
from django_webtest import WebTest # type: ignore
|
||||
import boto3_mocking # type: ignore
|
||||
|
@ -53,6 +54,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
UserPortfolioPermission.objects.all().delete()
|
||||
Portfolio.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
FederalAgency.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_form_intro_acknowledgement(self):
|
||||
|
@ -2546,6 +2548,128 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertContains(dotgov_page, "CityofEudoraKS.gov")
|
||||
self.assertNotContains(dotgov_page, "medicare.gov")
|
||||
|
||||
# @less_console_noise_decorator
|
||||
@override_flag("organization_feature", active=True)
|
||||
def test_domain_request_dotgov_domain_FEB_questions(self):
|
||||
"""
|
||||
Test that for a member of a federal executive branch portfolio with org feature on, the dotgov domain page
|
||||
contains additional questions for OMB.
|
||||
"""
|
||||
agency, _ = FederalAgency.objects.get_or_create(
|
||||
agency="US Treasury Dept",
|
||||
federal_type=BranchChoices.EXECUTIVE,
|
||||
)
|
||||
|
||||
portfolio, _ = Portfolio.objects.get_or_create(
|
||||
creator=self.user,
|
||||
organization_name="Test Portfolio",
|
||||
organization_type=Portfolio.OrganizationChoices.FEDERAL,
|
||||
federal_agency=agency,
|
||||
)
|
||||
|
||||
portfolio_perm, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
intro_page = self.app.get(reverse("domain-request:start"))
|
||||
# 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]
|
||||
|
||||
intro_form = intro_page.forms[0]
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
intro_result = intro_form.submit()
|
||||
|
||||
# follow first redirect
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
portfolio_requesting_entity = intro_result.follow()
|
||||
session_id = self.app.cookies[settings.SESSION_COOKIE_NAME]
|
||||
|
||||
# ---- REQUESTING ENTITY PAGE ----
|
||||
requesting_entity_form = portfolio_requesting_entity.forms[0]
|
||||
requesting_entity_form["portfolio_requesting_entity-requesting_entity_is_suborganization"] = False
|
||||
|
||||
# test next button
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
requesting_entity_result = requesting_entity_form.submit()
|
||||
|
||||
# ---- CURRENT SITES PAGE ----
|
||||
# Follow the redirect to the next form page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_page = requesting_entity_result.follow()
|
||||
current_sites_form = current_sites_page.forms[0]
|
||||
current_sites_form["current_sites-0-website"] = "www.treasury.com"
|
||||
|
||||
# test saving the page
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
current_sites_result = current_sites_form.submit()
|
||||
|
||||
# ---- DOTGOV DOMAIN PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
dotgov_page = current_sites_result.follow()
|
||||
|
||||
# separate out these tests for readability
|
||||
self.feb_dotgov_domain_tests(dotgov_page)
|
||||
|
||||
# Now proceed with the actual test
|
||||
domain_form = dotgov_page.forms[0]
|
||||
domain = "test.gov"
|
||||
domain_form["dotgov_domain-requested_domain"] = domain
|
||||
domain_form["dotgov_domain-feb_naming_requirements"] = "True"
|
||||
domain_form["dotgov_domain-feb_naming_requirements_details"] = "test"
|
||||
with patch(
|
||||
"registrar.forms.domain_request_wizard.DotGovDomainForm.clean_requested_domain", return_value=domain
|
||||
): # noqa
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
domain_result = domain_form.submit()
|
||||
|
||||
# ---- PURPOSE PAGE ----
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
purpose_page = domain_result.follow()
|
||||
|
||||
self.feb_purpose_page_tests(purpose_page)
|
||||
|
||||
def feb_purpose_page_tests(self, purpose_page):
|
||||
self.assertContains(purpose_page, "What is the purpose of your requested domain?")
|
||||
|
||||
# Make sure the purpose selector form is present
|
||||
self.assertContains(purpose_page, "feb_purpose_choice")
|
||||
|
||||
# Make sure the purpose details form is present
|
||||
self.assertContains(purpose_page, "purpose-details")
|
||||
|
||||
# Make sure the timeframe yes/no form is present
|
||||
self.assertContains(purpose_page, "purpose-has_timeframe")
|
||||
|
||||
# Make sure the timeframe details form is present
|
||||
self.assertContains(purpose_page, "purpose-time_frame_details")
|
||||
|
||||
# Make sure the interagency initiative yes/no form is present
|
||||
self.assertContains(purpose_page, "purpose-is_interagency_initiative")
|
||||
|
||||
# Make sure the interagency initiative details form is present
|
||||
self.assertContains(purpose_page, "purpose-interagency_initiative_details")
|
||||
|
||||
def feb_dotgov_domain_tests(self, dotgov_page):
|
||||
# Make sure the dynamic example content doesn't show
|
||||
self.assertNotContains(dotgov_page, "medicare.gov")
|
||||
|
||||
# Make sure the link at the top directs to OPM FEB guidance
|
||||
self.assertContains(dotgov_page, "https://get.gov/domains/executive-branch-guidance/")
|
||||
|
||||
# Check for header of first FEB form
|
||||
self.assertContains(dotgov_page, "Does this submission meet each domain naming requirement?")
|
||||
|
||||
# Check for label of second FEB form
|
||||
self.assertContains(dotgov_page, "Provide details below")
|
||||
|
||||
# Check that the yes/no form was included
|
||||
self.assertContains(dotgov_page, "feb_naming_requirements")
|
||||
|
||||
# Check that the details form was included
|
||||
self.assertContains(dotgov_page, "feb_naming_requirements_details")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_request_formsets(self):
|
||||
"""Users are able to add more than one of some fields."""
|
||||
|
@ -2796,7 +2920,7 @@ class DomainRequestTests(TestWithUser, WebTest):
|
|||
self.assertEqual(intro_page.status_code, 200)
|
||||
|
||||
# This user should also be allowed to edit existing ones
|
||||
domain_request = completed_domain_request(user=self.user)
|
||||
domain_request = completed_domain_request(user=self.user, portfolio=portfolio)
|
||||
edit_page = self.app.get(
|
||||
reverse("edit-domain-request", kwargs={"domain_request_pk": domain_request.pk})
|
||||
).follow()
|
||||
|
@ -2904,7 +3028,9 @@ class DomainRequestTestDifferentStatuses(TestWithUser, WebTest):
|
|||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||
)
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user)
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED, user=self.user, portfolio=portfolio
|
||||
)
|
||||
domain_request.save()
|
||||
|
||||
detail_page = self.app.get(f"/domain-request/{domain_request.id}")
|
||||
|
@ -3044,13 +3170,17 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
|||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
additional_permissions=[UserPortfolioPermissionChoices.EDIT_REQUESTS],
|
||||
)
|
||||
domain_request.portfolio = portfolio
|
||||
domain_request.save()
|
||||
domain_request.refresh_from_db()
|
||||
|
||||
# Check portfolio-specific breadcrumb
|
||||
portfolio_page = self.app.get(f"/domain-request/{domain_request.id}/edit/").follow()
|
||||
self.app.set_cookie(settings.SESSION_COOKIE_NAME, session_id)
|
||||
|
||||
self.assertContains(portfolio_page, "Domain requests")
|
||||
|
||||
domain_request.portfolio = None
|
||||
domain_request.save()
|
||||
# Clean up portfolio
|
||||
permission.delete()
|
||||
portfolio.delete()
|
||||
|
@ -3177,15 +3307,6 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
|||
- The user does not see the Domain and Domain requests buttons
|
||||
"""
|
||||
|
||||
# This should unlock 4 steps by default.
|
||||
# Purpose, .gov domain, current websites, and requirements for operating
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||
user=self.user,
|
||||
)
|
||||
domain_request.anything_else = None
|
||||
domain_request.save()
|
||||
|
||||
federal_agency = FederalAgency.objects.get(agency="Non-Federal Agency")
|
||||
# Add a portfolio
|
||||
portfolio = Portfolio.objects.create(
|
||||
|
@ -3203,6 +3324,14 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
|||
],
|
||||
)
|
||||
|
||||
# This should unlock 4 steps by default.
|
||||
# Purpose, .gov domain, current websites, and requirements for operating
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.STARTED, user=self.user, portfolio=portfolio
|
||||
)
|
||||
domain_request.anything_else = None
|
||||
domain_request.save()
|
||||
|
||||
response = self.app.get(f"/domain-request/{domain_request.id}/edit/")
|
||||
# django-webtest does not handle cookie-based sessions well because it keeps
|
||||
# resetting the session key on each new request, thus destroying the concept
|
||||
|
@ -3247,6 +3376,8 @@ class TestDomainRequestWizard(TestWithUser, WebTest):
|
|||
self.fail(f"Expected a redirect, but got a different response: {response}")
|
||||
|
||||
# Data cleanup
|
||||
domain_request.portfolio = None
|
||||
domain_request.save()
|
||||
user_portfolio_permission.delete()
|
||||
portfolio.delete()
|
||||
federal_agency.delete()
|
||||
|
@ -3311,7 +3442,9 @@ class TestPortfolioDomainRequestViewonly(TestWithUser, WebTest):
|
|||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
dummy_user, _ = User.objects.get_or_create(username="testusername123456")
|
||||
domain_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED, user=dummy_user)
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED, user=dummy_user, portfolio=portfolio
|
||||
)
|
||||
domain_request.save()
|
||||
|
||||
detail_page = self.app.get(f"/domain-request/viewonly/{domain_request.id}")
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from registrar.models.domain_request import DomainRequest
|
||||
from django.conf import settings
|
||||
from django.template.loader import get_template
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
|
@ -35,8 +36,13 @@ def _get_default_email(domain_request, file_path, reason, excluded_reasons=None)
|
|||
return None
|
||||
|
||||
recipient = domain_request.creator
|
||||
env_base_url = settings.BASE_URL
|
||||
# If NOT in prod, update instances of "manage.get.gov" links to point to
|
||||
# current environment, ie "getgov-rh.app.cloud.gov"
|
||||
manage_url = env_base_url if not settings.IS_PRODUCTION else "https://manage.get.gov"
|
||||
|
||||
# Return the context of the rendered views
|
||||
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason}
|
||||
context = {"domain_request": domain_request, "recipient": recipient, "reason": reason, "manage_url": manage_url}
|
||||
|
||||
email_body_text = get_template(file_path).render(context=context)
|
||||
email_body_text_cleaned = email_body_text.strip().lstrip("\n") if email_body_text else None
|
||||
|
|
|
@ -15,10 +15,12 @@ from registrar.decorators import (
|
|||
grant_access,
|
||||
)
|
||||
from registrar.forms import domain_request_wizard as forms
|
||||
from registrar.forms import feb
|
||||
from registrar.forms.utility.wizard_form_helper import request_step_list
|
||||
from registrar.models import DomainRequest
|
||||
from registrar.models.contact import Contact
|
||||
from registrar.models.user import User
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from registrar.views.utility import StepsHelper
|
||||
from registrar.utility.enums import Step, PortfolioDomainRequestStep
|
||||
|
||||
|
@ -180,6 +182,9 @@ class DomainRequestWizard(TemplateView):
|
|||
"""Determines which step enum we should use for the wizard"""
|
||||
return PortfolioDomainRequestStep if self.is_portfolio else Step
|
||||
|
||||
def requires_feb_questions(self) -> bool:
|
||||
return self.domain_request.is_feb() and flag_is_active_for_user(self.request.user, "organization_feature")
|
||||
|
||||
@property
|
||||
def prefix(self):
|
||||
"""Namespace the wizard to avoid clashes in session variable names."""
|
||||
|
@ -222,7 +227,6 @@ class DomainRequestWizard(TemplateView):
|
|||
creator=self.request.user,
|
||||
portfolio=portfolio,
|
||||
)
|
||||
|
||||
# Question for reviewers: we should probably be doing this right?
|
||||
if portfolio and not self._domain_request.generic_org_type:
|
||||
self._domain_request.generic_org_type = portfolio.organization_type
|
||||
|
@ -593,7 +597,6 @@ class RequestingEntity(DomainRequestWizard):
|
|||
"suborganization_state_territory": None,
|
||||
}
|
||||
)
|
||||
|
||||
super().save(forms)
|
||||
|
||||
|
||||
|
@ -652,18 +655,130 @@ class CurrentSites(DomainRequestWizard):
|
|||
|
||||
class DotgovDomain(DomainRequestWizard):
|
||||
template_name = "domain_request_dotgov_domain.html"
|
||||
forms = [forms.DotGovDomainForm, forms.AlternativeDomainFormSet]
|
||||
forms = [
|
||||
forms.DotGovDomainForm,
|
||||
forms.AlternativeDomainFormSet,
|
||||
feb.ExecutiveNamingRequirementsYesNoForm,
|
||||
feb.ExecutiveNamingRequirementsDetailsForm,
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["generic_org_type"] = self.domain_request.generic_org_type
|
||||
context["federal_type"] = self.domain_request.federal_type
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
return context
|
||||
|
||||
def is_valid(self, forms_list: list) -> bool:
|
||||
"""
|
||||
Expected order of forms_list:
|
||||
0: DotGovDomainForm
|
||||
1: AlternativeDomainFormSet
|
||||
2: ExecutiveNamingRequirementsYesNoForm
|
||||
3: ExecutiveNamingRequirementsDetailsForm
|
||||
"""
|
||||
logger.debug("Validating dotgov domain form")
|
||||
# If FEB questions aren't required, validate only non-FEB forms
|
||||
if not self.requires_feb_questions():
|
||||
forms_list[2].mark_form_for_deletion()
|
||||
forms_list[3].mark_form_for_deletion()
|
||||
return forms_list[0].is_valid() and forms_list[1].is_valid()
|
||||
|
||||
if not forms_list[2].is_valid():
|
||||
logger.debug("Dotgov domain form is invalid")
|
||||
# mark details form for deletion so that its errors don't show up
|
||||
forms_list[3].mark_form_for_deletion()
|
||||
return False
|
||||
|
||||
if forms_list[2].cleaned_data.get("feb_naming_requirements", None):
|
||||
logger.debug("Marking details form for deletion")
|
||||
# If the user selects "yes" or has made no selection, no details are needed.
|
||||
forms_list[3].mark_form_for_deletion()
|
||||
valid = all(form.is_valid() for i, form in enumerate(forms_list) if i != 3)
|
||||
else:
|
||||
# "No" was selected – details are required.
|
||||
valid = all(form.is_valid() for form in forms_list)
|
||||
return valid
|
||||
|
||||
|
||||
class Purpose(DomainRequestWizard):
|
||||
template_name = "domain_request_purpose.html"
|
||||
forms = [forms.PurposeForm]
|
||||
|
||||
forms = [
|
||||
feb.FEBPurposeOptionsForm,
|
||||
forms.PurposeDetailsForm,
|
||||
feb.FEBTimeFrameYesNoForm,
|
||||
feb.FEBTimeFrameDetailsForm,
|
||||
feb.FEBInteragencyInitiativeYesNoForm,
|
||||
feb.FEBInteragencyInitiativeDetailsForm,
|
||||
]
|
||||
|
||||
def get_context_data(self):
|
||||
context = super().get_context_data()
|
||||
context["requires_feb_questions"] = self.requires_feb_questions()
|
||||
return context
|
||||
|
||||
def is_valid(self, forms_list: list) -> bool:
|
||||
"""
|
||||
Expected order of forms_list:
|
||||
0: FEBPurposeOptionsForm
|
||||
1: PurposeDetailsForm
|
||||
2: FEBTimeFrameYesNoForm
|
||||
3: FEBTimeFrameDetailsForm
|
||||
4: FEBInteragencyInitiativeYesNoForm
|
||||
5: FEBInteragencyInitiativeDetailsForm
|
||||
"""
|
||||
|
||||
feb_purpose_options_form = forms_list[0]
|
||||
purpose_details_form = forms_list[1]
|
||||
feb_timeframe_yes_no_form = forms_list[2]
|
||||
feb_timeframe_details_form = forms_list[3]
|
||||
feb_initiative_yes_no_form = forms_list[4]
|
||||
feb_initiative_details_form = forms_list[5]
|
||||
|
||||
if not self.requires_feb_questions():
|
||||
# if FEB questions don't apply, mark those forms for deletion
|
||||
feb_purpose_options_form.mark_form_for_deletion()
|
||||
feb_timeframe_yes_no_form.mark_form_for_deletion()
|
||||
feb_timeframe_details_form.mark_form_for_deletion()
|
||||
feb_initiative_yes_no_form.mark_form_for_deletion()
|
||||
feb_initiative_details_form.mark_form_for_deletion()
|
||||
# we only care about the purpose details form in this case since it's used in both instances
|
||||
return purpose_details_form.is_valid()
|
||||
|
||||
if feb_purpose_options_form.is_valid():
|
||||
option = feb_purpose_options_form.cleaned_data.get("feb_purpose_choice")
|
||||
if option == "new":
|
||||
purpose_details_form.fields["purpose"].error_messages = {
|
||||
"required": "Explain why a new domain is required."
|
||||
}
|
||||
elif option == "redirect":
|
||||
purpose_details_form.fields["purpose"].error_messages = {
|
||||
"required": "Explain why a redirect is needed."
|
||||
}
|
||||
elif option == "other":
|
||||
purpose_details_form.fields["purpose"].error_messages = {
|
||||
"required": "Provide details on how this domain will be used."
|
||||
}
|
||||
# If somehow none of these are true use the default error message
|
||||
else:
|
||||
# Ensure details form doesn't throw errors if it's not showing
|
||||
purpose_details_form.mark_form_for_deletion()
|
||||
|
||||
feb_timeframe_valid = feb_timeframe_yes_no_form.is_valid()
|
||||
feb_initiative_valid = feb_initiative_yes_no_form.is_valid()
|
||||
|
||||
if not feb_timeframe_valid or not feb_timeframe_yes_no_form.cleaned_data.get("has_timeframe"):
|
||||
# Ensure details form doesn't throw errors if it's not showing
|
||||
feb_timeframe_details_form.mark_form_for_deletion()
|
||||
|
||||
if not feb_initiative_valid or not feb_initiative_yes_no_form.cleaned_data.get("is_interagency_initiative"):
|
||||
# Ensure details form doesn't throw errors if it's not showing
|
||||
feb_initiative_details_form.mark_form_for_deletion()
|
||||
|
||||
valid = all(form.is_valid() for form in forms_list if not form.form_data_marked_for_deletion)
|
||||
|
||||
return valid
|
||||
|
||||
|
||||
class OtherContacts(DomainRequestWizard):
|
||||
|
@ -711,9 +826,7 @@ class OtherContacts(DomainRequestWizard):
|
|||
|
||||
|
||||
class AdditionalDetails(DomainRequestWizard):
|
||||
|
||||
template_name = "domain_request_additional_details.html"
|
||||
|
||||
forms = [
|
||||
forms.CisaRepresentativeYesNoForm,
|
||||
forms.CisaRepresentativeForm,
|
||||
|
@ -816,11 +929,9 @@ class Finished(DomainRequestWizard):
|
|||
forms = [] # type: ignore
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = self.get_context_data()
|
||||
context["domain_request_id"] = self.domain_request.id
|
||||
# clean up this wizard session, because we are done with it
|
||||
del self.storage
|
||||
return render(self.request, self.template_name, context)
|
||||
return render(self.request, self.template_name)
|
||||
|
||||
|
||||
@grant_access(IS_DOMAIN_REQUEST_CREATOR, HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT)
|
||||
|
|
|
@ -213,9 +213,12 @@ class PortfolioMembersJson(View):
|
|||
view_only = not user.has_edit_members_portfolio_permission(portfolio) or not user_can_edit_other_users
|
||||
|
||||
is_admin = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in (item.get("roles") or [])
|
||||
action_url = reverse(item["type"], kwargs={"pk": item["id"]})
|
||||
|
||||
item_type = item.get("type", "")
|
||||
if item_type == "invitedmember":
|
||||
action_url = reverse(item["type"], kwargs={"invitedmember_pk": item["id"]})
|
||||
else:
|
||||
action_url = reverse(item["type"], kwargs={"member_pk": item["id"]})
|
||||
|
||||
# Ensure domain_info is properly processed for invites -
|
||||
# we need to un-concatenate the subquery
|
||||
|
|
|
@ -76,9 +76,10 @@ class PortfolioMemberView(DetailView, View):
|
|||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member.html"
|
||||
pk_url_kwarg = "member_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
def get(self, request, member_pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
member = portfolio_permission.user
|
||||
|
||||
# We have to explicitely name these with member_ otherwise we'll have conflicts with context preprocessors
|
||||
|
@ -102,8 +103,8 @@ class PortfolioMemberView(DetailView, View):
|
|||
request,
|
||||
self.template_name,
|
||||
{
|
||||
"edit_url": reverse("member-permissions", args=[pk]),
|
||||
"domains_url": reverse("member-domains", args=[pk]),
|
||||
"edit_url": reverse("member-permissions", args=[member_pk]),
|
||||
"domains_url": reverse("member-domains", args=[member_pk]),
|
||||
"portfolio_permission": portfolio_permission,
|
||||
"member": member,
|
||||
"member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission,
|
||||
|
@ -115,22 +116,23 @@ class PortfolioMemberView(DetailView, View):
|
|||
)
|
||||
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioMemberDeleteView(View):
|
||||
pk_url_kwarg = "member_pk"
|
||||
|
||||
def post(self, request, pk):
|
||||
def post(self, request, member_pk):
|
||||
"""
|
||||
Find and delete the portfolio member using the provided primary key (pk).
|
||||
Redirect to a success page after deletion (or any other appropriate page).
|
||||
"""
|
||||
portfolio_member_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
portfolio_member_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
member = portfolio_member_permission.user
|
||||
portfolio = portfolio_member_permission.portfolio
|
||||
|
||||
# Validate if the member can be removed
|
||||
error_message = self._validate_member_removal(request, member, portfolio)
|
||||
if error_message:
|
||||
return self._handle_error_response(request, error_message, pk)
|
||||
return self._handle_error_response(request, error_message, member_pk)
|
||||
|
||||
# Attempt to send notification emails
|
||||
self._send_removal_notifications(request, portfolio_member_permission)
|
||||
|
@ -161,14 +163,14 @@ class PortfolioMemberDeleteView(View):
|
|||
)
|
||||
return None
|
||||
|
||||
def _handle_error_response(self, request, error_message, pk):
|
||||
def _handle_error_response(self, request, error_message, member_pk):
|
||||
"""
|
||||
Return an error response (JSON or redirect with messages).
|
||||
"""
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({"error": error_message}, status=400)
|
||||
messages.error(request, error_message)
|
||||
return redirect(reverse("member", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member", kwargs={"member_pk": member_pk}))
|
||||
|
||||
def _send_removal_notifications(self, request, portfolio_member_permission):
|
||||
"""
|
||||
|
@ -223,9 +225,10 @@ class PortfolioMemberEditView(DetailView, View):
|
|||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = portfolioForms.PortfolioMemberForm
|
||||
pk_url_kwarg = "member_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
def get(self, request, member_pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
user = portfolio_permission.user
|
||||
|
||||
form = self.form_class(instance=portfolio_permission)
|
||||
|
@ -240,8 +243,8 @@ class PortfolioMemberEditView(DetailView, View):
|
|||
},
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
def post(self, request, member_pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
user = portfolio_permission.user
|
||||
form = self.form_class(request.POST, instance=portfolio_permission)
|
||||
removing_admin_role_on_self = False
|
||||
|
@ -276,7 +279,7 @@ class PortfolioMemberEditView(DetailView, View):
|
|||
self._handle_exceptions(e)
|
||||
form.save()
|
||||
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||
return redirect("member", pk=pk) if not removing_admin_role_on_self else redirect("home")
|
||||
return redirect("member", member_pk=member_pk) if not removing_admin_role_on_self else redirect("home")
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -304,9 +307,10 @@ class PortfolioMemberEditView(DetailView, View):
|
|||
class PortfolioMemberDomainsView(View):
|
||||
|
||||
template_name = "portfolio_member_domains.html"
|
||||
pk_url_kwarg = "member_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
def get(self, request, member_pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
member = portfolio_permission.user
|
||||
|
||||
return render(
|
||||
|
@ -324,9 +328,10 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
|||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_domains_edit.html"
|
||||
pk_url_kwarg = "member_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
def get(self, request, member_pk):
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
member = portfolio_permission.user
|
||||
|
||||
return render(
|
||||
|
@ -338,33 +343,33 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
|||
},
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
def post(self, request, member_pk):
|
||||
"""
|
||||
Handles adding and removing domains for a portfolio member.
|
||||
"""
|
||||
added_domains = request.POST.get("added_domains")
|
||||
removed_domains = request.POST.get("removed_domains")
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=pk)
|
||||
portfolio_permission = get_object_or_404(UserPortfolioPermission, pk=member_pk)
|
||||
member = portfolio_permission.user
|
||||
portfolio = portfolio_permission.portfolio
|
||||
|
||||
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
|
||||
if added_domain_ids is None:
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains", kwargs={"member_pk": member_pk}))
|
||||
|
||||
removed_domain_ids = self._parse_domain_ids(removed_domains, "removed domains")
|
||||
if removed_domain_ids is None:
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains", kwargs={"member_pk": member_pk}))
|
||||
|
||||
if not (added_domain_ids or removed_domain_ids):
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains", kwargs={"member_pk": member_pk}))
|
||||
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, member, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, member)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("member-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains", kwargs={"member_pk": member_pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
|
@ -372,7 +377,7 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
|||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains-edit", kwargs={"member_pk": member_pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
|
@ -380,7 +385,7 @@ class PortfolioMemberDomainsEditView(DetailView, View):
|
|||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}", exc_info=True)
|
||||
return redirect(reverse("member-domains-edit", kwargs={"pk": pk}))
|
||||
return redirect(reverse("member-domains-edit", kwargs={"member_pk": member_pk}))
|
||||
|
||||
def _parse_domain_ids(self, domain_data, domain_type):
|
||||
"""
|
||||
|
@ -437,9 +442,10 @@ class PortfolioInvitedMemberView(DetailView, View):
|
|||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member.html"
|
||||
# form_class = PortfolioInvitedMemberForm
|
||||
pk_url_kwarg = "invitedmember_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
def get(self, request, invitedmember_pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
# form = self.form_class(instance=portfolio_invitation)
|
||||
|
||||
# We have to explicitely name these with member_ otherwise we'll have conflicts with context preprocessors
|
||||
|
@ -463,8 +469,8 @@ class PortfolioInvitedMemberView(DetailView, View):
|
|||
request,
|
||||
self.template_name,
|
||||
{
|
||||
"edit_url": reverse("invitedmember-permissions", args=[pk]),
|
||||
"domains_url": reverse("invitedmember-domains", args=[pk]),
|
||||
"edit_url": reverse("invitedmember-permissions", args=[invitedmember_pk]),
|
||||
"domains_url": reverse("invitedmember-domains", args=[invitedmember_pk]),
|
||||
"portfolio_invitation": portfolio_invitation,
|
||||
"member_has_view_all_requests_portfolio_permission": member_has_view_all_requests_portfolio_permission,
|
||||
"member_has_edit_request_portfolio_permission": member_has_edit_request_portfolio_permission,
|
||||
|
@ -475,15 +481,16 @@ class PortfolioInvitedMemberView(DetailView, View):
|
|||
)
|
||||
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioInvitedMemberDeleteView(View):
|
||||
pk_url_kwarg = "invitedmember_pk"
|
||||
|
||||
def post(self, request, pk):
|
||||
def post(self, request, invitedmember_pk):
|
||||
"""
|
||||
Find and delete the portfolio invited member using the provided primary key (pk).
|
||||
Redirect to a success page after deletion (or any other appropriate page).
|
||||
"""
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
|
||||
try:
|
||||
# if invitation being removed is an admin
|
||||
|
@ -527,9 +534,10 @@ class PortfolioInvitedMemberEditView(DetailView, View):
|
|||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_permissions.html"
|
||||
form_class = portfolioForms.PortfolioInvitedMemberForm
|
||||
pk_url_kwarg = "invitedmember_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
def get(self, request, invitedmember_pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
form = self.form_class(instance=portfolio_invitation)
|
||||
|
||||
return render(
|
||||
|
@ -541,8 +549,8 @@ class PortfolioInvitedMemberEditView(DetailView, View):
|
|||
},
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
def post(self, request, invitedmember_pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
form = self.form_class(request.POST, instance=portfolio_invitation)
|
||||
if form.is_valid():
|
||||
try:
|
||||
|
@ -568,7 +576,7 @@ class PortfolioInvitedMemberEditView(DetailView, View):
|
|||
self._handle_exceptions(e)
|
||||
form.save()
|
||||
messages.success(self.request, "The member access and permission changes have been saved.")
|
||||
return redirect("invitedmember", pk=pk)
|
||||
return redirect("invitedmember", invitedmember_pk=invitedmember_pk)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -596,9 +604,10 @@ class PortfolioInvitedMemberEditView(DetailView, View):
|
|||
class PortfolioInvitedMemberDomainsView(View):
|
||||
|
||||
template_name = "portfolio_member_domains.html"
|
||||
pk_url_kwarg = "invitedmember_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
def get(self, request, invitedmember_pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -615,9 +624,10 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
|||
model = Portfolio
|
||||
context_object_name = "portfolio"
|
||||
template_name = "portfolio_member_domains_edit.html"
|
||||
pk_url_kwarg = "invitedmember_pk"
|
||||
|
||||
def get(self, request, pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
def get(self, request, invitedmember_pk):
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
@ -627,33 +637,33 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
|||
},
|
||||
)
|
||||
|
||||
def post(self, request, pk):
|
||||
def post(self, request, invitedmember_pk):
|
||||
"""
|
||||
Handles adding and removing domains for a portfolio invitee.
|
||||
"""
|
||||
added_domains = request.POST.get("added_domains")
|
||||
removed_domains = request.POST.get("removed_domains")
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=pk)
|
||||
portfolio_invitation = get_object_or_404(PortfolioInvitation, pk=invitedmember_pk)
|
||||
email = portfolio_invitation.email
|
||||
portfolio = portfolio_invitation.portfolio
|
||||
|
||||
added_domain_ids = self._parse_domain_ids(added_domains, "added domains")
|
||||
if added_domain_ids is None:
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
|
||||
removed_domain_ids = self._parse_domain_ids(removed_domains, "removed domains")
|
||||
if removed_domain_ids is None:
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
|
||||
if not (added_domain_ids or removed_domain_ids):
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
|
||||
try:
|
||||
self._process_added_domains(added_domain_ids, email, request.user, portfolio)
|
||||
self._process_removed_domains(removed_domain_ids, email)
|
||||
messages.success(request, "The domain assignment changes have been saved.")
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
except IntegrityError:
|
||||
messages.error(
|
||||
request,
|
||||
|
@ -661,7 +671,7 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
|||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error("A database error occurred while saving changes.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
except Exception as e:
|
||||
messages.error(
|
||||
request,
|
||||
|
@ -669,7 +679,7 @@ class PortfolioInvitedMemberDomainsEditView(DetailView, View):
|
|||
f"please contact {DefaultUserValues.HELP_EMAIL}.",
|
||||
)
|
||||
logger.error(f"An unexpected error occurred: {str(e)}.", exc_info=True)
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"pk": pk}))
|
||||
return redirect(reverse("invitedmember-domains-edit", kwargs={"invitedmember_pk": invitedmember_pk}))
|
||||
|
||||
def _parse_domain_ids(self, domain_data, domain_type):
|
||||
"""
|
||||
|
@ -903,7 +913,7 @@ class PortfolioMembersView(View):
|
|||
return render(request, "portfolio_members.html")
|
||||
|
||||
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_ANY_PERM)
|
||||
@grant_access(HAS_PORTFOLIO_MEMBERS_EDIT)
|
||||
class PortfolioAddMemberView(DetailView, FormMixin):
|
||||
|
||||
template_name = "portfolio_members_add_new.html"
|
||||
|
@ -960,7 +970,7 @@ class PortfolioAddMemberView(DetailView, FormMixin):
|
|||
portfolio = form.cleaned_data["portfolio"]
|
||||
is_admin_invitation = UserPortfolioRoleChoices.ORGANIZATION_ADMIN in form.cleaned_data["roles"]
|
||||
|
||||
requested_user = User.objects.filter(email=requested_email).first()
|
||||
requested_user = User.objects.filter(email__iexact=requested_email).first()
|
||||
permission_exists = UserPortfolioPermission.objects.filter(user=requested_user, portfolio=portfolio).exists()
|
||||
try:
|
||||
if not requested_user or not permission_exists:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue