mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-13 16:17:01 +02:00
Merge branch 'main' into ag/2616-populate-suborg-and-portfolio-script
This commit is contained in:
commit
fbabd2029c
38 changed files with 1301 additions and 147 deletions
|
@ -62,4 +62,5 @@ The class provides the following optional configuration variables:
|
||||||
The class also provides helper methods:
|
The class also provides helper methods:
|
||||||
- `get_class_name`: Returns a display-friendly class name for the terminal prompt
|
- `get_class_name`: Returns a display-friendly class name for the terminal prompt
|
||||||
- `get_failure_message`: Returns the message to display if a record fails to update
|
- `get_failure_message`: Returns the message to display if a record fails to update
|
||||||
- `should_skip_record`: Defines the condition for skipping a record (by default, no records are skipped)
|
- `should_skip_record`: Defines the condition for skipping a record (by default, no records are skipped)
|
||||||
|
- `custom_filter`: Allows for additional filters that cannot be expressed using django queryset field lookups
|
|
@ -817,6 +817,28 @@ Example: `cf ssh getgov-za`
|
||||||
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
|
|:-:|:-------------------------- |:-----------------------------------------------------------------------------------|
|
||||||
| 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is |
|
| 1 | **federal_cio_csv_path** | Specifies where the federal CIO csv is |
|
||||||
|
|
||||||
|
## Update First Ready Values
|
||||||
|
This section outlines how to run the populate_first_ready script
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-za`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
```./manage.py update_first_ready```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
```docker-compose exec app ./manage.py update_first_ready```
|
||||||
|
|
||||||
## Populate Domain Request Dates
|
## Populate Domain Request Dates
|
||||||
This section outlines how to run the populate_domain_request_dates script
|
This section outlines how to run the populate_domain_request_dates script
|
||||||
|
|
||||||
|
|
14
src/registrar/assets/js/get-gov-admin-extra.js
Normal file
14
src/registrar/assets/js/get-gov-admin-extra.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// Use Django's jQuery with Select2 to make the user select on the user transfer view a combobox
|
||||||
|
(function($) {
|
||||||
|
$(document).ready(function() {
|
||||||
|
if ($) {
|
||||||
|
$("#selected_user").select2({
|
||||||
|
width: 'resolve',
|
||||||
|
placeholder: 'Select a user',
|
||||||
|
allowClear: true
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error('jQuery is not available');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})(window.jQuery);
|
|
@ -172,40 +172,39 @@ function addOrRemoveSessionBoolean(name, add){
|
||||||
** To perform data operations on this - we need to use jQuery rather than vanilla js.
|
** To perform data operations on this - we need to use jQuery rather than vanilla js.
|
||||||
*/
|
*/
|
||||||
(function (){
|
(function (){
|
||||||
let selector = django.jQuery("#id_investigator")
|
if (document.getElementById("id_investigator") && django && django.jQuery) {
|
||||||
let assignSelfButton = document.querySelector("#investigator__assign_self");
|
let selector = django.jQuery("#id_investigator")
|
||||||
if (!selector || !assignSelfButton) {
|
let assignSelfButton = document.querySelector("#investigator__assign_self");
|
||||||
return;
|
if (!selector || !assignSelfButton) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
let currentUserId = assignSelfButton.getAttribute("data-user-id");
|
|
||||||
let currentUserName = assignSelfButton.getAttribute("data-user-name");
|
|
||||||
if (!currentUserId || !currentUserName){
|
|
||||||
console.error("Could not assign current user: no values found.")
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook a click listener to the "Assign to me" button.
|
|
||||||
// Logic borrowed from here: https://select2.org/programmatic-control/add-select-clear-items#create-if-not-exists
|
|
||||||
assignSelfButton.addEventListener("click", function() {
|
|
||||||
if (selector.find(`option[value='${currentUserId}']`).length) {
|
|
||||||
// Select the value that is associated with the current user.
|
|
||||||
selector.val(currentUserId).trigger("change");
|
|
||||||
} else {
|
|
||||||
// Create a DOM Option that matches the desired user. Then append it and select it.
|
|
||||||
let userOption = new Option(currentUserName, currentUserId, true, true);
|
|
||||||
selector.append(userOption).trigger("change");
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to any change events, and hide the parent container if investigator has a value.
|
let currentUserId = assignSelfButton.getAttribute("data-user-id");
|
||||||
selector.on('change', function() {
|
let currentUserName = assignSelfButton.getAttribute("data-user-name");
|
||||||
// The parent container has display type flex.
|
if (!currentUserId || !currentUserName){
|
||||||
assignSelfButton.parentElement.style.display = this.value === currentUserId ? "none" : "flex";
|
console.error("Could not assign current user: no values found.")
|
||||||
});
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Hook a click listener to the "Assign to me" button.
|
||||||
|
// Logic borrowed from here: https://select2.org/programmatic-control/add-select-clear-items#create-if-not-exists
|
||||||
|
assignSelfButton.addEventListener("click", function() {
|
||||||
|
if (selector.find(`option[value='${currentUserId}']`).length) {
|
||||||
|
// Select the value that is associated with the current user.
|
||||||
|
selector.val(currentUserId).trigger("change");
|
||||||
|
} else {
|
||||||
|
// Create a DOM Option that matches the desired user. Then append it and select it.
|
||||||
|
let userOption = new Option(currentUserName, currentUserId, true, true);
|
||||||
|
selector.append(userOption).trigger("change");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen to any change events, and hide the parent container if investigator has a value.
|
||||||
|
selector.on('change', function() {
|
||||||
|
// The parent container has display type flex.
|
||||||
|
assignSelfButton.parentElement.style.display = this.value === currentUserId ? "none" : "flex";
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/** An IIFE for pages in DjangoAdmin that use a clipboard button
|
/** An IIFE for pages in DjangoAdmin that use a clipboard button
|
||||||
|
@ -215,7 +214,6 @@ function addOrRemoveSessionBoolean(name, add){
|
||||||
function copyToClipboardAndChangeIcon(button) {
|
function copyToClipboardAndChangeIcon(button) {
|
||||||
// Assuming the input is the previous sibling of the button
|
// Assuming the input is the previous sibling of the button
|
||||||
let input = button.previousElementSibling;
|
let input = button.previousElementSibling;
|
||||||
let userId = input.getAttribute("user-id")
|
|
||||||
// Copy input value to clipboard
|
// Copy input value to clipboard
|
||||||
if (input) {
|
if (input) {
|
||||||
navigator.clipboard.writeText(input.value).then(function() {
|
navigator.clipboard.writeText(input.value).then(function() {
|
||||||
|
|
|
@ -126,7 +126,7 @@ html[data-theme="light"] {
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form,
|
body.change-form,
|
||||||
.analytics {
|
.custom-admin-template, dt {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
.usa-table td {
|
.usa-table td {
|
||||||
|
@ -155,7 +155,7 @@ html[data-theme="dark"] {
|
||||||
body.dashboard,
|
body.dashboard,
|
||||||
body.change-list,
|
body.change-list,
|
||||||
body.change-form,
|
body.change-form,
|
||||||
.analytics {
|
.custom-admin-template, dt {
|
||||||
color: var(--body-fg);
|
color: var(--body-fg);
|
||||||
}
|
}
|
||||||
.usa-table td {
|
.usa-table td {
|
||||||
|
@ -166,7 +166,7 @@ html[data-theme="dark"] {
|
||||||
// Remove when dark mode successfully applies to Django delete page.
|
// Remove when dark mode successfully applies to Django delete page.
|
||||||
.delete-confirmation .content a:not(.button) {
|
.delete-confirmation .content a:not(.button) {
|
||||||
color: color('primary');
|
color: color('primary');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -370,14 +370,60 @@ input.admin-confirm-button {
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
}
|
}
|
||||||
.button {
|
}
|
||||||
display: inline-block;
|
|
||||||
padding: 10px 8px;
|
// This block resolves some of the issues we're seeing on buttons due to css
|
||||||
line-height: normal;
|
// conflicts between DJ and USWDS
|
||||||
}
|
a.button,
|
||||||
a.button:active, a.button:focus {
|
.usa-button--dja {
|
||||||
text-decoration: none;
|
display: inline-block;
|
||||||
}
|
padding: 10px 15px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 16.1px;
|
||||||
|
font-kerning: auto;
|
||||||
|
font-family: inherit;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.button svg,
|
||||||
|
.button span,
|
||||||
|
.usa-button--dja svg,
|
||||||
|
.usa-button--dja span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
||||||
|
background: var(--button-bg);
|
||||||
|
}
|
||||||
|
.usa-button--dja span {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary):hover {
|
||||||
|
background: var(--button-hover-bg);
|
||||||
|
}
|
||||||
|
a.button:active, a.button:focus {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.usa-modal {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
input[type=submit].button--dja-toolbar {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: var(--body-bg);
|
||||||
|
box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--body-fg);
|
||||||
|
}
|
||||||
|
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
|
||||||
|
border-color: var(--body-quiet-color);
|
||||||
|
}
|
||||||
|
// Targets the DJA buttom with a nested icon
|
||||||
|
button .usa-icon,
|
||||||
|
.button .usa-icon,
|
||||||
|
.button--clipboard .usa-icon {
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module--custom {
|
.module--custom {
|
||||||
|
@ -471,13 +517,6 @@ address.dja-address-contact-list {
|
||||||
color: var(--link-fg);
|
color: var(--link-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Targets the DJA buttom with a nested icon
|
|
||||||
button .usa-icon,
|
|
||||||
.button .usa-icon,
|
|
||||||
.button--clipboard .usa-icon {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.errors span.select2-selection {
|
.errors span.select2-selection {
|
||||||
border: 1px solid var(--error-fg) !important;
|
border: 1px solid var(--error-fg) !important;
|
||||||
}
|
}
|
||||||
|
@ -738,7 +777,7 @@ div.dja__model-description{
|
||||||
|
|
||||||
li {
|
li {
|
||||||
list-style-type: disc;
|
list-style-type: disc;
|
||||||
font-family: Source Sans Pro Web,Helvetica Neue,Helvetica,Roboto,Arial,sans-serif;
|
font-family: family('sans');
|
||||||
}
|
}
|
||||||
|
|
||||||
a, a:link, a:visited {
|
a, a:link, a:visited {
|
||||||
|
@ -878,3 +917,16 @@ ul.add-list-reset {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fix the combobox when deployed outside admin (eg user transfer)
|
||||||
|
.submit-row .select2,
|
||||||
|
.submit-row .select2 span {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.transfer-user-selector .select2-selection__placeholder {
|
||||||
|
color: #3d4551!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dl-dja dt {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
|
@ -357,13 +357,18 @@ CSP_FORM_ACTION = allowed_sources
|
||||||
# and inline with a nonce, as well as allowing connections back to their domain.
|
# and inline with a nonce, as well as allowing connections back to their domain.
|
||||||
# Note: If needed, we can embed chart.js instead of using the CDN
|
# Note: If needed, we can embed chart.js instead of using the CDN
|
||||||
CSP_DEFAULT_SRC = ("'self'",)
|
CSP_DEFAULT_SRC = ("'self'",)
|
||||||
CSP_STYLE_SRC = ["'self'", "https://www.ssa.gov/accessibility/andi/andi.css"]
|
CSP_STYLE_SRC = [
|
||||||
|
"'self'",
|
||||||
|
"https://www.ssa.gov/accessibility/andi/andi.css",
|
||||||
|
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css",
|
||||||
|
]
|
||||||
CSP_SCRIPT_SRC_ELEM = [
|
CSP_SCRIPT_SRC_ELEM = [
|
||||||
"'self'",
|
"'self'",
|
||||||
"https://www.googletagmanager.com/",
|
"https://www.googletagmanager.com/",
|
||||||
"https://cdn.jsdelivr.net/npm/chart.js",
|
"https://cdn.jsdelivr.net/npm/chart.js",
|
||||||
"https://www.ssa.gov",
|
"https://www.ssa.gov",
|
||||||
"https://ajax.googleapis.com",
|
"https://ajax.googleapis.com",
|
||||||
|
"https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js",
|
||||||
]
|
]
|
||||||
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/", "https://www.ssa.gov/accessibility/andi/andi.js"]
|
CSP_CONNECT_SRC = ["'self'", "https://www.google-analytics.com/", "https://www.ssa.gov/accessibility/andi/andi.js"]
|
||||||
CSP_INCLUDE_NONCE_IN = ["script-src-elem", "style-src"]
|
CSP_INCLUDE_NONCE_IN = ["script-src-elem", "style-src"]
|
||||||
|
|
|
@ -24,6 +24,7 @@ from registrar.views.report_views import (
|
||||||
|
|
||||||
from registrar.views.domain_request import Step
|
from registrar.views.domain_request import Step
|
||||||
from registrar.views.domain_requests_json import get_domain_requests_json
|
from registrar.views.domain_requests_json import get_domain_requests_json
|
||||||
|
from registrar.views.transfer_user import TransferUserView
|
||||||
from registrar.views.utility.api_views import (
|
from registrar.views.utility.api_views import (
|
||||||
get_senior_official_from_federal_agency_json,
|
get_senior_official_from_federal_agency_json,
|
||||||
get_federal_and_portfolio_types_from_federal_agency_json,
|
get_federal_and_portfolio_types_from_federal_agency_json,
|
||||||
|
@ -137,6 +138,7 @@ urlpatterns = [
|
||||||
AnalyticsView.as_view(),
|
AnalyticsView.as_view(),
|
||||||
name="analytics",
|
name="analytics",
|
||||||
),
|
),
|
||||||
|
path("admin/registrar/user/<int:user_id>/transfer/", TransferUserView.as_view(), name="transfer_user"),
|
||||||
path(
|
path(
|
||||||
"admin/api/get-senior-official-from-federal-agency-json/",
|
"admin/api/get-senior-official-from-federal-agency-json/",
|
||||||
get_senior_official_from_federal_agency_json,
|
get_senior_official_from_federal_agency_json,
|
||||||
|
|
|
@ -60,6 +60,17 @@ def add_has_profile_feature_flag_to_context(request):
|
||||||
|
|
||||||
def portfolio_permissions(request):
|
def portfolio_permissions(request):
|
||||||
"""Make portfolio permissions for the request user available in global context"""
|
"""Make portfolio permissions for the request user available in global context"""
|
||||||
|
context = {
|
||||||
|
"has_base_portfolio_permission": False,
|
||||||
|
"has_domains_portfolio_permission": False,
|
||||||
|
"has_domain_requests_portfolio_permission": False,
|
||||||
|
"has_view_members_portfolio_permission": False,
|
||||||
|
"has_edit_members_portfolio_permission": False,
|
||||||
|
"has_view_suborganization": False,
|
||||||
|
"has_edit_suborganization": False,
|
||||||
|
"portfolio": None,
|
||||||
|
"has_organization_feature_flag": False,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
portfolio = request.session.get("portfolio")
|
portfolio = request.session.get("portfolio")
|
||||||
if portfolio:
|
if portfolio:
|
||||||
|
@ -69,29 +80,15 @@ def portfolio_permissions(request):
|
||||||
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
|
"has_domain_requests_portfolio_permission": request.user.has_domain_requests_portfolio_permission(
|
||||||
portfolio
|
portfolio
|
||||||
),
|
),
|
||||||
|
"has_view_members_portfolio_permission": request.user.has_view_members_portfolio_permission(portfolio),
|
||||||
|
"has_edit_members_portfolio_permission": request.user.has_edit_members_portfolio_permission(portfolio),
|
||||||
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
|
"has_view_suborganization": request.user.has_view_suborganization(portfolio),
|
||||||
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
|
"has_edit_suborganization": request.user.has_edit_suborganization(portfolio),
|
||||||
"portfolio": portfolio,
|
"portfolio": portfolio,
|
||||||
"has_organization_feature_flag": True,
|
"has_organization_feature_flag": True,
|
||||||
}
|
}
|
||||||
return {
|
return context
|
||||||
"has_base_portfolio_permission": False,
|
|
||||||
"has_domains_portfolio_permission": False,
|
|
||||||
"has_domain_requests_portfolio_permission": False,
|
|
||||||
"has_view_suborganization": False,
|
|
||||||
"has_edit_suborganization": False,
|
|
||||||
"portfolio": None,
|
|
||||||
"has_organization_feature_flag": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Handles cases where request.user might not exist
|
# Handles cases where request.user might not exist
|
||||||
return {
|
return context
|
||||||
"has_base_portfolio_permission": False,
|
|
||||||
"has_domains_portfolio_permission": False,
|
|
||||||
"has_domain_requests_portfolio_permission": False,
|
|
||||||
"has_view_suborganization": False,
|
|
||||||
"has_edit_suborganization": False,
|
|
||||||
"portfolio": None,
|
|
||||||
"has_organization_feature_flag": False,
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect="""
|
prompt_message="""
|
||||||
This script will delete all rows from the following tables:
|
This script will delete all rows from the following tables:
|
||||||
* Contact
|
* Contact
|
||||||
* Domain
|
* Domain
|
||||||
|
|
|
@ -130,7 +130,7 @@ class Command(BaseCommand):
|
||||||
"""Asks if the user wants to proceed with this action"""
|
"""Asks if the user wants to proceed with this action"""
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Extension Amount==
|
==Extension Amount==
|
||||||
Period: {extension_amount} year(s)
|
Period: {extension_amount} year(s)
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ class Command(BaseCommand):
|
||||||
# Will sys.exit() when prompt is "n"
|
# Will sys.exit() when prompt is "n"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Master data file==
|
==Master data file==
|
||||||
domain_additional_filename: {org_args.domain_additional_filename}
|
domain_additional_filename: {org_args.domain_additional_filename}
|
||||||
|
|
||||||
|
@ -84,7 +84,7 @@ class Command(BaseCommand):
|
||||||
# Will sys.exit() when prompt is "n"
|
# Will sys.exit() when prompt is "n"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Master data file==
|
==Master data file==
|
||||||
domain_additional_filename: {org_args.domain_additional_filename}
|
domain_additional_filename: {org_args.domain_additional_filename}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
CSV: {federal_cio_csv_path}
|
CSV: {federal_cio_csv_path}
|
||||||
|
|
||||||
|
|
|
@ -651,7 +651,7 @@ class Command(BaseCommand):
|
||||||
title = "Do you wish to load additional data for TransitionDomains?"
|
title = "Do you wish to load additional data for TransitionDomains?"
|
||||||
proceed = TerminalHelper.prompt_for_execution(
|
proceed = TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
!!! ENSURE THAT ALL FILENAMES ARE CORRECT BEFORE PROCEEDING
|
!!! ENSURE THAT ALL FILENAMES ARE CORRECT BEFORE PROCEEDING
|
||||||
==Master data file==
|
==Master data file==
|
||||||
domain_additional_filename: {domain_additional_filename}
|
domain_additional_filename: {domain_additional_filename}
|
||||||
|
|
|
@ -91,7 +91,7 @@ class Command(BaseCommand):
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
Number of DomainInformation objects to change: {len(human_readable_domain_names)}
|
Number of DomainInformation objects to change: {len(human_readable_domain_names)}
|
||||||
The following DomainInformation objects will be modified: {human_readable_domain_names}
|
The following DomainInformation objects will be modified: {human_readable_domain_names}
|
||||||
|
@ -148,7 +148,7 @@ class Command(BaseCommand):
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==File location==
|
==File location==
|
||||||
current-full.csv filepath: {file_path}
|
current-full.csv filepath: {file_path}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ class Command(BaseCommand):
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
Number of Domain objects to change: {len(domains)}
|
Number of Domain objects to change: {len(domains)}
|
||||||
""",
|
""",
|
||||||
|
|
|
@ -54,7 +54,7 @@ class Command(BaseCommand):
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
Number of DomainRequest objects to change: {len(domain_requests)}
|
Number of DomainRequest objects to change: {len(domain_requests)}
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ class Command(BaseCommand):
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=f"""
|
||||||
==Proposed Changes==
|
==Proposed Changes==
|
||||||
Number of DomainInformation objects to change: {len(domain_infos)}
|
Number of DomainInformation objects to change: {len(domain_infos)}
|
||||||
|
|
||||||
|
|
38
src/registrar/management/commands/update_first_ready.py
Normal file
38
src/registrar/management/commands/update_first_ready.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import logging
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from registrar.management.commands.utility.terminal_helper import PopulateScriptTemplate, TerminalColors
|
||||||
|
from registrar.models import Domain, TransitionDomain
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand, PopulateScriptTemplate):
|
||||||
|
help = "Loops through each domain object and populates the last_status_update and first_submitted_date"
|
||||||
|
|
||||||
|
def handle(self, **kwargs):
|
||||||
|
"""Loops through each valid Domain object and updates it's first_ready value if it is out of sync"""
|
||||||
|
filter_conditions = {"state__in": [Domain.State.READY, Domain.State.ON_HOLD, Domain.State.DELETED]}
|
||||||
|
self.mass_update_records(Domain, filter_conditions, ["first_ready"], verbose=True)
|
||||||
|
|
||||||
|
def update_record(self, record: Domain):
|
||||||
|
"""Defines how we update the first_ready field"""
|
||||||
|
# update the first_ready value based on the creation date.
|
||||||
|
record.first_ready = record.created_at.date()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"{TerminalColors.OKCYAN}Updating {record} => first_ready: " f"{record.first_ready}{TerminalColors.ENDC}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# check if a transition domain object for this domain name exists,
|
||||||
|
# or if so whether its first_ready value matches its created_at date
|
||||||
|
def custom_filter(self, records):
|
||||||
|
to_include_pks = []
|
||||||
|
for record in records:
|
||||||
|
if (
|
||||||
|
TransitionDomain.objects.filter(domain_name=record.name).exists()
|
||||||
|
and record.first_ready != record.created_at.date()
|
||||||
|
): # noqa
|
||||||
|
to_include_pks.append(record.pk)
|
||||||
|
|
||||||
|
return records.filter(pk__in=to_include_pks)
|
|
@ -2,9 +2,12 @@ import logging
|
||||||
import sys
|
import sys
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Model
|
||||||
|
from django.db.models.manager import BaseManager
|
||||||
from typing import List
|
from typing import List
|
||||||
from registrar.utility.enums import LogCode
|
from registrar.utility.enums import LogCode
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -76,27 +79,60 @@ class PopulateScriptTemplate(ABC):
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def update_record(self, record):
|
def update_record(self, record):
|
||||||
"""Defines how we update each field. Must be defined before using mass_update_records."""
|
"""Defines how we update each field.
|
||||||
|
|
||||||
|
raises:
|
||||||
|
NotImplementedError: If not defined before calling mass_update_records.
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def mass_update_records(self, object_class, filter_conditions, fields_to_update, debug=True):
|
def mass_update_records(self, object_class, filter_conditions, fields_to_update, debug=True, verbose=False):
|
||||||
"""Loops through each valid "object_class" object - specified by filter_conditions - and
|
"""Loops through each valid "object_class" object - specified by filter_conditions - and
|
||||||
updates fields defined by fields_to_update using update_record.
|
updates fields defined by fields_to_update using update_record.
|
||||||
|
|
||||||
You must define update_record before you can use this function.
|
Parameters:
|
||||||
|
object_class: The Django model class that you want to perform the bulk update on.
|
||||||
|
This should be the actual class, not a string of the class name.
|
||||||
|
|
||||||
|
filter_conditions: dictionary of valid Django Queryset filter conditions
|
||||||
|
(e.g. {'verification_type__isnull'=True}).
|
||||||
|
|
||||||
|
fields_to_update: List of strings specifying which fields to update.
|
||||||
|
(e.g. ["first_ready_date", "last_submitted_date"])
|
||||||
|
|
||||||
|
debug: Whether to log script run summary in debug mode.
|
||||||
|
Default: True.
|
||||||
|
|
||||||
|
verbose: Whether to print a detailed run summary *before* run confirmation.
|
||||||
|
Default: False.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: If you do not define update_record before using this function.
|
||||||
|
TypeError: If custom_filter is not Callable.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
records = object_class.objects.filter(**filter_conditions) if filter_conditions else object_class.objects.all()
|
records = object_class.objects.filter(**filter_conditions) if filter_conditions else object_class.objects.all()
|
||||||
|
|
||||||
|
# apply custom filter
|
||||||
|
records = self.custom_filter(records)
|
||||||
|
|
||||||
readable_class_name = self.get_class_name(object_class)
|
readable_class_name = self.get_class_name(object_class)
|
||||||
|
|
||||||
|
# for use in the execution prompt.
|
||||||
|
proposed_changes = f"""==Proposed Changes==
|
||||||
|
Number of {readable_class_name} objects to change: {len(records)}
|
||||||
|
These fields will be updated on each record: {fields_to_update}
|
||||||
|
"""
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
proposed_changes = f"""{proposed_changes}
|
||||||
|
These records will be updated: {list(records.all())}
|
||||||
|
"""
|
||||||
|
|
||||||
# Code execution will stop here if the user prompts "N"
|
# Code execution will stop here if the user prompts "N"
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
info_to_inspect=f"""
|
prompt_message=proposed_changes,
|
||||||
==Proposed Changes==
|
|
||||||
Number of {readable_class_name} objects to change: {len(records)}
|
|
||||||
These fields will be updated on each record: {fields_to_update}
|
|
||||||
""",
|
|
||||||
prompt_title=self.prompt_title,
|
prompt_title=self.prompt_title,
|
||||||
)
|
)
|
||||||
logger.info("Updating...")
|
logger.info("Updating...")
|
||||||
|
@ -141,10 +177,17 @@ class PopulateScriptTemplate(ABC):
|
||||||
return f"{TerminalColors.FAIL}" f"Failed to update {record}" f"{TerminalColors.ENDC}"
|
return f"{TerminalColors.FAIL}" f"Failed to update {record}" f"{TerminalColors.ENDC}"
|
||||||
|
|
||||||
def should_skip_record(self, record) -> bool: # noqa
|
def should_skip_record(self, record) -> bool: # noqa
|
||||||
"""Defines the condition in which we should skip updating a record. Override as needed."""
|
"""Defines the condition in which we should skip updating a record. Override as needed.
|
||||||
|
The difference between this and custom_filter is that records matching these conditions
|
||||||
|
*will* be included in the run but will be skipped (and logged as such)."""
|
||||||
# By default - don't skip
|
# By default - don't skip
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def custom_filter(self, records: BaseManager[Model]) -> BaseManager[Model]:
|
||||||
|
"""Override to define filters that can't be represented by django queryset field lookups.
|
||||||
|
Applied to individual records *after* filter_conditions. True means"""
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
class TerminalHelper:
|
class TerminalHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -220,6 +263,9 @@ class TerminalHelper:
|
||||||
an answer is required of the user).
|
an answer is required of the user).
|
||||||
|
|
||||||
The "answer" return value is True for "yes" or False for "no".
|
The "answer" return value is True for "yes" or False for "no".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: When "default" is not "yes", "no", or None.
|
||||||
"""
|
"""
|
||||||
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
||||||
if default is None:
|
if default is None:
|
||||||
|
@ -244,6 +290,7 @@ class TerminalHelper:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_yes_no_exit(question: str, default="yes"):
|
def query_yes_no_exit(question: str, default="yes"):
|
||||||
"""Ask a yes/no question via raw_input() and return their answer.
|
"""Ask a yes/no question via raw_input() and return their answer.
|
||||||
|
Allows for answer "e" to exit.
|
||||||
|
|
||||||
"question" is a string that is presented to the user.
|
"question" is a string that is presented to the user.
|
||||||
"default" is the presumed answer if the user just hits <Enter>.
|
"default" is the presumed answer if the user just hits <Enter>.
|
||||||
|
@ -251,6 +298,9 @@ class TerminalHelper:
|
||||||
an answer is required of the user).
|
an answer is required of the user).
|
||||||
|
|
||||||
The "answer" return value is True for "yes" or False for "no".
|
The "answer" return value is True for "yes" or False for "no".
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: When "default" is not "yes", "no", or None.
|
||||||
"""
|
"""
|
||||||
valid = {
|
valid = {
|
||||||
"yes": True,
|
"yes": True,
|
||||||
|
@ -317,9 +367,8 @@ class TerminalHelper:
|
||||||
case _:
|
case _:
|
||||||
logger.info(print_statement)
|
logger.info(print_statement)
|
||||||
|
|
||||||
# TODO - "info_to_inspect" should be refactored to "prompt_message"
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prompt_for_execution(system_exit_on_terminate: bool, info_to_inspect: str, prompt_title: str) -> bool:
|
def prompt_for_execution(system_exit_on_terminate: bool, prompt_message: str, prompt_title: str) -> bool:
|
||||||
"""Create to reduce code complexity.
|
"""Create to reduce code complexity.
|
||||||
Prompts the user to inspect the given string
|
Prompts the user to inspect the given string
|
||||||
and asks if they wish to proceed.
|
and asks if they wish to proceed.
|
||||||
|
@ -340,7 +389,7 @@ class TerminalHelper:
|
||||||
=====================================================
|
=====================================================
|
||||||
*** IMPORTANT: VERIFY THE FOLLOWING LOOKS CORRECT ***
|
*** IMPORTANT: VERIFY THE FOLLOWING LOOKS CORRECT ***
|
||||||
|
|
||||||
{info_to_inspect}
|
{prompt_message}
|
||||||
{TerminalColors.FAIL}
|
{TerminalColors.FAIL}
|
||||||
Proceed? (Y = proceed, N = {action_description_for_selecting_no})
|
Proceed? (Y = proceed, N = {action_description_for_selecting_no})
|
||||||
{TerminalColors.ENDC}"""
|
{TerminalColors.ENDC}"""
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
# Generated by Django 4.2.10 on 2024-09-04 21:29
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("registrar", "0122_create_groups_v16"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="portfolioinvitation",
|
||||||
|
name="portfolio_additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("view_created_requests", "View created requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="userportfoliopermission",
|
||||||
|
name="additional_permissions",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("view_all_domains", "View all domains and domain reports"),
|
||||||
|
("view_managed_domains", "View managed domains"),
|
||||||
|
("view_members", "View members"),
|
||||||
|
("edit_members", "Create and edit members"),
|
||||||
|
("view_all_requests", "View all requests"),
|
||||||
|
("view_created_requests", "View created requests"),
|
||||||
|
("edit_requests", "Create and edit requests"),
|
||||||
|
("view_portfolio", "View organization"),
|
||||||
|
("edit_portfolio", "Edit organization"),
|
||||||
|
("view_suborganization", "View suborganization"),
|
||||||
|
("edit_suborganization", "Edit suborganization"),
|
||||||
|
],
|
||||||
|
max_length=50,
|
||||||
|
),
|
||||||
|
blank=True,
|
||||||
|
help_text="Select one or more additional permissions.",
|
||||||
|
null=True,
|
||||||
|
size=None,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -6,7 +6,7 @@ from django.db.models import Q
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
|
|
||||||
from registrar.models import DomainInformation, UserDomainRole
|
from registrar.models import DomainInformation, UserDomainRole
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
|
||||||
|
|
||||||
from .domain_invitation import DomainInvitation
|
from .domain_invitation import DomainInvitation
|
||||||
from .portfolio_invitation import PortfolioInvitation
|
from .portfolio_invitation import PortfolioInvitation
|
||||||
|
@ -64,32 +64,6 @@ class User(AbstractUser):
|
||||||
# after they login.
|
# after they login.
|
||||||
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
FIXTURE_USER = "fixture_user", "Created by fixtures"
|
||||||
|
|
||||||
PORTFOLIO_ROLE_PERMISSIONS = {
|
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBER,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
|
||||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
|
||||||
# Domain: field specific permissions
|
|
||||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
|
||||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
|
||||||
],
|
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
|
||||||
# Domain: field specific permissions
|
|
||||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
|
||||||
],
|
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# #### Constants for choice fields ####
|
# #### Constants for choice fields ####
|
||||||
RESTRICTED = "restricted"
|
RESTRICTED = "restricted"
|
||||||
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
STATUS_CHOICES = ((RESTRICTED, RESTRICTED),)
|
||||||
|
@ -230,10 +204,40 @@ class User(AbstractUser):
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||||
|
|
||||||
def has_domain_requests_portfolio_permission(self, portfolio):
|
def has_domain_requests_portfolio_permission(self, portfolio):
|
||||||
|
# BEGIN
|
||||||
|
# Note code below is to add organization_request feature
|
||||||
|
request = HttpRequest()
|
||||||
|
request.user = self
|
||||||
|
has_organization_requests_flag = flag_is_active(request, "organization_requests")
|
||||||
|
if not has_organization_requests_flag:
|
||||||
|
return False
|
||||||
|
# END
|
||||||
return self._has_portfolio_permission(
|
return self._has_portfolio_permission(
|
||||||
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS
|
||||||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS)
|
||||||
|
|
||||||
|
def has_view_members_portfolio_permission(self, portfolio):
|
||||||
|
# BEGIN
|
||||||
|
# Note code below is to add organization_request feature
|
||||||
|
request = HttpRequest()
|
||||||
|
request.user = self
|
||||||
|
has_organization_members_flag = flag_is_active(request, "organization_members")
|
||||||
|
if not has_organization_members_flag:
|
||||||
|
return False
|
||||||
|
# END
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MEMBERS)
|
||||||
|
|
||||||
|
def has_edit_members_portfolio_permission(self, portfolio):
|
||||||
|
# BEGIN
|
||||||
|
# Note code below is to add organization_request feature
|
||||||
|
request = HttpRequest()
|
||||||
|
request.user = self
|
||||||
|
has_organization_members_flag = flag_is_active(request, "organization_members")
|
||||||
|
if not has_organization_members_flag:
|
||||||
|
return False
|
||||||
|
# END
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_MEMBERS)
|
||||||
|
|
||||||
def has_view_all_domains_permission(self, portfolio):
|
def has_view_all_domains_permission(self, portfolio):
|
||||||
"""Determines if the current user can view all available domains in a given portfolio"""
|
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)
|
||||||
|
|
|
@ -16,8 +16,8 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
PORTFOLIO_ROLE_PERMISSIONS = {
|
PORTFOLIO_ROLE_PERMISSIONS = {
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBER,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
@ -28,7 +28,7 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
],
|
],
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBER,
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
# Domain: field specific permissions
|
# Domain: field specific permissions
|
||||||
|
|
|
@ -17,8 +17,8 @@ class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
VIEW_ALL_DOMAINS = "view_all_domains", "View all domains and domain reports"
|
||||||
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
VIEW_MANAGED_DOMAINS = "view_managed_domains", "View managed domains"
|
||||||
|
|
||||||
VIEW_MEMBER = "view_member", "View members"
|
VIEW_MEMBERS = "view_members", "View members"
|
||||||
EDIT_MEMBER = "edit_member", "Create and edit members"
|
EDIT_MEMBERS = "edit_members", "Create and edit members"
|
||||||
|
|
||||||
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
VIEW_ALL_REQUESTS = "view_all_requests", "View all requests"
|
||||||
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
VIEW_CREATED_REQUESTS = "view_created_requests", "View created requests"
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
<div id="content-main" class="analytics">
|
<div id="content-main" class="custom-admin-template">
|
||||||
|
|
||||||
<div class="grid-row grid-gap-2">
|
<div class="grid-row grid-gap-2">
|
||||||
<div class="tablet:grid-col-6 margin-top-2">
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
@ -29,28 +29,28 @@
|
||||||
<div class="padding-top-2 padding-x-2">
|
<div class="padding-top-2 padding-x-2">
|
||||||
<ul class="usa-button-group wrapped-button-group">
|
<ul class="usa-button-group wrapped-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_type' %}" class="button text-no-wrap" role="button">
|
<a href="{% url 'export_data_type' %}" class="usa-button usa-button--dja text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">All domain metadata</span>
|
</svg><span class="margin-left-05">All domain metadata</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_full' %}" class="button text-no-wrap" role="button">
|
<a href="{% url 'export_data_full' %}" class="usa-button usa-button--dja text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Current full</span>
|
</svg><span class="margin-left-05">Current full</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_federal' %}" class="button text-no-wrap" role="button">
|
<a href="{% url 'export_data_federal' %}" class="usa-button usa-button--dja text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Current federal</span>
|
</svg><span class="margin-left-05">Current federal</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<a href="{% url 'export_data_domain_requests_full' %}" class="button text-no-wrap" role="button">
|
<a href="{% url 'export_data_domain_requests_full' %}" class="usa-button usa-button--dja text-no-wrap" role="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">All domain requests metadata</span>
|
</svg><span class="margin-left-05">All domain requests metadata</span>
|
||||||
|
@ -84,35 +84,35 @@
|
||||||
</div>
|
</div>
|
||||||
<ul class="usa-button-group">
|
<ul class="usa-button-group">
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button class="button exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
<button class="usa-button usa-button--dja exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Domain growth</span>
|
</svg><span class="margin-left-05">Domain growth</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button class="button exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
<button class="usa-button usa-button--dja exportLink" data-export-url="{% url 'export_requests_growth' %}" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Request growth</span>
|
</svg><span class="margin-left-05">Request growth</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button class="button exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
<button class="usa-button usa-button--dja exportLink" data-export-url="{% url 'export_managed_domains' %}" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Managed domains</span>
|
</svg><span class="margin-left-05">Managed domains</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button class="button exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
<button class="usa-button usa-button--dja exportLink" data-export-url="{% url 'export_unmanaged_domains' %}" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg><span class="margin-left-05">Unmanaged domains</span>
|
</svg><span class="margin-left-05">Unmanaged domains</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-button-group__item">
|
<li class="usa-button-group__item">
|
||||||
<button class="button exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
<button class="usa-button usa-button--dja exportLink usa-button--secondary" data-export-url="{% url 'analytics' %}" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#assessment"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#assessment"></use>
|
||||||
</svg><span class="margin-left-05">Update charts</span>
|
</svg><span class="margin-left-05">Update charts</span>
|
||||||
|
|
260
src/registrar/templates/admin/transfer_user.html
Normal file
260
src/registrar/templates/admin/transfer_user.html
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
{% extends 'admin/base_site.html' %}
|
||||||
|
{% load i18n static %}
|
||||||
|
|
||||||
|
{% block content_title %}<h1>Transfer user</h1>{% endblock %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
|
||||||
|
{{ block.super }}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrahead %}
|
||||||
|
{{ block.super }}
|
||||||
|
<!-- Making the user select a combobox: -->
|
||||||
|
<!-- Load Django Admin's base JavaScript. This is NEEDED because select2 relies on it. -->
|
||||||
|
<script src="{% static 'admin/js/vendor/jquery/jquery.min.js' %}"></script>
|
||||||
|
|
||||||
|
<!-- Include Select2 JavaScript. Since this view technically falls outside of admin, this is needed. -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||||
|
<script type="application/javascript" src="{% static 'js/get-gov-admin-extra.js' %}" defer></script>
|
||||||
|
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
<div class="breadcrumbs">
|
||||||
|
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
||||||
|
› <a href="{% url 'admin:app_list' 'registrar' %}">{% trans 'Registrar' %}</a>
|
||||||
|
› <a href="{% url 'admin:registrar_user_changelist' %}">{% trans 'Users' %}</a>
|
||||||
|
› <a href="{% url 'admin:registrar_user_change' current_user.pk %}">{{ current_user.first_name }} {{ current_user.last_name }}</a>
|
||||||
|
› {% trans 'Transfer User' %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="content-main" class="custom-admin-template">
|
||||||
|
|
||||||
|
<div class="module padding-4 display-flex flex-row flex-justify submit-row">
|
||||||
|
|
||||||
|
<div class="desktop:flex-align-center">
|
||||||
|
<form class="transfer-user-selector" method="GET" action="{% url 'transfer_user' current_user.pk %}">
|
||||||
|
<label for="selected_user" class="text-middle">Select user to transfer data from:</label>
|
||||||
|
<select name="selected_user" id="selected_user" class="admin-combobox margin-top-0" onchange="this.form.submit()">
|
||||||
|
<option value="">Select a user</option>
|
||||||
|
{% for user in other_users %}
|
||||||
|
<option value="{{ user.pk }}" {% if selected_user and user.pk == selected_user.pk %}selected{% endif %}>
|
||||||
|
{{ user.first_name }} {{ user.last_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Select and preview" class="button--dja-toolbar">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="desktop:flex-align-center">
|
||||||
|
{% if selected_user %}
|
||||||
|
<a class="usa-button usa-button--dja" href="#transfer-and-delete" aria-controls="transfer-and-delete" data-open-modal>
|
||||||
|
Transfer and delete user
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid-row grid-gap-2">
|
||||||
|
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>User to transfer data from</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
{% if selected_user %}
|
||||||
|
<dl class="dl-dja">
|
||||||
|
<dt>Username:</dt>
|
||||||
|
<dd>{{ selected_user.username }}</dd>
|
||||||
|
<dt>Created at:</dt>
|
||||||
|
<dd>{{ selected_user.created_at }}</dd>
|
||||||
|
<dt>Last login:</dt>
|
||||||
|
<dd>{{ selected_user.last_login }}</dd>
|
||||||
|
<dt>First name:</dt>
|
||||||
|
<dd>{{ selected_user.first_name }}</dd>
|
||||||
|
<dt>Middle name:</dt>
|
||||||
|
<dd>{{ selected_user.middle_name }}</dd>
|
||||||
|
<dt>Last name:</dt>
|
||||||
|
<dd>{{ selected_user.last_name }}</dd>
|
||||||
|
<dt>Title:</dt>
|
||||||
|
<dd>{{ selected_user.title }}</dd>
|
||||||
|
<dt>Email:</dt>
|
||||||
|
<dd>{{ selected_user.email }}</dd>
|
||||||
|
<dt>Phone:</dt>
|
||||||
|
<dd>{{ selected_user.phone }}</dd>
|
||||||
|
<h3 class="font-heading-md">Data that will get transferred:</h3>
|
||||||
|
<dt>Domains:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if selected_user_domains %}
|
||||||
|
<ul>
|
||||||
|
{% for domain in selected_user_domains %}
|
||||||
|
<li>{{ domain }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt>Domain requests:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if selected_user_domain_requests %}
|
||||||
|
<ul>
|
||||||
|
{% for request in selected_user_domain_requests %}
|
||||||
|
<li>{{ request }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt>Portfolios:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if selected_user_portfolios %}
|
||||||
|
<ul>
|
||||||
|
{% for portfolio in selected_user_portfolios %}
|
||||||
|
<li>{{ portfolio.portfolio }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
{% else %}
|
||||||
|
<p>No user selected yet.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tablet:grid-col-6 margin-top-2">
|
||||||
|
<div class="module height-full">
|
||||||
|
<h2>User to receive data</h2>
|
||||||
|
<div class="padding-top-2 padding-x-2">
|
||||||
|
<dl class="dl-dja">
|
||||||
|
<dt>Username:</dt>
|
||||||
|
<dd>{{ current_user.username }}</dd>
|
||||||
|
<dt>Created at:</dt>
|
||||||
|
<dd>{{ current_user.created_at }}</dd>
|
||||||
|
<dt>Last login:</dt>
|
||||||
|
<dd>{{ current_user.last_login }}</dd>
|
||||||
|
<dt>First name:</dt>
|
||||||
|
<dd>{{ current_user.first_name }}</dd>
|
||||||
|
<dt>Middle name:</dt>
|
||||||
|
<dd>{{ current_user.middle_name }}</dd>
|
||||||
|
<dt>Last name:</dt>
|
||||||
|
<dd>{{ current_user.last_name }}</dd>
|
||||||
|
<dt>Title:</dt>
|
||||||
|
<dd>{{ current_user.title }}</dd>
|
||||||
|
<dt>Email:</dt>
|
||||||
|
<dd>{{ current_user.email }}</dd>
|
||||||
|
<dt>Phone:</dt>
|
||||||
|
<dd>{{ current_user.phone }}</dd>
|
||||||
|
<h3 class="font-heading-md" aria-label="Data that will added to:"> </h3>
|
||||||
|
<dt>Domains:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if current_user_domains %}
|
||||||
|
<ul>
|
||||||
|
{% for domain in current_user_domains %}
|
||||||
|
<li>{{ domain }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt>Domain requests:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if current_user_domain_requests %}
|
||||||
|
<ul>
|
||||||
|
{% for request in current_user_domain_requests %}
|
||||||
|
<li>{{ request }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
<dt>Portfolios:</dt>
|
||||||
|
<dd>
|
||||||
|
{% if current_user_portfolios %}
|
||||||
|
<ul>
|
||||||
|
{% for portfolio in current_user_portfolios %}
|
||||||
|
<li>{{ portfolio.portfolio }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="usa-modal"
|
||||||
|
id="transfer-and-delete"
|
||||||
|
aria-labelledby="This action will delete {{ selected_user }}"
|
||||||
|
aria-describedby="This action will delete {{ selected_user }}"
|
||||||
|
>
|
||||||
|
<div class="usa-modal__content">
|
||||||
|
<div class="usa-modal__main">
|
||||||
|
<h2 class="usa-modal__heading" id="transfer-and-delete-heading">
|
||||||
|
Are you sure you want to transfer data and delete this user?
|
||||||
|
</h2>
|
||||||
|
<div class="usa-prose">
|
||||||
|
{% if selected_user != logged_in_user %}
|
||||||
|
<p>Username: <b>{{ selected_user.username }}</b><br>
|
||||||
|
Name: <b>{{ selected_user.first_name }} {{ selected_user.last_name }}</b><br>
|
||||||
|
Email: <b>{{ selected_user.email }}</b></p>
|
||||||
|
<p>This action cannot be undone.</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Don't do it!</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="usa-modal__footer">
|
||||||
|
<ul class="usa-button-group">
|
||||||
|
{% if selected_user != logged_in_user %}
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<form method="POST" action="{% url 'transfer_user' current_user.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="selected_user" value="{{ selected_user.pk }}">
|
||||||
|
<input type="submit" class="usa-button usa-button--dja" value="Yes, transfer and delete user">
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li class="usa-button-group__item">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||||
|
name="_cancel_domain_request_ineligible"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-button usa-modal__close"
|
||||||
|
aria-label="Close this window"
|
||||||
|
data-close-modal
|
||||||
|
>
|
||||||
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,6 +1,21 @@
|
||||||
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
||||||
{% load i18n static %}
|
{% load i18n static %}
|
||||||
|
|
||||||
|
|
||||||
|
{% block field_sets %}
|
||||||
|
<div class="display-flex flex-row flex-justify submit-row">
|
||||||
|
<div class="desktop:flex-align-self-end">
|
||||||
|
<a href="{% url 'transfer_user' original.pk %}" class="button">
|
||||||
|
Transfer data from old account
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% for fieldset in adminform %}
|
||||||
|
{% include "django/admin/includes/domain_fieldset.html" with state_help_message=state_help_message %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block after_related_objects %}
|
{% block after_related_objects %}
|
||||||
<div class="module aligned padding-3">
|
<div class="module aligned padding-3">
|
||||||
<h2>Associated requests and domains</h2>
|
<h2>Associated requests and domains</h2>
|
||||||
|
|
|
@ -46,11 +46,11 @@
|
||||||
Domains
|
Domains
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="usa-nav__primary-item">
|
<!-- <li class="usa-nav__primary-item">
|
||||||
<a href="#" class="usa-nav-link">
|
<a href="#" class="usa-nav-link">
|
||||||
Domain groups
|
Domain groups
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li> -->
|
||||||
|
|
||||||
{% if has_domain_requests_portfolio_permission %}
|
{% if has_domain_requests_portfolio_permission %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
|
@ -60,11 +60,13 @@
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if has_view_members_portfolio_permission %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
<a href="#" class="usa-nav-link">
|
<a href="#" class="usa-nav-link">
|
||||||
Members
|
Members
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li class="usa-nav__primary-item">
|
<li class="usa-nav__primary-item">
|
||||||
{% url 'organization' as url %}
|
{% url 'organization' as url %}
|
||||||
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
<!-- Move the padding from the a to the span so that the descenders do not get cut off -->
|
||||||
|
|
|
@ -62,7 +62,8 @@ from .common import (
|
||||||
)
|
)
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
from django.contrib.sessions.backends.db import SessionStore
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from unittest.mock import patch, Mock
|
from unittest.mock import ANY, patch, Mock
|
||||||
|
from django_webtest import WebTest # type: ignore
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -2208,3 +2209,222 @@ class TestPortfolioAdmin(TestCase):
|
||||||
self.assertIn("Agent Smith", display_members)
|
self.assertIn("Agent Smith", display_members)
|
||||||
self.assertIn("<span class='usa-tag'>Domain requestor</span>", display_members)
|
self.assertIn("<span class='usa-tag'>Domain requestor</span>", display_members)
|
||||||
self.assertIn("Program", display_members)
|
self.assertIn("Program", display_members)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTransferUser(WebTest):
|
||||||
|
"""User transfer custom admin page"""
|
||||||
|
|
||||||
|
# csrf checks do not work well with WebTest.
|
||||||
|
# We disable them here.
|
||||||
|
csrf_checks = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
cls.site = AdminSite()
|
||||||
|
cls.superuser = create_superuser()
|
||||||
|
cls.admin = PortfolioAdmin(model=Portfolio, admin_site=cls.site)
|
||||||
|
cls.factory = RequestFactory()
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.app.set_user(self.superuser)
|
||||||
|
self.user1, _ = User.objects.get_or_create(
|
||||||
|
username="madmax", first_name="Max", last_name="Rokatanski", title="Road warrior"
|
||||||
|
)
|
||||||
|
self.user2, _ = User.objects.get_or_create(
|
||||||
|
username="furiosa", first_name="Furiosa", last_name="Jabassa", title="Imperator"
|
||||||
|
)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
Suborganization.objects.all().delete()
|
||||||
|
DomainInformation.objects.all().delete()
|
||||||
|
DomainRequest.objects.all().delete()
|
||||||
|
Domain.objects.all().delete()
|
||||||
|
Portfolio.objects.all().delete()
|
||||||
|
UserDomainRole.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_shows_current_and_selected_user_information(self):
|
||||||
|
"""Assert we pull the current user info and display it on the transfer page"""
|
||||||
|
completed_domain_request(user=self.user1, name="wasteland.gov")
|
||||||
|
domain_request = completed_domain_request(
|
||||||
|
user=self.user1, name="citadel.gov", status=DomainRequest.DomainRequestStatus.SUBMITTED
|
||||||
|
)
|
||||||
|
domain_request.status = DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
domain_request.save()
|
||||||
|
portfolio1 = Portfolio.objects.create(organization_name="Hotel California", creator=self.user2)
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user1, portfolio=portfolio1, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
portfolio2 = Portfolio.objects.create(organization_name="Tokyo Hotel", creator=self.user2)
|
||||||
|
UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user2, portfolio=portfolio2, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
|
||||||
|
self.assertContains(user_transfer_page, "madmax")
|
||||||
|
self.assertContains(user_transfer_page, "Max")
|
||||||
|
self.assertContains(user_transfer_page, "Rokatanski")
|
||||||
|
self.assertContains(user_transfer_page, "Road warrior")
|
||||||
|
self.assertContains(user_transfer_page, "wasteland.gov")
|
||||||
|
self.assertContains(user_transfer_page, "citadel.gov")
|
||||||
|
self.assertContains(user_transfer_page, "Hotel California")
|
||||||
|
|
||||||
|
select_form = user_transfer_page.forms[0]
|
||||||
|
select_form["selected_user"] = str(self.user2.id)
|
||||||
|
preview_result = select_form.submit()
|
||||||
|
|
||||||
|
self.assertContains(preview_result, "furiosa")
|
||||||
|
self.assertContains(preview_result, "Furiosa")
|
||||||
|
self.assertContains(preview_result, "Jabassa")
|
||||||
|
self.assertContains(preview_result, "Imperator")
|
||||||
|
self.assertContains(preview_result, "Tokyo Hotel")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_user_portfolio_roles(self):
|
||||||
|
"""Assert that a portfolio user role gets transferred"""
|
||||||
|
portfolio = Portfolio.objects.create(organization_name="Hotel California", creator=self.user2)
|
||||||
|
user_portfolio_permission = UserPortfolioPermission.objects.create(
|
||||||
|
user=self.user2, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
|
||||||
|
user_portfolio_permission.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEquals(user_portfolio_permission.user, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_domain_request_creator_and_investigator(self):
|
||||||
|
"""Assert that domain request fields get transferred"""
|
||||||
|
domain_request = completed_domain_request(user=self.user2, name="wasteland.gov", investigator=self.user2)
|
||||||
|
|
||||||
|
self.assertEquals(domain_request.creator, self.user2)
|
||||||
|
self.assertEquals(domain_request.investigator, self.user2)
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
domain_request.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEquals(domain_request.creator, self.user1)
|
||||||
|
self.assertEquals(domain_request.investigator, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_domain_information_creator(self):
|
||||||
|
"""Assert that domain fields get transferred"""
|
||||||
|
domain_information, _ = DomainInformation.objects.get_or_create(creator=self.user2)
|
||||||
|
|
||||||
|
self.assertEquals(domain_information.creator, self.user2)
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
domain_information.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEquals(domain_information.creator, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_domain_role(self):
|
||||||
|
"""Assert that user domain role get transferred"""
|
||||||
|
domain_1, _ = Domain.objects.get_or_create(name="chrome.gov", state=Domain.State.READY)
|
||||||
|
domain_2, _ = Domain.objects.get_or_create(name="v8.gov", state=Domain.State.READY)
|
||||||
|
user_domain_role1, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user2, domain=domain_1, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
user_domain_role2, _ = UserDomainRole.objects.get_or_create(
|
||||||
|
user=self.user2, domain=domain_2, role=UserDomainRole.Roles.MANAGER
|
||||||
|
)
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
user_domain_role1.refresh_from_db()
|
||||||
|
user_domain_role2.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEquals(user_domain_role1.user, self.user1)
|
||||||
|
self.assertEquals(user_domain_role2.user, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_transfers_verified_by_staff_requestor(self):
|
||||||
|
"""Assert that verified by staff creator gets transferred"""
|
||||||
|
vip, _ = VerifiedByStaff.objects.get_or_create(requestor=self.user2, email="immortan.joe@citadel.com")
|
||||||
|
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
vip.refresh_from_db()
|
||||||
|
|
||||||
|
self.assertEquals(vip.requestor, self.user1)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_deletes_old_user(self):
|
||||||
|
"""Assert that the slected user gets deleted"""
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit()
|
||||||
|
# Refresh user2 from the database and check if it still exists
|
||||||
|
with self.assertRaises(User.DoesNotExist):
|
||||||
|
self.user2.refresh_from_db()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_throws_transfer_and_delete_success_messages(self):
|
||||||
|
"""Test that success messages for data transfer and user deletion are displayed."""
|
||||||
|
# Ensure the setup for VerifiedByStaff
|
||||||
|
VerifiedByStaff.objects.get_or_create(requestor=self.user2, email="immortan.joe@citadel.com")
|
||||||
|
|
||||||
|
# Access the transfer user page
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
|
||||||
|
with patch("django.contrib.messages.success") as mock_success_message:
|
||||||
|
|
||||||
|
# Fill the form with the selected user and submit
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
after_submit = submit_form.submit().follow()
|
||||||
|
|
||||||
|
self.assertContains(after_submit, "<h1>Change user</h1>")
|
||||||
|
|
||||||
|
mock_success_message.assert_any_call(
|
||||||
|
ANY,
|
||||||
|
(
|
||||||
|
"Data transferred successfully for the following objects: ['Changed requestor "
|
||||||
|
+ 'from "Furiosa Jabassa " to "Max Rokatanski " on immortan.joe@citadel.com\']'
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_success_message.assert_any_call(ANY, f"Deleted {self.user2} {self.user2.username}")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_throws_error_message(self):
|
||||||
|
"""Test that an error message is thrown if the transfer fails."""
|
||||||
|
with patch(
|
||||||
|
"registrar.views.TransferUserView.transfer_user_fields_and_log", side_effect=Exception("Simulated Error")
|
||||||
|
):
|
||||||
|
with patch("django.contrib.messages.error") as mock_error:
|
||||||
|
# Access the transfer user page
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
|
||||||
|
# Fill the form with the selected user and submit
|
||||||
|
submit_form = user_transfer_page.forms[1]
|
||||||
|
submit_form["selected_user"] = self.user2.pk
|
||||||
|
submit_form.submit().follow()
|
||||||
|
|
||||||
|
# Assert that the error message was called with the correct argument
|
||||||
|
mock_error.assert_called_once_with(ANY, "An error occurred during the transfer: Simulated Error")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_transfer_user_modal(self):
|
||||||
|
"""Assert modal on page"""
|
||||||
|
user_transfer_page = self.app.get(reverse("transfer_user", args=[self.user1.pk]))
|
||||||
|
self.assertContains(user_transfer_page, "This action cannot be undone.")
|
||||||
|
|
|
@ -1534,6 +1534,7 @@ class TestUser(TestCase):
|
||||||
self.assertFalse(self.user.has_contact_info())
|
self.assertFalse(self.user.has_contact_info())
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
def test_has_portfolio_permission(self):
|
def test_has_portfolio_permission(self):
|
||||||
"""
|
"""
|
||||||
0. Returns False when user does not have a permission
|
0. Returns False when user does not have a permission
|
||||||
|
@ -1555,7 +1556,10 @@ class TestUser(TestCase):
|
||||||
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||||
portfolio=portfolio,
|
portfolio=portfolio,
|
||||||
user=self.user,
|
user=self.user,
|
||||||
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
user_can_view_all_domains = self.user.has_domains_portfolio_permission(portfolio)
|
||||||
|
|
|
@ -25,6 +25,7 @@ SAMPLE_KWARGS = {
|
||||||
"domain": "whitehouse.gov",
|
"domain": "whitehouse.gov",
|
||||||
"user_pk": "1",
|
"user_pk": "1",
|
||||||
"portfolio_id": "1",
|
"portfolio_id": "1",
|
||||||
|
"user_id": "1",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Our test suite will ignore some namespaces.
|
# Our test suite will ignore some namespaces.
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio
|
from registrar.models import UserDomainRole, Domain, DomainInformation, Portfolio
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||||
from .test_views import TestWithUser
|
from .test_views import TestWithUser
|
||||||
from django_webtest import WebTest # type: ignore
|
from django_webtest import WebTest # type: ignore
|
||||||
from django.utils.dateparse import parse_date
|
from django.utils.dateparse import parse_date
|
||||||
from api.tests.common import less_console_noise_decorator
|
from api.tests.common import less_console_noise_decorator
|
||||||
|
from waffle.testutils import override_flag
|
||||||
|
|
||||||
|
|
||||||
class GetDomainsJsonTest(TestWithUser, WebTest):
|
class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
@ -31,6 +35,7 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
UserDomainRole.objects.all().delete()
|
UserDomainRole.objects.all().delete()
|
||||||
|
UserPortfolioPermission.objects.all().delete()
|
||||||
DomainInformation.objects.all().delete()
|
DomainInformation.objects.all().delete()
|
||||||
Portfolio.objects.all().delete()
|
Portfolio.objects.all().delete()
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
@ -115,8 +120,104 @@ class GetDomainsJsonTest(TestWithUser, WebTest):
|
||||||
self.assertEqual(svg_icon_expected, svg_icons[i])
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_get_domains_json_with_portfolio(self):
|
@override_flag("organization_feature", active=True)
|
||||||
"""Test that an authenticated user gets the list of 2 domains for portfolio."""
|
def test_get_domains_json_with_portfolio_view_managed_domains(self):
|
||||||
|
"""Test that an authenticated user gets the list of 1 domain for portfolio. The 1 domain
|
||||||
|
is the domain that they manage within the portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS],
|
||||||
|
)
|
||||||
|
|
||||||
|
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
data = response.json
|
||||||
|
|
||||||
|
# Check pagination info
|
||||||
|
self.assertEqual(data["page"], 1)
|
||||||
|
self.assertFalse(data["has_next"])
|
||||||
|
self.assertFalse(data["has_previous"])
|
||||||
|
self.assertEqual(data["num_pages"], 1)
|
||||||
|
|
||||||
|
# Check the number of domains
|
||||||
|
self.assertEqual(len(data["domains"]), 1)
|
||||||
|
|
||||||
|
# Expected domains
|
||||||
|
expected_domains = [self.domain3]
|
||||||
|
|
||||||
|
# Extract fields from response
|
||||||
|
domain_ids = [domain["id"] for domain in data["domains"]]
|
||||||
|
names = [domain["name"] for domain in data["domains"]]
|
||||||
|
expiration_dates = [domain["expiration_date"] for domain in data["domains"]]
|
||||||
|
states = [domain["state"] for domain in data["domains"]]
|
||||||
|
state_displays = [domain["state_display"] for domain in data["domains"]]
|
||||||
|
get_state_help_texts = [domain["get_state_help_text"] for domain in data["domains"]]
|
||||||
|
action_urls = [domain["action_url"] for domain in data["domains"]]
|
||||||
|
action_labels = [domain["action_label"] for domain in data["domains"]]
|
||||||
|
svg_icons = [domain["svg_icon"] for domain in data["domains"]]
|
||||||
|
|
||||||
|
# Check fields for each domain
|
||||||
|
for i, expected_domain in enumerate(expected_domains):
|
||||||
|
self.assertEqual(expected_domain.id, domain_ids[i])
|
||||||
|
self.assertEqual(expected_domain.name, names[i])
|
||||||
|
self.assertEqual(expected_domain.expiration_date, expiration_dates[i])
|
||||||
|
self.assertEqual(expected_domain.state, states[i])
|
||||||
|
|
||||||
|
# Parsing the expiration date from string to date
|
||||||
|
parsed_expiration_date = parse_date(expiration_dates[i])
|
||||||
|
expected_domain.expiration_date = parsed_expiration_date
|
||||||
|
|
||||||
|
# Check state_display and get_state_help_text
|
||||||
|
self.assertEqual(expected_domain.state_display(), state_displays[i])
|
||||||
|
self.assertEqual(expected_domain.get_state_help_text(), get_state_help_texts[i])
|
||||||
|
|
||||||
|
self.assertEqual(reverse("domain", kwargs={"pk": expected_domain.id}), action_urls[i])
|
||||||
|
|
||||||
|
# Check action_label
|
||||||
|
user_domain_role_exists = UserDomainRole.objects.filter(
|
||||||
|
domain_id=expected_domains[i].id, user=self.user
|
||||||
|
).exists()
|
||||||
|
action_label_expected = (
|
||||||
|
"View"
|
||||||
|
if not user_domain_role_exists
|
||||||
|
or expected_domains[i].state
|
||||||
|
in [
|
||||||
|
Domain.State.DELETED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
]
|
||||||
|
else "Manage"
|
||||||
|
)
|
||||||
|
self.assertEqual(action_label_expected, action_labels[i])
|
||||||
|
|
||||||
|
# Check svg_icon
|
||||||
|
svg_icon_expected = (
|
||||||
|
"visibility"
|
||||||
|
if not user_domain_role_exists
|
||||||
|
or expected_domains[i].state
|
||||||
|
in [
|
||||||
|
Domain.State.DELETED,
|
||||||
|
Domain.State.ON_HOLD,
|
||||||
|
]
|
||||||
|
else "settings"
|
||||||
|
)
|
||||||
|
self.assertEqual(svg_icon_expected, svg_icons[i])
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
def test_get_domains_json_with_portfolio_view_all_domains(self):
|
||||||
|
"""Test that an authenticated user gets the list of 2 domains for portfolio. One is a domain which
|
||||||
|
they manage within the portfolio. The other is a domain which they don't manage within the
|
||||||
|
portfolio."""
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER],
|
||||||
|
additional_permissions=[UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS],
|
||||||
|
)
|
||||||
|
|
||||||
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
response = self.app.get(reverse("get_domains_json"), {"portfolio": self.portfolio.id})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
|
@ -230,6 +230,7 @@ class TestPortfolio(WebTest):
|
||||||
self.assertContains(response, 'for="id_city"')
|
self.assertContains(response, 'for="id_city"')
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
def test_accessible_pages_when_user_does_not_have_permission(self):
|
def test_accessible_pages_when_user_does_not_have_permission(self):
|
||||||
"""Tests which pages are accessible when user does not have portfolio permissions"""
|
"""Tests which pages are accessible when user does not have portfolio permissions"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
@ -280,6 +281,7 @@ class TestPortfolio(WebTest):
|
||||||
self.assertEquals(domain_request_page.status_code, 403)
|
self.assertEquals(domain_request_page.status_code, 403)
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
def test_accessible_pages_when_user_does_not_have_role(self):
|
def test_accessible_pages_when_user_does_not_have_role(self):
|
||||||
"""Test that admin / memmber roles are associated with the right access"""
|
"""Test that admin / memmber roles are associated with the right access"""
|
||||||
self.app.set_user(self.user.username)
|
self.app.set_user(self.user.username)
|
||||||
|
@ -532,3 +534,99 @@ class TestPortfolio(WebTest):
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
self.assertContains(response, "Domain name")
|
self.assertContains(response, "Domain name")
|
||||||
permission.delete()
|
permission.delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=False)
|
||||||
|
def test_organization_requests_waffle_flag_off_hides_nav_link_and_restricts_permission(self):
|
||||||
|
"""Setting the organization_requests waffle off hides the nav link and restricts access to the requests page"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertNotContains(home, "Domain requests")
|
||||||
|
|
||||||
|
domain_requests = self.app.get(reverse("domain-requests"), expect_errors=True)
|
||||||
|
self.assertEqual(domain_requests.status_code, 403)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_organization_requests_waffle_flag_on_shows_nav_link_and_allows_permission(self):
|
||||||
|
"""Setting the organization_requests waffle on shows the nav link and allows access to the requests page"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertContains(home, "Domain requests")
|
||||||
|
|
||||||
|
domain_requests = self.app.get(reverse("domain-requests"))
|
||||||
|
self.assertEqual(domain_requests.status_code, 200)
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=False)
|
||||||
|
def test_organization_members_waffle_flag_off_hides_nav_link(self):
|
||||||
|
"""Setting the organization_members waffle off hides the nav link"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_CREATED_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertNotContains(home, "Members")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_members", active=True)
|
||||||
|
def test_organization_members_waffle_flag_on_shows_nav_link(self):
|
||||||
|
"""Setting the organization_members waffle on shows the nav link"""
|
||||||
|
self.app.set_user(self.user.username)
|
||||||
|
|
||||||
|
UserPortfolioPermission.objects.get_or_create(
|
||||||
|
user=self.user,
|
||||||
|
portfolio=self.portfolio,
|
||||||
|
additional_permissions=[
|
||||||
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
home = self.app.get(reverse("home")).follow()
|
||||||
|
|
||||||
|
self.assertContains(home, "Hotel California")
|
||||||
|
self.assertContains(home, "Members")
|
||||||
|
|
|
@ -19,3 +19,4 @@ from .user_profile import UserProfileView, FinishProfileSetupView
|
||||||
from .health import *
|
from .health import *
|
||||||
from .index import *
|
from .index import *
|
||||||
from .portfolios import *
|
from .portfolios import *
|
||||||
|
from .transfer_user import TransferUserView
|
||||||
|
|
|
@ -50,11 +50,15 @@ def get_domain_ids_from_request(request):
|
||||||
"""
|
"""
|
||||||
portfolio = request.GET.get("portfolio")
|
portfolio = request.GET.get("portfolio")
|
||||||
if portfolio:
|
if portfolio:
|
||||||
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
if request.user.is_org_user(request) and request.user.has_view_all_domains_permission(portfolio):
|
||||||
return domain_infos.values_list("domain_id", flat=True)
|
domain_infos = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
else:
|
return domain_infos.values_list("domain_id", flat=True)
|
||||||
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
else:
|
||||||
return user_domain_roles.values_list("domain_id", flat=True)
|
domain_info_ids = DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user).values_list("domain_id", flat=True)
|
||||||
|
return domain_info_ids.intersection(user_domain_roles)
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=request.user)
|
||||||
|
return user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
|
||||||
|
|
||||||
def apply_search(queryset, request):
|
def apply_search(queryset, request):
|
||||||
|
|
172
src/registrar/views/transfer_user.py
Normal file
172
src/registrar/views/transfer_user.py
Normal file
|
@ -0,0 +1,172 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
|
from django.views import View
|
||||||
|
from registrar.models.domain import Domain
|
||||||
|
from registrar.models.domain_information import DomainInformation
|
||||||
|
from registrar.models.domain_request import DomainRequest
|
||||||
|
from registrar.models.portfolio import Portfolio
|
||||||
|
from registrar.models.user import User
|
||||||
|
from django.contrib.admin import site
|
||||||
|
from django.contrib import messages
|
||||||
|
|
||||||
|
from registrar.models.user_domain_role import UserDomainRole
|
||||||
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
|
from registrar.models.verified_by_staff import VerifiedByStaff
|
||||||
|
from typing import Any, List
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class TransferUserView(View):
|
||||||
|
"""Transfer user methods that set up the transfer_user template and handle the forms on it."""
|
||||||
|
|
||||||
|
JOINS = [
|
||||||
|
(DomainRequest, "creator"),
|
||||||
|
(DomainInformation, "creator"),
|
||||||
|
(Portfolio, "creator"),
|
||||||
|
(DomainRequest, "investigator"),
|
||||||
|
(UserDomainRole, "user"),
|
||||||
|
(VerifiedByStaff, "requestor"),
|
||||||
|
(UserPortfolioPermission, "user"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Future-proofing in case joined fields get added on the user model side
|
||||||
|
# This was tested in the first portfolio model iteration and works
|
||||||
|
USER_FIELDS: List[Any] = []
|
||||||
|
|
||||||
|
def get(self, request, user_id):
|
||||||
|
"""current_user referes to the 'source' user where the button that redirects to this view was clicked.
|
||||||
|
other_users exclude current_user and populate a dropdown, selected_user is the selection in the dropdown.
|
||||||
|
|
||||||
|
This also querries the relevant domains and domain requests, and the admin context needed for the sidenav."""
|
||||||
|
|
||||||
|
current_user = get_object_or_404(User, pk=user_id)
|
||||||
|
other_users = User.objects.exclude(pk=user_id).order_by(
|
||||||
|
"first_name", "last_name"
|
||||||
|
) # Exclude the current user from the dropdown
|
||||||
|
|
||||||
|
# Get the default admin site context, needed for the sidenav
|
||||||
|
admin_context = site.each_context(request)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"current_user": current_user,
|
||||||
|
"other_users": other_users,
|
||||||
|
"logged_in_user": request.user,
|
||||||
|
**admin_context, # Include the admin context
|
||||||
|
"current_user_domains": self.get_domains(current_user),
|
||||||
|
"current_user_domain_requests": self.get_domain_requests(current_user),
|
||||||
|
"current_user_portfolios": self.get_portfolios(current_user),
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_user_id = request.GET.get("selected_user")
|
||||||
|
if selected_user_id:
|
||||||
|
selected_user = get_object_or_404(User, pk=selected_user_id)
|
||||||
|
context["selected_user"] = selected_user
|
||||||
|
context["selected_user_domains"] = self.get_domains(selected_user)
|
||||||
|
context["selected_user_domain_requests"] = self.get_domain_requests(selected_user)
|
||||||
|
context["selected_user_portfolios"] = self.get_portfolios(selected_user)
|
||||||
|
|
||||||
|
return render(request, "admin/transfer_user.html", context)
|
||||||
|
|
||||||
|
def post(self, request, user_id):
|
||||||
|
"""This handles the transfer from selected_user to current_user then deletes selected_user.
|
||||||
|
|
||||||
|
NOTE: We have a ticket to refactor this into a more solid lookup for related fields in #2645"""
|
||||||
|
|
||||||
|
current_user = get_object_or_404(User, pk=user_id)
|
||||||
|
selected_user_id = request.POST.get("selected_user")
|
||||||
|
selected_user = get_object_or_404(User, pk=selected_user_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
change_logs = []
|
||||||
|
|
||||||
|
# Transfer specific fields
|
||||||
|
self.transfer_user_fields_and_log(selected_user, current_user, change_logs)
|
||||||
|
|
||||||
|
# Perform the updates and log the changes
|
||||||
|
for model_class, field_name in self.JOINS:
|
||||||
|
self.update_joins_and_log(model_class, field_name, selected_user, current_user, change_logs)
|
||||||
|
|
||||||
|
# Success message if any related objects were updated
|
||||||
|
if change_logs:
|
||||||
|
success_message = f"Data transferred successfully for the following objects: {change_logs}"
|
||||||
|
messages.success(request, success_message)
|
||||||
|
|
||||||
|
selected_user.delete()
|
||||||
|
messages.success(request, f"Deleted {selected_user} {selected_user.username}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messages.error(request, f"An error occurred during the transfer: {e}")
|
||||||
|
|
||||||
|
return redirect("admin:registrar_user_change", object_id=user_id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_joins_and_log(cls, model_class, field_name, selected_user, current_user, change_logs):
|
||||||
|
"""
|
||||||
|
Helper function to update the user join fields for a given model and log the changes.
|
||||||
|
"""
|
||||||
|
|
||||||
|
filter_kwargs = {field_name: selected_user}
|
||||||
|
updated_objects = model_class.objects.filter(**filter_kwargs)
|
||||||
|
|
||||||
|
for obj in updated_objects:
|
||||||
|
# Check for duplicate UserDomainRole before updating
|
||||||
|
if model_class == UserDomainRole:
|
||||||
|
if model_class.objects.filter(user=current_user, domain=obj.domain).exists():
|
||||||
|
continue # Skip the update to avoid a duplicate
|
||||||
|
|
||||||
|
# Update the field on the object and save it
|
||||||
|
setattr(obj, field_name, current_user)
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
# Log the change
|
||||||
|
cls.log_change(obj, field_name, selected_user, current_user, change_logs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def transfer_user_fields_and_log(cls, selected_user, current_user, change_logs):
|
||||||
|
"""
|
||||||
|
Transfers portfolio fields from the selected_user to the current_user.
|
||||||
|
Logs the changes for each transferred field.
|
||||||
|
"""
|
||||||
|
for field in cls.USER_FIELDS:
|
||||||
|
field_value = getattr(selected_user, field, None)
|
||||||
|
|
||||||
|
if field_value:
|
||||||
|
setattr(current_user, field, field_value)
|
||||||
|
cls.log_change(current_user, field, field_value, field_value, change_logs)
|
||||||
|
|
||||||
|
current_user.save()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def log_change(cls, obj, field_name, field_value, new_value, change_logs):
|
||||||
|
"""Logs the change for a specific field on an object"""
|
||||||
|
log_entry = f'Changed {field_name} from "{field_value}" to "{new_value}" on {obj}'
|
||||||
|
|
||||||
|
logger.info(log_entry)
|
||||||
|
|
||||||
|
# Collect the related object for the success message
|
||||||
|
change_logs.append(log_entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_domains(cls, user):
|
||||||
|
"""A simplified version of domains_json"""
|
||||||
|
user_domain_roles = UserDomainRole.objects.filter(user=user)
|
||||||
|
domain_ids = user_domain_roles.values_list("domain_id", flat=True)
|
||||||
|
domains = Domain.objects.filter(id__in=domain_ids)
|
||||||
|
|
||||||
|
return domains
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_domain_requests(cls, user):
|
||||||
|
"""A simplified version of domain_requests_json"""
|
||||||
|
domain_requests = DomainRequest.objects.filter(creator=user)
|
||||||
|
|
||||||
|
return domain_requests
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_portfolios(cls, user):
|
||||||
|
"""Get portfolios"""
|
||||||
|
portfolios = UserPortfolioPermission.objects.filter(user=user)
|
||||||
|
|
||||||
|
return portfolios
|
|
@ -7,5 +7,6 @@ from .permission_views import (
|
||||||
DomainRequestPermissionWithdrawView,
|
DomainRequestPermissionWithdrawView,
|
||||||
DomainInvitationPermissionDeleteView,
|
DomainInvitationPermissionDeleteView,
|
||||||
DomainRequestWizardPermissionView,
|
DomainRequestWizardPermissionView,
|
||||||
|
PortfolioMembersPermission,
|
||||||
)
|
)
|
||||||
from .api_views import get_senior_official_from_federal_agency_json
|
from .api_views import get_senior_official_from_federal_agency_json
|
||||||
|
|
|
@ -454,3 +454,20 @@ class PortfolioDomainRequestsPermission(PortfolioBasePermission):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return super().has_permission()
|
return super().has_permission()
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioMembersPermission(PortfolioBasePermission):
|
||||||
|
"""Permission mixin that allows access to portfolio members pages if user
|
||||||
|
has access, otherwise 403"""
|
||||||
|
|
||||||
|
def has_permission(self):
|
||||||
|
"""Check if this user has access to members for this portfolio.
|
||||||
|
|
||||||
|
The user is in self.request.user and the portfolio can be looked
|
||||||
|
up from the portfolio's primary key in self.kwargs["pk"]"""
|
||||||
|
|
||||||
|
portfolio = self.request.session.get("portfolio")
|
||||||
|
if not self.request.user.has_view_members(portfolio):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return super().has_permission()
|
||||||
|
|
|
@ -18,6 +18,7 @@ from .mixins import (
|
||||||
UserDeleteDomainRolePermission,
|
UserDeleteDomainRolePermission,
|
||||||
UserProfilePermission,
|
UserProfilePermission,
|
||||||
PortfolioBasePermission,
|
PortfolioBasePermission,
|
||||||
|
PortfolioMembersPermission,
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
@ -229,3 +230,11 @@ class PortfolioDomainRequestsPermissionView(PortfolioDomainRequestsPermission, P
|
||||||
This abstract view cannot be instantiated. Actual views must specify
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
`template_name`.
|
`template_name`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class PortfolioMembersPermissionView(PortfolioMembersPermission, PortfolioBasePermissionView, abc.ABC):
|
||||||
|
"""Abstract base view for portfolio domain request views that enforces permissions.
|
||||||
|
|
||||||
|
This abstract view cannot be instantiated. Actual views must specify
|
||||||
|
`template_name`.
|
||||||
|
"""
|
||||||
|
|
|
@ -72,6 +72,7 @@
|
||||||
10038 OUTOFSCOPE http://app:8080/domains/
|
10038 OUTOFSCOPE http://app:8080/domains/
|
||||||
10038 OUTOFSCOPE http://app:8080/organization/
|
10038 OUTOFSCOPE http://app:8080/organization/
|
||||||
10038 OUTOFSCOPE http://app:8080/suborganization/
|
10038 OUTOFSCOPE http://app:8080/suborganization/
|
||||||
|
10038 OUTOFSCOPE http://app:8080/transfer/
|
||||||
# This URL always returns 404, so include it as well.
|
# This URL always returns 404, so include it as well.
|
||||||
10038 OUTOFSCOPE http://app:8080/todo
|
10038 OUTOFSCOPE http://app:8080/todo
|
||||||
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
# OIDC isn't configured in the test environment and DEBUG=True so this gives a 500 without CSP headers
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue