diff --git a/.github/workflows/deploy-manual.yaml b/.github/workflows/deploy-manual.yaml index ba85342b0..a85cc7565 100644 --- a/.github/workflows/deploy-manual.yaml +++ b/.github/workflows/deploy-manual.yaml @@ -14,6 +14,7 @@ on: options: - ab - backup + - el - cb - dk - es diff --git a/.github/workflows/deploy-sandbox.yaml b/.github/workflows/deploy-sandbox.yaml index fe0a19089..e9eb06627 100644 --- a/.github/workflows/deploy-sandbox.yaml +++ b/.github/workflows/deploy-sandbox.yaml @@ -30,6 +30,7 @@ jobs: || startsWith(github.head_ref, 'ag/') || startsWith(github.head_ref, 'ms/') || startsWith(github.head_ref, 'ad/') + || startsWith(github.head_ref, 'el/') outputs: environment: ${{ steps.var.outputs.environment}} runs-on: "ubuntu-latest" diff --git a/.github/workflows/migrate.yaml b/.github/workflows/migrate.yaml index 70ff8ee95..1853b3c4f 100644 --- a/.github/workflows/migrate.yaml +++ b/.github/workflows/migrate.yaml @@ -16,6 +16,7 @@ on: - stable - staging - development + - el - ad - ms - ag diff --git a/.github/workflows/reset-db.yaml b/.github/workflows/reset-db.yaml index b6fa0fec5..111555b3c 100644 --- a/.github/workflows/reset-db.yaml +++ b/.github/workflows/reset-db.yaml @@ -16,6 +16,7 @@ on: options: - staging - development + - el - ad - ms - ag diff --git a/ops/manifests/manifest-el.yaml b/ops/manifests/manifest-el.yaml new file mode 100644 index 000000000..4c7d4d4e4 --- /dev/null +++ b/ops/manifests/manifest-el.yaml @@ -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 diff --git a/src/registrar/admin.py b/src/registrar/admin.py index ca51e8b72..485a1b07d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -1976,18 +1976,30 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin): # If the status is not mapped properly, saving could cause # weird issues down the line. Instead, we should block this. + # NEEDS A UNIT TEST should_proceed = False - return should_proceed + return (obj, should_proceed) - request_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED - if request_is_not_approved and not obj.domain_is_not_active(): - # If an admin tried to set an approved domain request to - # another status and the related domain is already - # active, shortcut the action and throw a friendly - # error message. This action would still not go through - # shortcut or not as the rules are duplicated on the model, - # but the error would be an ugly Django error screen. + obj_is_not_approved = obj.status != models.DomainRequest.DomainRequestStatus.APPROVED + if obj_is_not_approved and not obj.domain_is_not_active(): + # REDUNDANT CHECK / ERROR SCREEN AVOIDANCE: + # This action (moving a request from approved to + # another status) when the domain is already active (READY), + # 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. + # This avoids an ugly Django error screen. 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: # This condition should never be triggered. # The opposite of this condition is acceptable (rejected -> other status and rejection_reason) diff --git a/src/registrar/assets/js/get-gov-admin.js b/src/registrar/assets/js/get-gov-admin.js index 73f3dded1..f44211c6d 100644 --- a/src/registrar/assets/js/get-gov-admin.js +++ b/src/registrar/assets/js/get-gov-admin.js @@ -515,10 +515,14 @@ document.addEventListener('DOMContentLoaded', function() { const formLabel = document.querySelector('label[for="id_action_needed_reason_email"]'); let lastSentEmailContent = document.getElementById("last-sent-email-content"); 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 - 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 function isEmailAlreadySent() { return lastSentEmailContent.value.replace(/\s+/g, '') === textarea.value.replace(/\s+/g, ''); @@ -706,18 +710,6 @@ document.addEventListener('DOMContentLoaded', function() { } 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 const seniorOfficialDiv = document.querySelector('.form-row.field-senior_official'); @@ -734,7 +726,6 @@ document.addEventListener('DOMContentLoaded', function() { `Current Websites: ${existingWebsites.join(', ')}
` + `Rationale:
` + `Alternative Domains: ${alternativeDomains.join(', ')}
` + - `Submitter: ${submitterInfo}
` + `Senior Official: ${seniorOfficialInfo}
` + `Other Employees: ${otherContactsSummary}
`; diff --git a/src/registrar/assets/js/get-gov.js b/src/registrar/assets/js/get-gov.js index 8dd0fcf14..8a07b3f27 100644 --- a/src/registrar/assets/js/get-gov.js +++ b/src/registrar/assets/js/get-gov.js @@ -1498,12 +1498,23 @@ class DomainsTable extends LoadTableBase { } } - class DomainRequestsTable extends LoadTableBase { 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'); } + + 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 * 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) { let baseUrl = document.getElementById("get_domain_requests_json_url"); + if (!baseUrl) { return; } @@ -1548,6 +1560,9 @@ class DomainRequestsTable extends LoadTableBase { 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 this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm); diff --git a/src/registrar/assets/sass/_theme/_admin.scss b/src/registrar/assets/sass/_theme/_admin.scss index 5cea72c4c..b6bc0d296 100644 --- a/src/registrar/assets/sass/_theme/_admin.scss +++ b/src/registrar/assets/sass/_theme/_admin.scss @@ -385,6 +385,7 @@ a.button, font-kerning: auto; font-family: inherit; font-weight: normal; + text-decoration: none !important; } .button svg, .button span, @@ -392,6 +393,9 @@ a.button, .usa-button--dja span { 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) { 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 { 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; +.admin-icon-group { + position: relative; + display: inline; + align-items: center; + + 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 { @@ -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 { outline: none !important; } -.usa-button__small-text { - font-size: small; -} - // Get rid of padding on all help texts form .aligned p.help, form .aligned div.help { padding-left: 0px !important; @@ -887,6 +853,9 @@ div.dja__model-description{ padding-top: 0 !important; } +.padding-bottom-0 { + padding-bottom: 0 !important; +} .flex-container { @media screen and (min-width: 700px) and (max-width: 1150px) { diff --git a/src/registrar/config/settings.py b/src/registrar/config/settings.py index 30882cd5d..d2689242a 100644 --- a/src/registrar/config/settings.py +++ b/src/registrar/config/settings.py @@ -723,6 +723,7 @@ ALLOWED_HOSTS = [ "getgov-stable.app.cloud.gov", "getgov-staging.app.cloud.gov", "getgov-development.app.cloud.gov", + "getgov-el.app.cloud.gov", "getgov-ad.app.cloud.gov", "getgov-ms.app.cloud.gov", "getgov-ag.app.cloud.gov", diff --git a/src/registrar/config/urls.py b/src/registrar/config/urls.py index 7d9b5b8cf..3c9e185b5 100644 --- a/src/registrar/config/urls.py +++ b/src/registrar/config/urls.py @@ -20,6 +20,7 @@ from registrar.views.report_views import ( AnalyticsView, ExportDomainRequestDataFull, ExportDataTypeUser, + ExportDataTypeRequests, ) # --jsons @@ -180,6 +181,16 @@ urlpatterns = [ ExportDataTypeUser.as_view(), 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( "domain-request//edit/", views.DomainRequestWizard.as_view(), diff --git a/src/registrar/models/user.py b/src/registrar/models/user.py index ae76d648b..80c972d38 100644 --- a/src/registrar/models/user.py +++ b/src/registrar/models/user.py @@ -229,6 +229,10 @@ class User(AbstractUser): """Determines if the current user can view all available domains in a given portfolio""" return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS) + def has_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): # BEGIN # 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) else: 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) diff --git a/src/registrar/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 198140c19..66011a3c4 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -20,10 +20,11 @@ {% if opts.model_name == 'domainrequest' %}
  • - + + {% translate "Copy request summary" %}
  • diff --git a/src/registrar/templates/admin/input_with_clipboard.html b/src/registrar/templates/admin/input_with_clipboard.html index 5ad2b27f7..d6a016fd5 100644 --- a/src/registrar/templates/admin/input_with_clipboard.html +++ b/src/registrar/templates/admin/input_with_clipboard.html @@ -8,7 +8,7 @@ Template for an input field with a clipboard
    {{ field }}
    {% else %} - + + - -