mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-19 02:49:21 +02:00
Merge branch 'main' into za/2760-portfolio-domain-request-entry-point-2
This commit is contained in:
commit
cd7b45a7f4
26 changed files with 1633 additions and 1330 deletions
1
.github/workflows/deploy-manual.yaml
vendored
1
.github/workflows/deploy-manual.yaml
vendored
|
@ -14,6 +14,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- ab
|
- ab
|
||||||
- backup
|
- backup
|
||||||
|
- el
|
||||||
- cb
|
- cb
|
||||||
- dk
|
- dk
|
||||||
- es
|
- es
|
||||||
|
|
1
.github/workflows/deploy-sandbox.yaml
vendored
1
.github/workflows/deploy-sandbox.yaml
vendored
|
@ -30,6 +30,7 @@ jobs:
|
||||||
|| startsWith(github.head_ref, 'ag/')
|
|| startsWith(github.head_ref, 'ag/')
|
||||||
|| startsWith(github.head_ref, 'ms/')
|
|| startsWith(github.head_ref, 'ms/')
|
||||||
|| startsWith(github.head_ref, 'ad/')
|
|| startsWith(github.head_ref, 'ad/')
|
||||||
|
|| startsWith(github.head_ref, 'el/')
|
||||||
outputs:
|
outputs:
|
||||||
environment: ${{ steps.var.outputs.environment}}
|
environment: ${{ steps.var.outputs.environment}}
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
- stable
|
- stable
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- el
|
||||||
- ad
|
- ad
|
||||||
- ms
|
- ms
|
||||||
- ag
|
- ag
|
||||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
||||||
options:
|
options:
|
||||||
- staging
|
- staging
|
||||||
- development
|
- development
|
||||||
|
- el
|
||||||
- ad
|
- ad
|
||||||
- ms
|
- ms
|
||||||
- ag
|
- ag
|
||||||
|
|
32
ops/manifests/manifest-el.yaml
Normal file
32
ops/manifests/manifest-el.yaml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
applications:
|
||||||
|
- name: getgov-el
|
||||||
|
buildpacks:
|
||||||
|
- python_buildpack
|
||||||
|
path: ../../src
|
||||||
|
instances: 1
|
||||||
|
memory: 512M
|
||||||
|
stack: cflinuxfs4
|
||||||
|
timeout: 180
|
||||||
|
command: ./run.sh
|
||||||
|
health-check-type: http
|
||||||
|
health-check-http-endpoint: /health
|
||||||
|
health-check-invocation-timeout: 40
|
||||||
|
env:
|
||||||
|
# Send stdout and stderr straight to the terminal without buffering
|
||||||
|
PYTHONUNBUFFERED: yup
|
||||||
|
# Tell Django where to find its configuration
|
||||||
|
DJANGO_SETTINGS_MODULE: registrar.config.settings
|
||||||
|
# Tell Django where it is being hosted
|
||||||
|
DJANGO_BASE_URL: https://getgov-el.app.cloud.gov
|
||||||
|
# Tell Django how much stuff to log
|
||||||
|
DJANGO_LOG_LEVEL: INFO
|
||||||
|
# default public site location
|
||||||
|
GETGOV_PUBLIC_SITE_URL: https://get.gov
|
||||||
|
# Flag to disable/enable features in prod environments
|
||||||
|
IS_PRODUCTION: False
|
||||||
|
routes:
|
||||||
|
- route: getgov-el.app.cloud.gov
|
||||||
|
services:
|
||||||
|
- getgov-credentials
|
||||||
|
- getgov-el-database
|
|
@ -1976,18 +1976,30 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
|
|
||||||
# If the status is not mapped properly, saving could cause
|
# If the status is not mapped properly, saving could cause
|
||||||
# weird issues down the line. Instead, we should block this.
|
# weird issues down the line. Instead, we should block this.
|
||||||
|
# NEEDS A UNIT TEST
|
||||||
should_proceed = False
|
should_proceed = False
|
||||||
return should_proceed
|
return (obj, should_proceed)
|
||||||
|
|
||||||
request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
if request_is_not_approved and not obj.domain_is_not_active():
|
if obj_is_not_approved and not obj.domain_is_not_active():
|
||||||
# If an admin tried to set an approved domain request to
|
# REDUNDANT CHECK / ERROR SCREEN AVOIDANCE:
|
||||||
# another status and the related domain is already
|
# This action (moving a request from approved to
|
||||||
# active, shortcut the action and throw a friendly
|
# another status) when the domain is already active (READY),
|
||||||
# error message. This action would still not go through
|
# would still not go through even without this check as the rules are
|
||||||
# shortcut or not as the rules are duplicated on the model,
|
# duplicated in the model and the error is raised from the model.
|
||||||
# but the error would be an ugly Django error screen.
|
# This avoids an ugly Django error screen.
|
||||||
error_message = "This action is not permitted. The domain is already active."
|
error_message = "This action is not permitted. The domain is already active."
|
||||||
|
elif (
|
||||||
|
original_obj.status != models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
and obj.status == models.DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
and original_obj.requested_domain is not None
|
||||||
|
and Domain.objects.filter(name=original_obj.requested_domain.name).exists()
|
||||||
|
):
|
||||||
|
# REDUNDANT CHECK:
|
||||||
|
# This action (approving a request when the domain exists)
|
||||||
|
# would still not go through even without this check as the rules are
|
||||||
|
# duplicated in the model and the error is raised from the model.
|
||||||
|
error_message = FSMDomainRequestError.get_error_message(FSMErrorCodes.APPROVE_DOMAIN_IN_USE)
|
||||||
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
elif obj.status == models.DomainRequest.DomainRequestStatus.REJECTED and not obj.rejection_reason:
|
||||||
# This condition should never be triggered.
|
# This condition should never be triggered.
|
||||||
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
# The opposite of this condition is acceptable (rejected -> other status and rejection_reason)
|
||||||
|
|
|
@ -515,10 +515,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]');
|
const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]');
|
||||||
let lastSentEmailContent = document.getElementById("last-sent-email-content");
|
let lastSentEmailContent = document.getElementById("last-sent-email-content");
|
||||||
const initialDropdownValue = dropdown ? dropdown.value : null;
|
const initialDropdownValue = dropdown ? dropdown.value : null;
|
||||||
const initialEmailValue = textarea.value;
|
let initialEmailValue;
|
||||||
|
if (textarea)
|
||||||
|
initialEmailValue = textarea.value
|
||||||
|
|
||||||
// We will use the const to control the modal
|
// We will use the const to control the modal
|
||||||
let isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
let isEmailAlreadySentConst;
|
||||||
|
if (lastSentEmailContent)
|
||||||
|
isEmailAlreadySentConst = lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
||||||
// We will use the function to control the label and help
|
// We will use the function to control the label and help
|
||||||
function isEmailAlreadySent() {
|
function isEmailAlreadySent() {
|
||||||
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, '');
|
||||||
|
@ -706,18 +710,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
}
|
}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
// Extract the submitter name, title, email, and phone number
|
|
||||||
const submitterDiv = document.querySelector('.form-row.field-submitter');
|
|
||||||
const submitterNameElement = document.getElementById('id_submitter');
|
|
||||||
// We have to account for different superuser and analyst markups
|
|
||||||
const submitterName = submitterNameElement
|
|
||||||
? submitterNameElement.options[submitterNameElement.selectedIndex].text
|
|
||||||
: submitterDiv.querySelector('a').text;
|
|
||||||
const submitterTitle = extractTextById('contact_info_title', submitterDiv);
|
|
||||||
const submitterEmail = extractTextById('contact_info_email', submitterDiv);
|
|
||||||
const submitterPhone = extractTextById('contact_info_phone', submitterDiv);
|
|
||||||
let submitterInfo = `${submitterName}${submitterTitle}${submitterEmail}${submitterPhone}`;
|
|
||||||
|
|
||||||
|
|
||||||
//------ Senior Official
|
//------ Senior Official
|
||||||
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
|
const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official');
|
||||||
|
@ -734,7 +726,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
`<strong>Current Websites:</strong> ${existingWebsites.join(', ')}</br>` +
|
`<strong>Current Websites:</strong> ${existingWebsites.join(', ')}</br>` +
|
||||||
`<strong>Rationale:</strong></br>` +
|
`<strong>Rationale:</strong></br>` +
|
||||||
`<strong>Alternative Domains:</strong> ${alternativeDomains.join(', ')}</br>` +
|
`<strong>Alternative Domains:</strong> ${alternativeDomains.join(', ')}</br>` +
|
||||||
`<strong>Submitter:</strong> ${submitterInfo}</br>` +
|
|
||||||
`<strong>Senior Official:</strong> ${seniorOfficialInfo}</br>` +
|
`<strong>Senior Official:</strong> ${seniorOfficialInfo}</br>` +
|
||||||
`<strong>Other Employees:</strong> ${otherContactsSummary}</br>`;
|
`<strong>Other Employees:</strong> ${otherContactsSummary}</br>`;
|
||||||
|
|
||||||
|
|
|
@ -1498,12 +1498,23 @@ class DomainsTable extends LoadTableBase {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class DomainRequestsTable extends LoadTableBase {
|
class DomainRequestsTable extends LoadTableBase {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results');
|
super('.domain-requests__table', '.domain-requests__table-wrapper', '#domain-requests__search-field', '#domain-requests__search-field-submit', '.domain-requests__reset-search', '.domain-requests__reset-filters', '.domain-requests__no-data', '.domain-requests__no-search-results');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleExportButton(requests) {
|
||||||
|
const exportButton = document.getElementById('export-csv');
|
||||||
|
if (exportButton) {
|
||||||
|
if (requests.length > 0) {
|
||||||
|
showElement(exportButton);
|
||||||
|
} else {
|
||||||
|
hideElement(exportButton);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads rows in the domains list, as well as updates pagination around the domains list
|
* Loads rows in the domains list, as well as updates pagination around the domains list
|
||||||
* based on the supplied attributes.
|
* based on the supplied attributes.
|
||||||
|
@ -1517,6 +1528,7 @@ class DomainRequestsTable extends LoadTableBase {
|
||||||
*/
|
*/
|
||||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
|
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm = this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||||
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
let baseUrl = document.getElementById("get_domain_requests_json_url");
|
||||||
|
|
||||||
if (!baseUrl) {
|
if (!baseUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1548,6 +1560,9 @@ class DomainRequestsTable extends LoadTableBase {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Manage "export as CSV" visibility for domain requests
|
||||||
|
this.toggleExportButton(data.domain_requests);
|
||||||
|
|
||||||
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
// handle the display of proper messaging in the event that no requests exist in the list or search returns no results
|
||||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||||
|
|
||||||
|
|
|
@ -385,6 +385,7 @@ a.button,
|
||||||
font-kerning: auto;
|
font-kerning: auto;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
text-decoration: none !important;
|
||||||
}
|
}
|
||||||
.button svg,
|
.button svg,
|
||||||
.button span,
|
.button span,
|
||||||
|
@ -392,6 +393,9 @@ a.button,
|
||||||
.usa-button--dja span {
|
.usa-button--dja span {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
.usa-button--dja.usa-button--unstyled {
|
||||||
|
color: var(--link-fg);
|
||||||
|
}
|
||||||
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
.usa-button--dja:not(.usa-button--unstyled, .usa-button--outline, .usa-modal__close, .usa-button--secondary) {
|
||||||
background: var(--button-bg);
|
background: var(--button-bg);
|
||||||
}
|
}
|
||||||
|
@ -421,11 +425,34 @@ input[type=submit].button--dja-toolbar {
|
||||||
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
|
input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-toolbar:hover {
|
||||||
border-color: var(--body-quiet-color);
|
border-color: var(--body-quiet-color);
|
||||||
}
|
}
|
||||||
// Targets the DJA buttom with a nested icon
|
.admin-icon-group {
|
||||||
button .usa-icon,
|
position: relative;
|
||||||
.button .usa-icon,
|
display: inline;
|
||||||
.button--clipboard .usa-icon {
|
align-items: center;
|
||||||
vertical-align: middle;
|
|
||||||
|
input {
|
||||||
|
// Allow for padding around the copy button
|
||||||
|
padding-right: 35px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1000px) {
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
padding-left: 0.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.usa-button__small-text,
|
||||||
|
.usa-button__small-text span {
|
||||||
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.module--custom {
|
.module--custom {
|
||||||
|
@ -673,71 +700,10 @@ address.dja-address-contact-list {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the clipboard button "float" inside of the input box
|
|
||||||
.admin-icon-group {
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
input {
|
|
||||||
// Allow for padding around the copy button
|
|
||||||
padding-right: 35px !important;
|
|
||||||
// Match the height of other inputs
|
|
||||||
min-height: 2.25rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
line-height: 14px;
|
|
||||||
width: max-content;
|
|
||||||
font-size: unset;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
padding-top: 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
span {
|
|
||||||
padding-left: 0.1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.admin-icon-group.admin-icon-group__clipboard-link {
|
|
||||||
position: relative;
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
|
|
||||||
.usa-button--icon {
|
|
||||||
position: absolute;
|
|
||||||
right: auto;
|
|
||||||
left: 4px;
|
|
||||||
height: 100%;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
font-size: unset !important;
|
|
||||||
display: inline-flex;
|
|
||||||
padding-top: 4px;
|
|
||||||
line-height: 14px;
|
|
||||||
width: max-content;
|
|
||||||
font-size: unset;
|
|
||||||
text-decoration: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.no-outline-on-click:focus {
|
.no-outline-on-click:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-button__small-text {
|
|
||||||
font-size: small;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get rid of padding on all help texts
|
// Get rid of padding on all help texts
|
||||||
form .aligned p.help, form .aligned div.help {
|
form .aligned p.help, form .aligned div.help {
|
||||||
padding-left: 0px !important;
|
padding-left: 0px !important;
|
||||||
|
@ -887,6 +853,9 @@ div.dja__model-description{
|
||||||
padding-top: 0 !important;
|
padding-top: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.padding-bottom-0 {
|
||||||
|
padding-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-container {
|
.flex-container {
|
||||||
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
@media screen and (min-width: 700px) and (max-width: 1150px) {
|
||||||
|
|
|
@ -723,6 +723,7 @@ ALLOWED_HOSTS = [
|
||||||
"getgov-stable.app.cloud.gov",
|
"getgov-stable.app.cloud.gov",
|
||||||
"getgov-staging.app.cloud.gov",
|
"getgov-staging.app.cloud.gov",
|
||||||
"getgov-development.app.cloud.gov",
|
"getgov-development.app.cloud.gov",
|
||||||
|
"getgov-el.app.cloud.gov",
|
||||||
"getgov-ad.app.cloud.gov",
|
"getgov-ad.app.cloud.gov",
|
||||||
"getgov-ms.app.cloud.gov",
|
"getgov-ms.app.cloud.gov",
|
||||||
"getgov-ag.app.cloud.gov",
|
"getgov-ag.app.cloud.gov",
|
||||||
|
|
|
@ -20,6 +20,7 @@ from registrar.views.report_views import (
|
||||||
AnalyticsView,
|
AnalyticsView,
|
||||||
ExportDomainRequestDataFull,
|
ExportDomainRequestDataFull,
|
||||||
ExportDataTypeUser,
|
ExportDataTypeUser,
|
||||||
|
ExportDataTypeRequests,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --jsons
|
# --jsons
|
||||||
|
@ -180,6 +181,16 @@ urlpatterns = [
|
||||||
ExportDataTypeUser.as_view(),
|
ExportDataTypeUser.as_view(),
|
||||||
name="export_data_type_user",
|
name="export_data_type_user",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"reports/export_data_type_requests/",
|
||||||
|
ExportDataTypeRequests.as_view(),
|
||||||
|
name="export_data_type_requests",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"reports/export_data_type_requests/",
|
||||||
|
ExportDataTypeRequests.as_view(),
|
||||||
|
name="export_data_type_requests",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"domain-request/<int:id>/edit/",
|
"domain-request/<int:id>/edit/",
|
||||||
views.DomainRequestWizard.as_view(),
|
views.DomainRequestWizard.as_view(),
|
||||||
|
|
|
@ -229,6 +229,10 @@ class User(AbstractUser):
|
||||||
"""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)
|
||||||
|
|
||||||
|
def has_view_all_domain_requests_portfolio_permission(self, portfolio):
|
||||||
|
"""Determines if the current user can view all available domains in a given portfolio"""
|
||||||
|
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)
|
||||||
|
|
||||||
def has_any_requests_portfolio_permission(self, portfolio):
|
def has_any_requests_portfolio_permission(self, portfolio):
|
||||||
# BEGIN
|
# BEGIN
|
||||||
# Note code below is to add organization_request feature
|
# Note code below is to add organization_request feature
|
||||||
|
@ -458,3 +462,12 @@ class User(AbstractUser):
|
||||||
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
return DomainInformation.objects.filter(portfolio=portfolio).values_list("domain_id", flat=True)
|
||||||
else:
|
else:
|
||||||
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
return UserDomainRole.objects.filter(user=self).values_list("domain_id", flat=True)
|
||||||
|
|
||||||
|
def get_user_domain_request_ids(self, request):
|
||||||
|
"""Returns either the domain request ids associated with this user on UserDomainRole or Portfolio"""
|
||||||
|
portfolio = request.session.get("portfolio")
|
||||||
|
|
||||||
|
if self.is_org_user(request) and self.has_view_all_domain_requests_portfolio_permission(portfolio):
|
||||||
|
return DomainRequest.objects.filter(portfolio=portfolio).values_list("id", flat=True)
|
||||||
|
else:
|
||||||
|
return UserDomainRole.objects.filter(user=self).values_list("id", flat=True)
|
||||||
|
|
|
@ -20,10 +20,11 @@
|
||||||
</li>
|
</li>
|
||||||
{% if opts.model_name == 'domainrequest' %}
|
{% if opts.model_name == 'domainrequest' %}
|
||||||
<li>
|
<li>
|
||||||
<a id="id-copy-to-clipboard-summary" class="button--clipboard" type="button" href="#">
|
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
|
||||||
<svg class="usa-icon" >
|
<svg class="usa-icon" >
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
<span>{% translate "Copy request summary" %}</span>
|
<span>{% translate "Copy request summary" %}</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -8,7 +8,7 @@ Template for an input field with a clipboard
|
||||||
<div class="admin-icon-group">
|
<div class="admin-icon-group">
|
||||||
{{ field }}
|
{{ field }}
|
||||||
<button
|
<button
|
||||||
class="usa-button usa-button--unstyled padding-left-1 usa-button--icon button--clipboard copy-to-clipboard"
|
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-left-1 usa-button--icon copy-to-clipboard"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<div class="no-outline-on-click">
|
<div class="no-outline-on-click">
|
||||||
|
@ -17,15 +17,17 @@ Template for an input field with a clipboard
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Copy
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
|
<span>Copy</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="admin-icon-group admin-icon-group__clipboard-link">
|
<div class="admin-icon-group">
|
||||||
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
|
<input aria-hidden="true" class="display-none" value="{{ field.email }}" />
|
||||||
|
{% if field.email is not None %}
|
||||||
<button
|
<button
|
||||||
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard text-no-underline padding-left-05"
|
class="usa-button--dja usa-button usa-button__small-text usa-button--unstyled padding-right-1 usa-button--icon copy-to-clipboard text-no-underline padding-left-05"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -33,7 +35,9 @@ Template for an input field with a clipboard
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
Copy
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
|
<span>Copy</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
|
@ -26,7 +26,7 @@
|
||||||
{% if user.email %}
|
{% if user.email %}
|
||||||
<span id="contact_info_email">{{ user.email }}</span>
|
<span id="contact_info_email">{{ user.email }}</span>
|
||||||
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
{% include "admin/input_with_clipboard.html" with field=user invisible_input_field=True %}
|
||||||
<br class="admin-icon-group__br">
|
<br>
|
||||||
{% else %}
|
{% else %}
|
||||||
None<br>
|
None<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -254,7 +254,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th colspan="4">Other contact information</th>
|
<th colspan="5">Other contact information</th>
|
||||||
<tr>
|
<tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -267,9 +267,20 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
</td>
|
</td>
|
||||||
<td class="padding-left-1">{{ contact.phone }}</td>
|
<td class="padding-left-1">{{ contact.phone }}</td>
|
||||||
<td class="padding-left-1 text-size-small">
|
<td class="padding-left-1 text-size-small">
|
||||||
|
{% if contact.email %}
|
||||||
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
|
<input aria-hidden="true" class="display-none" value="{{ contact.email }}" />
|
||||||
<button
|
<button
|
||||||
class="usa-button usa-button--unstyled padding-right-1 usa-button--icon button--clipboard copy-to-clipboard usa-button__small-text text-no-underline"
|
class="
|
||||||
|
usa-button--dja
|
||||||
|
usa-button
|
||||||
|
usa-button__small-text
|
||||||
|
usa-button--unstyled
|
||||||
|
padding-right-1
|
||||||
|
padding-top-0
|
||||||
|
padding-bottom-0
|
||||||
|
usa-button--icon
|
||||||
|
copy-to-clipboard
|
||||||
|
text-no-underline"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -277,8 +288,10 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
||||||
>
|
>
|
||||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
<!-- the span is targeted in JS, do not remove -->
|
||||||
<span>Copy email</span>
|
<span>Copy email</span>
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -3,21 +3,21 @@
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_domain_requests_json' as url %}
|
{% url 'get_domain_requests_json' as url %}
|
||||||
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
<span id="get_domain_requests_json_url" class="display-none">{{url}}</span>
|
||||||
|
|
||||||
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
<section class="section-outlined domain-requests{% if portfolio %} section-outlined--border-base-light{% endif %}" id="domain-requests">
|
||||||
<div class="grid-row">
|
<div class="section-outlined__header margin-bottom-3 {% if not portfolio %} section-outlined__header--no-portfolio justify-content-space-between{% else %} grid-row{% endif %}">
|
||||||
{% if not portfolio %}
|
{% if not portfolio %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
<h2 id="domain-requests-header" class="display-inline-block">Domain requests</h2>
|
||||||
<h2 id="domain-requests-header" class="flex-6">Domain requests</h2>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<!-- Embedding the portfolio value in a data attribute -->
|
<!-- Embedding the portfolio value in a data attribute -->
|
||||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="mobile:grid-col-12 desktop:grid-col-6">
|
|
||||||
<section aria-label="Domain requests search component" class="flex-6 margin-y-2">
|
<div class="section-outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %} {% if is_widescreen_mode %} section-outlined__search--widescreen {% endif %}">
|
||||||
|
<section aria-label="Domain requests search component" class="margin-top-2">
|
||||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button class="usa-button usa-button--unstyled margin-right-2 domain-requests__reset-search display-none" type="button">
|
<button class="usa-button usa-button--unstyled margin-right-3 domain-requests__reset-search display-none" type="button">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -49,7 +49,19 @@
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
{% if portfolio %}
|
||||||
|
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}" id="export-csv">
|
||||||
|
<section aria-label="Domain Requests report component" class="margin-top-205">
|
||||||
|
<a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
||||||
|
<svg class="usa-icon usa-icon--big" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
|
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
|
||||||
|
</svg>Export as CSV
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<div class="display-flex flex-align-center">
|
<div class="display-flex flex-align-center">
|
||||||
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
<span class="margin-right-2 margin-top-neg-1 usa-prose text-base-darker">Filter by</span>
|
||||||
|
@ -79,9 +91,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="started"
|
value="started"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-started"
|
<label class="usa-checkbox__label" for="filter-status-started">Started</label>
|
||||||
>Started</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -91,9 +101,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="submitted"
|
value="submitted"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-submitted"
|
<label class="usa-checkbox__label" for="filter-status-submitted">Submitted</label>
|
||||||
>Submitted</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -103,9 +111,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="in review"
|
value="in review"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-in-review"
|
<label class="usa-checkbox__label" for="filter-status-in-review">In review</label>
|
||||||
>In review</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -115,9 +121,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="action needed"
|
value="action needed"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-action-needed"
|
<label class="usa-checkbox__label" for="filter-status-action-needed">Action needed</label>
|
||||||
>Action needed</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -127,9 +131,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="rejected"
|
value="rejected"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-rejected"
|
<label class="usa-checkbox__label" for="filter-status-rejected">Rejected</label>
|
||||||
>Rejected</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -139,9 +141,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="withdrawn"
|
value="withdrawn"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-withdrawn"
|
<label class="usa-checkbox__label" for="filter-status-withdrawn">Withdrawn</label>
|
||||||
>Withdrawn</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
|
@ -151,9 +151,7 @@
|
||||||
name="filter-status"
|
name="filter-status"
|
||||||
value="ineligible"
|
value="ineligible"
|
||||||
/>
|
/>
|
||||||
<label class="usa-checkbox__label" for="filter-status-ineligible"
|
<label class="usa-checkbox__label" for="filter-status-ineligible">Ineligible</label>
|
||||||
>Ineligible</label
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
@ -169,6 +167,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
<div class="domain-requests__table-wrapper display-none usa-table-container--scrollable margin-top-0" tabindex="0">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked domain-requests__table">
|
||||||
<caption class="sr-only">Your domain requests</caption>
|
<caption class="sr-only">Your domain requests</caption>
|
||||||
|
@ -188,18 +187,18 @@
|
||||||
<!-- AJAX will populate this tbody -->
|
<!-- AJAX will populate this tbody -->
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div
|
<div class="usa-sr-only usa-table__announcement-region" aria-live="polite"></div>
|
||||||
class="usa-sr-only usa-table__announcement-region"
|
|
||||||
aria-live="polite"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="domain-requests__no-data display-none">
|
<div class="domain-requests__no-data display-none">
|
||||||
<p>You haven't requested any domains.</p>
|
<p>You haven't requested any domains.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="domain-requests__no-search-results display-none">
|
<div class="domain-requests__no-search-results display-none">
|
||||||
<p>No results found</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="domain-requests-pagination">
|
||||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||||
<!-- Count will be dynamically populated by JS -->
|
<!-- Count will be dynamically populated by JS -->
|
||||||
|
|
|
@ -654,7 +654,7 @@ class TestDomainInformationAdmin(TestCase):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=3)
|
self.assertContains(response, "copy-to-clipboard", count=3)
|
||||||
|
|
||||||
# cleanup this test
|
# cleanup this test
|
||||||
domain_info.delete()
|
domain_info.delete()
|
||||||
|
|
|
@ -535,7 +535,7 @@ class TestDomainAdminWithClient(TestCase):
|
||||||
self.assertContains(response, "Testy Tester")
|
self.assertContains(response, "Testy Tester")
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard")
|
self.assertContains(response, "copy-to-clipboard")
|
||||||
|
|
||||||
# cleanup from this test
|
# cleanup from this test
|
||||||
domain.delete()
|
domain.delete()
|
||||||
|
|
|
@ -1511,7 +1511,7 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
self.test_helper.assert_response_contains_distinct_values(response, expected_other_employees_fields)
|
||||||
|
|
||||||
# Test for the copy link
|
# Test for the copy link
|
||||||
self.assertContains(response, "button--clipboard", count=4)
|
self.assertContains(response, "copy-to-clipboard", count=4)
|
||||||
|
|
||||||
# Test that Creator counts display properly
|
# Test that Creator counts display properly
|
||||||
self.assertNotContains(response, "Approved domains")
|
self.assertNotContains(response, "Approved domains")
|
||||||
|
@ -1846,6 +1846,58 @@ class TestDomainRequestAdmin(MockEppLib):
|
||||||
def test_side_effects_when_saving_approved_to_ineligible(self):
|
def test_side_effects_when_saving_approved_to_ineligible(self):
|
||||||
self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE)
|
self.trigger_saving_approved_to_another_state(False, DomainRequest.DomainRequestStatus.INELIGIBLE)
|
||||||
|
|
||||||
|
@less_console_noise
|
||||||
|
def test_error_when_saving_to_approved_and_domain_exists(self):
|
||||||
|
"""Redundant admin check on model transition not allowed."""
|
||||||
|
Domain.objects.create(name="wabbitseason.gov")
|
||||||
|
|
||||||
|
new_request = completed_domain_request(
|
||||||
|
status=DomainRequest.DomainRequestStatus.SUBMITTED, name="wabbitseason.gov"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
request.session = {}
|
||||||
|
|
||||||
|
# Use ExitStack to combine patch contexts
|
||||||
|
with ExitStack() as stack:
|
||||||
|
# Patch django.contrib.messages.error
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
|
||||||
|
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
|
||||||
|
self.admin.save_model(request, new_request, None, True)
|
||||||
|
|
||||||
|
messages.error.assert_called_once_with(
|
||||||
|
request,
|
||||||
|
"Cannot approve. Requested domain is already in use.",
|
||||||
|
)
|
||||||
|
|
||||||
|
@less_console_noise
|
||||||
|
def test_no_error_when_saving_to_approved_and_domain_exists(self):
|
||||||
|
"""The negative of the redundant admin check on model transition not allowed."""
|
||||||
|
new_request = completed_domain_request(status=DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||||
|
|
||||||
|
# Create a request object with a superuser
|
||||||
|
request = self.factory.post("/admin/registrar/domainrequest/{}/change/".format(new_request.pk))
|
||||||
|
request.user = self.superuser
|
||||||
|
|
||||||
|
request.session = {}
|
||||||
|
|
||||||
|
# Use ExitStack to combine patch contexts
|
||||||
|
with ExitStack() as stack:
|
||||||
|
# Patch Domain.is_active and django.contrib.messages.error simultaneously
|
||||||
|
stack.enter_context(patch.object(messages, "error"))
|
||||||
|
|
||||||
|
new_request.status = DomainRequest.DomainRequestStatus.APPROVED
|
||||||
|
|
||||||
|
self.admin.save_model(request, new_request, None, True)
|
||||||
|
|
||||||
|
# Assert that the error message was never called
|
||||||
|
messages.error.assert_not_called()
|
||||||
|
|
||||||
def test_has_correct_filters(self):
|
def test_has_correct_filters(self):
|
||||||
"""
|
"""
|
||||||
This test verifies that DomainRequestAdmin has the correct filters set up.
|
This test verifies that DomainRequestAdmin has the correct filters set up.
|
||||||
|
|
File diff suppressed because it is too large
Load diff
1029
src/registrar/tests/test_models_requests.py
Normal file
1029
src/registrar/tests/test_models_requests.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ from registrar.models import (
|
||||||
Domain,
|
Domain,
|
||||||
UserDomainRole,
|
UserDomainRole,
|
||||||
)
|
)
|
||||||
from registrar.models import Portfolio
|
from registrar.models import Portfolio, DraftDomain
|
||||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
from registrar.utility.csv_export import (
|
from registrar.utility.csv_export import (
|
||||||
|
@ -14,6 +14,7 @@ from registrar.utility.csv_export import (
|
||||||
DomainDataType,
|
DomainDataType,
|
||||||
DomainDataFederal,
|
DomainDataFederal,
|
||||||
DomainDataTypeUser,
|
DomainDataTypeUser,
|
||||||
|
DomainRequestsDataType,
|
||||||
DomainGrowth,
|
DomainGrowth,
|
||||||
DomainManaged,
|
DomainManaged,
|
||||||
DomainUnmanaged,
|
DomainUnmanaged,
|
||||||
|
@ -389,6 +390,77 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
||||||
|
|
||||||
return csv_content
|
return csv_content
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
@override_flag("organization_feature", active=True)
|
||||||
|
@override_flag("organization_requests", active=True)
|
||||||
|
def test_domain_request_data_type_user_with_portfolio(self):
|
||||||
|
"""Tests DomainRequestsDataType export with portfolio permissions"""
|
||||||
|
|
||||||
|
# Create a portfolio and assign it to the user
|
||||||
|
portfolio = Portfolio.objects.create(creator=self.user, organization_name="Test Portfolio")
|
||||||
|
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(portfolio=portfolio, user=self.user)
|
||||||
|
|
||||||
|
# Create DraftDomain objects
|
||||||
|
dd_1 = DraftDomain.objects.create(name="example1.com")
|
||||||
|
dd_2 = DraftDomain.objects.create(name="example2.com")
|
||||||
|
dd_3 = DraftDomain.objects.create(name="example3.com")
|
||||||
|
|
||||||
|
# Create some domain requests
|
||||||
|
dr_1 = DomainRequest.objects.create(creator=self.user, requested_domain=dd_1, portfolio=portfolio)
|
||||||
|
dr_2 = DomainRequest.objects.create(creator=self.user, requested_domain=dd_2)
|
||||||
|
dr_3 = DomainRequest.objects.create(creator=self.user, requested_domain=dd_3, portfolio=portfolio)
|
||||||
|
|
||||||
|
# Set up user permissions
|
||||||
|
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||||
|
portfolio_permission.save()
|
||||||
|
portfolio_permission.refresh_from_db()
|
||||||
|
|
||||||
|
# Make a GET request using self.client to get a request object
|
||||||
|
request = get_wsgi_request_object(client=self.client, user=self.user)
|
||||||
|
|
||||||
|
# Get the CSV content
|
||||||
|
csv_content = self._run_domain_request_data_type_user_export(request)
|
||||||
|
|
||||||
|
# We expect only domain requests associated with the user's portfolio
|
||||||
|
self.assertIn(dd_1.name, csv_content)
|
||||||
|
self.assertIn(dd_3.name, csv_content)
|
||||||
|
self.assertNotIn(dd_2.name, csv_content)
|
||||||
|
|
||||||
|
# Get the csv content
|
||||||
|
csv_content = self._run_domain_request_data_type_user_export(request)
|
||||||
|
self.assertIn(dd_1.name, csv_content)
|
||||||
|
self.assertIn(dd_3.name, csv_content)
|
||||||
|
self.assertNotIn(dd_2.name, csv_content)
|
||||||
|
|
||||||
|
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||||
|
portfolio_permission.save()
|
||||||
|
portfolio_permission.refresh_from_db()
|
||||||
|
|
||||||
|
# Domain Request NOT in Portfolio
|
||||||
|
csv_content = self._run_domain_request_data_type_user_export(request)
|
||||||
|
self.assertNotIn(dd_1.name, csv_content)
|
||||||
|
self.assertNotIn(dd_3.name, csv_content)
|
||||||
|
self.assertNotIn(dd_2.name, csv_content)
|
||||||
|
|
||||||
|
# Clean up the created objects
|
||||||
|
dr_1.delete()
|
||||||
|
dr_2.delete()
|
||||||
|
dr_3.delete()
|
||||||
|
portfolio.delete()
|
||||||
|
|
||||||
|
def _run_domain_request_data_type_user_export(self, request):
|
||||||
|
"""Helper function to run the exporting_dr_data_to_csv function on DomainRequestsDataType"""
|
||||||
|
|
||||||
|
csv_file = StringIO()
|
||||||
|
|
||||||
|
DomainRequestsDataType.exporting_dr_data_to_csv(csv_file, request=request)
|
||||||
|
|
||||||
|
csv_file.seek(0)
|
||||||
|
|
||||||
|
csv_content = csv_file.read()
|
||||||
|
|
||||||
|
return csv_content
|
||||||
|
|
||||||
@less_console_noise_decorator
|
@less_console_noise_decorator
|
||||||
def test_domain_data_full(self):
|
def test_domain_data_full(self):
|
||||||
"""Shows security contacts, filtered by state"""
|
"""Shows security contacts, filtered by state"""
|
||||||
|
|
|
@ -82,7 +82,6 @@ class DomainRequestTests(TestWithUser, WebTest):
|
||||||
response = self.app.get(f"/domain-request/{domain_request.id}")
|
response = self.app.get(f"/domain-request/{domain_request.id}")
|
||||||
# Ensure that the date is still set to None
|
# Ensure that the date is still set to None
|
||||||
self.assertIsNone(domain_request.last_status_update)
|
self.assertIsNone(domain_request.last_status_update)
|
||||||
print(response)
|
|
||||||
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
# We should still grab a date for this field in this event - but it should come from the audit log instead
|
||||||
self.assertContains(response, "Started on:")
|
self.assertContains(response, "Started on:")
|
||||||
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
self.assertContains(response, fixed_date.strftime("%B %-d, %Y"))
|
||||||
|
|
|
@ -583,6 +583,105 @@ class DomainDataTypeUser(DomainDataType):
|
||||||
return Q(domain__id__in=request.user.get_user_domain_ids(request))
|
return Q(domain__id__in=request.user.get_user_domain_ids(request))
|
||||||
|
|
||||||
|
|
||||||
|
class DomainRequestsDataType:
|
||||||
|
"""
|
||||||
|
The DomainRequestsDataType report, but filtered based on the current request user
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_filter_conditions(cls, request=None):
|
||||||
|
if request is None or not hasattr(request, "user") or not request.user.is_authenticated:
|
||||||
|
return Q(id__in=[])
|
||||||
|
|
||||||
|
request_ids = request.user.get_user_domain_request_ids(request)
|
||||||
|
return Q(id__in=request_ids)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_queryset(cls, request):
|
||||||
|
return DomainRequest.objects.filter(cls.get_filter_conditions(request))
|
||||||
|
|
||||||
|
def safe_get(attribute, default="N/A"):
|
||||||
|
# Return the attribute value or default if not present
|
||||||
|
return attribute if attribute is not None else default
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def exporting_dr_data_to_csv(cls, response, request=None):
|
||||||
|
import csv
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
|
||||||
|
# CSV headers
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
"Domain request",
|
||||||
|
"Region",
|
||||||
|
"Status",
|
||||||
|
"Election office",
|
||||||
|
"Federal type",
|
||||||
|
"Domain type",
|
||||||
|
"Request additional details",
|
||||||
|
"Creator approved domains count",
|
||||||
|
"Creator active requests count",
|
||||||
|
"Alternative domains",
|
||||||
|
"Other contacts",
|
||||||
|
"Current websites",
|
||||||
|
"Federal agency",
|
||||||
|
"SO first name",
|
||||||
|
"SO last name",
|
||||||
|
"SO email",
|
||||||
|
"SO title/role",
|
||||||
|
"Creator first name",
|
||||||
|
"Creator last name",
|
||||||
|
"Creator email",
|
||||||
|
"Organization name",
|
||||||
|
"City",
|
||||||
|
"State/territory",
|
||||||
|
"Request purpose",
|
||||||
|
"CISA regional representative",
|
||||||
|
"Last submitted date",
|
||||||
|
"First submitted date",
|
||||||
|
"Last status update",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = cls.get_queryset(request)
|
||||||
|
for request in queryset:
|
||||||
|
writer.writerow(
|
||||||
|
[
|
||||||
|
request.requested_domain,
|
||||||
|
cls.safe_get(getattr(request, "region_field", None)),
|
||||||
|
request.status,
|
||||||
|
cls.safe_get(getattr(request, "election_office", None)),
|
||||||
|
request.federal_type,
|
||||||
|
cls.safe_get(getattr(request, "domain_type", None)),
|
||||||
|
cls.safe_get(getattr(request, "additional_details", None)),
|
||||||
|
cls.safe_get(getattr(request, "creator_approved_domains_count", None)),
|
||||||
|
cls.safe_get(getattr(request, "creator_active_requests_count", None)),
|
||||||
|
cls.safe_get(getattr(request, "all_alternative_domains", None)),
|
||||||
|
cls.safe_get(getattr(request, "all_other_contacts", None)),
|
||||||
|
cls.safe_get(getattr(request, "all_current_websites", None)),
|
||||||
|
cls.safe_get(getattr(request, "federal_agency", None)),
|
||||||
|
cls.safe_get(getattr(request.senior_official, "first_name", None)),
|
||||||
|
cls.safe_get(getattr(request.senior_official, "last_name", None)),
|
||||||
|
cls.safe_get(getattr(request.senior_official, "email", None)),
|
||||||
|
cls.safe_get(getattr(request.senior_official, "title", None)),
|
||||||
|
cls.safe_get(getattr(request.creator, "first_name", None)),
|
||||||
|
cls.safe_get(getattr(request.creator, "last_name", None)),
|
||||||
|
cls.safe_get(getattr(request.creator, "email", None)),
|
||||||
|
cls.safe_get(getattr(request, "organization_name", None)),
|
||||||
|
cls.safe_get(getattr(request, "city", None)),
|
||||||
|
cls.safe_get(getattr(request, "state_territory", None)),
|
||||||
|
cls.safe_get(getattr(request, "purpose", None)),
|
||||||
|
cls.safe_get(getattr(request, "cisa_representative_email", None)),
|
||||||
|
cls.safe_get(getattr(request, "last_submitted_date", None)),
|
||||||
|
cls.safe_get(getattr(request, "first_submitted_date", None)),
|
||||||
|
cls.safe_get(getattr(request, "last_status_update", None)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class DomainDataFull(DomainExport):
|
class DomainDataFull(DomainExport):
|
||||||
"""
|
"""
|
||||||
Shows security contacts, filtered by state
|
Shows security contacts, filtered by state
|
||||||
|
|
|
@ -169,6 +169,17 @@ class ExportDataTypeUser(View):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class ExportDataTypeRequests(View):
|
||||||
|
"""Returns a domain requests report for a given user on the request"""
|
||||||
|
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
response = HttpResponse(content_type="text/csv")
|
||||||
|
response["Content-Disposition"] = 'attachment; filename="domain-requests.csv"'
|
||||||
|
csv_export.DomainRequestsDataType.exporting_dr_data_to_csv(response, request=request)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
class ExportDataFull(View):
|
class ExportDataFull(View):
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
# Smaller export based on 1
|
# Smaller export based on 1
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue