mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-05-17 18:09:25 +02:00
Merge remote-tracking branch 'origin/main' into ms/2307-send-notification-emails
This commit is contained in:
commit
ac02e8cb81
116 changed files with 4493 additions and 1663 deletions
1
.github/workflows/deploy-manual.yaml
vendored
1
.github/workflows/deploy-manual.yaml
vendored
|
@ -14,6 +14,7 @@ on:
|
|||
options:
|
||||
- ab
|
||||
- backup
|
||||
- el
|
||||
- cb
|
||||
- dk
|
||||
- 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, 'ms/')
|
||||
|| startsWith(github.head_ref, 'ad/')
|
||||
|| startsWith(github.head_ref, 'el/')
|
||||
outputs:
|
||||
environment: ${{ steps.var.outputs.environment}}
|
||||
runs-on: "ubuntu-latest"
|
||||
|
|
1
.github/workflows/migrate.yaml
vendored
1
.github/workflows/migrate.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
|||
- stable
|
||||
- staging
|
||||
- development
|
||||
- el
|
||||
- ad
|
||||
- ms
|
||||
- ag
|
||||
|
|
1
.github/workflows/reset-db.yaml
vendored
1
.github/workflows/reset-db.yaml
vendored
|
@ -16,6 +16,7 @@ on:
|
|||
options:
|
||||
- staging
|
||||
- development
|
||||
- el
|
||||
- ad
|
||||
- ms
|
||||
- ag
|
||||
|
|
|
@ -173,7 +173,7 @@ You can change the logging verbosity, if needed. Do a web search for "django log
|
|||
|
||||
## Mock data
|
||||
|
||||
[load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures_users.py) and [fixtures_domain_requests.py](../../src/registrar/fixtures_domain_requests.py), giving you some test data to play with while developing.
|
||||
[load.py](../../src/registrar/management/commands/load.py) called from docker-compose (locally) and reset-db.yml (upper) loads the fixtures from [fixtures_user.py](../../src/registrar/fixtures/fixtures_users.py) and the rest of the data-loading fixtures in that fixtures folder, giving you some test data to play with while developing.
|
||||
|
||||
See the [database-access README](./database-access.md) for information on how to pull data to update these fixtures.
|
||||
|
||||
|
|
|
@ -754,7 +754,7 @@ Example: `cf ssh getgov-za`
|
|||
| 1 | **emailTo** | Specifies where the email will be emailed. Defaults to help@get.gov on production. |
|
||||
|
||||
## Populate federal agency initials and FCEB
|
||||
This script adds to the "is_fceb" and "initials" fields on the FederalAgency model. This script expects a CSV of federal CIOs to pull from, which can be sourced from [here](https://docs.google.com/spreadsheets/d/14oXHFpKyUXS5_mDWARPusghGdHCrP67jCleOknaSx38/edit?gid=479328070#gid=479328070).
|
||||
This script adds to the "is_fceb" and "acronym" fields on the FederalAgency model. This script expects a CSV of federal CIOs to pull from, which can be sourced from [here](https://docs.google.com/spreadsheets/d/14oXHFpKyUXS5_mDWARPusghGdHCrP67jCleOknaSx38/edit?gid=479328070#gid=479328070).
|
||||
|
||||
### Running on sandboxes
|
||||
|
||||
|
|
|
@ -9,17 +9,16 @@ Simple scripts are provided as detailed below.
|
|||
### Export
|
||||
|
||||
To export from the source environment, run the following command from src directory:
|
||||
manage.py export_tables
|
||||
|
||||
Connect to the source sandbox and run the command:
|
||||
cf ssh {source-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py export_tables
|
||||
`cf ssh {source-app}`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py export_tables`
|
||||
|
||||
example exporting from getgov-stable:
|
||||
cf ssh getgov-stable
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py export_tables
|
||||
`cf ssh getgov-stable`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py export_tables`
|
||||
|
||||
This exports a file, exported_tables.zip, to the tmp directory
|
||||
|
||||
|
@ -42,14 +41,16 @@ After exporting the file from the target environment, scp the exported_tables.zi
|
|||
file from the target environment to local. Run the below commands from local.
|
||||
|
||||
Get passcode by running:
|
||||
cf ssh-code
|
||||
`cf ssh-code`
|
||||
|
||||
scp file from source app to local file:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {source-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip {local_file_path}
|
||||
`scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {source-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip {local_file_path}`
|
||||
when prompted, supply the passcode retrieved in the 'cf ssh-code' command
|
||||
|
||||
example copying from stable to local cwd:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-stable --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip .
|
||||
`scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-stable --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip .`
|
||||
|
||||
`scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-stable --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 ssh.fr.cloud.gov:app/tmp/exported_tables.zip .`
|
||||
|
||||
|
||||
### Import
|
||||
|
@ -63,14 +64,14 @@ that there are no database conflicts on import.
|
|||
|
||||
In order to delete all rows from the appropriate tables, run the following
|
||||
command:
|
||||
cf ssh {target-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py clean_tables
|
||||
`cf ssh {target-app}`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py clean_tables`
|
||||
|
||||
example cleaning getgov-backup:
|
||||
cf ssh getgov-backup
|
||||
/tmp/lifecycle/backup
|
||||
./manage.py clean_tables
|
||||
`cf ssh getgov-backup`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py clean_tables`
|
||||
|
||||
For reference, this deletes all rows from the following tables:
|
||||
|
||||
|
@ -96,28 +97,30 @@ with --skipEppSave option set to False. If you set to False, it will attempt to
|
|||
records to the registry on load. If this is unset, or set to True, it will load the database and not
|
||||
attempt to update the registry on load.
|
||||
|
||||
Please note that there is currently a bug (missing batch importing, see #2862) so this may not work
|
||||
smoothly right now currently.
|
||||
|
||||
To scp the exported_tables.zip file from local to the sandbox, run the following:
|
||||
|
||||
Get passcode by running:
|
||||
cf ssh-code
|
||||
`cf ssh-code`
|
||||
|
||||
scp file from local to target app:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {target-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 {local_file_path} ssh.fr.cloud.gov:app/tmp/exported_tables.zip
|
||||
`scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app {target-app} --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 {local_file_path} ssh.fr.cloud.gov:app/tmp/exported_tables.zip`
|
||||
when prompted, supply the passcode retrieved in the 'cf ssh-code' command
|
||||
|
||||
example copy of local file in tmp to getgov-backup:
|
||||
scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-backup --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 tmp/exported_tables.zip ssh.fr.cloud.gov:app/tmp/exported_tables.zip
|
||||
|
||||
`scp -P 2222 -o User=cf:$(cf curl /v3/apps/$(cf app getgov-backup --guid)/processes | jq -r '.resources[] | select(.type=="web") | .guid')/0 exported_tables.zip ssh.fr.cloud.gov:app/tmp/exported_tables.zip`
|
||||
|
||||
Then connect to a shell in the target environment, and run the following import command:
|
||||
cf ssh {target-app}
|
||||
/tmp/lifecycle/shell
|
||||
./manage.py import_tables
|
||||
`cf ssh {target-app}`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py import_tables`
|
||||
|
||||
example cleaning getgov-backup:
|
||||
cf ssh getgov-backup
|
||||
/tmp/lifecycle/backup
|
||||
./manage.py import_tables --no-skipEppSave
|
||||
`cf ssh getgov-backup`
|
||||
`/tmp/lifecycle/shell`
|
||||
`./manage.py import_tables --no-skipEppSave`
|
||||
|
||||
For reference, this imports tables in the following order:
|
||||
|
||||
|
|
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
|
66
src/api/tests/test_rdap.py
Normal file
66
src/api/tests/test_rdap.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
"""Test the domain rdap lookup API."""
|
||||
|
||||
import json
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.test import RequestFactory
|
||||
from django.test import TestCase
|
||||
|
||||
from ..views import rdap
|
||||
|
||||
API_BASE_PATH = "/api/v1/rdap/?domain="
|
||||
|
||||
|
||||
class RdapViewTest(TestCase):
|
||||
"""Test that the RDAP view function works as expected"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = get_user_model().objects.create(username="username")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_rdap_get_no_tld(self):
|
||||
"""RDAP API successfully fetches RDAP for domain without a TLD"""
|
||||
request = self.factory.get(API_BASE_PATH + "whitehouse")
|
||||
request.user = self.user
|
||||
response = rdap(request, domain="whitehouse")
|
||||
# contains the right text
|
||||
self.assertContains(response, "rdap")
|
||||
# can be parsed into JSON with appropriate keys
|
||||
response_object = json.loads(response.content)
|
||||
self.assertIn("rdapConformance", response_object)
|
||||
|
||||
def test_rdap_invalid_domain(self):
|
||||
"""RDAP API accepts invalid domain queries and returns JSON response
|
||||
with appropriate error codes"""
|
||||
request = self.factory.get(API_BASE_PATH + "whitehouse.com")
|
||||
request.user = self.user
|
||||
response = rdap(request, domain="whitehouse.com")
|
||||
|
||||
self.assertContains(response, "errorCode")
|
||||
response_object = json.loads(response.content)
|
||||
self.assertIn("errorCode", response_object)
|
||||
|
||||
|
||||
class RdapAPITest(TestCase):
|
||||
"""Test that the API can be called as expected."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
username = "test_user"
|
||||
first_name = "First"
|
||||
last_name = "Last"
|
||||
email = "info@example.com"
|
||||
title = "title"
|
||||
phone = "8080102431"
|
||||
self.user = get_user_model().objects.create(
|
||||
username=username, title=title, first_name=first_name, last_name=last_name, email=email, phone=phone
|
||||
)
|
||||
|
||||
def test_rdap_get(self):
|
||||
"""Can call RDAP API"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(API_BASE_PATH + "whitehouse.gov")
|
||||
self.assertContains(response, "rdap")
|
||||
response_object = json.loads(response.content)
|
||||
self.assertIn("rdapConformance", response_object)
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from django.apps import apps
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
from registrar.templatetags.url_helpers import public_site_url
|
||||
|
@ -18,7 +18,7 @@ from cachetools.func import ttl_cache
|
|||
from registrar.utility.s3_bucket import S3ClientError, S3ClientHelper
|
||||
|
||||
|
||||
DOMAIN_FILE_URL = "https://raw.githubusercontent.com/cisagov/dotgov-data/main/current-full.csv"
|
||||
RDAP_URL = "https://rdap.cloudflareregistry.com/rdap/domain/{domain}"
|
||||
|
||||
|
||||
DOMAIN_API_MESSAGES = {
|
||||
|
@ -41,30 +41,6 @@ DOMAIN_API_MESSAGES = {
|
|||
}
|
||||
|
||||
|
||||
# this file doesn't change that often, nor is it that big, so cache the result
|
||||
# in memory for ten minutes
|
||||
@ttl_cache(ttl=600)
|
||||
def _domains():
|
||||
"""Return a list of the current .gov domains.
|
||||
|
||||
Fetch a file from DOMAIN_FILE_URL, parse the CSV for the domain,
|
||||
lowercase everything and return the list.
|
||||
"""
|
||||
DraftDomain = apps.get_model("registrar.DraftDomain")
|
||||
# 5 second timeout
|
||||
file_contents = requests.get(DOMAIN_FILE_URL, timeout=5).text
|
||||
domains = set()
|
||||
# skip the first line
|
||||
for line in file_contents.splitlines()[1:]:
|
||||
# get the domain before the first comma
|
||||
domain = line.split(",", 1)[0]
|
||||
# sanity-check the string we got from the file here
|
||||
if DraftDomain.string_could_be_domain(domain):
|
||||
# lowercase everything when we put it in domains
|
||||
domains.add(domain.lower())
|
||||
return domains
|
||||
|
||||
|
||||
def check_domain_available(domain):
|
||||
"""Return true if the given domain is available.
|
||||
|
||||
|
@ -99,6 +75,22 @@ def available(request, domain=""):
|
|||
return json_response
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_not_required
|
||||
# Since we cache domain RDAP data, cache time may need to be re-evaluated this if we encounter any memory issues
|
||||
@ttl_cache(ttl=600)
|
||||
def rdap(request, domain=""):
|
||||
"""Returns JSON dictionary of a domain's RDAP data from Cloudflare API"""
|
||||
domain = request.GET.get("domain", "")
|
||||
|
||||
# If inputted domain doesn't have a TLD, append .gov to it
|
||||
if "." not in domain:
|
||||
domain = f"{domain}.gov"
|
||||
|
||||
rdap_data = requests.get(RDAP_URL.format(domain=domain), timeout=5).json()
|
||||
return JsonResponse(rdap_data)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_not_required
|
||||
def get_current_full(request, file_name="current-full.csv"):
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
from datetime import date
|
||||
import logging
|
||||
import copy
|
||||
import json
|
||||
from django.template.loader import get_template
|
||||
from django import forms
|
||||
from django.db.models import Value, CharField, Q
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
|
@ -10,8 +8,7 @@ from django.http import HttpResponseRedirect
|
|||
from django.conf import settings
|
||||
from django.shortcuts import redirect
|
||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||
from registrar.models.domain_information import DomainInformation
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from waffle.decorators import flag_is_active
|
||||
from django.contrib import admin, messages
|
||||
|
@ -23,6 +20,11 @@ from epplibwrapper.errors import ErrorCode, RegistryError
|
|||
from registrar.models.user_domain_role import UserDomainRole
|
||||
from waffle.admin import FlagAdmin
|
||||
from waffle.models import Sample, Switch
|
||||
from registrar.utility.admin_helpers import (
|
||||
get_all_action_needed_reason_emails,
|
||||
get_action_needed_reason_default_email,
|
||||
get_field_links_as_list,
|
||||
)
|
||||
from registrar.models import Contact, Domain, DomainRequest, DraftDomain, User, Website, SeniorOfficial
|
||||
from registrar.utility.constants import BranchChoices
|
||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||
|
@ -757,9 +759,10 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
},
|
||||
),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
("Associated portfolios", {"fields": ("portfolios",)}),
|
||||
)
|
||||
|
||||
readonly_fields = ("verification_type",)
|
||||
readonly_fields = ("verification_type", "portfolios")
|
||||
|
||||
analyst_fieldsets = (
|
||||
(
|
||||
|
@ -782,6 +785,7 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
},
|
||||
),
|
||||
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||
("Associated portfolios", {"fields": ("portfolios",)}),
|
||||
)
|
||||
|
||||
# TODO: delete after we merge organization feature
|
||||
|
@ -861,6 +865,14 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
ordering = ["first_name", "last_name", "email"]
|
||||
search_help_text = "Search by first name, last name, or email."
|
||||
|
||||
def portfolios(self, obj: models.User):
|
||||
"""Returns a list of links for each related suborg"""
|
||||
portfolio_ids = obj.get_portfolios().values_list("portfolio", flat=True)
|
||||
queryset = models.Portfolio.objects.filter(id__in=portfolio_ids)
|
||||
return get_field_links_as_list(queryset, "portfolio", msg_for_none="No portfolios.")
|
||||
|
||||
portfolios.short_description = "Portfolios" # type: ignore
|
||||
|
||||
def get_search_results(self, request, queryset, search_term):
|
||||
"""
|
||||
Override for get_search_results. This affects any upstream model using autocomplete_fields,
|
||||
|
@ -1257,9 +1269,18 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
list_display = [
|
||||
"user",
|
||||
"portfolio",
|
||||
"get_roles",
|
||||
]
|
||||
|
||||
autocomplete_fields = ["user", "portfolio"]
|
||||
search_fields = ["user__first_name", "user__last_name", "user__email", "portfolio__organization_name"]
|
||||
search_help_text = "Search by first name, last name, email, or portfolio."
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
return ", ".join(readable_roles)
|
||||
|
||||
get_roles.short_description = "Roles" # type: ignore
|
||||
|
||||
|
||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
|
@ -1543,33 +1564,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
change_form_template = "django/admin/domain_information_change_form.html"
|
||||
|
||||
superuser_only_fields = [
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
]
|
||||
|
||||
# DEVELOPER's NOTE:
|
||||
# Normally, to exclude a field from an Admin form, we could simply utilize
|
||||
# Django's "exclude" feature. However, it causes a "missing key" error if we
|
||||
# go that route for this particular form. The error gets thrown by our
|
||||
# custom fieldset.html code and is due to the fact that "exclude" removes
|
||||
# fields from base_fields but not fieldsets. Rather than reworking our
|
||||
# custom frontend, it seems more straightforward (and easier to read) to simply
|
||||
# modify the fieldsets list so that it excludes any fields we want to remove
|
||||
# based on permissions (eg. superuser_only_fields) or other conditions.
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = self.fieldsets
|
||||
|
||||
# Create a modified version of fieldsets to exclude certain fields
|
||||
if not request.user.has_perm("registrar.full_access_permission"):
|
||||
modified_fieldsets = []
|
||||
for name, data in fieldsets:
|
||||
fields = data.get("fields", [])
|
||||
fields = tuple(field for field in fields if field not in DomainInformationAdmin.superuser_only_fields)
|
||||
modified_fieldsets.append((name, {**data, "fields": fields}))
|
||||
return modified_fieldsets
|
||||
return fieldsets
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
"""Set the read-only state on form elements.
|
||||
We have 1 conditions that determine which fields are read-only:
|
||||
|
@ -1865,33 +1859,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
]
|
||||
filter_horizontal = ("current_websites", "alternative_domains", "other_contacts")
|
||||
|
||||
superuser_only_fields = [
|
||||
"portfolio",
|
||||
"sub_organization",
|
||||
]
|
||||
|
||||
# DEVELOPER's NOTE:
|
||||
# Normally, to exclude a field from an Admin form, we could simply utilize
|
||||
# Django's "exclude" feature. However, it causes a "missing key" error if we
|
||||
# go that route for this particular form. The error gets thrown by our
|
||||
# custom fieldset.html code and is due to the fact that "exclude" removes
|
||||
# fields from base_fields but not fieldsets. Rather than reworking our
|
||||
# custom frontend, it seems more straightforward (and easier to read) to simply
|
||||
# modify the fieldsets list so that it excludes any fields we want to remove
|
||||
# based on permissions (eg. superuser_only_fields) or other conditions.
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
fieldsets = super().get_fieldsets(request, obj)
|
||||
|
||||
# Create a modified version of fieldsets to exclude certain fields
|
||||
if not request.user.has_perm("registrar.full_access_permission"):
|
||||
modified_fieldsets = []
|
||||
for name, data in fieldsets:
|
||||
fields = data.get("fields", [])
|
||||
fields = tuple(field for field in fields if field not in self.superuser_only_fields)
|
||||
modified_fieldsets.append((name, {**data, "fields": fields}))
|
||||
return modified_fieldsets
|
||||
return fieldsets
|
||||
|
||||
# Table ordering
|
||||
# NOTE: This impacts the select2 dropdowns (combobox)
|
||||
# Currentl, there's only one for requests on DomainInfo
|
||||
|
@ -1948,9 +1915,9 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Set the action_needed_reason_email to the default if nothing exists.
|
||||
# Since this check occurs after save, if the user enters a value then we won't update.
|
||||
|
||||
default_email = self._get_action_needed_reason_default_email(obj, obj.action_needed_reason)
|
||||
default_email = get_action_needed_reason_default_email(request, obj, obj.action_needed_reason)
|
||||
if obj.action_needed_reason_email:
|
||||
emails = self.get_all_action_needed_reason_emails(obj)
|
||||
emails = get_all_action_needed_reason_emails(request, obj)
|
||||
is_custom_email = obj.action_needed_reason_email not in emails.values()
|
||||
if not is_custom_email:
|
||||
obj.action_needed_reason_email = default_email
|
||||
|
@ -2180,8 +2147,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Initialize extra_context and add filtered entries
|
||||
extra_context = extra_context or {}
|
||||
extra_context["filtered_audit_log_entries"] = filtered_audit_log_entries
|
||||
emails = self.get_all_action_needed_reason_emails(obj)
|
||||
extra_context["action_needed_reason_emails"] = json.dumps(emails)
|
||||
|
||||
# Denote if an action needed email was sent or not
|
||||
email_sent = request.session.get("action_needed_email_sent", False)
|
||||
|
@ -2192,39 +2157,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Call the superclass method with updated extra_context
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def get_all_action_needed_reason_emails(self, domain_request):
|
||||
"""Returns a json dictionary of every action needed reason and its associated email
|
||||
for this particular domain request."""
|
||||
|
||||
emails = {}
|
||||
for action_needed_reason in domain_request.ActionNeededReasons:
|
||||
# Map the action_needed_reason to its default email
|
||||
emails[action_needed_reason.value] = self._get_action_needed_reason_default_email(
|
||||
domain_request, action_needed_reason.value
|
||||
)
|
||||
|
||||
return emails
|
||||
|
||||
def _get_action_needed_reason_default_email(self, domain_request, action_needed_reason):
|
||||
"""Returns the default email associated with the given action needed reason"""
|
||||
if not action_needed_reason or action_needed_reason == DomainRequest.ActionNeededReasons.OTHER:
|
||||
return None
|
||||
|
||||
recipient = domain_request.creator
|
||||
|
||||
# Return the context of the rendered views
|
||||
context = {"domain_request": domain_request, "recipient": recipient}
|
||||
|
||||
# Get the email body
|
||||
template_path = f"emails/action_needed_reasons/{action_needed_reason}.txt"
|
||||
|
||||
email_body_text = get_template(template_path).render(context=context)
|
||||
email_body_text_cleaned = None
|
||||
if email_body_text:
|
||||
email_body_text_cleaned = email_body_text.strip().lstrip("\n")
|
||||
|
||||
return email_body_text_cleaned
|
||||
|
||||
def process_log_entry(self, log_entry):
|
||||
"""Process a log entry and return filtered entry dictionary if applicable."""
|
||||
changes = log_entry.changes
|
||||
|
@ -2335,10 +2267,58 @@ class DomainInformationInline(admin.StackedInline):
|
|||
template = "django/admin/includes/domain_info_inline_stacked.html"
|
||||
model = models.DomainInformation
|
||||
|
||||
fieldsets = DomainInformationAdmin.fieldsets
|
||||
readonly_fields = DomainInformationAdmin.readonly_fields
|
||||
analyst_readonly_fields = DomainInformationAdmin.analyst_readonly_fields
|
||||
autocomplete_fields = DomainInformationAdmin.autocomplete_fields
|
||||
fieldsets = copy.deepcopy(list(DomainInformationAdmin.fieldsets))
|
||||
analyst_readonly_fields = copy.deepcopy(DomainInformationAdmin.analyst_readonly_fields)
|
||||
autocomplete_fields = copy.deepcopy(DomainInformationAdmin.autocomplete_fields)
|
||||
|
||||
def get_domain_managers(self, obj):
|
||||
user_domain_roles = UserDomainRole.objects.filter(domain=obj.domain)
|
||||
user_ids = user_domain_roles.values_list("user_id", flat=True)
|
||||
domain_managers = User.objects.filter(id__in=user_ids)
|
||||
return domain_managers
|
||||
|
||||
def get_domain_invitations(self, obj):
|
||||
domain_invitations = DomainInvitation.objects.filter(
|
||||
domain=obj.domain, status=DomainInvitation.DomainInvitationStatus.INVITED
|
||||
)
|
||||
return domain_invitations
|
||||
|
||||
def domain_managers(self, obj):
|
||||
"""Get domain managers for the domain, unpack and return an HTML block."""
|
||||
domain_managers = self.get_domain_managers(obj)
|
||||
if not domain_managers:
|
||||
return "No domain managers found."
|
||||
|
||||
domain_manager_details = "<table><thead><tr><th>UID</th><th>Name</th><th>Email</th></tr></thead><tbody>"
|
||||
for domain_manager in domain_managers:
|
||||
full_name = domain_manager.get_formatted_name()
|
||||
change_url = reverse("admin:registrar_user_change", args=[domain_manager.pk])
|
||||
domain_manager_details += "<tr>"
|
||||
domain_manager_details += f'<td><a href="{change_url}">{escape(domain_manager.username)}</a>'
|
||||
domain_manager_details += f"<td>{escape(full_name)}</td>"
|
||||
domain_manager_details += f"<td>{escape(domain_manager.email)}</td>"
|
||||
domain_manager_details += "</tr>"
|
||||
domain_manager_details += "</tbody></table>"
|
||||
return format_html(domain_manager_details)
|
||||
|
||||
domain_managers.short_description = "Domain managers" # type: ignore
|
||||
|
||||
def invited_domain_managers(self, obj):
|
||||
"""Get emails which have been invited to the domain, unpack and return an HTML block."""
|
||||
domain_invitations = self.get_domain_invitations(obj)
|
||||
if not domain_invitations:
|
||||
return "No invited domain managers found."
|
||||
|
||||
domain_invitation_details = "<table><thead><tr><th>Email</th><th>Status</th>" + "</tr></thead><tbody>"
|
||||
for domain_invitation in domain_invitations:
|
||||
domain_invitation_details += "<tr>"
|
||||
domain_invitation_details += f"<td>{escape(domain_invitation.email)}</td>"
|
||||
domain_invitation_details += f"<td>{escape(domain_invitation.status.capitalize())}</td>"
|
||||
domain_invitation_details += "</tr>"
|
||||
domain_invitation_details += "</tbody></table>"
|
||||
return format_html(domain_invitation_details)
|
||||
|
||||
invited_domain_managers.short_description = "Invited domain managers" # type: ignore
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
"""Custom has_change_permission override so that we can specify that
|
||||
|
@ -2378,7 +2358,9 @@ class DomainInformationInline(admin.StackedInline):
|
|||
return super().formfield_for_foreignkey(db_field, request, **kwargs)
|
||||
|
||||
def get_readonly_fields(self, request, obj=None):
|
||||
return DomainInformationAdmin.get_readonly_fields(self, request, obj=None)
|
||||
readonly_fields = copy.deepcopy(DomainInformationAdmin.get_readonly_fields(self, request, obj=None))
|
||||
readonly_fields.extend(["domain_managers", "invited_domain_managers"]) # type: ignore
|
||||
return readonly_fields
|
||||
|
||||
# Re-route the get_fieldsets method to utilize DomainInformationAdmin.get_fieldsets
|
||||
# since that has all the logic for excluding certain fields according to user permissions.
|
||||
|
@ -2387,13 +2369,34 @@ class DomainInformationInline(admin.StackedInline):
|
|||
def get_fieldsets(self, request, obj=None):
|
||||
# Grab fieldsets from DomainInformationAdmin so that it handles all logic
|
||||
# for permission-based field visibility.
|
||||
modified_fieldsets = DomainInformationAdmin.get_fieldsets(self, request, obj=None)
|
||||
modified_fieldsets = copy.deepcopy(DomainInformationAdmin.get_fieldsets(self, request, obj=None))
|
||||
|
||||
# remove .gov domain from fieldset
|
||||
# Modify fieldset sections in place
|
||||
for index, (title, options) in enumerate(modified_fieldsets):
|
||||
if title is None:
|
||||
options["fields"] = [
|
||||
field for field in options["fields"] if field not in ["creator", "domain_request", "notes"]
|
||||
]
|
||||
elif title == "Contacts":
|
||||
options["fields"] = [
|
||||
field
|
||||
for field in options["fields"]
|
||||
if field not in ["other_contacts", "no_other_contacts_rationale"]
|
||||
]
|
||||
options["fields"].extend(["domain_managers", "invited_domain_managers"]) # type: ignore
|
||||
elif title == "Background info":
|
||||
# move domain request and notes to background
|
||||
options["fields"].extend(["domain_request", "notes"]) # type: ignore
|
||||
|
||||
# Remove or remove fieldset sections
|
||||
for index, (title, f) in enumerate(modified_fieldsets):
|
||||
if title == ".gov domain":
|
||||
del modified_fieldsets[index]
|
||||
break
|
||||
# remove .gov domain from fieldset
|
||||
modified_fieldsets.pop(index)
|
||||
elif title == "Background info":
|
||||
# move Background info to the bottom of the list
|
||||
fieldsets_to_move = modified_fieldsets.pop(index)
|
||||
modified_fieldsets.append(fieldsets_to_move)
|
||||
|
||||
return modified_fieldsets
|
||||
|
||||
|
@ -2451,13 +2454,10 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
fieldsets = (
|
||||
(
|
||||
None,
|
||||
{"fields": ["name", "state", "expiration_date", "first_ready", "deleted"]},
|
||||
{"fields": ["state", "expiration_date", "first_ready", "deleted", "dnssecdata", "nameservers"]},
|
||||
),
|
||||
)
|
||||
|
||||
# this ordering effects the ordering of results in autocomplete_fields for domain
|
||||
ordering = ["name"]
|
||||
|
||||
def generic_org_type(self, obj):
|
||||
return obj.domain_info.get_generic_org_type_display()
|
||||
|
||||
|
@ -2478,6 +2478,28 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
organization_name.admin_order_field = "domain_info__organization_name" # type: ignore
|
||||
|
||||
def dnssecdata(self, obj):
|
||||
return "Yes" if obj.dnssecdata else "No"
|
||||
|
||||
dnssecdata.short_description = "DNSSEC enabled" # type: ignore
|
||||
|
||||
# Custom method to display formatted nameservers
|
||||
def nameservers(self, obj):
|
||||
if not obj.nameservers:
|
||||
return "No nameservers"
|
||||
|
||||
formatted_nameservers = []
|
||||
for server, ip_list in obj.nameservers:
|
||||
server_display = str(server)
|
||||
if ip_list:
|
||||
server_display += f" [{', '.join(ip_list)}]"
|
||||
formatted_nameservers.append(server_display)
|
||||
|
||||
# Join the formatted strings with line breaks
|
||||
return "\n".join(formatted_nameservers)
|
||||
|
||||
nameservers.short_description = "Name servers" # type: ignore
|
||||
|
||||
def custom_election_board(self, obj):
|
||||
domain_info = getattr(obj, "domain_info", None)
|
||||
if domain_info:
|
||||
|
@ -2504,7 +2526,15 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
search_fields = ["name"]
|
||||
search_help_text = "Search by domain name."
|
||||
change_form_template = "django/admin/domain_change_form.html"
|
||||
readonly_fields = ("state", "expiration_date", "first_ready", "deleted", "federal_agency")
|
||||
readonly_fields = (
|
||||
"state",
|
||||
"expiration_date",
|
||||
"first_ready",
|
||||
"deleted",
|
||||
"federal_agency",
|
||||
"dnssecdata",
|
||||
"nameservers",
|
||||
)
|
||||
|
||||
# Table ordering
|
||||
ordering = ["name"]
|
||||
|
@ -2943,39 +2973,59 @@ class VerifiedByStaffAdmin(ListHeaderAdmin):
|
|||
|
||||
|
||||
class PortfolioAdmin(ListHeaderAdmin):
|
||||
|
||||
class Meta:
|
||||
"""Contains meta information about this class"""
|
||||
|
||||
model = models.Portfolio
|
||||
fields = "__all__"
|
||||
|
||||
_meta = Meta()
|
||||
|
||||
change_form_template = "django/admin/portfolio_change_form.html"
|
||||
fieldsets = [
|
||||
# created_on is the created_at field, and portfolio_type is f"{organization_type} - {federal_type}"
|
||||
(None, {"fields": ["portfolio_type", "organization_name", "creator", "created_on", "notes"]}),
|
||||
("Portfolio members", {"fields": ["display_admins", "display_members"]}),
|
||||
("Portfolio domains", {"fields": ["domains", "domain_requests"]}),
|
||||
# created_on is the created_at field
|
||||
(None, {"fields": ["creator", "created_on", "notes"]}),
|
||||
("Type of organization", {"fields": ["organization_type", "federal_type"]}),
|
||||
(
|
||||
"Organization name and mailing address",
|
||||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"federal_agency",
|
||||
]
|
||||
},
|
||||
),
|
||||
(
|
||||
"Show details",
|
||||
{
|
||||
"classes": ["collapse--dgfieldset"],
|
||||
"description": "Extends organization name and mailing address",
|
||||
"fields": [
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"city",
|
||||
"zipcode",
|
||||
"urbanization",
|
||||
]
|
||||
],
|
||||
},
|
||||
),
|
||||
("Portfolio members", {"fields": ["display_admins", "display_members"]}),
|
||||
("Domains and requests", {"fields": ["domains", "domain_requests"]}),
|
||||
("Suborganizations", {"fields": ["suborganizations"]}),
|
||||
("Senior official", {"fields": ["senior_official"]}),
|
||||
]
|
||||
|
||||
# This is the fieldset display when adding a new model
|
||||
add_fieldsets = [
|
||||
(None, {"fields": ["organization_name", "creator", "notes"]}),
|
||||
(None, {"fields": ["creator", "notes"]}),
|
||||
("Type of organization", {"fields": ["organization_type"]}),
|
||||
(
|
||||
"Organization name and mailing address",
|
||||
{
|
||||
"fields": [
|
||||
"organization_name",
|
||||
"federal_agency",
|
||||
"state_territory",
|
||||
"address_line1",
|
||||
|
@ -2989,7 +3039,7 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
("Senior official", {"fields": ["senior_official"]}),
|
||||
]
|
||||
|
||||
list_display = ("organization_name", "federal_agency", "creator")
|
||||
list_display = ("organization_name", "organization_type", "federal_type", "creator")
|
||||
search_fields = ["organization_name"]
|
||||
search_help_text = "Search by organization name."
|
||||
readonly_fields = [
|
||||
|
@ -3002,23 +3052,35 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"domains",
|
||||
"domain_requests",
|
||||
"suborganizations",
|
||||
"portfolio_type",
|
||||
"display_admins",
|
||||
"display_members",
|
||||
"creator",
|
||||
# As of now this means that only federal agency can update this, but this will change.
|
||||
"senior_official",
|
||||
]
|
||||
|
||||
analyst_readonly_fields = [
|
||||
"organization_name",
|
||||
]
|
||||
|
||||
def get_admin_users(self, obj):
|
||||
# Filter UserPortfolioPermission objects related to the portfolio
|
||||
admin_permissions = UserPortfolioPermission.objects.filter(
|
||||
portfolio=obj, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
admin_permissions = self.get_user_portfolio_permission_admins(obj)
|
||||
|
||||
# Get the user objects associated with these permissions
|
||||
admin_users = User.objects.filter(portfolio_permissions__in=admin_permissions)
|
||||
|
||||
return admin_users
|
||||
|
||||
def get_user_portfolio_permission_admins(self, obj):
|
||||
"""Returns each admin on UserPortfolioPermission for a given portfolio."""
|
||||
if obj:
|
||||
return obj.portfolio_users.filter(
|
||||
portfolio=obj, roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN]
|
||||
)
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_non_admin_users(self, obj):
|
||||
# Filter UserPortfolioPermission objects related to the portfolio that do NOT have the "Admin" role
|
||||
non_admin_permissions = UserPortfolioPermission.objects.filter(portfolio=obj).exclude(
|
||||
|
@ -3030,82 +3092,12 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
return non_admin_users
|
||||
|
||||
def display_admins(self, obj):
|
||||
"""Get joined users who are Admin, unpack and return an HTML block.
|
||||
|
||||
'DJA readonly can't handle querysets, so we need to unpack and return html here.
|
||||
Alternatively, we could return querysets in context but that would limit where this
|
||||
data would display in a custom change form without extensive template customization.
|
||||
|
||||
Will be used in the field_readonly block"""
|
||||
admins = self.get_admin_users(obj)
|
||||
if not admins:
|
||||
return format_html("<p>No admins found.</p>")
|
||||
|
||||
admin_details = ""
|
||||
for portfolio_admin in admins:
|
||||
change_url = reverse("admin:registrar_user_change", args=[portfolio_admin.pk])
|
||||
admin_details += "<address class='margin-bottom-2 dja-address-contact-list'>"
|
||||
admin_details += f'<a href="{change_url}">{escape(portfolio_admin)}</a><br>'
|
||||
admin_details += f"{escape(portfolio_admin.title)}<br>"
|
||||
admin_details += f"{escape(portfolio_admin.email)}"
|
||||
admin_details += "<div class='admin-icon-group admin-icon-group__clipboard-link'>"
|
||||
admin_details += f"<input aria-hidden='true' class='display-none' value='{escape(portfolio_admin.email)}'>"
|
||||
admin_details += (
|
||||
"<button class='usa-button usa-button--unstyled padding-right-1 usa-button--icon padding-left-05"
|
||||
+ "button--clipboard copy-to-clipboard text-no-underline' type='button'>"
|
||||
)
|
||||
admin_details += "<svg class='usa-icon'>"
|
||||
admin_details += "<use aria-hidden='true' xlink:href='/public/img/sprite.svg#content_copy'></use>"
|
||||
admin_details += "</svg>"
|
||||
admin_details += "Copy"
|
||||
admin_details += "</button>"
|
||||
admin_details += "</div><br>"
|
||||
admin_details += f"{escape(portfolio_admin.phone)}"
|
||||
admin_details += "</address>"
|
||||
return format_html(admin_details)
|
||||
|
||||
display_admins.short_description = "Administrators" # type: ignore
|
||||
|
||||
def display_members(self, obj):
|
||||
"""Get joined users who have roles/perms that are not Admin, unpack and return an HTML block.
|
||||
|
||||
DJA readonly can't handle querysets, so we need to unpack and return html here.
|
||||
Alternatively, we could return querysets in context but that would limit where this
|
||||
data would display in a custom change form without extensive template customization.
|
||||
|
||||
Will be used in the after_help_text block."""
|
||||
members = self.get_non_admin_users(obj)
|
||||
if not members:
|
||||
return ""
|
||||
|
||||
member_details = (
|
||||
"<table><thead><tr><th>Name</th><th>Title</th><th>Email</th>"
|
||||
+ "<th>Phone</th><th>Roles</th></tr></thead><tbody>"
|
||||
)
|
||||
for member in members:
|
||||
full_name = member.get_formatted_name()
|
||||
member_details += "<tr>"
|
||||
member_details += f"<td>{escape(full_name)}</td>"
|
||||
member_details += f"<td>{escape(member.title)}</td>"
|
||||
member_details += f"<td>{escape(member.email)}</td>"
|
||||
member_details += f"<td>{escape(member.phone)}</td>"
|
||||
member_details += "<td>"
|
||||
for role in member.portfolio_role_summary(obj):
|
||||
member_details += f"<span class='usa-tag'>{escape(role)}</span> "
|
||||
member_details += "</td></tr>"
|
||||
member_details += "</tbody></table>"
|
||||
return format_html(member_details)
|
||||
|
||||
display_members.short_description = "Members" # type: ignore
|
||||
|
||||
def display_members_summary(self, obj):
|
||||
"""Will be passed as context and used in the field_readonly block."""
|
||||
members = self.get_non_admin_users(obj)
|
||||
if not members:
|
||||
return {}
|
||||
|
||||
return self.get_field_links_as_list(members, "user", separator=", ")
|
||||
def get_user_portfolio_permission_non_admins(self, obj):
|
||||
"""Returns each admin on UserPortfolioPermission for a given portfolio."""
|
||||
if obj:
|
||||
return obj.portfolio_users.exclude(roles__contains=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN])
|
||||
else:
|
||||
return []
|
||||
|
||||
def federal_type(self, obj: models.Portfolio):
|
||||
"""Returns the federal_type field"""
|
||||
|
@ -3120,16 +3112,10 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
created_on.short_description = "Created on" # type: ignore
|
||||
|
||||
def portfolio_type(self, obj: models.Portfolio):
|
||||
"""Returns the portfolio type, or "-" if the result is empty"""
|
||||
return obj.portfolio_type if obj.portfolio_type else "-"
|
||||
|
||||
portfolio_type.short_description = "Portfolio type" # type: ignore
|
||||
|
||||
def suborganizations(self, obj: models.Portfolio):
|
||||
"""Returns a list of links for each related suborg"""
|
||||
queryset = obj.get_suborganizations()
|
||||
return self.get_field_links_as_list(queryset, "suborganization")
|
||||
return get_field_links_as_list(queryset, "suborganization")
|
||||
|
||||
suborganizations.short_description = "Suborganizations" # type: ignore
|
||||
|
||||
|
@ -3158,6 +3144,28 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
|
||||
domain_requests.short_description = "Domain requests" # type: ignore
|
||||
|
||||
def display_admins(self, obj):
|
||||
"""Returns the number of administrators for this portfolio"""
|
||||
admin_count = len(self.get_user_portfolio_permission_admins(obj))
|
||||
if admin_count > 0:
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{admin_count} administrators</a>')
|
||||
return "No administrators found."
|
||||
|
||||
display_admins.short_description = "Administrators" # type: ignore
|
||||
|
||||
def display_members(self, obj):
|
||||
"""Returns the number of members for this portfolio"""
|
||||
member_count = len(self.get_user_portfolio_permission_non_admins(obj))
|
||||
if member_count > 0:
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={obj.id}"
|
||||
# Create a clickable link with the count
|
||||
return format_html(f'<a href="{url}">{member_count} members</a>')
|
||||
return "No additional members found."
|
||||
|
||||
display_members.short_description = "Members" # type: ignore
|
||||
|
||||
# Creates select2 fields (with search bars)
|
||||
autocomplete_fields = [
|
||||
"creator",
|
||||
|
@ -3165,59 +3173,6 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
"senior_official",
|
||||
]
|
||||
|
||||
def get_field_links_as_list(
|
||||
self, queryset, model_name, attribute_name=None, link_info_attribute=None, separator=None
|
||||
):
|
||||
"""
|
||||
Generate HTML links for items in a queryset, using a specified attribute for link text.
|
||||
|
||||
Args:
|
||||
queryset: The queryset of items to generate links for.
|
||||
model_name: The model name used to construct the admin change URL.
|
||||
attribute_name: The attribute or method name to use for link text. If None, the item itself is used.
|
||||
link_info_attribute: Appends f"({value_of_attribute})" to the end of the link.
|
||||
separator: The separator to use between links in the resulting HTML.
|
||||
If none, an unordered list is returned.
|
||||
|
||||
Returns:
|
||||
A formatted HTML string with links to the admin change pages for each item.
|
||||
"""
|
||||
links = []
|
||||
for item in queryset:
|
||||
|
||||
# This allows you to pass in attribute_name="get_full_name" for instance.
|
||||
if attribute_name:
|
||||
item_display_value = self.value_of_attribute(item, attribute_name)
|
||||
else:
|
||||
item_display_value = item
|
||||
|
||||
if item_display_value:
|
||||
change_url = reverse(f"admin:registrar_{model_name}_change", args=[item.pk])
|
||||
|
||||
link = f'<a href="{change_url}">{escape(item_display_value)}</a>'
|
||||
if link_info_attribute:
|
||||
link += f" ({self.value_of_attribute(item, link_info_attribute)})"
|
||||
|
||||
if separator:
|
||||
links.append(link)
|
||||
else:
|
||||
links.append(f"<li>{link}</li>")
|
||||
|
||||
# If no separator is specified, just return an unordered list.
|
||||
if separator:
|
||||
return format_html(separator.join(links)) if links else "-"
|
||||
else:
|
||||
links = "".join(links)
|
||||
return format_html(f'<ul class="add-list-reset">{links}</ul>') if links else "-"
|
||||
|
||||
def value_of_attribute(self, obj, attribute_name: str):
|
||||
"""Returns the value of getattr if the attribute isn't callable.
|
||||
If it is, execute the underlying function and return that result instead."""
|
||||
value = getattr(obj, attribute_name)
|
||||
if callable(value):
|
||||
value = value()
|
||||
return value
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
"""Override of the default get_fieldsets definition to add an add_fieldsets view"""
|
||||
# This is the add view if no obj exists
|
||||
|
@ -3250,10 +3205,15 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Add related suborganizations and domain groups.
|
||||
Add the summary for the portfolio members field (list of members that link to change_forms)."""
|
||||
obj = self.get_object(request, object_id)
|
||||
obj: Portfolio = self.get_object(request, object_id)
|
||||
extra_context = extra_context or {}
|
||||
extra_context["skip_additional_contact_info"] = True
|
||||
extra_context["display_members_summary"] = self.display_members_summary(obj)
|
||||
|
||||
if obj:
|
||||
extra_context["members"] = self.get_user_portfolio_permission_non_admins(obj)
|
||||
extra_context["admins"] = self.get_user_portfolio_permission_admins(obj)
|
||||
extra_context["domains"] = obj.get_domains(order_by=["domain__name"])
|
||||
extra_context["domain_requests"] = obj.get_domain_requests(order_by=["requested_domain__name"])
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
|
@ -3270,6 +3230,14 @@ class PortfolioAdmin(ListHeaderAdmin):
|
|||
is_federal = obj.organization_type == DomainRequest.OrganizationChoices.FEDERAL
|
||||
if is_federal and obj.organization_name is None:
|
||||
obj.organization_name = obj.federal_agency.agency
|
||||
|
||||
# Remove this line when senior_official is no longer readonly in /admin.
|
||||
if obj.federal_agency:
|
||||
if obj.federal_agency.so_federal_agency.exists():
|
||||
obj.senior_official = obj.federal_agency.so_federal_agency.first()
|
||||
else:
|
||||
obj.senior_official = None
|
||||
|
||||
super().save_model(request, obj, form, change)
|
||||
|
||||
|
||||
|
@ -3284,7 +3252,7 @@ class FederalAgencyResource(resources.ModelResource):
|
|||
class FederalAgencyAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||
list_display = ["agency"]
|
||||
search_fields = ["agency"]
|
||||
search_help_text = "Search by agency name."
|
||||
search_help_text = "Search by federal agency."
|
||||
ordering = ["agency"]
|
||||
resource_classes = [FederalAgencyResource]
|
||||
|
||||
|
@ -3341,6 +3309,7 @@ class SuborganizationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
"portfolio",
|
||||
]
|
||||
search_fields = ["name"]
|
||||
search_help_text = "Search by suborganization."
|
||||
|
||||
change_form_template = "django/admin/suborg_change_form.html"
|
||||
|
||||
|
|
|
@ -504,167 +504,111 @@ function initializeWidgetOnList(list, parentId) {
|
|||
/** An IIFE that hooks to the show/hide button underneath action needed reason.
|
||||
* This shows the auto generated email on action needed reason.
|
||||
*/
|
||||
(function () {
|
||||
// Since this is an iife, these vars will be removed from memory afterwards
|
||||
var actionNeededReasonDropdown = document.querySelector("#id_action_needed_reason");
|
||||
|
||||
// Placeholder text (for certain "action needed" reasons that do not involve e=mails)
|
||||
var placeholderText = document.querySelector("#action-needed-reason-email-placeholder-text")
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const dropdown = document.getElementById("id_action_needed_reason");
|
||||
const textarea = document.getElementById("id_action_needed_reason_email")
|
||||
const domainRequestId = dropdown ? document.getElementById("domain_request_id").value : null
|
||||
const textareaPlaceholder = document.querySelector(".field-action_needed_reason_email__placeholder");
|
||||
const directEditButton = document.querySelector('.field-action_needed_reason_email__edit');
|
||||
const modalTrigger = document.querySelector('.field-action_needed_reason_email__modal-trigger');
|
||||
const modalConfirm = document.getElementById('confirm-edit-email');
|
||||
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;
|
||||
|
||||
// E-mail divs and textarea components
|
||||
var actionNeededEmail = document.querySelector("#id_action_needed_reason_email")
|
||||
var actionNeededEmailReadonly = document.querySelector("#action-needed-reason-email-readonly")
|
||||
var actionNeededEmailReadonlyTextarea = document.querySelector("#action-needed-reason-email-readonly-textarea")
|
||||
|
||||
// Edit e-mail modal (and its confirmation button)
|
||||
var confirmEditEmailButton = document.querySelector("#email-already-sent-modal_continue-editing-button")
|
||||
|
||||
// Headers and footers (which change depending on if the e-mail was sent or not)
|
||||
var actionNeededEmailHeader = document.querySelector("#action-needed-email-header")
|
||||
var actionNeededEmailHeaderOnSave = document.querySelector("#action-needed-email-header-email-sent")
|
||||
var actionNeededEmailFooter = document.querySelector("#action-needed-email-footer")
|
||||
|
||||
let emailWasSent = document.getElementById("action-needed-email-sent");
|
||||
let lastSentEmailText = document.getElementById("action-needed-email-last-sent-text");
|
||||
|
||||
// Get the list of e-mails associated with each action-needed dropdown value
|
||||
let emailData = document.getElementById('action-needed-emails-data');
|
||||
if (!emailData) {
|
||||
return;
|
||||
}
|
||||
let actionNeededEmailData = emailData.textContent;
|
||||
if(!actionNeededEmailData) {
|
||||
return;
|
||||
}
|
||||
let actionNeededEmailsJson = JSON.parse(actionNeededEmailData);
|
||||
|
||||
const domainRequestId = actionNeededReasonDropdown ? document.querySelector("#domain_request_id").value : null
|
||||
const emailSentSessionVariableName = `actionNeededEmailSent-${domainRequestId}`;
|
||||
const oldDropdownValue = actionNeededReasonDropdown ? actionNeededReasonDropdown.value : null;
|
||||
const oldEmailValue = actionNeededEmailData ? actionNeededEmailData.value : null;
|
||||
|
||||
if(actionNeededReasonDropdown && actionNeededEmail && domainRequestId) {
|
||||
// Add a change listener to dom load
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let reason = actionNeededReasonDropdown.value;
|
||||
|
||||
// Handle the session boolean (to enable/disable editing)
|
||||
if (emailWasSent && emailWasSent.value === "True") {
|
||||
// An email was sent out - store that information in a session variable
|
||||
addOrRemoveSessionBoolean(emailSentSessionVariableName, add=true);
|
||||
}
|
||||
|
||||
// Show an editable email field or a readonly one
|
||||
updateActionNeededEmailDisplay(reason)
|
||||
});
|
||||
|
||||
// editEmailButton.addEventListener("click", function() {
|
||||
// if (!checkEmailAlreadySent()) {
|
||||
// showEmail(canEdit=true)
|
||||
// }
|
||||
// });
|
||||
|
||||
confirmEditEmailButton.addEventListener("click", function() {
|
||||
// Show editable view
|
||||
showEmail(canEdit=true)
|
||||
});
|
||||
|
||||
|
||||
// Add a change listener to the action needed reason dropdown
|
||||
actionNeededReasonDropdown.addEventListener("change", function() {
|
||||
let reason = actionNeededReasonDropdown.value;
|
||||
let emailBody = reason in actionNeededEmailsJson ? actionNeededEmailsJson[reason] : null;
|
||||
|
||||
if (reason && emailBody) {
|
||||
// Reset the session object on change since change refreshes the email content.
|
||||
if (oldDropdownValue !== actionNeededReasonDropdown.value || oldEmailValue !== actionNeededEmail.value) {
|
||||
// Replace the email content
|
||||
actionNeededEmail.value = emailBody;
|
||||
actionNeededEmailReadonlyTextarea.value = emailBody;
|
||||
hideEmailAlreadySentView();
|
||||
}
|
||||
}
|
||||
|
||||
// Show either a preview of the email or some text describing no email will be sent
|
||||
updateActionNeededEmailDisplay(reason)
|
||||
});
|
||||
// We will use the const to control the modal
|
||||
let 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, '');
|
||||
}
|
||||
|
||||
function checkEmailAlreadySent()
|
||||
{
|
||||
lastEmailSent = lastSentEmailText.value.replace(/\s+/g, '')
|
||||
currentEmailInTextArea = actionNeededEmail.value.replace(/\s+/g, '')
|
||||
return lastEmailSent === currentEmailInTextArea
|
||||
}
|
||||
if (!dropdown || !textarea || !domainRequestId || !formLabel || !modalConfirm) return;
|
||||
const apiUrl = document.getElementById("get-action-needed-email-for-user-json").value;
|
||||
|
||||
// Shows a readonly preview of the email with updated messaging to indicate this email was sent
|
||||
function showEmailAlreadySentView()
|
||||
{
|
||||
hideElement(actionNeededEmailHeader)
|
||||
showElement(actionNeededEmailHeaderOnSave)
|
||||
actionNeededEmailFooter.innerHTML = "This email has been sent to the creator of this request";
|
||||
}
|
||||
|
||||
// Shows a readonly preview of the email with updated messaging to indicate this email was sent
|
||||
function hideEmailAlreadySentView()
|
||||
{
|
||||
showElement(actionNeededEmailHeader)
|
||||
hideElement(actionNeededEmailHeaderOnSave)
|
||||
actionNeededEmailFooter.innerHTML = "This email will be sent to the creator of this request after saving";
|
||||
}
|
||||
|
||||
// Shows either a preview of the email or some text describing no email will be sent.
|
||||
// If the email doesn't exist or if we're of reason "other", display that no email was sent.
|
||||
function updateActionNeededEmailDisplay(reason) {
|
||||
hideElement(actionNeededEmail.parentElement)
|
||||
|
||||
if (reason) {
|
||||
if (reason === "other") {
|
||||
// Hide email preview and show this text instead
|
||||
showPlaceholderText("No email will be sent");
|
||||
}
|
||||
else {
|
||||
// Always show readonly view of email to start
|
||||
showEmail(canEdit=false)
|
||||
if(checkEmailAlreadySent())
|
||||
{
|
||||
showEmailAlreadySentView();
|
||||
}
|
||||
}
|
||||
function updateUserInterface(reason) {
|
||||
if (!reason) {
|
||||
// No reason selected, we will set the label to "Email", show the "Make a selection" placeholder, hide the trigger, textarea, hide the help text
|
||||
formLabel.innerHTML = "Email:";
|
||||
textareaPlaceholder.innerHTML = "Select an action needed reason to see email";
|
||||
showElement(textareaPlaceholder);
|
||||
hideElement(directEditButton);
|
||||
hideElement(modalTrigger);
|
||||
hideElement(textarea);
|
||||
} else if (reason === 'other') {
|
||||
// 'Other' selected, we will set the label to "Email", show the "No email will be sent" placeholder, hide the trigger, textarea, hide the help text
|
||||
formLabel.innerHTML = "Email:";
|
||||
textareaPlaceholder.innerHTML = "No email will be sent";
|
||||
showElement(textareaPlaceholder);
|
||||
hideElement(directEditButton);
|
||||
hideElement(modalTrigger);
|
||||
hideElement(textarea);
|
||||
} else {
|
||||
// Hide email preview and show this text instead
|
||||
showPlaceholderText("Select an action needed reason to see email");
|
||||
// A triggering selection is selected, all hands on board:
|
||||
textarea.setAttribute('readonly', true);
|
||||
showElement(textarea);
|
||||
hideElement(textareaPlaceholder);
|
||||
|
||||
if (isEmailAlreadySentConst) {
|
||||
hideElement(directEditButton);
|
||||
showElement(modalTrigger);
|
||||
} else {
|
||||
showElement(directEditButton);
|
||||
hideElement(modalTrigger);
|
||||
}
|
||||
if (isEmailAlreadySent()) {
|
||||
formLabel.innerHTML = "Email sent to creator:";
|
||||
} else {
|
||||
formLabel.innerHTML = "Email:";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shows either a readonly view (canEdit=false) or editable view (canEdit=true) of the action needed email
|
||||
function showEmail(canEdit)
|
||||
{
|
||||
if(!canEdit)
|
||||
{
|
||||
showElement(actionNeededEmailReadonly)
|
||||
hideElement(actionNeededEmail.parentElement)
|
||||
}
|
||||
else
|
||||
{
|
||||
hideElement(actionNeededEmailReadonly)
|
||||
showElement(actionNeededEmail.parentElement)
|
||||
}
|
||||
showElement(actionNeededEmailFooter) // this is the same for both views, so it was separated out
|
||||
hideElement(placeholderText)
|
||||
}
|
||||
// Initialize UI
|
||||
updateUserInterface(dropdown.value);
|
||||
|
||||
// Hides preview of action needed email and instead displays the given text (innerHTML)
|
||||
function showPlaceholderText(innerHTML)
|
||||
{
|
||||
hideElement(actionNeededEmail.parentElement)
|
||||
hideElement(actionNeededEmailReadonly)
|
||||
hideElement(actionNeededEmailFooter)
|
||||
dropdown.addEventListener("change", function() {
|
||||
const reason = dropdown.value;
|
||||
// Update the UI
|
||||
updateUserInterface(reason);
|
||||
if (reason && reason !== "other") {
|
||||
// If it's not the initial value
|
||||
if (initialDropdownValue !== dropdown.value || initialEmailValue !== textarea.value) {
|
||||
// Replace the email content
|
||||
fetch(`${apiUrl}?reason=${reason}&domain_request_id=${domainRequestId}`)
|
||||
.then(response => {
|
||||
return response.json().then(data => data);
|
||||
})
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
}else {
|
||||
textarea.value = data.action_needed_email;
|
||||
}
|
||||
updateUserInterface(reason);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Error action needed email: ", error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
placeholderText.innerHTML = innerHTML;
|
||||
showElement(placeholderText)
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
modalConfirm.addEventListener("click", () => {
|
||||
textarea.removeAttribute('readonly');
|
||||
textarea.focus();
|
||||
hideElement(directEditButton);
|
||||
hideElement(modalTrigger);
|
||||
});
|
||||
directEditButton.addEventListener("click", () => {
|
||||
textarea.removeAttribute('readonly');
|
||||
textarea.focus();
|
||||
hideElement(directEditButton);
|
||||
hideElement(modalTrigger);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
/** An IIFE for copy summary button (appears in DomainRegistry models)
|
||||
|
@ -858,10 +802,15 @@ function initializeWidgetOnList(list, parentId) {
|
|||
// $ symbolically denotes that this is using jQuery
|
||||
let $federalAgency = django.jQuery("#id_federal_agency");
|
||||
let organizationType = document.getElementById("id_organization_type");
|
||||
if ($federalAgency && organizationType) {
|
||||
let readonlyOrganizationType = document.querySelector(".field-organization_type .readonly");
|
||||
|
||||
let organizationNameContainer = document.querySelector(".field-organization_name");
|
||||
let federalType = document.querySelector(".field-federal_type");
|
||||
|
||||
if ($federalAgency && (organizationType || readonlyOrganizationType)) {
|
||||
// Attach the change event listener
|
||||
$federalAgency.on("change", function() {
|
||||
handleFederalAgencyChange($federalAgency, organizationType);
|
||||
handleFederalAgencyChange($federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -877,9 +826,33 @@ function initializeWidgetOnList(list, parentId) {
|
|||
handleStateTerritoryChange(stateTerritory, urbanizationField);
|
||||
});
|
||||
}
|
||||
|
||||
// Handle hiding the organization name field when the organization_type is federal.
|
||||
// Run this first one page load, then secondly on a change event.
|
||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
||||
organizationType.addEventListener("change", function() {
|
||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
||||
});
|
||||
});
|
||||
|
||||
function handleFederalAgencyChange(federalAgency, organizationType) {
|
||||
function handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType) {
|
||||
if (organizationType && organizationNameContainer) {
|
||||
let selectedValue = organizationType.value;
|
||||
if (selectedValue === "federal") {
|
||||
hideElement(organizationNameContainer);
|
||||
if (federalType) {
|
||||
showElement(federalType);
|
||||
}
|
||||
} else {
|
||||
showElement(organizationNameContainer);
|
||||
if (federalType) {
|
||||
hideElement(federalType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleFederalAgencyChange(federalAgency, organizationType, readonlyOrganizationType, organizationNameContainer, federalType) {
|
||||
// Don't do anything on page load
|
||||
if (isInitialPageLoad) {
|
||||
isInitialPageLoad = false;
|
||||
|
@ -894,27 +867,31 @@ function initializeWidgetOnList(list, parentId) {
|
|||
return;
|
||||
}
|
||||
|
||||
let organizationTypeValue = organizationType ? organizationType.value : readonlyOrganizationType.innerText.toLowerCase();
|
||||
if (selectedText !== "Non-Federal Agency") {
|
||||
if (organizationType.value !== "federal") {
|
||||
organizationType.value = "federal";
|
||||
if (organizationTypeValue !== "federal") {
|
||||
if (organizationType){
|
||||
organizationType.value = "federal";
|
||||
}else {
|
||||
readonlyOrganizationType.innerText = "Federal"
|
||||
}
|
||||
}
|
||||
}else {
|
||||
if (organizationType.value === "federal") {
|
||||
organizationType.value = "";
|
||||
if (organizationTypeValue === "federal") {
|
||||
if (organizationType){
|
||||
organizationType.value = "";
|
||||
}else {
|
||||
readonlyOrganizationType.innerText = "-"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the associated senior official with this federal agency
|
||||
let $seniorOfficial = django.jQuery("#id_senior_official");
|
||||
if (!$seniorOfficial) {
|
||||
console.log("Could not find the senior official field");
|
||||
return;
|
||||
}
|
||||
handleOrganizationTypeChange(organizationType, organizationNameContainer, federalType);
|
||||
|
||||
// Determine if any changes are necessary to the display of portfolio type or federal type
|
||||
// based on changes to the Federal Agency
|
||||
let federalPortfolioApi = document.getElementById("federal_and_portfolio_types_from_agency_json_url").value;
|
||||
fetch(`${federalPortfolioApi}?organization_type=${organizationType.value}&agency_name=${selectedText}`)
|
||||
fetch(`${federalPortfolioApi}?&agency_name=${selectedText}`)
|
||||
.then(response => {
|
||||
const statusCode = response.status;
|
||||
return response.json().then(data => ({ statusCode, data }));
|
||||
|
@ -925,7 +902,6 @@ function initializeWidgetOnList(list, parentId) {
|
|||
return;
|
||||
}
|
||||
updateReadOnly(data.federal_type, '.field-federal_type');
|
||||
updateReadOnly(data.portfolio_type, '.field-portfolio_type');
|
||||
})
|
||||
.catch(error => console.error("Error fetching federal and portfolio types: ", error));
|
||||
|
||||
|
@ -933,6 +909,9 @@ function initializeWidgetOnList(list, parentId) {
|
|||
// If we can update the contact information, it'll be shown again.
|
||||
hideElement(contactList.parentElement);
|
||||
|
||||
let seniorOfficialAddUrl = document.getElementById("senior-official-add-url").value;
|
||||
let $seniorOfficial = django.jQuery("#id_senior_official");
|
||||
let readonlySeniorOfficial = document.querySelector(".field-senior_official .readonly");
|
||||
let seniorOfficialApi = document.getElementById("senior_official_from_agency_json_url").value;
|
||||
fetch(`${seniorOfficialApi}?agency_name=${selectedText}`)
|
||||
.then(response => {
|
||||
|
@ -943,7 +922,12 @@ function initializeWidgetOnList(list, parentId) {
|
|||
if (data.error) {
|
||||
// Clear the field if the SO doesn't exist.
|
||||
if (statusCode === 404) {
|
||||
$seniorOfficial.val("").trigger("change");
|
||||
if ($seniorOfficial && $seniorOfficial.length > 0) {
|
||||
$seniorOfficial.val("").trigger("change");
|
||||
}else {
|
||||
// Show the "create one now" text if this field is none in readonly mode.
|
||||
readonlySeniorOfficial.innerHTML = `<a href="${seniorOfficialAddUrl}">No senior official found. Create one now.</a>`;
|
||||
}
|
||||
console.warn("Record not found: " + data.error);
|
||||
}else {
|
||||
console.error("Error in AJAX call: " + data.error);
|
||||
|
@ -954,30 +938,43 @@ function initializeWidgetOnList(list, parentId) {
|
|||
// Update the "contact details" blurb beneath senior official
|
||||
updateContactInfo(data);
|
||||
showElement(contactList.parentElement);
|
||||
|
||||
|
||||
// Get the associated senior official with this federal agency
|
||||
let seniorOfficialId = data.id;
|
||||
let seniorOfficialName = [data.first_name, data.last_name].join(" ");
|
||||
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
|
||||
// Clear the field if the SO doesn't exist
|
||||
$seniorOfficial.val("").trigger("change");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the senior official to the dropdown.
|
||||
// This format supports select2 - if we decide to convert this field in the future.
|
||||
if ($seniorOfficial.find(`option[value='${seniorOfficialId}']`).length) {
|
||||
// Select the value that is associated with the current Senior Official.
|
||||
$seniorOfficial.val(seniorOfficialId).trigger("change");
|
||||
} else {
|
||||
// Create a DOM Option that matches the desired Senior Official. Then append it and select it.
|
||||
let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true);
|
||||
$seniorOfficial.append(userOption).trigger("change");
|
||||
if ($seniorOfficial && $seniorOfficial.length > 0) {
|
||||
// If the senior official is a dropdown field, edit that
|
||||
updateSeniorOfficialDropdown($seniorOfficial, seniorOfficialId, seniorOfficialName);
|
||||
}else {
|
||||
if (readonlySeniorOfficial) {
|
||||
let seniorOfficialLink = `<a href=/admin/registrar/seniorofficial/${seniorOfficialId}/change/>${seniorOfficialName}</a>`
|
||||
readonlySeniorOfficial.innerHTML = seniorOfficialName ? seniorOfficialLink : "-";
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => console.error("Error fetching senior official: ", error));
|
||||
|
||||
}
|
||||
|
||||
function updateSeniorOfficialDropdown(dropdown, seniorOfficialId, seniorOfficialName) {
|
||||
if (!seniorOfficialId || !seniorOfficialName || !seniorOfficialName.trim()){
|
||||
// Clear the field if the SO doesn't exist
|
||||
dropdown.val("").trigger("change");
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the senior official to the dropdown.
|
||||
// This format supports select2 - if we decide to convert this field in the future.
|
||||
if (dropdown.find(`option[value='${seniorOfficialId}']`).length) {
|
||||
// Select the value that is associated with the current Senior Official.
|
||||
dropdown.val(seniorOfficialId).trigger("change");
|
||||
} else {
|
||||
// Create a DOM Option that matches the desired Senior Official. Then append it and select it.
|
||||
let userOption = new Option(seniorOfficialName, seniorOfficialId, true, true);
|
||||
dropdown.append(userOption).trigger("change");
|
||||
}
|
||||
}
|
||||
|
||||
function handleStateTerritoryChange(stateTerritory, urbanizationField) {
|
||||
let selectedValue = stateTerritory.value;
|
||||
if (selectedValue === "PR") {
|
||||
|
|
|
@ -1853,6 +1853,125 @@ class DomainRequestsTable extends LoadTableBase {
|
|||
}
|
||||
}
|
||||
|
||||
class MembersTable extends LoadTableBase {
|
||||
|
||||
constructor() {
|
||||
super('.members__table', '.members__table-wrapper', '#members__search-field', '#members__search-field-submit', '.members__reset-search', '.members__reset-filters', '.members__no-data', '.members__no-search-results');
|
||||
}
|
||||
/**
|
||||
* Loads rows in the members list, as well as updates pagination around the members list
|
||||
* based on the supplied attributes.
|
||||
* @param {*} page - the page number of the results (starts with 1)
|
||||
* @param {*} sortBy - the sort column option
|
||||
* @param {*} order - the sort order {asc, desc}
|
||||
* @param {*} scroll - control for the scrollToElement functionality
|
||||
* @param {*} status - control for the status filter
|
||||
* @param {*} searchTerm - the search term
|
||||
* @param {*} portfolio - the portfolio id
|
||||
*/
|
||||
loadTable(page, sortBy = this.currentSortBy, order = this.currentOrder, scroll = this.scrollToTable, status = this.currentStatus, searchTerm =this.currentSearchTerm, portfolio = this.portfolioValue) {
|
||||
|
||||
// --------- SEARCH
|
||||
let searchParams = new URLSearchParams(
|
||||
{
|
||||
"page": page,
|
||||
"sort_by": sortBy,
|
||||
"order": order,
|
||||
"status": status,
|
||||
"search_term": searchTerm
|
||||
}
|
||||
);
|
||||
if (portfolio)
|
||||
searchParams.append("portfolio", portfolio)
|
||||
|
||||
|
||||
// --------- FETCH DATA
|
||||
// fetch json of page of domais, given params
|
||||
let baseUrl = document.getElementById("get_members_json_url");
|
||||
if (!baseUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
let baseUrlValue = baseUrl.innerHTML;
|
||||
if (!baseUrlValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
let url = `${baseUrlValue}?${searchParams.toString()}` //TODO: uncomment for search function
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('Error in AJAX call: ' + data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// handle the display of proper messaging in the event that no members exist in the list or search returns no results
|
||||
this.updateDisplay(data, this.tableWrapper, this.noTableWrapper, this.noSearchResultsWrapper, this.currentSearchTerm);
|
||||
|
||||
// identify the DOM element where the domain list will be inserted into the DOM
|
||||
const memberList = document.querySelector('.members__table tbody');
|
||||
memberList.innerHTML = '';
|
||||
|
||||
data.members.forEach(member => {
|
||||
// const actionUrl = domain.action_url;
|
||||
const member_name = member.name;
|
||||
const member_email = member.email;
|
||||
const last_active = member.last_active;
|
||||
const action_url = member.action_url;
|
||||
const action_label = member.action_label;
|
||||
const svg_icon = member.svg_icon;
|
||||
|
||||
const row = document.createElement('tr');
|
||||
|
||||
let admin_tagHTML = ``;
|
||||
if (member.is_admin)
|
||||
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`
|
||||
|
||||
row.innerHTML = `
|
||||
<th scope="row" role="rowheader" data-label="member email">
|
||||
${member_email ? member_email : member_name} ${admin_tagHTML}
|
||||
</th>
|
||||
<td data-sort-value="${last_active}" data-label="last_active">
|
||||
${last_active}
|
||||
</td>
|
||||
<td>
|
||||
<a href="${action_url}">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#${svg_icon}"></use>
|
||||
</svg>
|
||||
${action_label} <span class="usa-sr-only">${member_name}</span>
|
||||
</a>
|
||||
</td>
|
||||
`;
|
||||
memberList.appendChild(row);
|
||||
});
|
||||
|
||||
// Do not scroll on first page load
|
||||
if (scroll)
|
||||
ScrollToElement('class', 'members');
|
||||
this.scrollToTable = true;
|
||||
|
||||
// update pagination
|
||||
this.updatePagination(
|
||||
'member',
|
||||
'#members-pagination',
|
||||
'#members-pagination .usa-pagination__counter',
|
||||
'#members',
|
||||
data.page,
|
||||
data.num_pages,
|
||||
data.has_previous,
|
||||
data.has_next,
|
||||
data.total,
|
||||
);
|
||||
this.currentSortBy = sortBy;
|
||||
this.currentOrder = order;
|
||||
this.currentSearchTerm = searchTerm;
|
||||
})
|
||||
.catch(error => console.error('Error fetching members:', error));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||
|
@ -1926,6 +2045,23 @@ const utcDateString = (dateString) => {
|
|||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An IIFE that listens for DOM Content to be loaded, then executes. This function
|
||||
* initializes the domains list and associated functionality on the home page of the app.
|
||||
*
|
||||
*/
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isMembersPage = document.querySelector("#members")
|
||||
if (isMembersPage){
|
||||
const membersTable = new MembersTable();
|
||||
if (membersTable.tableWrapper) {
|
||||
// Initial load
|
||||
membersTable.loadTable(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* An IIFE that displays confirmation modal on the user profile page
|
||||
*/
|
||||
|
|
|
@ -126,7 +126,8 @@ html[data-theme="light"] {
|
|||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form,
|
||||
.custom-admin-template, dt {
|
||||
.custom-admin-template,
|
||||
.dl-dja dt {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
.usa-table td {
|
||||
|
@ -155,7 +156,8 @@ html[data-theme="dark"] {
|
|||
body.dashboard,
|
||||
body.change-list,
|
||||
body.change-form,
|
||||
.custom-admin-template, dt {
|
||||
.custom-admin-template,
|
||||
.dl-dja dt {
|
||||
color: var(--body-fg);
|
||||
}
|
||||
.usa-table td {
|
||||
|
@ -453,7 +455,8 @@ details.dja-detail-table {
|
|||
background-color: var(--body-bg);
|
||||
.dja-details-summary {
|
||||
cursor: pointer;
|
||||
color: var(--body-quiet-color);
|
||||
color: var(--link-fg);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px){
|
||||
|
@ -893,23 +896,6 @@ div.dja__model-description{
|
|||
}
|
||||
}
|
||||
|
||||
.vertical-separator {
|
||||
min-height: 20px;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: #d1d2d2;
|
||||
vertical-align: middle
|
||||
}
|
||||
|
||||
.usa-summary-box_admin {
|
||||
color: var(--body-fg);
|
||||
border-color: var(--summary-box-border);
|
||||
background-color: var(--summary-box-bg);
|
||||
min-width: fit-content;
|
||||
padding: .5rem;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.text-faded {
|
||||
color: #{$dhs-gray-60};
|
||||
}
|
||||
|
@ -930,3 +916,16 @@ ul.add-list-reset {
|
|||
.dl-dja dt {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.domain-name-wrap {
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
overflow: visible;
|
||||
word-break: break-all;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.organization-admin-label {
|
||||
font-weight: 600;
|
||||
font-size: .8125rem;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@use "base" as *;
|
||||
|
||||
// Fixes some font size disparities with the Figma
|
||||
// for usa-alert alert elements
|
||||
.usa-alert {
|
||||
|
@ -22,3 +24,8 @@
|
|||
margin-left: 0.5rem!important;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: !important is used because _font.scss overrides this
|
||||
.usa-alert__body--widescreen {
|
||||
max-width: $widescreen-max-width !important;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
|
||||
$widescreen-max-width: 1920px;
|
||||
|
||||
/* Styles for making visible to screen reader / AT users only. */
|
||||
.sr-only {
|
||||
@include sr-only;
|
||||
|
@ -102,6 +104,24 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
This is a hack to keep the "Export" button on Domain Requests page inline
|
||||
with the searchbar in widescreen mode.
|
||||
|
||||
EXPLANATION: The existing frontend implementation puts the searchbar and export
|
||||
button in two separate columns in a grid, which creates a solid wrap-around effect
|
||||
for mobile devices. The searchbar had a max-width that exactly equaled the max width
|
||||
of its parent column (for non-widescreens), so there wasn't any issue at this time of
|
||||
implementation.
|
||||
However, during immplementation of widescreen mode this small max-width caused the searchbar to
|
||||
no longer fill its parent grid column for larger screen sizes, creating a visual gap between
|
||||
it and the adjacent export button. To fix this, we will limit the width of the first
|
||||
grid column to the max-width of the searchbar, which was calculated to be 33rem.
|
||||
*/
|
||||
.section-outlined__search--widescreen {
|
||||
max-width: 33rem;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
@ -222,6 +242,14 @@ abbr[title] {
|
|||
left: auto!important;
|
||||
}
|
||||
|
||||
.usa-banner__inner--widescreen {
|
||||
max-width: $widescreen-max-width;
|
||||
}
|
||||
|
||||
.margin-right-neg-4px {
|
||||
margin-right: -4px;
|
||||
}
|
||||
|
||||
.break-word {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
8
src/registrar/assets/sass/_theme/_containers.scss
Normal file
8
src/registrar/assets/sass/_theme/_containers.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "base" as *;
|
||||
|
||||
//NOTE: !important is needed because it gets overriden by other .scss for footer nav
|
||||
.grid-container--widescreen,
|
||||
.usa-identifier__container--widescreen {
|
||||
max-width: $widescreen-max-width !important;
|
||||
}
|
|
@ -68,21 +68,6 @@ legend.float-left-tablet + button.float-right-tablet {
|
|||
}
|
||||
}
|
||||
|
||||
// Custom style for disabled inputs
|
||||
@media (prefers-color-scheme: light) {
|
||||
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||
background-color: #eeeeee;
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.usa-input:disabled, .usa-select:disabled, .usa-textarea:disabled {
|
||||
background-color: var(--body-fg);
|
||||
color: var(--close-button-hover-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.read-only-label {
|
||||
font-size: size('body', 'sm');
|
||||
color: color('primary-dark');
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
@use "uswds-core" as *;
|
||||
@use "cisa_colors" as *;
|
||||
@use "base" as *;
|
||||
|
||||
// Define some styles for the .gov header/logo
|
||||
.usa-logo button {
|
||||
|
@ -127,3 +128,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.usa-nav__inner--widescreen,
|
||||
.usa-navbar--widescreen,
|
||||
.usa-nav-container--widescreen {
|
||||
max-width: $widescreen-max-width !important;
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
@forward "identifier";
|
||||
@forward "header";
|
||||
@forward "register-form";
|
||||
@forward "containers";
|
||||
|
||||
/*--------------------------------------------------
|
||||
--- Admin ---------------------------------*/
|
||||
|
|
|
@ -246,6 +246,7 @@ TEMPLATES = [
|
|||
"registrar.context_processors.org_user_status",
|
||||
"registrar.context_processors.add_path_to_context",
|
||||
"registrar.context_processors.portfolio_permissions",
|
||||
"registrar.context_processors.is_widescreen_mode",
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -478,7 +479,7 @@ class JsonServerFormatter(ServerFormatter):
|
|||
|
||||
if not hasattr(record, "server_time"):
|
||||
record.server_time = self.formatTime(record, self.datefmt)
|
||||
|
||||
|
||||
log_entry = {"server_time": record.server_time, "level": record.levelname, "message": formatted_record}
|
||||
return json.dumps(log_entry)
|
||||
|
||||
|
@ -724,6 +725,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",
|
||||
|
|
|
@ -22,17 +22,20 @@ from registrar.views.report_views import (
|
|||
ExportDataTypeUser,
|
||||
)
|
||||
|
||||
from registrar.views.domain_request import Step
|
||||
# --jsons
|
||||
from registrar.views.domain_requests_json import get_domain_requests_json
|
||||
from registrar.views.transfer_user import TransferUserView
|
||||
from registrar.views.domains_json import get_domains_json
|
||||
from registrar.views.portfolio_members_json import get_portfolio_members_json
|
||||
from registrar.views.utility.api_views import (
|
||||
get_senior_official_from_federal_agency_json,
|
||||
get_federal_and_portfolio_types_from_federal_agency_json,
|
||||
get_action_needed_email_for_user_json,
|
||||
)
|
||||
from registrar.views.domains_json import get_domains_json
|
||||
from registrar.views.utility import always_404
|
||||
from api.views import available, get_current_federal, get_current_full
|
||||
|
||||
from registrar.views.domain_request import Step
|
||||
from registrar.views.transfer_user import TransferUserView
|
||||
from registrar.views.utility import always_404
|
||||
from api.views import available, rdap, get_current_federal, get_current_full
|
||||
|
||||
DOMAIN_REQUEST_NAMESPACE = views.DomainRequestWizard.URL_NAMESPACE
|
||||
domain_request_urls = [
|
||||
|
@ -73,6 +76,16 @@ urlpatterns = [
|
|||
views.PortfolioNoDomainsView.as_view(),
|
||||
name="no-portfolio-domains",
|
||||
),
|
||||
path(
|
||||
"members/",
|
||||
views.PortfolioMembersView.as_view(),
|
||||
name="members",
|
||||
),
|
||||
# path(
|
||||
# "no-organization-members/",
|
||||
# views.PortfolioNoMembersView.as_view(),
|
||||
# name="no-portfolio-members",
|
||||
# ),
|
||||
path(
|
||||
"requests/",
|
||||
views.PortfolioDomainRequestsView.as_view(),
|
||||
|
@ -153,6 +166,11 @@ urlpatterns = [
|
|||
get_federal_and_portfolio_types_from_federal_agency_json,
|
||||
name="get-federal-and-portfolio-types-from-federal-agency-json",
|
||||
),
|
||||
path(
|
||||
"admin/api/get-action-needed-email-for-user-json/",
|
||||
get_action_needed_email_for_user_json,
|
||||
name="get-action-needed-email-for-user-json",
|
||||
),
|
||||
path("admin/", admin.site.urls),
|
||||
path(
|
||||
"reports/export_data_type_user/",
|
||||
|
@ -169,6 +187,11 @@ urlpatterns = [
|
|||
views.DomainRequestStatus.as_view(),
|
||||
name="domain-request-status",
|
||||
),
|
||||
path(
|
||||
"domain-request/viewonly/<int:pk>",
|
||||
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
||||
name="domain-request-status-viewonly",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/withdraw",
|
||||
views.DomainRequestWithdrawConfirmation.as_view(),
|
||||
|
@ -183,6 +206,7 @@ urlpatterns = [
|
|||
path("openid/", include("djangooidc.urls")),
|
||||
path("request/", include((domain_request_urls, DOMAIN_REQUEST_NAMESPACE))),
|
||||
path("api/v1/available/", available, name="available"),
|
||||
path("api/v1/rdap/", rdap, name="rdap"),
|
||||
path("api/v1/get-report/current-federal", get_current_federal, name="get-current-federal"),
|
||||
path("api/v1/get-report/current-full", get_current_full, name="get-current-full"),
|
||||
path(
|
||||
|
@ -264,6 +288,7 @@ urlpatterns = [
|
|||
),
|
||||
path("get-domains-json/", get_domains_json, name="get_domains_json"),
|
||||
path("get-domain-requests-json/", get_domain_requests_json, name="get_domain_requests_json"),
|
||||
path("get-portfolio-members-json/", get_portfolio_members_json, name="get_portfolio_members_json"),
|
||||
]
|
||||
|
||||
# Djangooidc strips out context data from that context, so we define a custom error
|
||||
|
|
|
@ -94,3 +94,8 @@ def portfolio_permissions(request):
|
|||
except AttributeError:
|
||||
# Handles cases where request.user might not exist
|
||||
return portfolio_context
|
||||
|
||||
|
||||
def is_widescreen_mode(request):
|
||||
widescreen_paths = ["/domains/", "/requests/", "/members/"]
|
||||
return {"is_widescreen_mode": any(path in request.path for path in widescreen_paths) or request.path == "/"}
|
||||
|
|
138
src/registrar/fixtures/fixtures_domains.py
Normal file
138
src/registrar/fixtures/fixtures_domains.py
Normal file
|
@ -0,0 +1,138 @@
|
|||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.fixtures.fixtures_requests import DomainRequestFixture
|
||||
from registrar.fixtures.fixtures_users import UserFixture
|
||||
from registrar.models import User, DomainRequest
|
||||
from registrar.models.domain import Domain
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainFixture(DomainRequestFixture):
|
||||
"""Create two domains and permissions on them for each user.
|
||||
One domain will have a past expiration date.
|
||||
|
||||
Depends on fixtures_requests.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
to run this code.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Get the usernames of users created in the UserFixture
|
||||
created_usernames = [user_data["username"] for user_data in UserFixture.ADMINS + UserFixture.STAFF]
|
||||
|
||||
# Filter users to only include those created by the fixture
|
||||
users = list(User.objects.filter(username__in=created_usernames))
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Approve each user associated with `in review` status domains
|
||||
cls._approve_domain_requests(users)
|
||||
|
||||
@staticmethod
|
||||
def _generate_fake_expiration_date(days_in_future=365):
|
||||
"""Generates a fake expiration date between 1 and 365 days in the future."""
|
||||
current_date = timezone.now().date() # nosec
|
||||
return current_date + timedelta(days=random.randint(1, days_in_future)) # nosec
|
||||
|
||||
@staticmethod
|
||||
def _generate_fake_expiration_date_in_past():
|
||||
"""Generates a fake expiration date up to 365 days in the past."""
|
||||
current_date = timezone.now().date() # nosec
|
||||
return current_date + timedelta(days=random.randint(-365, -1)) # nosec
|
||||
|
||||
@classmethod
|
||||
def _approve_request(cls, domain_request, users):
|
||||
"""Helper function to approve a domain request."""
|
||||
if not domain_request:
|
||||
return None
|
||||
|
||||
if domain_request.investigator is None:
|
||||
# Assign random investigator if not already assigned
|
||||
domain_request.investigator = random.choice(users) # nosec
|
||||
|
||||
# Approve the domain request
|
||||
domain_request.approve(send_email=False)
|
||||
|
||||
return domain_request
|
||||
|
||||
@classmethod
|
||||
def _approve_domain_requests(cls, users):
|
||||
"""Approves one current and one expired request per user."""
|
||||
domain_requests_to_update = []
|
||||
expired_requests = []
|
||||
|
||||
for user in users:
|
||||
# Get the latest and second-to-last domain requests
|
||||
domain_requests = DomainRequest.objects.filter(
|
||||
creator=user, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
).order_by("-id")[:2]
|
||||
|
||||
# Latest domain request
|
||||
domain_request = domain_requests[0] if domain_requests else None
|
||||
# Second-to-last domain request (expired)
|
||||
domain_request_expired = domain_requests[1] if len(domain_requests) > 1 else None
|
||||
|
||||
# Approve the current domain request
|
||||
if domain_request:
|
||||
cls._approve_request(domain_request, users)
|
||||
domain_requests_to_update.append(domain_request)
|
||||
|
||||
# Approve the expired domain request
|
||||
if domain_request_expired:
|
||||
cls._approve_request(domain_request_expired, users)
|
||||
domain_requests_to_update.append(domain_request_expired)
|
||||
expired_requests.append(domain_request_expired)
|
||||
|
||||
# Perform bulk update for the domain requests
|
||||
cls._bulk_update_requests(domain_requests_to_update)
|
||||
|
||||
# Retrieve all domains associated with the domain requests
|
||||
domains_to_update = Domain.objects.filter(domain_info__domain_request__in=domain_requests_to_update)
|
||||
|
||||
# Loop through and update expiration dates for domains
|
||||
for domain in domains_to_update:
|
||||
domain_request = domain.domain_info.domain_request
|
||||
|
||||
# Set the expiration date based on whether the request is expired
|
||||
if domain_request in expired_requests:
|
||||
domain.expiration_date = cls._generate_fake_expiration_date_in_past()
|
||||
else:
|
||||
domain.expiration_date = cls._generate_fake_expiration_date()
|
||||
|
||||
# Perform bulk update for the domains
|
||||
cls._bulk_update_domains(domains_to_update)
|
||||
|
||||
@classmethod
|
||||
def _bulk_update_requests(cls, domain_requests_to_update):
|
||||
"""Bulk update domain requests."""
|
||||
if domain_requests_to_update:
|
||||
try:
|
||||
DomainRequest.objects.bulk_update(domain_requests_to_update, ["status", "investigator"])
|
||||
logger.info(f"Successfully updated {len(domain_requests_to_update)} requests.")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during requests bulk update: {e}")
|
||||
|
||||
@classmethod
|
||||
def _bulk_update_domains(cls, domains_to_update):
|
||||
"""Bulk update domains with expiration dates."""
|
||||
if domains_to_update:
|
||||
try:
|
||||
Domain.objects.bulk_update(domains_to_update, ["expiration_date"])
|
||||
logger.info(f"Successfully updated {len(domains_to_update)} domains.")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during domains bulk update: {e}")
|
125
src/registrar/fixtures/fixtures_portfolios.py
Normal file
125
src/registrar/fixtures/fixtures_portfolios.py
Normal file
|
@ -0,0 +1,125 @@
|
|||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import User, DomainRequest, FederalAgency
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.senior_official import SeniorOfficial
|
||||
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortfolioFixture:
|
||||
"""
|
||||
Creates 2 pre-defined portfolios with the infrastructure to add more.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
to run this code.
|
||||
"""
|
||||
|
||||
PORTFOLIOS = [
|
||||
{
|
||||
"organization_name": "Hotel California",
|
||||
},
|
||||
{
|
||||
"organization_name": "Wish You Were Here",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def fake_so(cls):
|
||||
return {
|
||||
"first_name": fake.first_name(),
|
||||
"last_name": fake.last_name(),
|
||||
"title": fake.job(),
|
||||
"email": fake.ascii_safe_email(),
|
||||
"phone": "201-555-5555",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _set_non_foreign_key_fields(cls, portfolio: Portfolio, portfolio_dict: dict):
|
||||
"""Helper method used by `load`."""
|
||||
portfolio.organization_type = (
|
||||
portfolio_dict["organization_type"]
|
||||
if "organization_type" in portfolio_dict
|
||||
else DomainRequest.OrganizationChoices.FEDERAL
|
||||
)
|
||||
portfolio.notes = portfolio_dict["notes"] if "notes" in portfolio_dict else None
|
||||
portfolio.address_line1 = (
|
||||
portfolio_dict["address_line1"] if "address_line1" in portfolio_dict else fake.street_address()
|
||||
)
|
||||
portfolio.address_line2 = portfolio_dict["address_line2"] if "address_line2" in portfolio_dict else None
|
||||
portfolio.city = portfolio_dict["city"] if "city" in portfolio_dict else fake.city()
|
||||
portfolio.state_territory = (
|
||||
portfolio_dict["state_territory"] if "state_territory" in portfolio_dict else fake.state_abbr()
|
||||
)
|
||||
portfolio.zipcode = portfolio_dict["zipcode"] if "zipcode" in portfolio_dict else fake.postalcode()
|
||||
portfolio.urbanization = portfolio_dict["urbanization"] if "urbanization" in portfolio_dict else None
|
||||
portfolio.security_contact_email = (
|
||||
portfolio_dict["security_contact_email"] if "security_contact_email" in portfolio_dict else fake.email()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _set_foreign_key_fields(cls, portfolio: Portfolio, portfolio_dict: dict, user: User):
|
||||
"""Helper method used by `load`."""
|
||||
if not portfolio.senior_official:
|
||||
if portfolio_dict.get("senior_official") is not None:
|
||||
portfolio.senior_official, _ = SeniorOfficial.objects.get_or_create(**portfolio_dict["senior_official"])
|
||||
else:
|
||||
portfolio.senior_official = SeniorOfficial.objects.create(**cls.fake_so())
|
||||
|
||||
if not portfolio.federal_agency:
|
||||
if portfolio_dict.get("federal_agency") is not None:
|
||||
portfolio.federal_agency, _ = FederalAgency.objects.get_or_create(name=portfolio_dict["federal_agency"])
|
||||
else:
|
||||
federal_agencies = FederalAgency.objects.all()
|
||||
# Random choice of agency for selects, used as placeholders for testing.
|
||||
portfolio.federal_agency = random.choice(federal_agencies) # nosec
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Creates portfolios."""
|
||||
logger.info("Going to load %s portfolios" % len(cls.PORTFOLIOS))
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
try:
|
||||
user = User.objects.all().last()
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
portfolios_to_create = []
|
||||
for portfolio_data in cls.PORTFOLIOS:
|
||||
organization_name = portfolio_data["organization_name"]
|
||||
|
||||
# Check if portfolio with the organization name already exists
|
||||
if Portfolio.objects.filter(organization_name=organization_name).exists():
|
||||
logger.info(
|
||||
f"Portfolio with organization name '{organization_name}' already exists, skipping creation."
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
portfolio = Portfolio(
|
||||
creator=user,
|
||||
organization_name=portfolio_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(portfolio, portfolio_data)
|
||||
cls._set_foreign_key_fields(portfolio, portfolio_data, user)
|
||||
portfolios_to_create.append(portfolio)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
# Bulk create domain requests
|
||||
if len(portfolios_to_create) > 0:
|
||||
try:
|
||||
Portfolio.objects.bulk_create(portfolios_to_create)
|
||||
logger.info(f"Successfully created {len(portfolios_to_create)} portfolios")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error bulk creating portfolios: {e}")
|
325
src/registrar/fixtures/fixtures_requests.py
Normal file
325
src/registrar/fixtures/fixtures_requests.py
Normal file
|
@ -0,0 +1,325 @@
|
|||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.fixtures.fixtures_portfolios import PortfolioFixture
|
||||
from registrar.fixtures.fixtures_users import UserFixture
|
||||
from registrar.models import User, DomainRequest, DraftDomain, Contact, Website, FederalAgency
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.suborganization import Suborganization
|
||||
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainRequestFixture:
|
||||
"""
|
||||
Creates domain requests for each user in the database,
|
||||
assign portfolios and suborgs.
|
||||
|
||||
Creates 3 in_review requests, one for approving with an expired domain,
|
||||
one for approving with a non-expired domain, and one for leaving in in_review.
|
||||
|
||||
Depends on fixtures_portfolios and fixtures_suborganizations.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
to run this code.
|
||||
"""
|
||||
|
||||
# any fields not specified here will be filled in with fake data or defaults
|
||||
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
|
||||
# Here is a more complete example as a template:
|
||||
# {
|
||||
# "status": "started",
|
||||
# "organization_name": "Example - Just started",
|
||||
# "generic_org_type": "federal",
|
||||
# "federal_agency": None,
|
||||
# "federal_type": None,
|
||||
# "address_line1": None,
|
||||
# "address_line2": None,
|
||||
# "city": None,
|
||||
# "state_territory": None,
|
||||
# "zipcode": None,
|
||||
# "urbanization": None,
|
||||
# "purpose": None,
|
||||
# "anything_else": None,
|
||||
# "is_policy_acknowledged": None,
|
||||
# "senior_official": None,
|
||||
# "other_contacts": [],
|
||||
# "current_websites": [],
|
||||
# "alternative_domains": [],
|
||||
# },
|
||||
DOMAINREQUESTS = [
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.STARTED,
|
||||
"organization_name": "Example - Finished but not submitted",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"organization_name": "Example - Submitted but pending investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - In investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - Approved",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - Approved, domain expired",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
"organization_name": "Example - Withdrawn",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
"organization_name": "Example - Action needed",
|
||||
},
|
||||
{
|
||||
"status": "rejected",
|
||||
"organization_name": "Example - Rejected",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def fake_contact(cls):
|
||||
return {
|
||||
"first_name": fake.first_name(),
|
||||
"middle_name": None,
|
||||
"last_name": fake.last_name(),
|
||||
"title": fake.job(),
|
||||
"email": fake.ascii_safe_email(),
|
||||
"phone": "201-555-5555",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def fake_dot_gov(cls):
|
||||
return f"{fake.slug()}.gov"
|
||||
|
||||
@classmethod
|
||||
def fake_expiration_date(cls):
|
||||
"""Generates a fake expiration date between 0 and 1 year in the future."""
|
||||
current_date = timezone.now().date()
|
||||
days_in_future = random.randint(0, 365) # nosec
|
||||
return current_date + timedelta(days=days_in_future)
|
||||
|
||||
@classmethod
|
||||
def _set_non_foreign_key_fields(cls, request: DomainRequest, request_dict: dict):
|
||||
"""Helper method used by `load`."""
|
||||
request.status = request_dict["status"] if "status" in request_dict else "started"
|
||||
|
||||
# TODO for a future ticket: Allow for more than just "federal" here
|
||||
request.generic_org_type = request_dict["generic_org_type"] if "generic_org_type" in request_dict else "federal"
|
||||
if request.status != "started":
|
||||
request.last_submitted_date = fake.date()
|
||||
request.federal_type = (
|
||||
request_dict["federal_type"]
|
||||
if "federal_type" in request_dict
|
||||
else random.choice(["executive", "judicial", "legislative"]) # nosec
|
||||
)
|
||||
request.address_line1 = (
|
||||
request_dict["address_line1"] if "address_line1" in request_dict else fake.street_address()
|
||||
)
|
||||
request.address_line2 = request_dict["address_line2"] if "address_line2" in request_dict else None
|
||||
request.city = request_dict["city"] if "city" in request_dict else fake.city()
|
||||
request.state_territory = (
|
||||
request_dict["state_territory"] if "state_territory" in request_dict else fake.state_abbr()
|
||||
)
|
||||
request.zipcode = request_dict["zipcode"] if "zipcode" in request_dict else fake.postalcode()
|
||||
request.urbanization = request_dict["urbanization"] if "urbanization" in request_dict else None
|
||||
request.purpose = request_dict["purpose"] if "purpose" in request_dict else fake.paragraph()
|
||||
request.has_cisa_representative = (
|
||||
request_dict["has_cisa_representative"] if "has_cisa_representative" in request_dict else True
|
||||
)
|
||||
request.cisa_representative_email = (
|
||||
request_dict["cisa_representative_email"] if "cisa_representative_email" in request_dict else fake.email()
|
||||
)
|
||||
request.cisa_representative_first_name = (
|
||||
request_dict["cisa_representative_first_name"]
|
||||
if "cisa_representative_first_name" in request_dict
|
||||
else fake.first_name()
|
||||
)
|
||||
request.cisa_representative_last_name = (
|
||||
request_dict["cisa_representative_last_name"]
|
||||
if "cisa_representative_last_name" in request_dict
|
||||
else fake.last_name()
|
||||
)
|
||||
request.has_anything_else_text = (
|
||||
request_dict["has_anything_else_text"] if "has_anything_else_text" in request_dict else True
|
||||
)
|
||||
request.anything_else = request_dict["anything_else"] if "anything_else" in request_dict else fake.paragraph()
|
||||
request.is_policy_acknowledged = (
|
||||
request_dict["is_policy_acknowledged"] if "is_policy_acknowledged" in request_dict else True
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _set_foreign_key_fields(cls, request: DomainRequest, request_dict: dict, user: User):
|
||||
"""Helper method used by `load`."""
|
||||
request.investigator = cls._get_investigator(request, request_dict, user)
|
||||
request.senior_official = cls._get_senior_official(request, request_dict)
|
||||
request.requested_domain = cls._get_requested_domain(request, request_dict)
|
||||
request.federal_agency = cls._get_federal_agency(request, request_dict)
|
||||
request.portfolio = cls._get_portfolio(request, request_dict)
|
||||
request.sub_organization = cls._get_sub_organization(request, request_dict)
|
||||
|
||||
@classmethod
|
||||
def _get_investigator(cls, request: DomainRequest, request_dict: dict, user: User):
|
||||
if not request.investigator:
|
||||
return User.objects.get(username=user.username) if "investigator" in request_dict else None
|
||||
return request.investigator
|
||||
|
||||
@classmethod
|
||||
def _get_senior_official(cls, request: DomainRequest, request_dict: dict):
|
||||
if not request.senior_official:
|
||||
if "senior_official" in request_dict and request_dict["senior_official"] is not None:
|
||||
return Contact.objects.get_or_create(**request_dict["senior_official"])[0]
|
||||
return Contact.objects.create(**cls.fake_contact())
|
||||
return request.senior_official
|
||||
|
||||
@classmethod
|
||||
def _get_requested_domain(cls, request: DomainRequest, request_dict: dict):
|
||||
if not request.requested_domain:
|
||||
if "requested_domain" in request_dict and request_dict["requested_domain"] is not None:
|
||||
return DraftDomain.objects.get_or_create(name=request_dict["requested_domain"])[0]
|
||||
return DraftDomain.objects.create(name=cls.fake_dot_gov())
|
||||
return request.requested_domain
|
||||
|
||||
@classmethod
|
||||
def _get_federal_agency(cls, request: DomainRequest, request_dict: dict):
|
||||
if not request.federal_agency:
|
||||
if "federal_agency" in request_dict and request_dict["federal_agency"] is not None:
|
||||
return FederalAgency.objects.get_or_create(name=request_dict["federal_agency"])[0]
|
||||
return random.choice(FederalAgency.objects.all()) # nosec
|
||||
return request.federal_agency
|
||||
|
||||
@classmethod
|
||||
def _get_portfolio(cls, request: DomainRequest, request_dict: dict):
|
||||
if not request.portfolio:
|
||||
if "portfolio" in request_dict and request_dict["portfolio"] is not None:
|
||||
return Portfolio.objects.get_or_create(name=request_dict["portfolio"])[0]
|
||||
return cls._get_random_portfolio()
|
||||
return request.portfolio
|
||||
|
||||
@classmethod
|
||||
def _get_sub_organization(cls, request: DomainRequest, request_dict: dict):
|
||||
if not request.sub_organization:
|
||||
if "sub_organization" in request_dict and request_dict["sub_organization"] is not None:
|
||||
return Suborganization.objects.get_or_create(name=request_dict["sub_organization"])[0]
|
||||
return cls._get_random_sub_organization()
|
||||
return request.sub_organization
|
||||
|
||||
@classmethod
|
||||
def _get_random_portfolio(cls):
|
||||
try:
|
||||
organization_names = [portfolio["organization_name"] for portfolio in PortfolioFixture.PORTFOLIOS]
|
||||
|
||||
portfolio_options = Portfolio.objects.filter(organization_name__in=organization_names)
|
||||
return random.choice(portfolio_options) if portfolio_options.exists() else None # nosec
|
||||
except Exception as e:
|
||||
logger.warning(f"Expected fixture portfolio, did not find it: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _get_random_sub_organization(cls):
|
||||
try:
|
||||
suborg_options = [Suborganization.objects.first(), Suborganization.objects.last()]
|
||||
return random.choice(suborg_options) # nosec
|
||||
except Exception as e:
|
||||
logger.warning(f"Expected fixture sub_organization, did not find it: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _set_many_to_many_relations(cls, request: DomainRequest, request_dict: dict):
|
||||
"""Helper method used by `load`."""
|
||||
if "other_contacts" in request_dict:
|
||||
for contact in request_dict["other_contacts"]:
|
||||
request.other_contacts.add(Contact.objects.get_or_create(**contact)[0])
|
||||
elif not request.other_contacts.exists():
|
||||
other_contacts = [
|
||||
Contact.objects.create(**cls.fake_contact()) for _ in range(random.randint(1, 3)) # nosec
|
||||
]
|
||||
request.other_contacts.add(*other_contacts)
|
||||
|
||||
if "current_websites" in request_dict:
|
||||
for website in request_dict["current_websites"]:
|
||||
request.current_websites.add(Website.objects.get_or_create(website=website)[0])
|
||||
elif not request.current_websites.exists():
|
||||
current_websites = [
|
||||
Website.objects.create(website=fake.uri()) for _ in range(random.randint(0, 3)) # nosec
|
||||
]
|
||||
request.current_websites.add(*current_websites)
|
||||
|
||||
if "alternative_domains" in request_dict:
|
||||
for domain in request_dict["alternative_domains"]:
|
||||
request.alternative_domains.add(Website.objects.get_or_create(website=domain)[0])
|
||||
elif not request.alternative_domains.exists():
|
||||
alternative_domains = [
|
||||
Website.objects.create(website=cls.fake_dot_gov()) for _ in range(random.randint(0, 3)) # nosec
|
||||
]
|
||||
request.alternative_domains.add(*alternative_domains)
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Creates domain requests for each user in the database."""
|
||||
logger.info("Going to load %s domain requests" % len(cls.DOMAINREQUESTS))
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Get the usernames of users created in the UserFixture
|
||||
created_usernames = [user_data["username"] for user_data in UserFixture.ADMINS + UserFixture.STAFF]
|
||||
|
||||
# Filter users to only include those created by the fixture
|
||||
users = list(User.objects.filter(username__in=created_usernames))
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
cls._create_domain_requests(users)
|
||||
|
||||
@classmethod
|
||||
def _create_domain_requests(cls, users):
|
||||
"""Creates DomainRequests given a list of users."""
|
||||
domain_requests_to_create = []
|
||||
for user in users:
|
||||
for request_data in cls.DOMAINREQUESTS:
|
||||
# Prepare DomainRequest objects
|
||||
try:
|
||||
domain_request = DomainRequest(
|
||||
creator=user,
|
||||
organization_name=request_data["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(domain_request, request_data)
|
||||
cls._set_foreign_key_fields(domain_request, request_data, user)
|
||||
domain_requests_to_create.append(domain_request)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
# Bulk create domain requests
|
||||
cls._bulk_create_requests(domain_requests_to_create)
|
||||
|
||||
# Now many-to-many relationships
|
||||
for domain_request in domain_requests_to_create:
|
||||
try:
|
||||
cls._set_many_to_many_relations(domain_request, request_data)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
@classmethod
|
||||
def _bulk_create_requests(cls, domain_requests_to_create):
|
||||
"""Bulk create domain requests."""
|
||||
if len(domain_requests_to_create) > 0:
|
||||
try:
|
||||
DomainRequest.objects.bulk_create(domain_requests_to_create)
|
||||
logger.info(f"Successfully created {len(domain_requests_to_create)} requests.")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during requests bulk creation: {e}")
|
87
src/registrar/fixtures/fixtures_suborganizations.py
Normal file
87
src/registrar/fixtures/fixtures_suborganizations.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
import logging
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.suborganization import Suborganization
|
||||
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SuborganizationFixture:
|
||||
"""
|
||||
Creates 2 pre-defined suborg with the infrastructure to add more.
|
||||
|
||||
Depends on fixtures_portfolios.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
to run this code.
|
||||
"""
|
||||
|
||||
SUBORGS = [
|
||||
{
|
||||
"name": "Take it Easy",
|
||||
},
|
||||
{
|
||||
"name": "Welcome to the Machine",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Creates suborganizations."""
|
||||
logger.info(f"Going to load {len(cls.SUBORGS)} suborgs")
|
||||
|
||||
with transaction.atomic():
|
||||
portfolios = cls._get_portfolios()
|
||||
if not portfolios:
|
||||
return
|
||||
|
||||
suborgs_to_create = cls._prepare_suborgs_to_create(portfolios)
|
||||
cls._bulk_create_suborgs(suborgs_to_create)
|
||||
|
||||
@classmethod
|
||||
def _get_portfolios(cls):
|
||||
"""Fetches portfolios with organization_name 'Hotel California' and 'Wish You Were Here'
|
||||
and logs warnings if not found."""
|
||||
try:
|
||||
portfolio1 = Portfolio.objects.filter(organization_name="Hotel California").first()
|
||||
portfolio2 = Portfolio.objects.filter(organization_name="Wish You Were Here").first()
|
||||
|
||||
if not portfolio1 or not portfolio2:
|
||||
logger.warning("One or both portfolios not found.")
|
||||
return None
|
||||
return portfolio1, portfolio2
|
||||
except Exception as e:
|
||||
logger.warning(f"Error fetching portfolios: {e}")
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _prepare_suborgs_to_create(cls, portfolios):
|
||||
"""Prepares a list of suborganizations to create, avoiding duplicates."""
|
||||
portfolio1, portfolio2 = portfolios
|
||||
suborgs_to_create = []
|
||||
|
||||
try:
|
||||
if not Suborganization.objects.filter(name=cls.SUBORGS[0]["name"]).exists():
|
||||
suborgs_to_create.append(Suborganization(portfolio=portfolio1, name=cls.SUBORGS[0]["name"]))
|
||||
|
||||
if not Suborganization.objects.filter(name=cls.SUBORGS[1]["name"]).exists():
|
||||
suborgs_to_create.append(Suborganization(portfolio=portfolio2, name=cls.SUBORGS[1]["name"]))
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating suborg objects: {e}")
|
||||
|
||||
return suborgs_to_create
|
||||
|
||||
@classmethod
|
||||
def _bulk_create_suborgs(cls, suborgs_to_create):
|
||||
"""Bulk creates suborganizations and logs success or errors."""
|
||||
if suborgs_to_create:
|
||||
try:
|
||||
Suborganization.objects.bulk_create(suborgs_to_create)
|
||||
logger.info(f"Successfully created {len(suborgs_to_create)} suborgs")
|
||||
except Exception as e:
|
||||
logger.warning(f"Error bulk creating suborgs: {e}")
|
|
@ -0,0 +1,86 @@
|
|||
import logging
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.fixtures.fixtures_portfolios import PortfolioFixture
|
||||
from registrar.fixtures.fixtures_users import UserFixture
|
||||
from registrar.models import User
|
||||
from registrar.models.portfolio import Portfolio
|
||||
from registrar.models.user_portfolio_permission import UserPortfolioPermission
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UserPortfolioPermissionFixture:
|
||||
"""Create user portfolio permissions for each user.
|
||||
Each user will be admin on 2 portfolios.
|
||||
|
||||
Depends on fixture_portfolios"""
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
logger.info("Going to set user portfolio permissions")
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
try:
|
||||
# Get the usernames of users created in the UserFixture
|
||||
created_usernames = [user_data["username"] for user_data in UserFixture.ADMINS + UserFixture.STAFF]
|
||||
|
||||
# Filter users to only include those created by the fixture
|
||||
users = list(User.objects.filter(username__in=created_usernames))
|
||||
|
||||
organization_names = [portfolio["organization_name"] for portfolio in PortfolioFixture.PORTFOLIOS]
|
||||
|
||||
portfolios = list(Portfolio.objects.filter(organization_name__in=organization_names))
|
||||
|
||||
if not users:
|
||||
logger.warning("User fixtures missing.")
|
||||
return
|
||||
|
||||
if not portfolios:
|
||||
logger.warning("Portfolio fixtures missing.")
|
||||
return
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
user_portfolio_permissions_to_create = []
|
||||
for user in users:
|
||||
for portfolio in portfolios:
|
||||
try:
|
||||
if not UserPortfolioPermission.objects.filter(user=user, portfolio=portfolio).exists():
|
||||
user_portfolio_permission = UserPortfolioPermission(
|
||||
user=user,
|
||||
portfolio=portfolio,
|
||||
roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN],
|
||||
)
|
||||
user_portfolio_permissions_to_create.append(user_portfolio_permission)
|
||||
else:
|
||||
logger.info(
|
||||
f"Permission exists for user '{user.username}' "
|
||||
f"on portfolio '{portfolio.organization_name}'."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
# Bulk create permissions
|
||||
cls._bulk_create_permissions(user_portfolio_permissions_to_create)
|
||||
|
||||
@classmethod
|
||||
def _bulk_create_permissions(cls, user_portfolio_permissions_to_create):
|
||||
"""Bulk creates permissions and logs success or errors."""
|
||||
if user_portfolio_permissions_to_create:
|
||||
try:
|
||||
UserPortfolioPermission.objects.bulk_create(user_portfolio_permissions_to_create)
|
||||
logger.info(
|
||||
f"Successfully created {len(user_portfolio_permissions_to_create)} user portfolio permissions."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during portfolio permission bulk creation: {e}")
|
||||
else:
|
||||
logger.info("No new user portfolio permissions to create.")
|
|
@ -23,129 +23,123 @@ class UserFixture:
|
|||
"""
|
||||
|
||||
ADMINS = [
|
||||
{
|
||||
"username": "43a7fa8d-0550-4494-a6fe-81500324d590",
|
||||
"first_name": "Jyoti",
|
||||
"last_name": "Bock",
|
||||
"email": "jyotibock@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "aad084c3-66cc-4632-80eb-41cdf5c5bcbf",
|
||||
"first_name": "Aditi",
|
||||
"last_name": "Green",
|
||||
"email": "aditidevelops+01@gmail.com",
|
||||
"title": "Positive vibes",
|
||||
},
|
||||
{
|
||||
"username": "be17c826-e200-4999-9389-2ded48c43691",
|
||||
"first_name": "Matthew",
|
||||
"last_name": "Spence",
|
||||
"title": "Hollywood hair",
|
||||
},
|
||||
{
|
||||
"username": "5f283494-31bd-49b5-b024-a7e7cae00848",
|
||||
"first_name": "Rachid",
|
||||
"last_name": "Mrad",
|
||||
"email": "rachid.mrad@associates.cisa.dhs.gov",
|
||||
"title": "Common pirate",
|
||||
},
|
||||
{
|
||||
"username": "eb2214cd-fc0c-48c0-9dbd-bc4cd6820c74",
|
||||
"first_name": "Alysia",
|
||||
"last_name": "Broddrick",
|
||||
"email": "abroddrick@truss.works",
|
||||
"title": "I drink coffee and know things",
|
||||
},
|
||||
{
|
||||
"username": "8f8e7293-17f7-4716-889b-1990241cbd39",
|
||||
"first_name": "Katherine",
|
||||
"last_name": "Osos",
|
||||
"email": "kosos@truss.works",
|
||||
"title": "Grove keeper",
|
||||
},
|
||||
{
|
||||
"username": "70488e0a-e937-4894-a28c-16f5949effd4",
|
||||
"first_name": "Gaby",
|
||||
"last_name": "DiSarli",
|
||||
"email": "gaby@truss.works",
|
||||
"title": "De Stijl",
|
||||
},
|
||||
{
|
||||
"username": "83c2b6dd-20a2-4cac-bb40-e22a72d2955c",
|
||||
"first_name": "Cameron",
|
||||
"last_name": "Dixon",
|
||||
"email": "cameron.dixon@cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "0353607a-cbba-47d2-98d7-e83dcd5b90ea",
|
||||
"first_name": "Ryan",
|
||||
"last_name": "Brooks",
|
||||
"title": "Product owner",
|
||||
},
|
||||
{
|
||||
"username": "30001ee7-0467-4df2-8db2-786e79606060",
|
||||
"first_name": "Zander",
|
||||
"last_name": "Adkinson",
|
||||
"title": "ACME specialist",
|
||||
},
|
||||
{
|
||||
"username": "2bf518c2-485a-4c42-ab1a-f5a8b0a08484",
|
||||
"first_name": "Paul",
|
||||
"last_name": "Kuykendall",
|
||||
"title": "Dr. Silvertongue",
|
||||
},
|
||||
{
|
||||
"username": "2a88a97b-be96-4aad-b99e-0b605b492c78",
|
||||
"first_name": "Rebecca",
|
||||
"last_name": "Hsieh",
|
||||
"email": "rebecca.hsieh@truss.works",
|
||||
"title": "Catlady",
|
||||
},
|
||||
{
|
||||
"username": "fa69c8e8-da83-4798-a4f2-263c9ce93f52",
|
||||
"first_name": "David",
|
||||
"last_name": "Kennedy",
|
||||
"email": "david.kennedy@ecstech.com",
|
||||
"title": "Mean lean coding machine",
|
||||
},
|
||||
{
|
||||
"username": "f14433d8-f0e9-41bf-9c72-b99b110e665d",
|
||||
"first_name": "Nicolle",
|
||||
"last_name": "LeClair",
|
||||
"email": "nicolle.leclair@ecstech.com",
|
||||
"title": "Nightowl",
|
||||
},
|
||||
{
|
||||
"username": "24840450-bf47-4d89-8aa9-c612fe68f9da",
|
||||
"first_name": "Erin",
|
||||
"last_name": "Song",
|
||||
"title": "Catlady 2",
|
||||
},
|
||||
{
|
||||
"username": "e0ea8b94-6e53-4430-814a-849a7ca45f21",
|
||||
"first_name": "Kristina",
|
||||
"last_name": "Yin",
|
||||
"title": "Hufflepuff prefect",
|
||||
},
|
||||
{
|
||||
"username": "ac49d7c1-368a-4e6b-8f1d-60250e20a16f",
|
||||
"first_name": "Vicky",
|
||||
"last_name": "Chin",
|
||||
"email": "szu.chin@associates.cisa.dhs.gov",
|
||||
"title": "Ze whip",
|
||||
},
|
||||
{
|
||||
"username": "66bb1a5a-a091-4d7f-a6cf-4d772b4711c7",
|
||||
"first_name": "Christina",
|
||||
"last_name": "Burnett",
|
||||
"email": "christina.burnett@cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "012f844d-8a0f-4225-9d82-cbf87bff1d3e",
|
||||
"first_name": "Riley",
|
||||
"last_name": "Orr",
|
||||
"email": "riley+320@truss.works",
|
||||
"title": "Groovy",
|
||||
},
|
||||
{
|
||||
"username": "76612d84-66b0-4ae9-9870-81e98b9858b6",
|
||||
"first_name": "Anna",
|
||||
"last_name": "Gingle",
|
||||
"email": "annagingle@truss.works",
|
||||
"title": "Sweetwater sailor",
|
||||
},
|
||||
]
|
||||
|
||||
STAFF = [
|
||||
{
|
||||
"username": "a5906815-dd80-4c64-aebe-2da6a4c9d7a4",
|
||||
"first_name": "Jyoti-Analyst",
|
||||
"last_name": "Bock-Analyst",
|
||||
"email": "jyotibock+1@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "ffec5987-aa84-411b-a05a-a7ee5cbcde54",
|
||||
"first_name": "Aditi-Analyst",
|
||||
|
@ -231,18 +225,6 @@ class UserFixture:
|
|||
"last_name": "Burnett-Analyst",
|
||||
"email": "christina.burnett@gwe.cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "d9839768-0c17-4fa2-9c8e-36291eef5c11",
|
||||
"first_name": "Alex-Analyst",
|
||||
"last_name": "Mcelya-Analyst",
|
||||
"email": "ALEXANDER.MCELYA@cisa.dhs.gov",
|
||||
},
|
||||
{
|
||||
"username": "082a066f-e0a4-45f6-8672-4343a1208a36",
|
||||
"first_name": "Riley-Analyst",
|
||||
"last_name": "Orr-Analyst",
|
||||
"email": "riley+321@truss.works",
|
||||
},
|
||||
{
|
||||
"username": "e1e350b1-cfc1-4753-a6cb-3ae6d912f99c",
|
||||
"first_name": "Anna-Analyst",
|
||||
|
@ -254,29 +236,61 @@ class UserFixture:
|
|||
# Additional emails to add to the AllowedEmail whitelist.
|
||||
ADDITIONAL_ALLOWED_EMAILS: list[str] = ["davekenn4242@gmail.com", "rachid_mrad@hotmail.com"]
|
||||
|
||||
@classmethod
|
||||
def load_users(cls, users, group_name, are_superusers=False):
|
||||
logger.info(f"Going to load {len(users)} users in group {group_name}")
|
||||
for user_data in users:
|
||||
"""Loads the users into the database and assigns them to the specified group."""
|
||||
logger.info(f"Going to load {len(users)} users for group {group_name}")
|
||||
|
||||
group = UserGroup.objects.get(name=group_name)
|
||||
|
||||
# Prepare sets of existing usernames and IDs in one query
|
||||
user_identifiers = [(user.get("username"), user.get("id")) for user in users]
|
||||
existing_users = User.objects.filter(
|
||||
username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers]
|
||||
).values_list("username", "id")
|
||||
|
||||
existing_usernames = set(user[0] for user in existing_users)
|
||||
existing_user_ids = set(user[1] for user in existing_users)
|
||||
|
||||
# Filter out users with existing IDs or usernames
|
||||
new_users = [
|
||||
User(
|
||||
id=user_data.get("id"),
|
||||
first_name=user_data.get("first_name"),
|
||||
last_name=user_data.get("last_name"),
|
||||
username=user_data.get("username"),
|
||||
email=user_data.get("email", ""),
|
||||
title=user_data.get("title", "Peon"),
|
||||
phone=user_data.get("phone", "2022222222"),
|
||||
is_active=user_data.get("is_active", True),
|
||||
is_staff=True,
|
||||
is_superuser=are_superusers,
|
||||
)
|
||||
for user_data in users
|
||||
if user_data.get("username") not in existing_usernames and user_data.get("id") not in existing_user_ids
|
||||
]
|
||||
|
||||
# Perform bulk creation for new users
|
||||
if new_users:
|
||||
try:
|
||||
user, _ = User.objects.get_or_create(username=user_data["username"])
|
||||
user.is_superuser = are_superusers
|
||||
user.first_name = user_data["first_name"]
|
||||
user.last_name = user_data["last_name"]
|
||||
if "email" in user_data:
|
||||
user.email = user_data["email"]
|
||||
user.is_staff = True
|
||||
user.is_active = True
|
||||
# This verification type will get reverted to "regular" (or whichever is applicables)
|
||||
# once the user logs in for the first time (as they then got verified through different means).
|
||||
# In the meantime, we can still describe how the user got here in the first place.
|
||||
user.verification_type = User.VerificationTypeChoices.FIXTURE_USER
|
||||
group = UserGroup.objects.get(name=group_name)
|
||||
user.groups.add(group)
|
||||
user.save()
|
||||
logger.debug(f"User object created for {user_data['first_name']}")
|
||||
User.objects.bulk_create(new_users)
|
||||
logger.info(f"Created {len(new_users)} new users.")
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
logger.info(f"All users in group {group_name} loaded.")
|
||||
logger.error(f"Unexpected error during user bulk creation: {e}")
|
||||
else:
|
||||
logger.info("No new users to create.")
|
||||
|
||||
# Get all users to be updated (both new and existing)
|
||||
created_or_existing_users = User.objects.filter(username__in=[user.get("username") for user in users])
|
||||
|
||||
# Filter out users who are already in the group
|
||||
users_not_in_group = created_or_existing_users.exclude(groups__id=group.id)
|
||||
|
||||
# Add only users who are not already in the group
|
||||
if users_not_in_group.exists():
|
||||
group.user_set.add(*users_not_in_group)
|
||||
|
||||
logger.info(f"Users loaded for group {group_name}.")
|
||||
|
||||
def load_allowed_emails(cls, users, additional_emails):
|
||||
"""Populates a whitelist of allowed emails (as defined in this list)"""
|
||||
|
@ -284,37 +298,33 @@ class UserFixture:
|
|||
if additional_emails:
|
||||
logger.info(f"Going to load {len(additional_emails)} additional allowed emails")
|
||||
|
||||
# Load user emails
|
||||
allowed_emails = []
|
||||
existing_emails = set(AllowedEmail.objects.values_list("email", flat=True))
|
||||
new_allowed_emails = []
|
||||
|
||||
for user_data in users:
|
||||
user_email = user_data.get("email")
|
||||
if user_email and user_email not in allowed_emails:
|
||||
allowed_emails.append(AllowedEmail(email=user_email))
|
||||
else:
|
||||
first_name = user_data.get("first_name")
|
||||
last_name = user_data.get("last_name")
|
||||
logger.warning(f"Could not add email to whitelist for {first_name} {last_name}.")
|
||||
if user_email and user_email not in existing_emails:
|
||||
new_allowed_emails.append(AllowedEmail(email=user_email))
|
||||
|
||||
# Load additional emails
|
||||
allowed_emails.extend([AllowedEmail(email=email) for email in additional_emails])
|
||||
# Load additional emails, only if they don't exist already
|
||||
for email in additional_emails:
|
||||
if email not in existing_emails:
|
||||
new_allowed_emails.append(AllowedEmail(email=email))
|
||||
|
||||
if allowed_emails:
|
||||
AllowedEmail.objects.bulk_create(allowed_emails)
|
||||
logger.info(f"Loaded {len(allowed_emails)} allowed emails")
|
||||
if new_allowed_emails:
|
||||
try:
|
||||
AllowedEmail.objects.bulk_create(new_allowed_emails)
|
||||
logger.info(f"Loaded {len(new_allowed_emails)} allowed emails")
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during allowed emails bulk creation: {e}")
|
||||
else:
|
||||
logger.info("No allowed emails to load")
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
# This is slightly different then bulk_create or bulk_update, in that
|
||||
# you still get the same behaviour of .save(), but those incremental
|
||||
# steps now do not need to close/reopen a db connection,
|
||||
# instead they share one.
|
||||
with transaction.atomic():
|
||||
cls.load_users(cls, cls.ADMINS, "full_access_group", are_superusers=True)
|
||||
cls.load_users(cls, cls.STAFF, "cisa_analysts_group")
|
||||
cls.load_users(cls.ADMINS, "full_access_group", are_superusers=True)
|
||||
cls.load_users(cls.STAFF, "cisa_analysts_group")
|
||||
|
||||
# Combine ADMINS and STAFF lists
|
||||
all_users = cls.ADMINS + cls.STAFF
|
|
@ -1,236 +0,0 @@
|
|||
import logging
|
||||
import random
|
||||
from faker import Faker
|
||||
from django.db import transaction
|
||||
|
||||
from registrar.models import User, DomainRequest, DraftDomain, Contact, Website, FederalAgency
|
||||
|
||||
fake = Faker()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DomainRequestFixture:
|
||||
"""
|
||||
Load domain requests into the database.
|
||||
|
||||
Make sure this class' `load` method is called from `handle`
|
||||
in management/commands/load.py, then use `./manage.py load`
|
||||
to run this code.
|
||||
"""
|
||||
|
||||
# any fields not specified here will be filled in with fake data or defaults
|
||||
# NOTE BENE: each fixture must have `organization_name` for uniqueness!
|
||||
# Here is a more complete example as a template:
|
||||
# {
|
||||
# "status": "started",
|
||||
# "organization_name": "Example - Just started",
|
||||
# "generic_org_type": "federal",
|
||||
# "federal_agency": None,
|
||||
# "federal_type": None,
|
||||
# "address_line1": None,
|
||||
# "address_line2": None,
|
||||
# "city": None,
|
||||
# "state_territory": None,
|
||||
# "zipcode": None,
|
||||
# "urbanization": None,
|
||||
# "purpose": None,
|
||||
# "anything_else": None,
|
||||
# "is_policy_acknowledged": None,
|
||||
# "senior_official": None,
|
||||
# "other_contacts": [],
|
||||
# "current_websites": [],
|
||||
# "alternative_domains": [],
|
||||
# },
|
||||
DA = [
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.STARTED,
|
||||
"organization_name": "Example - Finished but not submitted",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
"organization_name": "Example - Submitted but pending investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - In investigation",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
"organization_name": "Example - Approved",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.WITHDRAWN,
|
||||
"organization_name": "Example - Withdrawn",
|
||||
},
|
||||
{
|
||||
"status": DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
"organization_name": "Example - Action needed",
|
||||
},
|
||||
{
|
||||
"status": "rejected",
|
||||
"organization_name": "Example - Rejected",
|
||||
},
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def fake_contact(cls):
|
||||
return {
|
||||
"first_name": fake.first_name(),
|
||||
"middle_name": None,
|
||||
"last_name": fake.last_name(),
|
||||
"title": fake.job(),
|
||||
"email": fake.ascii_safe_email(),
|
||||
"phone": "201-555-5555",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def fake_dot_gov(cls):
|
||||
return f"{fake.slug()}.gov"
|
||||
|
||||
@classmethod
|
||||
def _set_non_foreign_key_fields(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
da.status = app["status"] if "status" in app else "started"
|
||||
|
||||
# TODO for a future ticket: Allow for more than just "federal" here
|
||||
da.generic_org_type = app["generic_org_type"] if "generic_org_type" in app else "federal"
|
||||
da.last_submitted_date = fake.date()
|
||||
da.federal_type = (
|
||||
app["federal_type"]
|
||||
if "federal_type" in app
|
||||
else random.choice(["executive", "judicial", "legislative"]) # nosec
|
||||
)
|
||||
da.address_line1 = app["address_line1"] if "address_line1" in app else fake.street_address()
|
||||
da.address_line2 = app["address_line2"] if "address_line2" in app else None
|
||||
da.city = app["city"] if "city" in app else fake.city()
|
||||
da.state_territory = app["state_territory"] if "state_territory" in app else fake.state_abbr()
|
||||
da.zipcode = app["zipcode"] if "zipcode" in app else fake.postalcode()
|
||||
da.urbanization = app["urbanization"] if "urbanization" in app else None
|
||||
da.purpose = app["purpose"] if "purpose" in app else fake.paragraph()
|
||||
da.anything_else = app["anything_else"] if "anything_else" in app else None
|
||||
da.is_policy_acknowledged = app["is_policy_acknowledged"] if "is_policy_acknowledged" in app else True
|
||||
|
||||
@classmethod
|
||||
def _set_foreign_key_fields(cls, da: DomainRequest, app: dict, user: User):
|
||||
"""Helper method used by `load`."""
|
||||
if not da.investigator:
|
||||
da.investigator = User.objects.get(username=user.username) if "investigator" in app else None
|
||||
|
||||
if not da.senior_official:
|
||||
if "senior_official" in app and app["senior_official"] is not None:
|
||||
da.senior_official, _ = Contact.objects.get_or_create(**app["senior_official"])
|
||||
else:
|
||||
da.senior_official = Contact.objects.create(**cls.fake_contact())
|
||||
|
||||
if not da.requested_domain:
|
||||
if "requested_domain" in app and app["requested_domain"] is not None:
|
||||
da.requested_domain, _ = DraftDomain.objects.get_or_create(name=app["requested_domain"])
|
||||
else:
|
||||
da.requested_domain = DraftDomain.objects.create(name=cls.fake_dot_gov())
|
||||
if not da.federal_agency:
|
||||
if "federal_agency" in app and app["federal_agency"] is not None:
|
||||
da.federal_agency, _ = FederalAgency.objects.get_or_create(name=app["federal_agency"])
|
||||
else:
|
||||
federal_agencies = FederalAgency.objects.all()
|
||||
# Random choice of agency for selects, used as placeholders for testing.
|
||||
da.federal_agency = random.choice(federal_agencies) # nosec
|
||||
|
||||
@classmethod
|
||||
def _set_many_to_many_relations(cls, da: DomainRequest, app: dict):
|
||||
"""Helper method used by `load`."""
|
||||
if "other_contacts" in app:
|
||||
for contact in app["other_contacts"]:
|
||||
da.other_contacts.add(Contact.objects.get_or_create(**contact)[0])
|
||||
elif not da.other_contacts.exists():
|
||||
other_contacts = [
|
||||
Contact.objects.create(**cls.fake_contact()) for _ in range(random.randint(0, 3)) # nosec
|
||||
]
|
||||
da.other_contacts.add(*other_contacts)
|
||||
|
||||
if "current_websites" in app:
|
||||
for website in app["current_websites"]:
|
||||
da.current_websites.add(Website.objects.get_or_create(website=website)[0])
|
||||
elif not da.current_websites.exists():
|
||||
current_websites = [
|
||||
Website.objects.create(website=fake.uri()) for _ in range(random.randint(0, 3)) # nosec
|
||||
]
|
||||
da.current_websites.add(*current_websites)
|
||||
|
||||
if "alternative_domains" in app:
|
||||
for domain in app["alternative_domains"]:
|
||||
da.alternative_domains.add(Website.objects.get_or_create(website=domain)[0])
|
||||
elif not da.alternative_domains.exists():
|
||||
alternative_domains = [
|
||||
Website.objects.create(website=cls.fake_dot_gov()) for _ in range(random.randint(0, 3)) # nosec
|
||||
]
|
||||
da.alternative_domains.add(*alternative_domains)
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
"""Creates domain requests for each user in the database."""
|
||||
logger.info("Going to load %s domain requests" % len(cls.DA))
|
||||
try:
|
||||
users = list(User.objects.all()) # force evaluation to catch db errors
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
cls._create_domain_requests(users)
|
||||
|
||||
@classmethod
|
||||
def _create_domain_requests(cls, users):
|
||||
"""Creates DomainRequests given a list of users"""
|
||||
for user in users:
|
||||
logger.debug("Loading domain requests for %s" % user)
|
||||
for app in cls.DA:
|
||||
try:
|
||||
da, _ = DomainRequest.objects.get_or_create(
|
||||
creator=user,
|
||||
organization_name=app["organization_name"],
|
||||
)
|
||||
cls._set_non_foreign_key_fields(da, app)
|
||||
cls._set_foreign_key_fields(da, app, user)
|
||||
da.save()
|
||||
cls._set_many_to_many_relations(da, app)
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
|
||||
|
||||
class DomainFixture(DomainRequestFixture):
|
||||
"""Create one domain and permissions on it for each user."""
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
try:
|
||||
users = list(User.objects.all()) # force evaluation to catch db errors
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
return
|
||||
|
||||
# Lumped under .atomic to ensure we don't make redundant DB calls.
|
||||
# This bundles them all together, and then saves it in a single call.
|
||||
with transaction.atomic():
|
||||
# approve each user associated with `in review` status domains
|
||||
DomainFixture._approve_domain_requests(users)
|
||||
|
||||
@staticmethod
|
||||
def _approve_domain_requests(users):
|
||||
"""Approves all provided domain requests if they are in the state in_review"""
|
||||
for user in users:
|
||||
domain_request = DomainRequest.objects.filter(
|
||||
creator=user, status=DomainRequest.DomainRequestStatus.IN_REVIEW
|
||||
).last()
|
||||
logger.debug(f"Approving {domain_request} for {user}")
|
||||
|
||||
# All approvals require an investigator, so if there is none,
|
||||
# assign one.
|
||||
if domain_request.investigator is None:
|
||||
# All "users" in fixtures have admin perms per prior config.
|
||||
# No need to check for that.
|
||||
domain_request.investigator = random.choice(users) # nosec
|
||||
|
||||
domain_request.approve(send_email=False)
|
||||
domain_request.save()
|
|
@ -4,7 +4,6 @@ from itertools import zip_longest
|
|||
from typing import Callable
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django import forms
|
||||
|
||||
from registrar.models import DomainRequest, Contact
|
||||
|
||||
|
||||
|
@ -278,3 +277,15 @@ class BaseYesNoForm(RegistrarForm):
|
|||
# No pre-selection for new domain requests
|
||||
initial_value = self.form_is_checked if self.domain_request else None
|
||||
return initial_value
|
||||
|
||||
|
||||
def request_step_list(request_wizard):
|
||||
"""Dynamically generated list of steps in the form wizard."""
|
||||
step_list = []
|
||||
for step in request_wizard.StepEnum:
|
||||
condition = request_wizard.WIZARD_CONDITIONS.get(step, True)
|
||||
if callable(condition):
|
||||
condition = condition(request_wizard)
|
||||
if condition:
|
||||
step_list.append(step)
|
||||
return step_list
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import logging
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from auditlog.context import disable_auditlog # type: ignore
|
||||
|
||||
|
||||
from registrar.fixtures_users import UserFixture
|
||||
from registrar.fixtures_domain_requests import DomainRequestFixture, DomainFixture
|
||||
from auditlog.context import disable_auditlog
|
||||
from registrar.fixtures.fixtures_domains import DomainFixture
|
||||
from registrar.fixtures.fixtures_portfolios import PortfolioFixture
|
||||
from registrar.fixtures.fixtures_requests import DomainRequestFixture
|
||||
from registrar.fixtures.fixtures_suborganizations import SuborganizationFixture
|
||||
from registrar.fixtures.fixtures_user_portfolio_permissions import UserPortfolioPermissionFixture
|
||||
from registrar.fixtures.fixtures_users import UserFixture # type: ignore
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -16,6 +18,9 @@ class Command(BaseCommand):
|
|||
# https://github.com/jazzband/django-auditlog/issues/17
|
||||
with disable_auditlog():
|
||||
UserFixture.load()
|
||||
PortfolioFixture.load()
|
||||
SuborganizationFixture.load()
|
||||
DomainRequestFixture.load()
|
||||
DomainFixture.load()
|
||||
UserPortfolioPermissionFixture.load()
|
||||
logger.info("All fixtures loaded.")
|
||||
|
|
|
@ -36,13 +36,13 @@ class Command(BaseCommand, PopulateScriptTemplate):
|
|||
self.federal_agency_dict[agency_name.strip()] = (initials, agency_status)
|
||||
|
||||
# Update every federal agency record
|
||||
self.mass_update_records(FederalAgency, {"agency__isnull": False}, ["initials", "is_fceb"])
|
||||
self.mass_update_records(FederalAgency, {"agency__isnull": False}, ["acronym", "is_fceb"])
|
||||
|
||||
def update_record(self, record: FederalAgency):
|
||||
"""For each record, update the initials and is_fceb field if data exists for it"""
|
||||
initials, agency_status = self.federal_agency_dict.get(record.agency)
|
||||
|
||||
record.initials = initials
|
||||
record.acronym = initials
|
||||
if agency_status and isinstance(agency_status, str) and agency_status.strip().upper() == "FCEB":
|
||||
record.is_fceb = True
|
||||
else:
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
# Generated by Django 4.2.10 on 2024-09-25 00:49
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0128_alter_domaininformation_state_territory_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="portfolioinvitation",
|
||||
name="portfolio_roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[("organization_admin", "Admin"), ("organization_member", "Member")], max_length=50
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="userportfoliopermission",
|
||||
name="roles",
|
||||
field=django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.CharField(
|
||||
choices=[("organization_admin", "Admin"), ("organization_member", "Member")], max_length=50
|
||||
),
|
||||
blank=True,
|
||||
help_text="Select one or more roles.",
|
||||
null=True,
|
||||
size=None,
|
||||
),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,146 @@
|
|||
# Generated by Django 4.2.10 on 2024-09-30 17:59
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import registrar.models.federal_agency
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0129_alter_portfolioinvitation_portfolio_roles_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name="federalagency",
|
||||
old_name="initials",
|
||||
new_name="acronym",
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federalagency",
|
||||
name="acronym",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
help_text="Acronym commonly used to reference the federal agency (Optional)",
|
||||
max_length=10,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="portfolio",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="If blank, domain is not associated with a portfolio.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="information_portfolio",
|
||||
to="registrar.portfolio",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="sub_organization",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="If blank, domain is associated with the overarching organization for this portfolio.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="information_sub_organization",
|
||||
to="registrar.suborganization",
|
||||
verbose_name="Suborganization",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="portfolio",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="If blank, request is not associated with a portfolio.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="DomainRequest_portfolio",
|
||||
to="registrar.portfolio",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="sub_organization",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text="If blank, request is associated with the overarching organization for this portfolio.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="request_sub_organization",
|
||||
to="registrar.suborganization",
|
||||
verbose_name="Suborganization",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federalagency",
|
||||
name="federal_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[("executive", "Executive"), ("judicial", "Judicial"), ("legislative", "Legislative")],
|
||||
max_length=20,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="federalagency",
|
||||
name="is_fceb",
|
||||
field=models.BooleanField(
|
||||
blank=True, help_text="Federal Civilian Executive Branch (FCEB)", null=True, verbose_name="FCEB"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="federal_agency",
|
||||
field=models.ForeignKey(
|
||||
default=registrar.models.federal_agency.FederalAgency.get_non_federal_agency,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
to="registrar.federalagency",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="organization_name",
|
||||
field=models.CharField(blank=True, null=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="organization_type",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
choices=[
|
||||
("federal", "Federal"),
|
||||
("interstate", "Interstate"),
|
||||
("state_or_territory", "State or territory"),
|
||||
("tribal", "Tribal"),
|
||||
("county", "County"),
|
||||
("city", "City"),
|
||||
("special_district", "Special district"),
|
||||
("school_district", "School district"),
|
||||
],
|
||||
max_length=255,
|
||||
null=True,
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="portfolio",
|
||||
name="senior_official",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="portfolios",
|
||||
to="registrar.seniorofficial",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="suborganization",
|
||||
name="name",
|
||||
field=models.CharField(max_length=1000, unique=True, verbose_name="Suborganization"),
|
||||
),
|
||||
]
|
37
src/registrar/migrations/0131_create_groups_v17.py
Normal file
37
src/registrar/migrations/0131_create_groups_v17.py
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This migration creates the create_full_access_group and create_cisa_analyst_group groups
|
||||
# It is dependent on 0079 (which populates federal agencies)
|
||||
# If permissions on the groups need changing, edit CISA_ANALYST_GROUP_PERMISSIONS
|
||||
# in the user_group model then:
|
||||
# [NOT RECOMMENDED]
|
||||
# step 1: docker-compose exec app ./manage.py migrate --fake registrar 0035_contenttypes_permissions
|
||||
# step 2: docker-compose exec app ./manage.py migrate registrar 0036_create_groups
|
||||
# step 3: fake run the latest migration in the migrations list
|
||||
# [RECOMMENDED]
|
||||
# Alternatively:
|
||||
# step 1: duplicate the migration that loads data
|
||||
# step 2: docker-compose exec app ./manage.py migrate
|
||||
|
||||
from django.db import migrations
|
||||
from registrar.models import UserGroup
|
||||
from typing import Any
|
||||
|
||||
|
||||
# For linting: RunPython expects a function reference,
|
||||
# so let's give it one
|
||||
def create_groups(apps, schema_editor) -> Any:
|
||||
UserGroup.create_cisa_analyst_group(apps, schema_editor)
|
||||
UserGroup.create_full_access_group(apps, schema_editor)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("registrar", "0130_remove_federalagency_initials_federalagency_acronym_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
create_groups,
|
||||
reverse_code=migrations.RunPython.noop,
|
||||
atomic=True,
|
||||
),
|
||||
]
|
|
@ -0,0 +1,36 @@
|
|||
# Generated by Django 4.2.10 on 2024-10-02 14:17
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("registrar", "0131_create_groups_v17"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="domaininformation",
|
||||
name="portfolio",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="information_portfolio",
|
||||
to="registrar.portfolio",
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="domainrequest",
|
||||
name="portfolio",
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.PROTECT,
|
||||
related_name="DomainRequest_portfolio",
|
||||
to="registrar.portfolio",
|
||||
),
|
||||
),
|
||||
]
|
|
@ -63,7 +63,6 @@ class DomainInformation(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
related_name="information_portfolio",
|
||||
help_text="Portfolio associated with this domain",
|
||||
)
|
||||
|
||||
sub_organization = models.ForeignKey(
|
||||
|
@ -72,7 +71,8 @@ class DomainInformation(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
related_name="information_sub_organization",
|
||||
help_text="The suborganization that this domain is included under",
|
||||
help_text="If blank, domain is associated with the overarching organization for this portfolio.",
|
||||
verbose_name="Suborganization",
|
||||
)
|
||||
|
||||
domain_request = models.OneToOneField(
|
||||
|
|
|
@ -327,7 +327,6 @@ class DomainRequest(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
related_name="DomainRequest_portfolio",
|
||||
help_text="Portfolio associated with this domain request",
|
||||
)
|
||||
|
||||
sub_organization = models.ForeignKey(
|
||||
|
@ -336,7 +335,8 @@ class DomainRequest(TimeStampedModel):
|
|||
null=True,
|
||||
blank=True,
|
||||
related_name="request_sub_organization",
|
||||
help_text="The suborganization that this domain request is included under",
|
||||
help_text="If blank, request is associated with the overarching organization for this portfolio.",
|
||||
verbose_name="Suborganization",
|
||||
)
|
||||
|
||||
# This is the domain request user who created this domain request.
|
||||
|
@ -583,6 +583,10 @@ class DomainRequest(TimeStampedModel):
|
|||
blank=True,
|
||||
)
|
||||
|
||||
def is_awaiting_review(self) -> bool:
|
||||
"""Checks if the current status is in submitted or in_review"""
|
||||
return self.status in [self.DomainRequestStatus.SUBMITTED, self.DomainRequestStatus.IN_REVIEW]
|
||||
|
||||
def get_first_status_set_date(self, status):
|
||||
"""Returns the date when the domain request was first set to the given status."""
|
||||
log_entry = (
|
||||
|
@ -999,6 +1003,17 @@ class DomainRequest(TimeStampedModel):
|
|||
send_email=send_email,
|
||||
)
|
||||
|
||||
def is_withdrawable(self):
|
||||
"""Helper function that determines if the request can be withdrawn in its current status"""
|
||||
# This list is equivalent to the source field on withdraw. We need a better way to
|
||||
# consolidate these two lists - i.e. some sort of method that keeps these two lists in sync.
|
||||
# django fsm is very picky with what we can define in that field.
|
||||
return self.status in [
|
||||
self.DomainRequestStatus.SUBMITTED,
|
||||
self.DomainRequestStatus.IN_REVIEW,
|
||||
self.DomainRequestStatus.ACTION_NEEDED,
|
||||
]
|
||||
|
||||
@transition(
|
||||
field="status",
|
||||
source=[DomainRequestStatus.SUBMITTED, DomainRequestStatus.IN_REVIEW, DomainRequestStatus.ACTION_NEEDED],
|
||||
|
|
|
@ -22,21 +22,20 @@ class FederalAgency(TimeStampedModel):
|
|||
choices=BranchChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Federal agency type (executive, judicial, legislative, etc.)",
|
||||
)
|
||||
|
||||
initials = models.CharField(
|
||||
acronym = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Agency initials",
|
||||
help_text="Acronym commonly used to reference the federal agency (Optional)",
|
||||
)
|
||||
|
||||
is_fceb = models.BooleanField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="FCEB",
|
||||
help_text="Determines if this agency is FCEB",
|
||||
help_text="Federal Civilian Executive Branch (FCEB)",
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
|
|
@ -2,7 +2,6 @@ from django.db import models
|
|||
|
||||
from registrar.models.domain_request import DomainRequest
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
||||
|
@ -34,7 +33,6 @@ class Portfolio(TimeStampedModel):
|
|||
organization_name = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="Portfolio organization",
|
||||
)
|
||||
|
||||
organization_type = models.CharField(
|
||||
|
@ -42,7 +40,6 @@ class Portfolio(TimeStampedModel):
|
|||
choices=OrganizationChoices.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
help_text="Type of organization",
|
||||
)
|
||||
|
||||
notes = models.TextField(
|
||||
|
@ -53,7 +50,6 @@ class Portfolio(TimeStampedModel):
|
|||
federal_agency = models.ForeignKey(
|
||||
"registrar.FederalAgency",
|
||||
on_delete=models.PROTECT,
|
||||
help_text="Associated federal agency",
|
||||
unique=False,
|
||||
default=FederalAgency.get_non_federal_agency,
|
||||
)
|
||||
|
@ -64,6 +60,7 @@ class Portfolio(TimeStampedModel):
|
|||
unique=False,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="portfolios",
|
||||
)
|
||||
|
||||
address_line1 = models.CharField(
|
||||
|
@ -125,23 +122,6 @@ class Portfolio(TimeStampedModel):
|
|||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def portfolio_type(self):
|
||||
"""
|
||||
Returns a combination of organization_type / federal_type, seperated by ' - '.
|
||||
If no federal_type is found, we just return the org type.
|
||||
"""
|
||||
return self.get_portfolio_type(self.organization_type, self.federal_type)
|
||||
|
||||
@classmethod
|
||||
def get_portfolio_type(cls, organization_type, federal_type):
|
||||
org_type_label = cls.OrganizationChoices.get_org_label(organization_type)
|
||||
agency_type_label = BranchChoices.get_branch_label(federal_type)
|
||||
if organization_type == cls.OrganizationChoices.FEDERAL and agency_type_label:
|
||||
return " - ".join([org_type_label, agency_type_label])
|
||||
else:
|
||||
return org_type_label
|
||||
|
||||
@property
|
||||
def federal_type(self):
|
||||
"""Returns the federal_type value on the underlying federal_agency field"""
|
||||
|
@ -152,13 +132,19 @@ class Portfolio(TimeStampedModel):
|
|||
return federal_agency.federal_type if federal_agency else None
|
||||
|
||||
# == Getters for domains == #
|
||||
def get_domains(self):
|
||||
def get_domains(self, order_by=None):
|
||||
"""Returns all DomainInformations associated with this portfolio"""
|
||||
return self.information_portfolio.all()
|
||||
if not order_by:
|
||||
return self.information_portfolio.all()
|
||||
else:
|
||||
return self.information_portfolio.all().order_by(*order_by)
|
||||
|
||||
def get_domain_requests(self):
|
||||
def get_domain_requests(self, order_by=None):
|
||||
"""Returns all DomainRequests associated with this portfolio"""
|
||||
return self.DomainRequest_portfolio.all()
|
||||
if not order_by:
|
||||
return self.DomainRequest_portfolio.all()
|
||||
else:
|
||||
return self.DomainRequest_portfolio.all().order_by(*order_by)
|
||||
|
||||
# == Getters for suborganization == #
|
||||
def get_suborganizations(self):
|
||||
|
|
|
@ -10,7 +10,7 @@ class Suborganization(TimeStampedModel):
|
|||
name = models.CharField(
|
||||
unique=True,
|
||||
max_length=1000,
|
||||
help_text="Suborganization",
|
||||
verbose_name="Suborganization",
|
||||
)
|
||||
|
||||
portfolio = models.ForeignKey(
|
||||
|
|
|
@ -3,7 +3,6 @@ import logging
|
|||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest
|
||||
|
||||
from registrar.models import DomainInformation, UserDomainRole
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
|
||||
|
@ -14,6 +13,7 @@ from .transition_domain import TransitionDomain
|
|||
from .verified_by_staff import VerifiedByStaff
|
||||
from .domain import Domain
|
||||
from .domain_request import DomainRequest
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
from phonenumber_field.modelfields import PhoneNumberField # type: ignore
|
||||
|
@ -204,14 +204,10 @@ class User(AbstractUser):
|
|||
) or self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)
|
||||
|
||||
def has_organization_requests_flag(self):
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
return flag_is_active(request, "organization_requests")
|
||||
return flag_is_active_for_user(self, "organization_requests")
|
||||
|
||||
def has_organization_members_flag(self):
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
return flag_is_active(request, "organization_members")
|
||||
return flag_is_active_for_user(self, "organization_members")
|
||||
|
||||
def has_view_members_portfolio_permission(self, portfolio):
|
||||
# BEGIN
|
||||
|
@ -422,12 +418,8 @@ class User(AbstractUser):
|
|||
for invitation in PortfolioInvitation.objects.filter(
|
||||
email__iexact=self.email, status=PortfolioInvitation.PortfolioInvitationStatus.INVITED
|
||||
):
|
||||
# need to create a bogus request and assign user to it, in order to pass request
|
||||
# to flag_is_active
|
||||
request = HttpRequest()
|
||||
request.user = self
|
||||
only_single_portfolio = (
|
||||
not flag_is_active(request, "multiple_portfolios") and self.get_first_portfolio() is None
|
||||
not flag_is_active_for_user(self, "multiple_portfolios") and self.get_first_portfolio() is None
|
||||
)
|
||||
if only_single_portfolio or flag_is_active(None, "multiple_portfolios"):
|
||||
try:
|
||||
|
|
|
@ -66,6 +66,30 @@ class UserGroup(Group):
|
|||
"model": "federalagency",
|
||||
"permissions": ["add_federalagency", "change_federalagency", "delete_federalagency"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "portfolio",
|
||||
"permissions": ["add_portfolio", "change_portfolio", "delete_portfolio"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "suborganization",
|
||||
"permissions": ["add_suborganization", "change_suborganization", "delete_suborganization"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "seniorofficial",
|
||||
"permissions": ["add_seniorofficial", "change_seniorofficial", "delete_seniorofficial"],
|
||||
},
|
||||
{
|
||||
"app_label": "registrar",
|
||||
"model": "userportfoliopermission",
|
||||
"permissions": [
|
||||
"add_userportfoliopermission",
|
||||
"change_userportfoliopermission",
|
||||
"delete_userportfoliopermission",
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
# Avoid error: You can't execute queries until the end
|
||||
|
@ -113,7 +137,6 @@ class UserGroup(Group):
|
|||
+ cisa_analysts_group.name
|
||||
)
|
||||
|
||||
cisa_analysts_group.save()
|
||||
logger.debug("CISA Analyst permissions added to group " + cisa_analysts_group.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating analyst permissions group: {e}")
|
||||
|
@ -135,7 +158,6 @@ class UserGroup(Group):
|
|||
# Assign all permissions to the group
|
||||
full_access_group.permissions.add(*all_permissions)
|
||||
|
||||
full_access_group.save()
|
||||
logger.debug("All permissions added to group " + full_access_group.name)
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating full access group: {e}")
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from django.db import models
|
||||
from django.forms import ValidationError
|
||||
from django.http import HttpRequest
|
||||
from waffle import flag_is_active
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
@ -16,8 +15,6 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
PORTFOLIO_ROLE_PERMISSIONS = {
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
|
@ -26,14 +23,6 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||
],
|
||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY: [
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
# Domain: field specific permissions
|
||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||
],
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||
],
|
||||
|
@ -76,7 +65,19 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"User '{self.user}' on Portfolio '{self.portfolio}' " f"<Roles: {self.roles}>" if self.roles else ""
|
||||
readable_roles = []
|
||||
if self.roles:
|
||||
readable_roles = self.get_readable_roles()
|
||||
return f"{self.user}" f" <Roles: {', '.join(readable_roles)}>" if self.roles else ""
|
||||
|
||||
def get_readable_roles(self):
|
||||
"""Returns a readable list of self.roles"""
|
||||
readable_roles = []
|
||||
if self.roles:
|
||||
readable_roles = sorted(
|
||||
[UserPortfolioRoleChoices.get_user_portfolio_role_label(role) for role in self.roles]
|
||||
)
|
||||
return readable_roles
|
||||
|
||||
def _get_portfolio_permissions(self):
|
||||
"""
|
||||
|
@ -101,13 +102,11 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
# Check if a user is set without accessing the related object.
|
||||
has_user = bool(self.user_id)
|
||||
if self.pk is None and has_user:
|
||||
# Have to create a bogus request to set the user and pass to flag_is_active
|
||||
request = HttpRequest()
|
||||
request.user = self.user
|
||||
existing_permissions = UserPortfolioPermission.objects.filter(user=self.user)
|
||||
if not flag_is_active(request, "multiple_portfolios") and existing_permissions.exists():
|
||||
if not flag_is_active_for_user(self.user, "multiple_portfolios") and existing_permissions.exists():
|
||||
raise ValidationError(
|
||||
"Only one portfolio permission is allowed per user when multiple portfolios are disabled."
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
)
|
||||
|
||||
# Check if portfolio is set without accessing the related object.
|
||||
|
|
|
@ -334,3 +334,12 @@ def get_url_name(path):
|
|||
except Resolver404:
|
||||
logger.error(f"No matching URL name found for path: {path}")
|
||||
return None
|
||||
|
||||
|
||||
def value_of_attribute(obj, attribute_name: str):
|
||||
"""Returns the value of getattr if the attribute isn't callable.
|
||||
If it is, execute the underlying function and return that result instead."""
|
||||
value = getattr(obj, attribute_name)
|
||||
if callable(value):
|
||||
value = value()
|
||||
return value
|
||||
|
|
|
@ -7,9 +7,12 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
|||
"""
|
||||
|
||||
ORGANIZATION_ADMIN = "organization_admin", "Admin"
|
||||
ORGANIZATION_ADMIN_READ_ONLY = "organization_admin_read_only", "Admin read only"
|
||||
ORGANIZATION_MEMBER = "organization_member", "Member"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_role_label(cls, user_portfolio_role):
|
||||
return cls(user_portfolio_role).label if user_portfolio_role else None
|
||||
|
||||
|
||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||
""" """
|
||||
|
@ -29,3 +32,7 @@ class UserPortfolioPermissionChoices(models.TextChoices):
|
|||
# Domain: field specific permissions
|
||||
VIEW_SUBORGANIZATION = "view_suborganization", "View suborganization"
|
||||
EDIT_SUBORGANIZATION = "edit_suborganization", "Edit suborganization"
|
||||
|
||||
@classmethod
|
||||
def get_user_portfolio_permission_label(cls, user_portfolio_permission):
|
||||
return cls(user_portfolio_permission).label if user_portfolio_permission else None
|
||||
|
|
|
@ -49,11 +49,15 @@ class CheckUserProfileMiddleware:
|
|||
self.setup_page,
|
||||
self.logout_page,
|
||||
"/admin",
|
||||
# These are here as there is a bug with this middleware that breaks djangos built in debug console.
|
||||
# The debug console uses this directory, but since this overrides that, it throws errors.
|
||||
"/__debug__",
|
||||
]
|
||||
self.other_excluded_pages = [
|
||||
self.profile_page,
|
||||
self.logout_page,
|
||||
"/admin",
|
||||
"/__debug__",
|
||||
]
|
||||
|
||||
self.excluded_pages = {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %}{% translate "Unauthorized | " %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
<div class="grid-row grow-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %}{% translate "Forbidden | " %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
<div class="grid-row grow-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %}{% translate "Page not found | " %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %}{% translate "Server error | " %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-6 usa-prose margin-bottom-3">
|
||||
<h1>
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
<section class="usa-banner" aria-label="Official website of the United States government">
|
||||
<div class="usa-accordion">
|
||||
<header class="usa-banner__header">
|
||||
<div class="usa-banner__inner">
|
||||
<div class="usa-banner__inner {% if is_widescreen_mode %} usa-banner__inner--widescreen {% endif %}">
|
||||
<div class="grid-col-auto">
|
||||
<img class="usa-banner__header-flag" src="{% static 'img/us_flag_small.png' %}" alt="U.S. flag" />
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
{# Store the current object id so we can access it easier #}
|
||||
<input id="domain_request_id" class="display-none" value="{{original.id}}" />
|
||||
<input id="has_audit_logs" class="display-none" value="{%if filtered_audit_log_entries %}true{% else %}false{% endif %}"/>
|
||||
{% url 'get-action-needed-email-for-user-json' as url %}
|
||||
<input id="get-action-needed-email-for-user-json" class="display-none" value="{{ url }}" />
|
||||
{% for fieldset in adminform %}
|
||||
{% comment %}
|
||||
TODO: this will eventually need to be changed to something like this
|
||||
|
|
|
@ -39,7 +39,7 @@
|
|||
None<br>
|
||||
{% endif %}
|
||||
|
||||
{% else %}
|
||||
{% elif not hide_no_contact_info_message %}
|
||||
No additional contact information found.<br>
|
||||
{% endif %}
|
||||
|
||||
|
|
|
@ -66,24 +66,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
No changelog to display.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif field.field.name == "action_needed_reason_email" %}
|
||||
<div class="readonly textarea-wrapper">
|
||||
<div id="action_needed_reason_email_readonly" class="dja-readonly-textarea-container padding-1 margin-top-0 padding-top-0 margin-bottom-1 thin-border collapse--dgsimple collapsed">
|
||||
<label class="max-full" for="action_needed_reason_email_view_more">
|
||||
<strong>Sent to creator</strong>
|
||||
</label>
|
||||
<textarea id="action_needed_reason_email_view_more" cols="40" rows="20" class="{% if not original_object.action_needed_reason %}display-none{% endif %}" readonly>
|
||||
{{ original_object.action_needed_reason_email }}
|
||||
</textarea>
|
||||
<p id="no-email-message" class="{% if original_object.action_needed_reason %}display-none{% endif %}">No email will be sent.</p>
|
||||
</div>
|
||||
<button type="button" class="collapse-toggle--dgsimple usa-button usa-button--unstyled margin-top-0 margin-bottom-1 margin-left-1">
|
||||
<span>Show details</span>
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% elif field.field.name == "other_contacts" %}
|
||||
{% if all_contacts.count > 2 %}
|
||||
<div class="readonly">
|
||||
|
@ -137,7 +119,7 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
{% endfor %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
{% elif field.field.name == "display_admins" %}
|
||||
{% elif field.field.name == "domain_managers" or field.field.name == "invited_domain_managers" %}
|
||||
<div class="readonly">{{ field.contents|safe }}</div>
|
||||
{% elif field.field.name == "display_members" %}
|
||||
<div class="readonly">
|
||||
|
@ -155,131 +137,102 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
|
||||
{% block field_other %}
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
<div>
|
||||
<div id="action-needed-reason-email-placeholder-text" class="margin-top-05 text-faded">
|
||||
-
|
||||
|
||||
<div class="margin-top-05 text-faded field-action_needed_reason_email__placeholder">
|
||||
–
|
||||
</div>
|
||||
<div>
|
||||
<div id="action-needed-reason-email-readonly" class="display-none usa-summary-box_admin padding-top-0 margin-top-0">
|
||||
<div class="flex-container">
|
||||
<div class="margin-top-05">
|
||||
<p class="{% if action_needed_email_sent %}display-none{% endif %}" id="action-needed-email-header"><b>Auto-generated email that will be sent to the creator</b></p>
|
||||
<p class="{% if not action_needed_email_sent %}display-none{% endif %}" id="action-needed-email-header-email-sent">
|
||||
<svg class="usa-icon text-green" aria-hidden="true" focusable="false" role="img">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_circle"></use>
|
||||
</svg>
|
||||
<b>Email sent to the creator</b>
|
||||
|
||||
{{ field.field }}
|
||||
|
||||
<button
|
||||
aria-label="Edit email in textarea"
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text margin-left-1 text-no-underline field-action_needed_reason_email__edit flex-align-self-start"
|
||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</button
|
||||
>
|
||||
<a
|
||||
href="#email-already-sent-modal"
|
||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1 field-action_needed_reason_email__modal-trigger flex-align-self-start"
|
||||
aria-controls="email-already-sent-modal"
|
||||
data-open-modal
|
||||
><img src="/public/admin/img/icon-changelink.svg" alt="Change"> Edit email</a
|
||||
>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="email-already-sent-modal"
|
||||
aria-labelledby="Are you sure you want to edit this email?"
|
||||
aria-describedby="The creator of this request already received an email"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading">
|
||||
Are you sure you want to edit this email?
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>
|
||||
The creator of this request already received an email for this status/reason:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="font-body-sm">Status: <b>Action needed</b></li>
|
||||
<li class="font-body-sm">Reason: <b>{{ original_object.get_action_needed_reason_display }}</b></li>
|
||||
</ul>
|
||||
<p>
|
||||
If you edit this email's text, <b>the system will send another email</b> to
|
||||
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
|
||||
</p>
|
||||
</div>
|
||||
<div class="vertical-separator margin-top-1 margin-bottom-1"></div>
|
||||
<a
|
||||
href="#email-already-sent-modal"
|
||||
class="usa-button usa-button--unstyled usa-button--dja-link-color usa-button__small-text text-no-underline margin-left-1"
|
||||
aria-controls="email-already-sent-modal"
|
||||
data-open-modal
|
||||
>Edit email</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="usa-modal"
|
||||
id="email-already-sent-modal"
|
||||
aria-labelledby="Are you sure you want to edit this email?"
|
||||
aria-describedby="The creator of this request already received an email"
|
||||
>
|
||||
<div class="usa-modal__content">
|
||||
<div class="usa-modal__main">
|
||||
<h2 class="usa-modal__heading" id="modal-1-heading">
|
||||
Are you sure you want to edit this email?
|
||||
</h2>
|
||||
<div class="usa-prose">
|
||||
<p>
|
||||
The creator of this request already received an email for this status/reason:
|
||||
</p>
|
||||
<ul>
|
||||
<li class="font-body-sm">Status: <b>Action needed</b></li>
|
||||
<li class="font-body-sm">Reason: <b>{{ original_object.get_action_needed_reason_display }}</b></li>
|
||||
</ul>
|
||||
<p>
|
||||
If you edit this email's text, <b>the system will send another email</b> to
|
||||
the creator after you “save” your changes. If you do not want to send another email, click “cancel” below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
id="email-already-sent-modal_continue-editing-button"
|
||||
data-close-modal
|
||||
>
|
||||
Yes, continue editing
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
name="_cancel_edit_email"
|
||||
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 class="usa-modal__footer">
|
||||
<ul class="usa-button-group">
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="submit"
|
||||
class="usa-button"
|
||||
id="confirm-edit-email"
|
||||
data-close-modal
|
||||
>
|
||||
Yes, continue editing
|
||||
</button>
|
||||
</li>
|
||||
<li class="usa-button-group__item">
|
||||
<button
|
||||
type="button"
|
||||
class="usa-button usa-button--unstyled padding-105 text-center"
|
||||
name="_cancel_edit_email"
|
||||
data-close-modal
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<label class="sr-only" for="action-needed-reason-email-readonly-textarea">Email:</label>
|
||||
<textarea cols="40" rows="10" class="vLargeTextField" id="action-needed-reason-email-readonly-textarea" readonly>{{ field.field.value|striptags }}
|
||||
</textarea>
|
||||
</div>
|
||||
<div>
|
||||
{{ field.field }}
|
||||
<input id="action-needed-email-sent" class="display-none" value="{{action_needed_email_sent}}">
|
||||
<input id="action-needed-email-last-sent-text" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
||||
<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>
|
||||
<span id="action-needed-email-footer" class="help">
|
||||
{% if not action_needed_email_sent %}
|
||||
This email will be sent to the creator of this request after saving
|
||||
|
||||
{% if original_object.action_needed_reason_email %}
|
||||
<input id="last-sent-email-content" class="display-none" value="{{original_object.action_needed_reason_email}}">
|
||||
{% else %}
|
||||
This email has been sent to the creator of this request
|
||||
<input id="last-sent-email-content" class="display-none" value="None">
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
{{ field.field }}
|
||||
{% endif %}
|
||||
{% endblock field_other %}
|
||||
|
||||
{% block after_help_text %}
|
||||
{% if field.field.name == "action_needed_reason_email" %}
|
||||
{% comment %}
|
||||
Store the action needed reason emails in a json-based dictionary.
|
||||
This allows us to change the action_needed_reason_email field dynamically, depending on value.
|
||||
The alternative to this is an API endpoint.
|
||||
|
||||
Given that we have a limited number of emails, doing it this way makes sense.
|
||||
{% endcomment %}
|
||||
{% if action_needed_reason_emails %}
|
||||
<script id="action-needed-emails-data" type="application/json">
|
||||
{{ action_needed_reason_emails|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% elif field.field.name == "creator" %}
|
||||
{% if field.field.name == "creator" %}
|
||||
<div class="flex-container tablet:margin-top-2">
|
||||
<label aria-label="Creator contact details"></label>
|
||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.creator no_title_top_padding=field.is_readonly user_verification_type=original_object.creator.get_verification_type_display%}
|
||||
|
@ -335,13 +288,6 @@ This is using a custom implementation fieldset.html (see admin/fieldset.html)
|
|||
</details>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% elif field.field.name == "display_members" and field.contents %}
|
||||
<details class="margin-top-1 dja-detail-table" aria-role="button" open>
|
||||
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{{ field.contents|safe }}
|
||||
</div>
|
||||
</details>
|
||||
{% elif field.field.name == "state_territory" and original_object|model_name_lowercase != 'portfolio' %}
|
||||
<div class="flex-container margin-top-2">
|
||||
<span>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
|
||||
{% comment %} This view provides a detail button that can be used to show/hide content {% endcomment %}
|
||||
<details class="margin-top-1 dja-detail-table" aria-role="button" {% if start_open %}open{% else %}closed{% endif %}>
|
||||
<summary class="padding-1 padding-left-0 dja-details-summary">Details</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% block detail_content %}
|
||||
{% endblock detail_content%}
|
||||
</div>
|
||||
</details>
|
|
@ -0,0 +1,48 @@
|
|||
{% extends "django/admin/includes/details_button.html" %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block detail_content %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for admin in admins %}
|
||||
{% url 'admin:registrar_userportfoliopermission_change' admin.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ admin.user.get_formatted_name}}</a></td>
|
||||
<td>{{ admin.user.title }}</td>
|
||||
<td>
|
||||
{% if admin.user.email %}
|
||||
{{ admin.user.email }}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ admin.user.phone }}</td>
|
||||
<td class="padding-left-1 text-size-small">
|
||||
{% if admin.user.email %}
|
||||
<input aria-hidden="true" class="display-none" value="{{ admin.user.email }}" />
|
||||
<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"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="usa-icon"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<span>Copy email</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock detail_content %}
|
|
@ -0,0 +1,26 @@
|
|||
{% extends "django/admin/includes/details_button.html" %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block detail_content %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain_request in domain_requests %}
|
||||
{% url 'admin:registrar_domainrequest_change' domain_request.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ domain_request }}</a></td>
|
||||
{% if domain_request.get_status_display %}
|
||||
<td>{{ domain_request.get_status_display }}</td>
|
||||
{% else %}
|
||||
<td>None</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock detail_content %}
|
|
@ -0,0 +1,30 @@
|
|||
{% extends "django/admin/includes/details_button.html" %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block detail_content %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>State</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for domain_info in domains %}
|
||||
{% if domain_info.domain %}
|
||||
{% with domain=domain_info.domain %}
|
||||
{% url 'admin:registrar_domain_change' domain.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ domain }}</a></td>
|
||||
{% if domain and domain.get_state_display %}
|
||||
<td>{{ domain.get_state_display }}</td>
|
||||
{% else %}
|
||||
<td>None</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock detail_content%}
|
|
@ -0,0 +1,61 @@
|
|||
{% extends "django/admin/includes/detail_table_fieldset.html" %}
|
||||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block field_readonly %}
|
||||
{% if field.field.name == "display_admins" or field.field.name == "display_members" %}
|
||||
<div class="readonly">{{ field.contents|safe }}</div>
|
||||
{% elif field.field.name == "roles" %}
|
||||
<div class="readonly">
|
||||
{% if get_readable_roles %}
|
||||
{{ get_readable_roles }}
|
||||
{% else %}
|
||||
<p>No roles found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.name == "additional_permissions" %}
|
||||
<div class="readonly">
|
||||
{% if display_permissions %}
|
||||
{{ display_permissions }}
|
||||
{% else %}
|
||||
<p>No additional permissions found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% elif field.field.name == "senior_official" %}
|
||||
{% if original_object.senior_official %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% else %}
|
||||
{% url "admin:registrar_seniorofficial_add" as url %}
|
||||
<div class="readonly">
|
||||
<a href={{ url }}>No senior official found. Create one now.</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="readonly">{{ field.contents }}</div>
|
||||
{% endif %}
|
||||
{% endblock field_readonly%}
|
||||
|
||||
{% block after_help_text %}
|
||||
{% if field.field.name == "senior_official" %}
|
||||
<div class="flex-container">
|
||||
<label aria-label="Senior official contact details"></label>
|
||||
{% include "django/admin/includes/contact_detail_list.html" with user=original_object.senior_official no_title_top_padding=field.is_readonly hide_no_contact_info_message=True %}
|
||||
</div>
|
||||
{% elif field.field.name == "display_admins" %}
|
||||
{% if admins|length > 0 %}
|
||||
{% include "django/admin/includes/portfolio/portfolio_admins_table.html" with admins=admins %}
|
||||
{% endif %}
|
||||
{% elif field.field.name == "display_members" %}
|
||||
{% if members|length > 0 %}
|
||||
{% include "django/admin/includes/portfolio/portfolio_members_table.html" with members=members %}
|
||||
{% endif %}
|
||||
{% elif field.field.name == "domains" %}
|
||||
{% if domains|length > 0 %}
|
||||
{% include "django/admin/includes/portfolio/portfolio_domains_table.html" with domains=domains %}
|
||||
{% endif %}
|
||||
{% elif field.field.name == "domain_requests" %}
|
||||
{% if domain_requests|length > 0 %}
|
||||
{% include "django/admin/includes/portfolio/portfolio_domain_requests_table.html" with domain_requests=domain_requests %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock after_help_text %}
|
|
@ -0,0 +1,55 @@
|
|||
{% extends "django/admin/includes/details_button.html" %}
|
||||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block detail_content %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Title</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Roles</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for member in members %}
|
||||
{% url 'admin:registrar_userportfoliopermission_change' member.pk as url %}
|
||||
<tr>
|
||||
<td><a href={{url}}>{{ member.user.get_formatted_name}}</a></td>
|
||||
<td>{{ member.user.title }}</td>
|
||||
<td>
|
||||
{% if member.user.email %}
|
||||
{{ member.user.email }}
|
||||
{% else %}
|
||||
None
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ member.user.phone }}</td>
|
||||
<td>
|
||||
{% for role in member.user|portfolio_role_summary:original %}
|
||||
<span class="usa-tag">{{ role }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="padding-left-1 text-size-small">
|
||||
{% if member.user.email %}
|
||||
<input aria-hidden="true" class="display-none" value="{{ member.user.email }}" />
|
||||
<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"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
class="usa-icon"
|
||||
>
|
||||
<use aria-hidden="true" xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<span>Copy email</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endblock %}
|
|
@ -8,19 +8,14 @@
|
|||
<input id="senior_official_from_agency_json_url" class="display-none" value="{{url}}" />
|
||||
{% url 'get-federal-and-portfolio-types-from-federal-agency-json' as url %}
|
||||
<input id="federal_and_portfolio_types_from_agency_json_url" class="display-none" value="{{url}}" />
|
||||
{% url "admin:registrar_seniorofficial_add" as url %}
|
||||
<input id="senior-official-add-url" class="display-none" value="{{url}}" />
|
||||
{{ block.super }}
|
||||
{% endblock content %}
|
||||
|
||||
{% block field_sets %}
|
||||
{% for fieldset in adminform %}
|
||||
{% comment %}
|
||||
This is a placeholder for now.
|
||||
|
||||
Disclaimer:
|
||||
When extending the fieldset view consider whether you need to make a new one that extends from detail_table_fieldset.
|
||||
detail_table_fieldset is used on multiple admin pages, so a change there can have unintended consequences.
|
||||
{% endcomment %}
|
||||
{% include "django/admin/includes/detail_table_fieldset.html" with original_object=original %}
|
||||
{% include "django/admin/includes/portfolio/portfolio_fieldset.html" with original_object=original %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -8,27 +8,35 @@
|
|||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Domain requests</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for domain_request in domain_requests %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
|
||||
{{ domain_request.requested_domain }}
|
||||
</a>
|
||||
({{ domain_request.status }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if domains|length > 0 %}
|
||||
{% for domain_request in domain_requests %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domainrequest_change' domain_request.pk %}">
|
||||
{{ domain_request.requested_domain }}
|
||||
</a>
|
||||
({{ domain_request.status }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li>No domain requests.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Domains</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for domain in domains %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domain_change' domain.pk %}">
|
||||
{{ domain.name }}
|
||||
</a>
|
||||
({{ domain.state }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if domains|length > 0 %}
|
||||
{% for domain in domains %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_domain_change' domain.pk %}">
|
||||
{{ domain.name }}
|
||||
</a>
|
||||
({{ domain.state }})
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<li>No domains.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -17,26 +17,6 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block after_related_objects %}
|
||||
{% if portfolios %}
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Portfolio information</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6 desktop:grid-col-4">
|
||||
<h3>Portfolios</h3>
|
||||
<ul class="margin-0 padding-0">
|
||||
{% for portfolio in portfolios %}
|
||||
<li>
|
||||
<a href="{% url 'admin:registrar_portfolio_change' portfolio.pk %}">
|
||||
{{ portfolio }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="module aligned padding-3">
|
||||
<h2>Associated requests and domains</h2>
|
||||
<div class="grid-row grid-gap mobile:padding-x-1 desktop:padding-x-4">
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
{% extends 'django/admin/email_clipboard_change_form.html' %}
|
||||
{% load custom_filters %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block field_sets %}
|
||||
{% for fieldset in adminform %}
|
||||
{% comment %}
|
||||
This is a placeholder for now.
|
||||
|
||||
Disclaimer:
|
||||
When extending the fieldset view consider whether you need to make a new one that extends from detail_table_fieldset.
|
||||
detail_table_fieldset is used on multiple admin pages, so a change there can have unintended consequences.
|
||||
{% endcomment %}
|
||||
{% include "django/admin/includes/user_portfolio_permission_fieldset.html" with original_object=original %}
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -9,7 +9,7 @@
|
|||
<div class="grid-row grid-gap">
|
||||
<div class="tablet:grid-col-3">
|
||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||
text-primary-darker text-semibold"
|
||||
text-primary-darker text-semibold domain-name-wrap"
|
||||
>
|
||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||
</p>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block domain_content %}
|
||||
{{ block.super }}
|
||||
<div class="margin-top-4 tablet:grid-col-10">
|
||||
<h2 class="text-bold text-primary-dark">{{ domain.name }}</h2>
|
||||
<h2 class="text-bold text-primary-dark domain-name-wrap">{{ domain.name }}</h2>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box padding-bottom-0 margin-top-3 padding-left-2{% if not domain.is_expired %}{% if domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %} dotgov-status-box--action-need{% endif %}{% endif %}"
|
||||
role="region"
|
||||
|
|
|
@ -19,183 +19,5 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% for step in steps.all|slice:":-1" %}
|
||||
<section class="summary-item margin-top-3">
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.generic_org_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if domain_request.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=domain_request %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.SENIOR_OFFICIAL %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.senior_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.senior_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe%}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in domain_request.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.creator is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.creator %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% if domain_request.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step %}
|
||||
{% if domain_request.has_additional_details %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.cisa_representative_first_name %}
|
||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||
{% if domain_request.cisa_representative_email %}
|
||||
<li>{{domain_request.cisa_representative_email}}</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.anything_else %}
|
||||
{{domain_request.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=True edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% include "includes/request_review_steps.html" with is_editable=True %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,206 +1,10 @@
|
|||
{% extends 'base.html' %}
|
||||
|
||||
{% load custom_filters %}
|
||||
|
||||
{% block title %}Domain request status | {{ DomainRequest.requested_domain.name }} | {% endblock %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block title %}Domain request status | {{ DomainRequest.requested_domain.name }} | {% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url %}
|
||||
{% else %}
|
||||
{% url 'home' as url %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
{% if portfolio %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
{% else %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<span>New domain request</span>
|
||||
{% else %}
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
|
||||
{% comment %}
|
||||
These are intentionally seperated this way.
|
||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||
Leave it this way until we've solidified our requirements.
|
||||
{% endcomment %}
|
||||
{% if DomainRequest.status == statuses.STARTED %}
|
||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||
<p class="margin-top-1">
|
||||
{% comment %}
|
||||
A newly created domain request will not have a value for last_status update.
|
||||
This is because the status never really updated.
|
||||
However, if this somehow goes back to started we can default to displaying that new date.
|
||||
{% endcomment %}
|
||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
||||
</p>
|
||||
{% endwith %}
|
||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% else %}
|
||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.status != 'rejected' %}
|
||||
<p>{% include "includes/domain_request.html" %}</p>
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.senior_official %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
{% include "includes/request_status_manage.html" %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block title %} Home | {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<main id="main-content" class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
{% if user.is_authenticated %}
|
||||
{# the entire logged in page goes here #}
|
||||
|
||||
|
|
|
@ -5,8 +5,10 @@
|
|||
</h2>
|
||||
<p>We received your .gov domain request. Our next step is to review your request. This usually takes 30 business days. We’ll email you if we have questions and when we complete our review. <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'contact' %}">Contact us with any questions</a>.</p>
|
||||
|
||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
||||
Need to make changes?
|
||||
</h2>
|
||||
{% if show_withdraw_text %}
|
||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
||||
Need to make changes?
|
||||
</h2>
|
||||
|
||||
<p>If you need to change your request you have to first withdraw it. Once you withdraw the request you can edit it and submit it again. Changing your request might add to the wait time.</p>
|
||||
<p>If you need to change your request you have to first withdraw it. Once you withdraw the request you can edit it and submit it again. Changing your request might add to the wait time.</p>
|
||||
{% endif %}
|
|
@ -0,0 +1,236 @@
|
|||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
{% block breadcrumb %}
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url %}
|
||||
{% else %}
|
||||
{% url 'home' as url %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
{% if portfolio %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
{% else %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<span>New domain request</span>
|
||||
{% else %}
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock breadcrumb %}
|
||||
|
||||
{% block header %}
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<h1>New domain request</h1>
|
||||
{% else %}
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
{% endif %}
|
||||
{% endblock header %}
|
||||
|
||||
{% block status_summary %}
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
{% endblock status_summary %}
|
||||
|
||||
{% block status_metadata %}
|
||||
|
||||
{% if portfolio %}
|
||||
{% if DomainRequest.creator %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
|
||||
{% comment %}
|
||||
These are intentionally seperated this way.
|
||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||
Leave it this way until we've solidified our requirements.
|
||||
{% endcomment %}
|
||||
{% if DomainRequest.status == statuses.STARTED %}
|
||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||
<p class="margin-top-1">
|
||||
{% comment %}
|
||||
A newly created domain request will not have a value for last_status update.
|
||||
This is because the status never really updated.
|
||||
However, if this somehow goes back to started we can default to displaying that new date.
|
||||
{% endcomment %}
|
||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
||||
</p>
|
||||
{% endwith %}
|
||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% else %}
|
||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock status_metadata %}
|
||||
|
||||
{% block status_blurb %}
|
||||
{% if DomainRequest.is_awaiting_review %}
|
||||
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
|
||||
{% endif %}
|
||||
{% endblock status_blurb %}
|
||||
|
||||
{% block modify_request %}
|
||||
{% if DomainRequest.is_withdrawable %}
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock modify_request %}
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
{% block request_summary_header %}
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% endblock request_summary_header%}
|
||||
|
||||
{% block request_summary %}
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.senior_official %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock request_summary%}
|
||||
</div>
|
||||
</main>
|
|
@ -13,7 +13,7 @@
|
|||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% endif %}
|
||||
<div class="section-outlined__search {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6{% endif %}">
|
||||
<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="Domains search component" class="margin-top-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
|
@ -44,7 +44,7 @@
|
|||
{% if user_domain_count and user_domain_count > 0 %}
|
||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
||||
<section aria-label="Domains report component" class="margin-top-205">
|
||||
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled usa-button--with-icon" role="button">
|
||||
<a href="{% url 'export_data_type_user' %}" 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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
<footer class="usa-footer">
|
||||
<div class="usa-footer__secondary-section">
|
||||
<div class="grid-container">
|
||||
<div class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
<div class="grid-row grid-gap">
|
||||
<div
|
||||
class="
|
||||
|
@ -51,7 +51,7 @@
|
|||
class="usa-identifier__section usa-identifier__section--masthead"
|
||||
aria-label="Agency identifier"
|
||||
>
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
|
||||
<div class="usa-identifier__logos">
|
||||
<a rel="noopener noreferrer" target="_blank" href="https://www.cisa.gov" class="usa-identifier__logo"
|
||||
><img
|
||||
|
@ -77,7 +77,7 @@
|
|||
class="usa-identifier__section usa-identifier__section--required-links"
|
||||
aria-label="Important links"
|
||||
>
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
|
||||
<ul class="usa-identifier__required-links-list">
|
||||
<li class="usa-identifier__required-links-item">
|
||||
<a rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/' %}"
|
||||
|
@ -119,7 +119,7 @@
|
|||
class="usa-identifier__section usa-identifier__section--usagov"
|
||||
aria-label="U.S. government information and services"
|
||||
>
|
||||
<div class="usa-identifier__container">
|
||||
<div class="usa-identifier__container {% if is_widescreen_mode %} usa-identifier__container--widescreen {% endif %}">
|
||||
<div class="usa-identifier__usagov-description">
|
||||
Looking for U.S. government information and services?
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load static %}
|
||||
|
||||
<header class="usa-header usa-header--basic">
|
||||
<div class="usa-nav-container">
|
||||
<div class="usa-nav-container {% if is_widescreen_mode %} usa-nav-container--widescreen {% endif %}">
|
||||
<div class="usa-navbar">
|
||||
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
{% load custom_filters %}
|
||||
|
||||
<header class="usa-header usa-header--extended">
|
||||
<div class="usa-navbar">
|
||||
<div class="usa-navbar {% if is_widescreen_mode %} usa-navbar--widescreen {% endif %}">
|
||||
{% include "includes/gov_extended_logo.html" with logo_clickable=logo_clickable %}
|
||||
<button type="button" class="usa-menu-btn">Menu</button>
|
||||
</div>
|
||||
{% block usa_nav %}
|
||||
<nav class="usa-nav" aria-label="Primary navigation">
|
||||
<div class="usa-nav__inner">
|
||||
<div class="usa-nav__inner {% if is_widescreen_mode %} usa-nav__inner--widescreen {% endif %}">
|
||||
<button type="button" class="usa-nav__close">
|
||||
<img src="{%static 'img/usa-icons/close.svg'%}" role="img" alt="Close" />
|
||||
</button>
|
||||
|
@ -91,9 +91,9 @@
|
|||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% if has_organization_members_flag %}
|
||||
{% if has_organization_members_flag and has_view_members_portfolio_permission %}
|
||||
<li class="usa-nav__primary-item">
|
||||
<a href="#" class="usa-nav-link">
|
||||
<a href="/members/" class="usa-nav-link {% if path|is_members_subpage %} usa-current{% endif %}">
|
||||
Members
|
||||
</a>
|
||||
</li>
|
||||
|
|
80
src/registrar/templates/includes/members_table.html
Normal file
80
src/registrar/templates/includes/members_table.html
Normal file
|
@ -0,0 +1,80 @@
|
|||
{% load static %}
|
||||
|
||||
<!-- Embedding the portfolio value in a data attribute -->
|
||||
<span id="portfolio-js-value" class="display-none" data-portfolio="{{ portfolio.id }}"></span>
|
||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||
{% url 'get_portfolio_members_json' as url %}
|
||||
<span id="get_members_json_url" class="display-none">{{url}}</span>
|
||||
<section class="section-outlined members margin-top-0 section-outlined--border-base-light" id="members">
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-6">
|
||||
<section aria-label="Members search component" class="margin-top-2">
|
||||
<form class="usa-search usa-search--small" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 members__reset-search display-none" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<label class="usa-sr-only" for="members__search-field">Search by member name</label>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="members__search-field"
|
||||
type="search"
|
||||
name="search"
|
||||
placeholder="Search by member name"
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="members__search-field-submit">
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
<div class="members__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 members__table">
|
||||
<caption class="sr-only">Your registered members</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortable="member" scope="col" role="columnheader">Member</th>
|
||||
<th data-sortable="last_active" scope="col" role="columnheader">Last Active</th>
|
||||
<th
|
||||
scope="col"
|
||||
role="columnheader"
|
||||
>
|
||||
<span class="usa-sr-only">Action</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<!-- AJAX will populate this tbody -->
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
class="usa-sr-only usa-table__announcement-region"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
</div>
|
||||
<div class="members__no-data display-none">
|
||||
<p>You don't have any members.</p>
|
||||
</div>
|
||||
<div class="members__no-search-results display-none">
|
||||
<p>No results found</p>
|
||||
</div>
|
||||
</section>
|
||||
<nav aria-label="Pagination" class="usa-pagination flex-justify" id="members-pagination">
|
||||
<span class="usa-pagination__counter text-base-dark padding-left-2 margin-bottom-1">
|
||||
<!-- Count will be dynamically populated by JS -->
|
||||
</span>
|
||||
<ul class="usa-pagination__list">
|
||||
<!-- Pagination links will be dynamically populated by JS -->
|
||||
</ul>
|
||||
</nav>
|
|
@ -1,6 +1,6 @@
|
|||
<div class="usa-site-alert--emergency margin-y-0 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||
<div class="usa-alert">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %}">
|
||||
<div class="usa-alert__body {% if add_body_class %}{{ add_body_class }}{% endif %} {% if is_widescreen_mode %}usa-alert__body--widescreen{% endif %}">
|
||||
<b>Attention:</b> You are on a test site.
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -25,4 +25,4 @@
|
|||
{% if organization.urbanization %}
|
||||
<br />{{ organization.urbanization }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</address>
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.REQUESTING_ENTITY %}
|
||||
|
||||
{% if domain_request.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=domain_request %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif%}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% if domain_request.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe%}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in domain_request.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% with title=form_titles|get_item:step %}
|
||||
{% if domain_request.has_additional_details %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.cisa_representative_first_name %}
|
||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||
{% if domain_request.cisa_representative_email %}
|
||||
<li>{{domain_request.cisa_representative_email}}</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.anything_else %}
|
||||
{{domain_request.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
|
@ -0,0 +1,18 @@
|
|||
{% extends 'includes/request_status_manage.html' %}
|
||||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% comment %} Do not show the withdrawal text in viewonly mode {% endcomment %}
|
||||
{% block status_blurb %}
|
||||
{% if DomainRequest.is_awaiting_review %}
|
||||
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=False %}</p>
|
||||
{% endif %}
|
||||
{% endblock status_blurb %}
|
||||
|
||||
{% comment %} Do not show action buttons in viewonly mode {% endcomment %}
|
||||
{% block modify_request %}
|
||||
{% endblock modify_request %}
|
||||
|
||||
{% block request_summary %}
|
||||
{% include "includes/portfolio_request_review_steps.html" with domain_request=DomainRequest is_editable=False %}
|
||||
{% endblock request_summary %}
|
168
src/registrar/templates/includes/request_review_steps.html
Normal file
168
src/registrar/templates/includes/request_review_steps.html
Normal file
|
@ -0,0 +1,168 @@
|
|||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
{% if domain_request.generic_org_type is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_generic_org_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.TRIBAL_GOVERNMENT %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.tribe_name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% if domain_request.federally_recognized_tribe %}<p>Federally-recognized tribe</p>{% endif %}
|
||||
{% if domain_request.state_recognized_tribe %}<p>State-recognized tribe</p>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ORGANIZATION_FEDERAL %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.get_federal_type_display|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_ELECTION %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_CONTACT %}
|
||||
{% if domain_request.organization_name %}
|
||||
{% with title=form_titles|get_item:step value=domain_request %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url address='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ABOUT_YOUR_ORGANIZATION %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.about_your_organization|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.SENIOR_OFFICIAL %}
|
||||
{% if domain_request.senior_official is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.senior_official %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.CURRENT_SITES %}
|
||||
{% if domain_request.current_websites.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value='None' %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.DOTGOV_DOMAIN %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.requested_domain.name|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe%}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
|
||||
{% if domain_request.alternative_domains.all %}
|
||||
<h3 class="register-form-review-header">Alternative domains</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for site in domain_request.alternative_domains.all %}
|
||||
<li>{{ site.website }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.PURPOSE %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.purpose|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.YOUR_CONTACT %}
|
||||
{% if domain_request.creator is not None %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.creator %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url contact='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.OTHER_CONTACTS %}
|
||||
{% if domain_request.other_contacts.all %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url contact='true' list='true' %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.no_other_contacts_rationale|default:"<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.ADDITIONAL_DETAILS %}
|
||||
{% with title=form_titles|get_item:step %}
|
||||
{% if domain_request.has_additional_details %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.cisa_representative_first_name %}
|
||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||
{% if domain_request.cisa_representative_email %}
|
||||
<li>{{domain_request.cisa_representative_email}}</li>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if domain_request.anything_else %}
|
||||
{{domain_request.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title="Additional Details" value="<span class='text-bold text-secondary-dark'>Incomplete</span>"|safe heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if step == Step.REQUIREMENTS %}
|
||||
{% with title=form_titles|get_item:step value=domain_request.is_policy_acknowledged|yesno:"I agree.,I do not agree.,I do not agree." %}
|
||||
{% include "includes/summary_item.html" with title=title value=value heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
236
src/registrar/templates/includes/request_status_manage.html
Normal file
236
src/registrar/templates/includes/request_status_manage.html
Normal file
|
@ -0,0 +1,236 @@
|
|||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
<main id="main-content" class="grid-container">
|
||||
<div class="grid-col desktop:grid-offset-2 desktop:grid-col-8">
|
||||
{% block breadcrumb %}
|
||||
{% if portfolio %}
|
||||
{% url 'domain-requests' as url %}
|
||||
{% else %}
|
||||
{% url 'home' as url %}
|
||||
{% endif %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain request breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
{% if portfolio %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Domain requests</span></a>
|
||||
{% else %}
|
||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Manage your domains</span></a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<span>New domain request</span>
|
||||
{% else %}
|
||||
<span>{{ DomainRequest.requested_domain.name }}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
{% endblock breadcrumb %}
|
||||
|
||||
{% block header %}
|
||||
{% if not DomainRequest.requested_domain and DomainRequest.status == DomainRequest.DomainRequestStatus.STARTED %}
|
||||
<h1>New domain request</h1>
|
||||
{% else %}
|
||||
<h1>Domain request for {{ DomainRequest.requested_domain.name }}</h1>
|
||||
{% endif %}
|
||||
{% endblock header %}
|
||||
|
||||
{% block status_summary %}
|
||||
<div
|
||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
||||
role="region"
|
||||
aria-labelledby="summary-box-key-information"
|
||||
>
|
||||
<div class="usa-summary-box__body">
|
||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
||||
id="summary-box-key-information"
|
||||
>
|
||||
<span class="text-bold text-primary-darker">
|
||||
Status:
|
||||
</span>
|
||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
{% endblock status_summary %}
|
||||
|
||||
{% block status_metadata %}
|
||||
|
||||
{% if portfolio %}
|
||||
{% if DomainRequest.creator %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">No creator found:</b> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% with statuses=DomainRequest.DomainRequestStatus last_submitted=DomainRequest.last_submitted_date|date:"F j, Y" first_submitted=DomainRequest.first_submitted_date|date:"F j, Y" last_status_update=DomainRequest.last_status_update|date:"F j, Y" %}
|
||||
{% comment %}
|
||||
These are intentionally seperated this way.
|
||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
||||
Leave it this way until we've solidified our requirements.
|
||||
{% endcomment %}
|
||||
{% if DomainRequest.status == statuses.STARTED %}
|
||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||
<p class="margin-top-1">
|
||||
{% comment %}
|
||||
A newly created domain request will not have a value for last_status update.
|
||||
This is because the status never really updated.
|
||||
However, if this somehow goes back to started we can default to displaying that new date.
|
||||
{% endcomment %}
|
||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
||||
</p>
|
||||
{% endwith %}
|
||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||
<p class="margin-top-1 margin-bottom-1">
|
||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
||||
</p>
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
||||
</p>
|
||||
{% else %}
|
||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||
<p class="margin-top-1">
|
||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock status_metadata %}
|
||||
|
||||
{% block status_blurb %}
|
||||
{% if DomainRequest.is_awaiting_review %}
|
||||
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
|
||||
{% endif %}
|
||||
{% endblock status_blurb %}
|
||||
|
||||
{% block modify_request %}
|
||||
{% if DomainRequest.is_withdrawable %}
|
||||
<p><a href="{% url 'domain-request-withdraw-confirmation' pk=DomainRequest.id %}" class="usa-button usa-button--outline withdraw_outline">
|
||||
Withdraw request</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock modify_request %}
|
||||
</div>
|
||||
|
||||
<div class="grid-col desktop:grid-offset-2 maxw-tablet">
|
||||
{% block request_summary_header %}
|
||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
||||
{% endblock request_summary_header%}
|
||||
|
||||
{% block request_summary %}
|
||||
{% with heading_level='h3' %}
|
||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Type of organization' value=org_type heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
|
||||
{% if DomainRequest.tribe_name %}
|
||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
||||
|
||||
{% if DomainRequest.federally_recognized_tribe %}
|
||||
<p>Federally-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.state_recognized_tribe %}
|
||||
<p>State-recognized tribe</p>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.get_federal_type_display %}
|
||||
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.is_election_board %}
|
||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
||||
{% include "includes/summary_item.html" with title='Election office' value=value heading_level=heading_level %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.organization_name %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.about_your_organization %}
|
||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.senior_official %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.current_websites.all %}
|
||||
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.requested_domain %}
|
||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.alternative_domains.all %}
|
||||
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.purpose %}
|
||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.creator %}
|
||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{% if DomainRequest.other_contacts.all %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' heading_level=heading_level %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
||||
{% endif %}
|
||||
|
||||
{# We always show this field even if None #}
|
||||
{% if DomainRequest %}
|
||||
<h3 class="register-form-review-header">CISA Regional Representative</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.cisa_representative_first_name %}
|
||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
<h3 class="register-form-review-header">Anything else</h3>
|
||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% if DomainRequest.anything_else %}
|
||||
{{DomainRequest.anything_else}}
|
||||
{% else %}
|
||||
No
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endblock request_summary%}
|
||||
</div>
|
||||
</main>
|
|
@ -35,8 +35,15 @@
|
|||
<dl class="usa-list usa-list--unstyled margin-top-0">
|
||||
{% for item in value %}
|
||||
<dt>
|
||||
Contact {{forloop.counter}}
|
||||
</dt>
|
||||
|
||||
<h4 class="summary-item__title
|
||||
font-sans-md
|
||||
text-primary-dark text-semibold
|
||||
margin-bottom-05
|
||||
padding-right-1">
|
||||
Contact {{forloop.counter}}
|
||||
</h4>
|
||||
</dt>
|
||||
<dd>
|
||||
{% include "includes/contact.html" with contact=item %}
|
||||
</dd>
|
||||
|
|
|
@ -4,11 +4,14 @@
|
|||
<div id="wrapper" class="{% block wrapper_class %}dashboard--portfolio{% endblock %}">
|
||||
{% block content %}
|
||||
|
||||
<main class="grid-container">
|
||||
<main class="grid-container {% if is_widescreen_mode %} grid-container--widescreen {% endif %}">
|
||||
{% if user.is_authenticated %}
|
||||
{# the entire logged in page goes here #}
|
||||
|
||||
<div class="tablet:grid-col-12">
|
||||
<div class="tablet:grid-col-11 desktop:grid-col-10 tablet:grid-offset-1">
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block portfolio_content %}{% endblock %}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load custom_filters %}
|
||||
{% load static url_helpers %}
|
||||
|
||||
{% block title %}Domain request status | {{ DomainRequest.requested_domain.name }} | {% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "includes/portfolio_request_status_view.html" %}
|
||||
{% endblock %}
|
33
src/registrar/templates/portfolio_members.html
Normal file
33
src/registrar/templates/portfolio_members.html
Normal file
|
@ -0,0 +1,33 @@
|
|||
{% extends 'portfolio_base.html' %}
|
||||
|
||||
{% load static %}
|
||||
|
||||
{% block title %} Members | {% endblock %}
|
||||
|
||||
{% block wrapper_class %}
|
||||
{{ block.super }} dashboard--grey-1
|
||||
{% endblock %}
|
||||
|
||||
{% block portfolio_content %}
|
||||
{% block messages %}
|
||||
{% include "includes/form_messages.html" %}
|
||||
{% endblock %}
|
||||
|
||||
<div id="main-content">
|
||||
<div class="grid-row grid-gap">
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<h1 id="members-header">Members</h1>
|
||||
</div>
|
||||
<div class="mobile:grid-col-12 tablet:grid-col-6">
|
||||
<p class="float-right-tablet tablet:margin-y-0">
|
||||
<a href="#" class="usa-button"
|
||||
>
|
||||
Add a new member
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include "includes/members_table.html" with portfolio=portfolio %}
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -239,3 +239,23 @@ def is_portfolio_subpage(path):
|
|||
"senior-official",
|
||||
]
|
||||
return get_url_name(path) in url_names
|
||||
|
||||
|
||||
@register.filter(name="is_members_subpage")
|
||||
def is_members_subpage(path):
|
||||
"""Checks if the given page is a subpage of members.
|
||||
Takes a path name, like '/organization/'."""
|
||||
# Since our pages aren't unified under a common path, we need this approach for now.
|
||||
url_names = [
|
||||
"members",
|
||||
]
|
||||
return get_url_name(path) in url_names
|
||||
|
||||
|
||||
@register.filter(name="portfolio_role_summary")
|
||||
def portfolio_role_summary(user, portfolio):
|
||||
"""Returns the value of user.portfolio_role_summary"""
|
||||
if user and portfolio:
|
||||
return user.portfolio_role_summary(portfolio)
|
||||
else:
|
||||
return []
|
||||
|
|
|
@ -2097,36 +2097,11 @@ class TestPortfolioAdmin(TestCase):
|
|||
)
|
||||
|
||||
display_admins = self.admin.display_admins(self.portfolio)
|
||||
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">Gerald Meoward meaoward@gov.gov</a>',
|
||||
display_admins,
|
||||
)
|
||||
self.assertIn("Captain", display_admins)
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">Arnold Poopy poopy@gov.gov</a>', display_admins
|
||||
)
|
||||
self.assertIn("Major", display_admins)
|
||||
|
||||
display_members_summary = self.admin.display_members_summary(self.portfolio)
|
||||
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_3.pk}/change/">Mad Max madmax@gov.gov</a>',
|
||||
display_members_summary,
|
||||
)
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_4.pk}/change/">Agent Smith thematrix@gov.gov</a>',
|
||||
display_members_summary,
|
||||
)
|
||||
url = reverse("admin:registrar_userportfoliopermission_changelist") + f"?portfolio={self.portfolio.id}"
|
||||
self.assertIn(f'<a href="{url}">2 administrators</a>', display_admins)
|
||||
|
||||
display_members = self.admin.display_members(self.portfolio)
|
||||
|
||||
self.assertIn("Mad Max", display_members)
|
||||
self.assertIn("<span class='usa-tag'>Member</span>", display_members)
|
||||
self.assertIn("Road warrior", display_members)
|
||||
self.assertIn("Agent Smith", display_members)
|
||||
self.assertIn("<span class='usa-tag'>Domain requestor</span>", display_members)
|
||||
self.assertIn("Program", display_members)
|
||||
self.assertIn(f'<a href="{url}">2 members</a>', display_members)
|
||||
|
||||
|
||||
class TestTransferUser(WebTest):
|
||||
|
|
|
@ -167,12 +167,6 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
expected_organization_name = "MonkeySeeMonkeyDo"
|
||||
self.assertContains(response, expected_organization_name)
|
||||
|
||||
# clean up this test's data
|
||||
domain.delete()
|
||||
domain_information.delete()
|
||||
_domain_request.delete()
|
||||
_creator.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_deletion_is_successful(self):
|
||||
"""
|
||||
|
@ -227,9 +221,6 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
# clean up data within this test
|
||||
domain.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_deletion_ready_fsm_failure(self):
|
||||
"""
|
||||
|
@ -269,9 +260,6 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
|
||||
self.assertEqual(domain.state, Domain.State.READY)
|
||||
|
||||
# delete data created in this test
|
||||
domain.delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_analyst_deletes_domain_idempotent(self):
|
||||
"""
|
||||
|
@ -330,8 +318,130 @@ class TestDomainAdminAsStaff(MockEppLib):
|
|||
)
|
||||
self.assertEqual(domain.state, Domain.State.DELETED)
|
||||
|
||||
# delete data created in this test
|
||||
domain.delete()
|
||||
|
||||
class TestDomainInformationInline(MockEppLib):
|
||||
"""Test DomainAdmin class, specifically the DomainInformationInline class, as staff user.
|
||||
|
||||
Notes:
|
||||
all tests share staffuser; do not change staffuser model in tests
|
||||
tests have available staffuser, client, and admin
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.staffuser = create_user()
|
||||
cls.site = AdminSite()
|
||||
cls.admin = DomainAdmin(model=Domain, admin_site=cls.site)
|
||||
cls.factory = RequestFactory()
|
||||
|
||||
def setUp(self):
|
||||
self.client = Client(HTTP_HOST="localhost:8080")
|
||||
self.client.force_login(self.staffuser)
|
||||
super().setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
Host.objects.all().delete()
|
||||
UserDomainRole.objects.all().delete()
|
||||
Domain.objects.all().delete()
|
||||
DomainInformation.objects.all().delete()
|
||||
DomainRequest.objects.all().delete()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
User.objects.all().delete()
|
||||
super().tearDownClass()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_domain_managers_display(self):
|
||||
"""Tests the custom domain managers field"""
|
||||
admin_user_1 = User.objects.create(
|
||||
username="testuser1",
|
||||
first_name="Gerald",
|
||||
last_name="Meoward",
|
||||
email="meoward@gov.gov",
|
||||
)
|
||||
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
|
||||
)
|
||||
domain_request.approve()
|
||||
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
|
||||
domain = Domain.objects.filter(domain_info=_domain_info).get()
|
||||
|
||||
UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
|
||||
|
||||
admin_user_2 = User.objects.create(
|
||||
username="testuser2",
|
||||
first_name="Arnold",
|
||||
last_name="Poopy",
|
||||
email="poopy@gov.gov",
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
|
||||
|
||||
# Get the first inline (DomainInformationInline)
|
||||
inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
|
||||
|
||||
# Call the domain_managers method
|
||||
domain_managers = inline_instance.domain_managers(domain.domain_info)
|
||||
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
|
||||
domain_managers,
|
||||
)
|
||||
self.assertIn("Gerald Meoward", domain_managers)
|
||||
self.assertIn("meoward@gov.gov", domain_managers)
|
||||
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', domain_managers)
|
||||
self.assertIn("Arnold Poopy", domain_managers)
|
||||
self.assertIn("poopy@gov.gov", domain_managers)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_invited_domain_managers_display(self):
|
||||
"""Tests the custom invited domain managers field"""
|
||||
admin_user_1 = User.objects.create(
|
||||
username="testuser1",
|
||||
first_name="Gerald",
|
||||
last_name="Meoward",
|
||||
email="meoward@gov.gov",
|
||||
)
|
||||
|
||||
domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW, user=self.staffuser, name="fake.gov"
|
||||
)
|
||||
domain_request.approve()
|
||||
_domain_info = DomainInformation.objects.filter(domain=domain_request.approved_domain).get()
|
||||
domain = Domain.objects.filter(domain_info=_domain_info).get()
|
||||
|
||||
# domain, _ = Domain.objects.get_or_create(name="fake.gov", state=Domain.State.READY)
|
||||
UserDomainRole.objects.get_or_create(user=admin_user_1, domain=domain, role=UserDomainRole.Roles.MANAGER)
|
||||
|
||||
admin_user_2 = User.objects.create(
|
||||
username="testuser2",
|
||||
first_name="Arnold",
|
||||
last_name="Poopy",
|
||||
email="poopy@gov.gov",
|
||||
)
|
||||
|
||||
UserDomainRole.objects.get_or_create(user=admin_user_2, domain=domain, role=UserDomainRole.Roles.MANAGER)
|
||||
|
||||
# Get the first inline (DomainInformationInline)
|
||||
inline_instance = self.admin.inlines[0](self.admin.model, self.admin.admin_site)
|
||||
|
||||
# Call the domain_managers method
|
||||
domain_managers = inline_instance.domain_managers(domain.domain_info)
|
||||
# domain_managers = self.admin.get_inlinesdomain_managers(self.domain)
|
||||
|
||||
self.assertIn(
|
||||
f'<a href="/admin/registrar/user/{admin_user_1.pk}/change/">testuser1</a>',
|
||||
domain_managers,
|
||||
)
|
||||
self.assertIn("Gerald Meoward", domain_managers)
|
||||
self.assertIn("meoward@gov.gov", domain_managers)
|
||||
self.assertIn(f'<a href="/admin/registrar/user/{admin_user_2.pk}/change/">testuser2</a>', domain_managers)
|
||||
self.assertIn("Arnold Poopy", domain_managers)
|
||||
self.assertIn("poopy@gov.gov", domain_managers)
|
||||
|
||||
|
||||
class TestDomainAdminWithClient(TestCase):
|
||||
|
@ -415,17 +525,6 @@ class TestDomainAdminWithClient(TestCase):
|
|||
self.assertContains(response, domain.name)
|
||||
|
||||
# Check that the fields have the right values.
|
||||
# == Check for the creator == #
|
||||
|
||||
# Check for the right title, email, and phone number in the response.
|
||||
# We only need to check for the end tag
|
||||
# (Otherwise this test will fail if we change classes, etc)
|
||||
self.assertContains(response, "Treat inspector")
|
||||
self.assertContains(response, "meoward.jones@igorville.gov")
|
||||
self.assertContains(response, "(555) 123 12345")
|
||||
|
||||
# Check for the field itself
|
||||
self.assertContains(response, "Meoward Jones")
|
||||
|
||||
# == Check for the senior_official == #
|
||||
self.assertContains(response, "testy@town.com")
|
||||
|
@ -435,11 +534,6 @@ class TestDomainAdminWithClient(TestCase):
|
|||
# Includes things like readonly fields
|
||||
self.assertContains(response, "Testy Tester")
|
||||
|
||||
# == Test the other_employees field == #
|
||||
self.assertContains(response, "testy2@town.com")
|
||||
self.assertContains(response, "Another Tester")
|
||||
self.assertContains(response, "(555) 555 5557")
|
||||
|
||||
# Test for the copy link
|
||||
self.assertContains(response, "button--clipboard")
|
||||
|
||||
|
|
|
@ -872,23 +872,6 @@ class TestDomainRequestAdmin(MockEppLib):
|
|||
self.transition_state_and_send_email(domain_request, DomainRequest.DomainRequestStatus.SUBMITTED)
|
||||
self.assertEqual(len(self.mock_client.EMAILS_SENT), 3)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_model_displays_action_needed_email(self):
|
||||
"""Tests if the action needed email is visible for Domain Requests"""
|
||||
|
||||
_domain_request = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
action_needed_reason=DomainRequest.ActionNeededReasons.BAD_NAME,
|
||||
)
|
||||
|
||||
self.client.force_login(self.staffuser)
|
||||
response = self.client.get(
|
||||
"/admin/registrar/domainrequest/{}/change/".format(_domain_request.pk),
|
||||
follow=True,
|
||||
)
|
||||
|
||||
self.assertContains(response, "DOMAIN NAME DOES NOT MEET .GOV REQUIREMENTS")
|
||||
|
||||
@override_settings(IS_PRODUCTION=True)
|
||||
@less_console_noise_decorator
|
||||
def test_save_model_sends_submitted_email_with_bcc_on_prod(self):
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from django.urls import reverse
|
||||
from django.test import TestCase, Client
|
||||
from registrar.models import FederalAgency, SeniorOfficial, User
|
||||
from registrar.models import FederalAgency, SeniorOfficial, User, DomainRequest
|
||||
from django.contrib.auth import get_user_model
|
||||
from registrar.tests.common import create_superuser, create_user
|
||||
from registrar.tests.common import create_superuser, create_user, completed_domain_request
|
||||
|
||||
from api.tests.common import less_console_noise_decorator
|
||||
from registrar.utility.constants import BranchChoices
|
||||
|
@ -100,7 +100,6 @@ class GetFederalPortfolioTypeJsonTest(TestCase):
|
|||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertEqual(data["federal_type"], "Judicial")
|
||||
self.assertEqual(data["portfolio_type"], "Federal - Judicial")
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_federal_and_portfolio_types_json_authenticated_regularuser(self):
|
||||
|
@ -109,3 +108,71 @@ class GetFederalPortfolioTypeJsonTest(TestCase):
|
|||
self.client.login(username="testuser", password=p)
|
||||
response = self.client.get(self.api_url, {"agency_name": "Test Agency", "organization_type": "federal"})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
|
||||
class GetActionNeededEmailForUserJsonTest(TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.superuser = create_superuser()
|
||||
self.analyst_user = create_user()
|
||||
self.agency = FederalAgency.objects.create(agency="Test Agency")
|
||||
self.domain_request = completed_domain_request(
|
||||
federal_agency=self.agency,
|
||||
name="test.gov",
|
||||
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
)
|
||||
|
||||
self.api_url = reverse("get-action-needed-email-for-user-json")
|
||||
|
||||
def tearDown(self):
|
||||
DomainRequest.objects.all().delete()
|
||||
User.objects.all().delete()
|
||||
FederalAgency.objects.all().delete()
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_action_needed_email_for_user_json_superuser(self):
|
||||
"""Test that a superuser can fetch the action needed email."""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
response = self.client.get(
|
||||
self.api_url,
|
||||
{
|
||||
"reason": DomainRequest.ActionNeededReasons.ELIGIBILITY_UNCLEAR,
|
||||
"domain_request_id": self.domain_request.id,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn("action_needed_email", data)
|
||||
self.assertIn("ORGANIZATION MAY NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_action_needed_email_for_user_json_analyst(self):
|
||||
"""Test that an analyst can fetch the action needed email."""
|
||||
self.client.force_login(self.analyst_user)
|
||||
|
||||
response = self.client.get(
|
||||
self.api_url,
|
||||
{
|
||||
"reason": DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL,
|
||||
"domain_request_id": self.domain_request.id,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertIn("action_needed_email", data)
|
||||
self.assertIn("SENIOR OFFICIAL DOES NOT MEET ELIGIBILITY REQUIREMENTS", data["action_needed_email"])
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_get_action_needed_email_for_user_json_regular(self):
|
||||
"""Test that a regular user receives a 403 with an error message."""
|
||||
p = "password"
|
||||
self.client.login(username="testuser", password=p)
|
||||
response = self.client.get(
|
||||
self.api_url,
|
||||
{
|
||||
"reason": DomainRequest.ActionNeededReasons.QUESTIONABLE_SENIOR_OFFICIAL,
|
||||
"domain_request_id": self.domain_request.id,
|
||||
},
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
|
|
@ -1387,18 +1387,18 @@ class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
|
|||
self.agency4.refresh_from_db()
|
||||
|
||||
# Check if FederalAgency objects were updated correctly
|
||||
self.assertEqual(self.agency1.initials, "ABMC")
|
||||
self.assertEqual(self.agency1.acronym, "ABMC")
|
||||
self.assertTrue(self.agency1.is_fceb)
|
||||
|
||||
self.assertEqual(self.agency2.initials, "ACHP")
|
||||
self.assertEqual(self.agency2.acronym, "ACHP")
|
||||
self.assertTrue(self.agency2.is_fceb)
|
||||
|
||||
# We expect that this field doesn't have any data,
|
||||
# as none is specified in the CSV
|
||||
self.assertIsNone(self.agency3.initials)
|
||||
self.assertIsNone(self.agency3.acronym)
|
||||
self.assertIsNone(self.agency3.is_fceb)
|
||||
|
||||
self.assertEqual(self.agency4.initials, "KC")
|
||||
self.assertEqual(self.agency4.acronym, "KC")
|
||||
self.assertFalse(self.agency4.is_fceb)
|
||||
|
||||
@less_console_noise_decorator
|
||||
|
@ -1411,7 +1411,7 @@ class TestPopulateFederalAgencyInitialsAndFceb(TestCase):
|
|||
|
||||
# Verify that the missing agency was not updated
|
||||
missing_agency.refresh_from_db()
|
||||
self.assertIsNone(missing_agency.initials)
|
||||
self.assertIsNone(missing_agency.acronym)
|
||||
self.assertIsNone(missing_agency.is_fceb)
|
||||
|
||||
|
||||
|
|
|
@ -40,10 +40,22 @@ class TestGroups(TestCase):
|
|||
"add_federalagency",
|
||||
"change_federalagency",
|
||||
"delete_federalagency",
|
||||
"add_portfolio",
|
||||
"change_portfolio",
|
||||
"delete_portfolio",
|
||||
"add_seniorofficial",
|
||||
"change_seniorofficial",
|
||||
"delete_seniorofficial",
|
||||
"add_suborganization",
|
||||
"change_suborganization",
|
||||
"delete_suborganization",
|
||||
"analyst_access_permission",
|
||||
"change_user",
|
||||
"delete_userdomainrole",
|
||||
"view_userdomainrole",
|
||||
"add_userportfoliopermission",
|
||||
"change_userportfoliopermission",
|
||||
"delete_userportfoliopermission",
|
||||
"add_verifiedbystaff",
|
||||
"change_verifiedbystaff",
|
||||
"delete_verifiedbystaff",
|
||||
|
@ -51,6 +63,7 @@ class TestGroups(TestCase):
|
|||
|
||||
# Get the codenames of actual permissions associated with the group
|
||||
actual_permissions = [p.codename for p in cisa_analysts_group.permissions.all()]
|
||||
self.maxDiff = None
|
||||
|
||||
# Assert that the actual permissions match the expected permissions
|
||||
self.assertListEqual(actual_permissions, expected_permissions)
|
||||
|
|
|
@ -114,6 +114,54 @@ class TestDomainRequest(TestCase):
|
|||
with less_console_noise():
|
||||
return self.assertRaises(Exception, None, exception_type)
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_request_is_withdrawable(self):
|
||||
"""Tests the is_withdrawable function"""
|
||||
domain_request_1 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
name="city2.gov",
|
||||
)
|
||||
domain_request_2 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
name="city3.gov",
|
||||
)
|
||||
domain_request_3 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
name="city4.gov",
|
||||
)
|
||||
domain_request_4 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||
name="city5.gov",
|
||||
)
|
||||
self.assertTrue(domain_request_1.is_withdrawable())
|
||||
self.assertTrue(domain_request_2.is_withdrawable())
|
||||
self.assertTrue(domain_request_3.is_withdrawable())
|
||||
self.assertFalse(domain_request_4.is_withdrawable())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_request_is_awaiting_review(self):
|
||||
"""Tests the is_awaiting_review function"""
|
||||
domain_request_1 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.SUBMITTED,
|
||||
name="city2.gov",
|
||||
)
|
||||
domain_request_2 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
name="city3.gov",
|
||||
)
|
||||
domain_request_3 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
name="city4.gov",
|
||||
)
|
||||
domain_request_4 = completed_domain_request(
|
||||
status=DomainRequest.DomainRequestStatus.REJECTED,
|
||||
name="city5.gov",
|
||||
)
|
||||
self.assertTrue(domain_request_1.is_awaiting_review())
|
||||
self.assertTrue(domain_request_2.is_awaiting_review())
|
||||
self.assertFalse(domain_request_3.is_awaiting_review())
|
||||
self.assertFalse(domain_request_4.is_awaiting_review())
|
||||
|
||||
@less_console_noise_decorator
|
||||
def test_federal_agency_set_to_non_federal_on_approve(self):
|
||||
"""Ensures that when the federal_agency field is 'none' when .approve() is called,
|
||||
|
@ -1284,7 +1332,10 @@ class TestUserPortfolioPermission(TestCase):
|
|||
|
||||
self.assertEqual(
|
||||
cm.exception.message,
|
||||
"Only one portfolio permission is allowed per user when multiple portfolios are disabled.",
|
||||
(
|
||||
"This user is already assigned to a portfolio. "
|
||||
"Based on current waffle flag settings, users cannot be assigned to multiple portfolios."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -356,11 +356,6 @@ class ExportDataTest(MockDbForIndividualTests, MockEppLib):
|
|||
self.assertIn(self.domain_3.name, csv_content)
|
||||
self.assertNotIn(self.domain_2.name, csv_content)
|
||||
|
||||
# Test the output for readonly admin
|
||||
portfolio_permission.roles = [UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
|
||||
portfolio_permission.save()
|
||||
portfolio_permission.refresh_from_db()
|
||||
|
||||
# Get the csv content
|
||||
csv_content = self._run_domain_data_type_user_export(request)
|
||||
self.assertIn(self.domain_1.name, csv_content)
|
||||
|
|
|
@ -116,6 +116,7 @@ class TestURLAuth(TestCase):
|
|||
"/api/v1/available/",
|
||||
"/api/v1/get-report/current-federal",
|
||||
"/api/v1/get-report/current-full",
|
||||
"/api/v1/rdap/",
|
||||
"/health",
|
||||
]
|
||||
|
||||
|
|
23
src/registrar/tests/test_utilities.py
Normal file
23
src/registrar/tests/test_utilities.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
from django.test import TestCase
|
||||
from registrar.models import User
|
||||
from waffle.testutils import override_flag
|
||||
from registrar.utility.waffle import flag_is_active_for_user
|
||||
|
||||
|
||||
class FlagIsActiveForUserTest(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Set up a test user
|
||||
self.user = User.objects.create_user(username="testuser")
|
||||
|
||||
@override_flag("test_flag", active=True)
|
||||
def test_flag_active_for_user(self):
|
||||
# Test that the flag is active for the user
|
||||
is_active = flag_is_active_for_user(self.user, "test_flag")
|
||||
self.assertTrue(is_active)
|
||||
|
||||
@override_flag("test_flag", active=False)
|
||||
def test_flag_inactive_for_user(self):
|
||||
# Test that the flag is inactive for the user
|
||||
is_active = flag_is_active_for_user(self.user, "test_flag")
|
||||
self.assertFalse(is_active)
|
|
@ -1577,7 +1577,7 @@ class TestDomainSuborganization(TestDomainOverview):
|
|||
|
||||
# Add portfolio perms to the user object
|
||||
portfolio_permission, _ = UserPortfolioPermission.objects.get_or_create(
|
||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_ADMIN_READ_ONLY]
|
||||
user=self.user, portfolio=portfolio, roles=[UserPortfolioRoleChoices.ORGANIZATION_MEMBER]
|
||||
)
|
||||
|
||||
self.assertEqual(self.domain_information.sub_organization, suborg)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue