mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-27 04:58:42 +02:00
merge main, address bulk deletion in DJA
This commit is contained in:
commit
f699c7d0b3
128 changed files with 2750 additions and 1375 deletions
8
.github/workflows/security-check.yaml
vendored
8
.github/workflows/security-check.yaml
vendored
|
@ -2,17 +2,9 @@ name: Security checks
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '**.md'
|
|
||||||
- '.gitignore'
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '**.md'
|
|
||||||
- '.gitignore'
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
|
|
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
|
@ -3,10 +3,6 @@ name: Testing
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths-ignore:
|
|
||||||
- 'docs/**'
|
|
||||||
- '**.md'
|
|
||||||
- '.gitignore'
|
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|
17
docs/developer/cloning-databases.md
Normal file
17
docs/developer/cloning-databases.md
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Cloning Databases
|
||||||
|
The clone-db workflow clones a Source database to a Destination database using cloud.gov's cg-manage-rds tool. This document contains additional information needed to understand how the workflow functions.
|
||||||
|
|
||||||
|
## Additional Roles Required
|
||||||
|
The clone-db workflow functions by temporarily sharing the Destination database with the space of the Source database. This is because cloning databases across spaces is hard. Sharing is done via the `cf share-service` command, but requires that the authenticated user (in this case this will be a user from the Source space) have the `space-developer` role in *both* the Source and Destination spaces. This must be set by someone with permission to edit space roles *before* the workflow runs. The user in question can be found using the `cf space-users [ORG] [SPACE]` command where the SPACE is the Source space, and will appear as a UAA user with a UUID as the name. There is only one such user per space by default (this is a [service account](https://cloud.gov/docs/services/cloud-gov-service-account/) set up by cloud.gov for our Github workflows). This user needs to be provided with the `space-developer` role in the Destination space, which can be accomplished using `cf set-space-role [USER] [ORG] [DESTINATION SPACE] SpaceDeveloper`.
|
||||||
|
|
||||||
|
## Turning Off DB Cloning Fast (For Emergencies or other Scenarios)
|
||||||
|
Note: In less urgent situations it may be better to make a PR removing the scheduled workflow trigger.
|
||||||
|
|
||||||
|
Step 1:
|
||||||
|
Get the name of the correct service using `cf spaces-users cisa-dotgov stable`. There should only be one user with a name that is a UUID, that is the one you want.
|
||||||
|
|
||||||
|
step 2:
|
||||||
|
Remove the space developer role by doing the following command:
|
||||||
|
`cf unset-space-role [USER] cisa-dotgov staging SpaceDeveloper`
|
||||||
|
|
||||||
|
This will cause the job to fail without requiring pushing anything to main.
|
|
@ -908,12 +908,13 @@ Example (only requests): `./manage.py create_federal_portfolio --branch "executi
|
||||||
|
|
||||||
##### Parameters
|
##### Parameters
|
||||||
| | Parameter | Description |
|
| | Parameter | Description |
|
||||||
|:-:|:-------------------------- |:-------------------------------------------------------------------------------------------|
|
|:-:|:---------------------------- |:-------------------------------------------------------------------------------------------|
|
||||||
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
| 1 | **agency_name** | Name of the FederalAgency record surrounded by quotes. For instance,"AMTRAK". |
|
||||||
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
| 2 | **branch** | Creates a portfolio for each federal agency in a branch: executive, legislative, judicial |
|
||||||
| 3 | **both** | If True, runs parse_requests and parse_domains. |
|
| 3 | **both** | If True, runs parse_requests and parse_domains. |
|
||||||
| 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
| 4 | **parse_requests** | If True, then the created portfolio is added to all related DomainRequests. |
|
||||||
| 5 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
| 5 | **parse_domains** | If True, then the created portfolio is added to all related Domains. |
|
||||||
|
| 6 | **skip_existing_portfolios** | If True, then the script will only create suborganizations, modify DomainRequest, and modify DomainInformation records only when creating a new portfolio. Use this flag when you do not want to modify existing records. |
|
||||||
|
|
||||||
- Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both.
|
- Parameters #1-#2: Either `--agency_name` or `--branch` must be specified. Not both.
|
||||||
- Parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
- Parameters #2-#3, you cannot use `--both` while using these. You must specify either `--parse_requests` or `--parse_domains` seperately. While all of these parameters are optional in that you do not need to specify all of them,
|
||||||
|
@ -953,3 +954,40 @@ To create a specific portfolio:
|
||||||
|
|
||||||
#### Step 1: Running the script
|
#### Step 1: Running the script
|
||||||
```docker-compose exec app ./manage.py patch_suborganizations```
|
```docker-compose exec app ./manage.py patch_suborganizations```
|
||||||
|
|
||||||
|
|
||||||
|
## Remove Non-whitelisted Portfolios
|
||||||
|
This script removes Portfolio entries from the database that are not part of a predefined list of allowed portfolios (`ALLOWED_PORTFOLIOS`).
|
||||||
|
It performs the following actions:
|
||||||
|
1. Prompts the user for confirmation before proceeding with deletions.
|
||||||
|
2. Updates related objects such as `DomainInformation`, `Domain`, and `DomainRequest` to set their `portfolio` field to `None` to prevent integrity errors.
|
||||||
|
3. Deletes associated objects such as `PortfolioInvitation`, `UserPortfolioPermission`, and `Suborganization`.
|
||||||
|
4. Logs a detailed summary of all cascading deletions and orphaned objects.
|
||||||
|
|
||||||
|
### Running on sandboxes
|
||||||
|
|
||||||
|
#### Step 1: Login to CloudFoundry
|
||||||
|
```cf login -a api.fr.cloud.gov --sso```
|
||||||
|
|
||||||
|
#### Step 2: SSH into your environment
|
||||||
|
```cf ssh getgov-{space}```
|
||||||
|
|
||||||
|
Example: `cf ssh getgov-nl`
|
||||||
|
|
||||||
|
#### Step 3: Create a shell instance
|
||||||
|
```/tmp/lifecycle/shell```
|
||||||
|
|
||||||
|
#### Step 4: Running the script
|
||||||
|
To remove portfolios:
|
||||||
|
```./manage.py remove_unused_portfolios```
|
||||||
|
|
||||||
|
If you wish to enable debug mode for additional logging:
|
||||||
|
```./manage.py remove_unused_portfolios --debug```
|
||||||
|
|
||||||
|
### Running locally
|
||||||
|
|
||||||
|
#### Step 1: Running the script
|
||||||
|
```docker-compose exec app ./manage.py remove_unused_portfolios```
|
||||||
|
|
||||||
|
To enable debug mode locally:
|
||||||
|
```docker-compose exec app ./manage.py remove_unused_portfolios --debug```
|
|
@ -21,48 +21,65 @@ class OpenIdConnectBackend(ModelBackend):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def authenticate(self, request, **kwargs):
|
def authenticate(self, request, **kwargs):
|
||||||
logger.debug("kwargs %s" % kwargs)
|
logger.debug("kwargs %s", kwargs)
|
||||||
user = None
|
|
||||||
if not kwargs or "sub" not in kwargs.keys():
|
if not kwargs or "sub" not in kwargs:
|
||||||
return user
|
return None
|
||||||
|
|
||||||
UserModel = get_user_model()
|
UserModel = get_user_model()
|
||||||
username = self.clean_username(kwargs["sub"])
|
username = self.clean_username(kwargs["sub"])
|
||||||
|
openid_data = self.extract_openid_data(kwargs)
|
||||||
|
|
||||||
# Some OP may actually choose to withhold some information, so we must
|
|
||||||
# test if it is present
|
|
||||||
openid_data = {"last_login": timezone.now()}
|
|
||||||
openid_data["first_name"] = kwargs.get("given_name", "")
|
|
||||||
openid_data["last_name"] = kwargs.get("family_name", "")
|
|
||||||
openid_data["email"] = kwargs.get("email", "")
|
|
||||||
openid_data["phone"] = kwargs.get("phone", "")
|
|
||||||
|
|
||||||
# Note that this could be accomplished in one try-except clause, but
|
|
||||||
# instead we use get_or_create when creating unknown users since it has
|
|
||||||
# built-in safeguards for multiple threads.
|
|
||||||
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
|
if getattr(settings, "OIDC_CREATE_UNKNOWN_USER", True):
|
||||||
args = {
|
user = self.get_or_create_user(UserModel, username, openid_data, kwargs)
|
||||||
UserModel.USERNAME_FIELD: username,
|
else:
|
||||||
# defaults _will_ be updated, these are not fallbacks
|
user = self.get_user_by_username(UserModel, username)
|
||||||
"defaults": openid_data,
|
|
||||||
|
if user:
|
||||||
|
user.on_each_login()
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def extract_openid_data(self, kwargs):
|
||||||
|
"""Extract OpenID data from authentication kwargs."""
|
||||||
|
return {
|
||||||
|
"last_login": timezone.now(),
|
||||||
|
"first_name": kwargs.get("given_name", ""),
|
||||||
|
"last_name": kwargs.get("family_name", ""),
|
||||||
|
"email": kwargs.get("email", ""),
|
||||||
|
"phone": kwargs.get("phone", ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
user, created = UserModel.objects.get_or_create(**args)
|
def get_or_create_user(self, UserModel, username, openid_data, kwargs):
|
||||||
|
"""Retrieve user by username or email, or create a new user."""
|
||||||
|
user = self.get_user_by_username(UserModel, username)
|
||||||
|
|
||||||
if not created:
|
if not user and openid_data["email"]:
|
||||||
# If user exists, update existing user
|
user = self.get_user_by_email(UserModel, openid_data["email"])
|
||||||
self.update_existing_user(user, args["defaults"])
|
if user:
|
||||||
else:
|
# if found by email, update the username
|
||||||
# If user is created, configure the user
|
setattr(user, UserModel.USERNAME_FIELD, username)
|
||||||
user = self.configure_user(user, **kwargs)
|
|
||||||
else:
|
if not user:
|
||||||
|
user = UserModel.objects.create(**{UserModel.USERNAME_FIELD: username}, **openid_data)
|
||||||
|
return self.configure_user(user, **kwargs)
|
||||||
|
|
||||||
|
self.update_existing_user(user, openid_data)
|
||||||
|
return user
|
||||||
|
|
||||||
|
def get_user_by_username(self, UserModel, username):
|
||||||
|
"""Retrieve user by username."""
|
||||||
try:
|
try:
|
||||||
user = UserModel.objects.get_by_natural_key(username)
|
return UserModel.objects.get(**{UserModel.USERNAME_FIELD: username})
|
||||||
|
except UserModel.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_user_by_email(self, UserModel, email):
|
||||||
|
"""Retrieve user by email."""
|
||||||
|
try:
|
||||||
|
return UserModel.objects.get(email=email)
|
||||||
except UserModel.DoesNotExist:
|
except UserModel.DoesNotExist:
|
||||||
return None
|
return None
|
||||||
# run this callback for a each login
|
|
||||||
user.on_each_login()
|
|
||||||
return user
|
|
||||||
|
|
||||||
def update_existing_user(self, user, kwargs):
|
def update_existing_user(self, user, kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from registrar.models import User
|
from registrar.models import User
|
||||||
|
from api.tests.common import less_console_noise_decorator
|
||||||
from ..backends import OpenIdConnectBackend # Adjust the import path based on your project structure
|
from ..backends import OpenIdConnectBackend # Adjust the import path based on your project structure
|
||||||
|
|
||||||
|
|
||||||
|
@ -17,6 +18,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
User.objects.all().delete()
|
User.objects.all().delete()
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_authenticate_with_create_user(self):
|
def test_authenticate_with_create_user(self):
|
||||||
"""Test that authenticate creates a new user if it does not find
|
"""Test that authenticate creates a new user if it does not find
|
||||||
existing user"""
|
existing user"""
|
||||||
|
@ -32,6 +34,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
||||||
self.assertEqual(user.email, "john.doe@example.com")
|
self.assertEqual(user.email, "john.doe@example.com")
|
||||||
self.assertEqual(user.phone, "123456789")
|
self.assertEqual(user.phone, "123456789")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_authenticate_with_existing_user(self):
|
def test_authenticate_with_existing_user(self):
|
||||||
"""Test that authenticate updates an existing user if it finds one.
|
"""Test that authenticate updates an existing user if it finds one.
|
||||||
For this test, given_name and family_name are supplied"""
|
For this test, given_name and family_name are supplied"""
|
||||||
|
@ -50,6 +53,30 @@ class OpenIdConnectBackendTestCase(TestCase):
|
||||||
self.assertEqual(user.email, "john.doe@example.com")
|
self.assertEqual(user.email, "john.doe@example.com")
|
||||||
self.assertEqual(user.phone, "123456789")
|
self.assertEqual(user.phone, "123456789")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
|
def test_authenticate_with_existing_user_same_email_different_username(self):
|
||||||
|
"""Test that authenticate updates an existing user if it finds one.
|
||||||
|
In this case, match is to an existing record with matching email but
|
||||||
|
a non-matching username. The existing record's username should be udpated.
|
||||||
|
For this test, given_name and family_name are supplied"""
|
||||||
|
# Create an existing user with the same username
|
||||||
|
User.objects.create_user(username="old_username", email="john.doe@example.com")
|
||||||
|
|
||||||
|
# Ensure that the authenticate method updates the existing user
|
||||||
|
user = self.backend.authenticate(request=None, **self.kwargs)
|
||||||
|
self.assertIsNotNone(user)
|
||||||
|
self.assertIsInstance(user, User)
|
||||||
|
|
||||||
|
# Verify that user fields are correctly updated
|
||||||
|
self.assertEqual(user.first_name, "John")
|
||||||
|
self.assertEqual(user.last_name, "Doe")
|
||||||
|
self.assertEqual(user.email, "john.doe@example.com")
|
||||||
|
self.assertEqual(user.phone, "123456789")
|
||||||
|
self.assertEqual(user.username, "test_user")
|
||||||
|
# Assert that a user no longer exists by the old username
|
||||||
|
self.assertFalse(User.objects.filter(username="old_username").exists())
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_authenticate_with_existing_user_with_existing_first_last_phone(self):
|
def test_authenticate_with_existing_user_with_existing_first_last_phone(self):
|
||||||
"""Test that authenticate updates an existing user if it finds one.
|
"""Test that authenticate updates an existing user if it finds one.
|
||||||
For this test, given_name and family_name are not supplied.
|
For this test, given_name and family_name are not supplied.
|
||||||
|
@ -79,6 +106,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
||||||
self.assertEqual(user.email, "john.doe@example.com")
|
self.assertEqual(user.email, "john.doe@example.com")
|
||||||
self.assertEqual(user.phone, "9999999999")
|
self.assertEqual(user.phone, "9999999999")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_authenticate_with_existing_user_different_name_phone(self):
|
def test_authenticate_with_existing_user_different_name_phone(self):
|
||||||
"""Test that authenticate updates an existing user if it finds one.
|
"""Test that authenticate updates an existing user if it finds one.
|
||||||
For this test, given_name and family_name are supplied and overwrite"""
|
For this test, given_name and family_name are supplied and overwrite"""
|
||||||
|
@ -100,6 +128,7 @@ class OpenIdConnectBackendTestCase(TestCase):
|
||||||
self.assertEqual(user.email, "john.doe@example.com")
|
self.assertEqual(user.email, "john.doe@example.com")
|
||||||
self.assertEqual(user.phone, "123456789")
|
self.assertEqual(user.phone, "123456789")
|
||||||
|
|
||||||
|
@less_console_noise_decorator
|
||||||
def test_authenticate_with_unknown_user(self):
|
def test_authenticate_with_unknown_user(self):
|
||||||
"""Test that authenticate returns None when no kwargs are supplied"""
|
"""Test that authenticate returns None when no kwargs are supplied"""
|
||||||
# Ensure that the authenticate method handles the case when the user is not found
|
# Ensure that the authenticate method handles the case when the user is not found
|
||||||
|
|
6
src/package-lock.json
generated
6
src/package-lock.json
generated
|
@ -7074,9 +7074,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.1",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
|
||||||
"integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==",
|
"integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
|
|
|
@ -1222,9 +1222,9 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
class SeniorOfficialAdmin(ListHeaderAdmin):
|
class SeniorOfficialAdmin(ListHeaderAdmin):
|
||||||
"""Custom Senior Official Admin class."""
|
"""Custom Senior Official Admin class."""
|
||||||
|
|
||||||
search_fields = ["first_name", "last_name", "email"]
|
search_fields = ["first_name", "last_name", "email", "federal_agency__agency"]
|
||||||
search_help_text = "Search by first name, last name or email."
|
search_help_text = "Search by first name, last name or email."
|
||||||
list_display = ["first_name", "last_name", "email", "federal_agency"]
|
list_display = ["federal_agency", "first_name", "last_name", "email"]
|
||||||
|
|
||||||
# this ordering effects the ordering of results
|
# this ordering effects the ordering of results
|
||||||
# in autocomplete_fields for Senior Official
|
# in autocomplete_fields for Senior Official
|
||||||
|
@ -1329,6 +1329,14 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
||||||
|
|
||||||
get_roles.short_description = "Roles" # type: ignore
|
get_roles.short_description = "Roles" # type: ignore
|
||||||
|
|
||||||
|
def delete_queryset(self, request, queryset):
|
||||||
|
"""We override the delete method in the model.
|
||||||
|
When deleting in DJA, if you select multiple items in a table using checkboxes and apply a delete action, the model
|
||||||
|
delete does not get called. This method gets called instead.
|
||||||
|
This override makes sure our code in the model gets executed in these situations."""
|
||||||
|
for obj in queryset:
|
||||||
|
obj.delete() # Calls the overridden delete method on each instance
|
||||||
|
|
||||||
|
|
||||||
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
class UserDomainRoleAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
"""Custom user domain role admin class."""
|
"""Custom user domain role admin class."""
|
||||||
|
@ -1407,10 +1415,13 @@ class BaseInvitationAdmin(ListHeaderAdmin):
|
||||||
Normal flow on successful save_model on add is to redirect to changelist_view.
|
Normal flow on successful save_model on add is to redirect to changelist_view.
|
||||||
If there are errors, flow is modified to instead render change form.
|
If there are errors, flow is modified to instead render change form.
|
||||||
"""
|
"""
|
||||||
# store current messages from request so that they are preserved throughout the method
|
# store current messages from request in storage so that they are preserved throughout the
|
||||||
|
# method, as some flows remove and replace all messages, and so we store here to retrieve
|
||||||
|
# later
|
||||||
storage = get_messages(request)
|
storage = get_messages(request)
|
||||||
# Check if there are any error or warning messages in the `messages` framework
|
# Check if there are any error messages in the `messages` framework
|
||||||
has_errors = any(message.level_tag in ["error", "warning"] for message in storage)
|
# error messages stop the workflow; other message levels allow flow to continue as normal
|
||||||
|
has_errors = any(message.level_tag in ["error"] for message in storage)
|
||||||
|
|
||||||
if has_errors:
|
if has_errors:
|
||||||
# Re-render the change form if there are errors or warnings
|
# Re-render the change form if there are errors or warnings
|
||||||
|
@ -1552,13 +1563,14 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
||||||
portfolio_invitation.save()
|
portfolio_invitation.save()
|
||||||
messages.success(request, f"{requested_email} has been invited to the organization: {domain_org}")
|
messages.success(request, f"{requested_email} has been invited to the organization: {domain_org}")
|
||||||
|
|
||||||
send_domain_invitation_email(
|
if not send_domain_invitation_email(
|
||||||
email=requested_email,
|
email=requested_email,
|
||||||
requestor=requestor,
|
requestor=requestor,
|
||||||
domains=domain,
|
domains=domain,
|
||||||
is_member_of_different_org=member_of_a_different_org,
|
is_member_of_different_org=member_of_a_different_org,
|
||||||
requested_user=requested_user,
|
requested_user=requested_user,
|
||||||
)
|
):
|
||||||
|
messages.warning(request, "Could not send email confirmation to existing domain managers.")
|
||||||
if requested_user is not None:
|
if requested_user is not None:
|
||||||
# Domain Invitation creation for an existing User
|
# Domain Invitation creation for an existing User
|
||||||
obj.retrieve()
|
obj.retrieve()
|
||||||
|
@ -1657,6 +1669,14 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
||||||
# Call the parent save method to save the object
|
# Call the parent save method to save the object
|
||||||
super().save_model(request, obj, form, change)
|
super().save_model(request, obj, form, change)
|
||||||
|
|
||||||
|
def delete_queryset(self, request, queryset):
|
||||||
|
"""We override the delete method in the model.
|
||||||
|
When deleting in DJA, if you select multiple items in a table using checkboxes and apply a delete action, the model
|
||||||
|
delete does not get called. This method gets called instead.
|
||||||
|
This override makes sure our code in the model gets executed in these situations."""
|
||||||
|
for obj in queryset:
|
||||||
|
obj.delete() # Calls the overridden delete method on each instance
|
||||||
|
|
||||||
|
|
||||||
class DomainInformationResource(resources.ModelResource):
|
class DomainInformationResource(resources.ModelResource):
|
||||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||||
|
@ -1678,22 +1698,25 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainInformation.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2031,22 +2054,25 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
DomainRequest.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(portfolio__organization_type__isnull=False, then="portfolio__organization_type"),
|
||||||
|
When(portfolio__isnull=True, generic_org_type__isnull=False, then="generic_org_type"),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_request in DomainRequest.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_request.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_request.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__organization_type=self.value())
|
Q(portfolio__organization_type=self.value())
|
||||||
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
| Q(portfolio__isnull=True, generic_org_type=self.value())
|
||||||
|
@ -2062,24 +2088,39 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
DomainRequest.objects.annotate(
|
||||||
for domain_request in DomainRequest.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_request.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_request.converted_federal_type_display # Display value
|
portfolio__isnull=False,
|
||||||
|
portfolio__federal_agency__federal_type__isnull=False,
|
||||||
if converted_federal_type:
|
then="portfolio__federal_agency__federal_type",
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
portfolio__isnull=True,
|
||||||
|
federal_agency__federal_type__isnull=False,
|
||||||
|
then="federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(portfolio__federal_agency__federal_type=self.value())
|
Q(portfolio__federal_agency__federal_type=self.value())
|
||||||
| Q(portfolio__isnull=True, federal_type=self.value())
|
| Q(portfolio__isnull=True, federal_type=self.value())
|
||||||
|
@ -3226,59 +3267,86 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
||||||
parameter_name = "converted_generic_orgs"
|
parameter_name = "converted_generic_orgs"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_generic_orgs = set()
|
# Annotate the queryset to avoid Python-side iteration
|
||||||
|
queryset = (
|
||||||
|
Domain.objects.annotate(
|
||||||
|
converted_generic_org=Case(
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__organization_type__isnull=False,
|
||||||
|
then="domain_info__portfolio__organization_type",
|
||||||
|
),
|
||||||
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__generic_org_type__isnull=False,
|
||||||
|
then="domain_info__generic_org_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_generic_org", flat=True)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Populate the set with tuples of (value, display value)
|
# Filter out empty results and return sorted list of unique values
|
||||||
for domain_info in DomainInformation.objects.all():
|
return sorted([(org, DomainRequest.OrganizationChoices.get_org_label(org)) for org in queryset if org])
|
||||||
converted_generic_org = domain_info.converted_generic_org_type # Actual value
|
|
||||||
converted_generic_org_display = domain_info.converted_generic_org_type_display # Display value
|
|
||||||
|
|
||||||
if converted_generic_org:
|
|
||||||
converted_generic_orgs.add((converted_generic_org, converted_generic_org_display)) # Value, Display
|
|
||||||
|
|
||||||
# Sort the set by display value
|
|
||||||
return sorted(converted_generic_orgs, key=lambda x: x[1]) # x[1] is the display value
|
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a generic org is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__organization_type=self.value())
|
Q(domain_info__portfolio__organization_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__generic_org_type=self.value())
|
||||||
)
|
)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
class FederalTypeFilter(admin.SimpleListFilter):
|
class FederalTypeFilter(admin.SimpleListFilter):
|
||||||
"""Custom Federal Type filter that accomodates portfolio feature.
|
"""Custom Federal Type filter that accomodates portfolio feature.
|
||||||
If we have a portfolio, use the portfolio's federal type. If not, use the
|
If we have a portfolio, use the portfolio's federal type. If not, use the
|
||||||
federal type in the Domain Information object."""
|
organization in the Domain Request object."""
|
||||||
|
|
||||||
title = "federal type"
|
title = "federal type"
|
||||||
parameter_name = "converted_federal_types"
|
parameter_name = "converted_federal_types"
|
||||||
|
|
||||||
def lookups(self, request, model_admin):
|
def lookups(self, request, model_admin):
|
||||||
converted_federal_types = set()
|
# Annotate the queryset for efficient filtering
|
||||||
|
queryset = (
|
||||||
# Populate the set with tuples of (value, display value)
|
Domain.objects.annotate(
|
||||||
for domain_info in DomainInformation.objects.all():
|
converted_federal_type=Case(
|
||||||
converted_federal_type = domain_info.converted_federal_type # Actual value
|
When(
|
||||||
converted_federal_type_display = domain_info.converted_federal_type_display # Display value
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=False,
|
||||||
if converted_federal_type:
|
then=F("domain_info__portfolio__federal_agency__federal_type"),
|
||||||
converted_federal_types.add(
|
),
|
||||||
(converted_federal_type, converted_federal_type_display) # Value, Display
|
When(
|
||||||
|
domain_info__isnull=False,
|
||||||
|
domain_info__portfolio__isnull=True,
|
||||||
|
domain_info__federal_type__isnull=False,
|
||||||
|
then="domain_info__federal_agency__federal_type",
|
||||||
|
),
|
||||||
|
default=Value(""),
|
||||||
|
output_field=CharField(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("converted_federal_type", flat=True)
|
||||||
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort the set by display value
|
# Filter out empty values and return sorted unique entries
|
||||||
return sorted(converted_federal_types, key=lambda x: x[1]) # x[1] is the display value
|
return sorted(
|
||||||
|
[
|
||||||
|
(federal_type, BranchChoices.get_branch_label(federal_type))
|
||||||
|
for federal_type in queryset
|
||||||
|
if federal_type
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
# Filter queryset
|
|
||||||
def queryset(self, request, queryset):
|
def queryset(self, request, queryset):
|
||||||
if self.value(): # Check if a federal type is selected in the filter
|
if self.value():
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
Q(domain_info__portfolio__federal_agency__federal_type=self.value())
|
Q(domain_info__portfolio__federal_type=self.value())
|
||||||
| Q(domain_info__portfolio__isnull=True, domain_info__federal_agency__federal_type=self.value())
|
| Q(domain_info__portfolio__isnull=True, domain_info__federal_type=self.value())
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
* - tooltip dynamic content updated to include nested element (for better sizing control)
|
||||||
* - modal exposed to window to be accessible in other js files
|
* - modal exposed to window to be accessible in other js files
|
||||||
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
* - fixed bug in createHeaderButton which added newlines to header button tooltips
|
||||||
|
* - modified combobox to handle error class
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if ("document" in window.self) {
|
if ("document" in window.self) {
|
||||||
|
@ -1213,6 +1214,11 @@ const enhanceComboBox = _comboBoxEl => {
|
||||||
input.setAttribute("class", INPUT_CLASS);
|
input.setAttribute("class", INPUT_CLASS);
|
||||||
input.setAttribute("type", "text");
|
input.setAttribute("type", "text");
|
||||||
input.setAttribute("role", "combobox");
|
input.setAttribute("role", "combobox");
|
||||||
|
// DOTGOV - handle error class for combobox
|
||||||
|
// Check if 'usa-input--error' exists in selectEl and add it to input if true
|
||||||
|
if (selectEl.classList.contains('usa-input--error')) {
|
||||||
|
input.classList.add('usa-input--error');
|
||||||
|
}
|
||||||
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
additionalAttributes.forEach(attr => Object.keys(attr).forEach(key => {
|
||||||
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
const value = Sanitizer.escapeHTML`${attr[key]}`;
|
||||||
input.setAttribute(key, value);
|
input.setAttribute(key, value);
|
||||||
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { hideElement, showElement } from './helpers.js';
|
|
||||||
|
|
||||||
export function loadInitialValuesForComboBoxes() {
|
|
||||||
var overrideDefaultClearButton = true;
|
|
||||||
var isTyping = false;
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', (event) => {
|
|
||||||
handleAllComboBoxElements();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleAllComboBoxElements() {
|
|
||||||
const comboBoxElements = document.querySelectorAll(".usa-combo-box");
|
|
||||||
comboBoxElements.forEach(comboBox => {
|
|
||||||
const input = comboBox.querySelector("input");
|
|
||||||
const select = comboBox.querySelector("select");
|
|
||||||
if (!input || !select) {
|
|
||||||
console.warn("No combobox element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Set the initial value of the combobox
|
|
||||||
let initialValue = select.getAttribute("data-default-value");
|
|
||||||
let clearInputButton = comboBox.querySelector(".usa-combo-box__clear-input");
|
|
||||||
if (!clearInputButton) {
|
|
||||||
console.warn("No clear element found");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override the default clear button behavior such that it no longer clears the input,
|
|
||||||
// it just resets to the data-initial-value.
|
|
||||||
// Due to the nature of how uswds works, this is slightly hacky.
|
|
||||||
// Use a MutationObserver to watch for changes in the dropdown list
|
|
||||||
const dropdownList = comboBox.querySelector(`#${input.id}--list`);
|
|
||||||
const observer = new MutationObserver(function(mutations) {
|
|
||||||
mutations.forEach(function(mutation) {
|
|
||||||
if (mutation.type === "childList") {
|
|
||||||
addBlankOption(clearInputButton, dropdownList, initialValue);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configure the observer to watch for changes in the dropdown list
|
|
||||||
const config = { childList: true, subtree: true };
|
|
||||||
observer.observe(dropdownList, config);
|
|
||||||
|
|
||||||
// Input event listener to detect typing
|
|
||||||
input.addEventListener("input", () => {
|
|
||||||
isTyping = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Blur event listener to reset typing state
|
|
||||||
input.addEventListener("blur", () => {
|
|
||||||
isTyping = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Hide the reset button when there is nothing to reset.
|
|
||||||
// Do this once on init, then everytime a change occurs.
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
select.addEventListener("change", () => {
|
|
||||||
updateClearButtonVisibility(select, initialValue, clearInputButton)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Change the default input behaviour - have it reset to the data default instead
|
|
||||||
clearInputButton.addEventListener("click", (e) => {
|
|
||||||
if (overrideDefaultClearButton && initialValue) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
input.click();
|
|
||||||
// Find the dropdown option with the desired value
|
|
||||||
const dropdownOptions = document.querySelectorAll(".usa-combo-box__list-option");
|
|
||||||
if (dropdownOptions) {
|
|
||||||
dropdownOptions.forEach(option => {
|
|
||||||
if (option.getAttribute("data-value") === initialValue) {
|
|
||||||
// Simulate a click event on the dropdown option
|
|
||||||
option.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateClearButtonVisibility(select, initialValue, clearInputButton) {
|
|
||||||
if (select.value === initialValue) {
|
|
||||||
hideElement(clearInputButton);
|
|
||||||
}else {
|
|
||||||
showElement(clearInputButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addBlankOption(clearInputButton, dropdownList, initialValue) {
|
|
||||||
if (dropdownList && !dropdownList.querySelector('[data-value=""]') && !isTyping) {
|
|
||||||
const blankOption = document.createElement("li");
|
|
||||||
blankOption.setAttribute("role", "option");
|
|
||||||
blankOption.setAttribute("data-value", "");
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option");
|
|
||||||
if (!initialValue){
|
|
||||||
blankOption.classList.add("usa-combo-box__list-option--selected")
|
|
||||||
}
|
|
||||||
blankOption.textContent = "⎯";
|
|
||||||
|
|
||||||
dropdownList.insertBefore(blankOption, dropdownList.firstChild);
|
|
||||||
blankOption.addEventListener("click", (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
overrideDefaultClearButton = false;
|
|
||||||
// Trigger the default clear behavior
|
|
||||||
clearInputButton.click();
|
|
||||||
overrideDefaultClearButton = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ import { initDomainValidators } from './domain-validators.js';
|
||||||
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
import { initFormsetsForms, triggerModalOnDsDataForm, nameserversFormListener } from './formset-forms.js';
|
||||||
import { initializeUrbanizationToggle } from './urbanization.js';
|
import { initializeUrbanizationToggle } from './urbanization.js';
|
||||||
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
import { userProfileListener, finishUserSetupListener } from './user-profile.js';
|
||||||
import { loadInitialValuesForComboBoxes } from './combobox.js';
|
|
||||||
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
import { handleRequestingEntityFieldset } from './requesting-entity.js';
|
||||||
import { initDomainsTable } from './table-domains.js';
|
import { initDomainsTable } from './table-domains.js';
|
||||||
import { initDomainRequestsTable } from './table-domain-requests.js';
|
import { initDomainRequestsTable } from './table-domain-requests.js';
|
||||||
|
@ -31,8 +30,6 @@ initializeUrbanizationToggle();
|
||||||
userProfileListener();
|
userProfileListener();
|
||||||
finishUserSetupListener();
|
finishUserSetupListener();
|
||||||
|
|
||||||
loadInitialValuesForComboBoxes();
|
|
||||||
|
|
||||||
handleRequestingEntityFieldset();
|
handleRequestingEntityFieldset();
|
||||||
|
|
||||||
initDomainsTable();
|
initDomainsTable();
|
||||||
|
|
|
@ -18,11 +18,11 @@ export function initPortfolioNewMemberPageToggle() {
|
||||||
const unique_id = `${member_type}-${member_id}`;
|
const unique_id = `${member_type}-${member_id}`;
|
||||||
|
|
||||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member_name}`);
|
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
|
||||||
|
|
||||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||||
// All prod users will have emails linked to their account
|
// All prod users will have emails linked to their account
|
||||||
MembersTable.addMemberModal(num_domains, member_email || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
MembersTable.addMemberDeleteModal(num_domains, member_email || member_name || "Samwise Gamgee", member_delete_url, unique_id, wrapperDeleteAction);
|
||||||
|
|
||||||
uswdsInitializeModals();
|
uswdsInitializeModals();
|
||||||
|
|
||||||
|
@ -87,14 +87,6 @@ export function initAddNewMemberPageListeners() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
|
||||||
Helper function to capitalize the first letter in a string (for display purposes)
|
|
||||||
*/
|
|
||||||
function capitalizeFirstLetter(text) {
|
|
||||||
if (!text) return ''; // Return empty string if input is falsy
|
|
||||||
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Populates contents of the "Add Member" confirmation modal
|
Populates contents of the "Add Member" confirmation modal
|
||||||
*/
|
*/
|
||||||
|
@ -102,6 +94,8 @@ export function initAddNewMemberPageListeners() {
|
||||||
const permissionDetailsContainer = document.getElementById("permission_details");
|
const permissionDetailsContainer = document.getElementById("permission_details");
|
||||||
permissionDetailsContainer.innerHTML = ""; // Clear previous content
|
permissionDetailsContainer.innerHTML = ""; // Clear previous content
|
||||||
|
|
||||||
|
if (permission_details_div_id == 'member-basic-permissions') {
|
||||||
|
// for basic users, display values are based on selections in the form
|
||||||
// Get all permission sections (divs with h3 and radio inputs)
|
// Get all permission sections (divs with h3 and radio inputs)
|
||||||
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||||
|
|
||||||
|
@ -120,24 +114,39 @@ export function initAddNewMemberPageListeners() {
|
||||||
let selectedPermission = "No permission selected";
|
let selectedPermission = "No permission selected";
|
||||||
if (selectedRadio) {
|
if (selectedRadio) {
|
||||||
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
|
const label = fieldset.querySelector(`label[for="${selectedRadio.id}"]`);
|
||||||
selectedPermission = label ? label.textContent : "No permission selected";
|
if (label) {
|
||||||
|
// Get only the text node content (excluding subtext in <p>)
|
||||||
|
const mainText = Array.from(label.childNodes)
|
||||||
|
.filter(node => node.nodeType === Node.TEXT_NODE)
|
||||||
|
.map(node => node.textContent.trim())
|
||||||
|
.join(""); // Combine and trim whitespace
|
||||||
|
selectedPermission = mainText || "No permission selected";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Create new elements for the modal content
|
appendPermissionInContainer(sectionTitle, selectedPermission, permissionDetailsContainer);
|
||||||
const titleElement = document.createElement("h4");
|
|
||||||
titleElement.textContent = sectionTitle;
|
|
||||||
titleElement.classList.add("text-primary");
|
|
||||||
titleElement.classList.add("margin-bottom-0");
|
|
||||||
|
|
||||||
const permissionElement = document.createElement("p");
|
|
||||||
permissionElement.textContent = selectedPermission;
|
|
||||||
permissionElement.classList.add("margin-top-0");
|
|
||||||
|
|
||||||
// Append to the modal content container
|
|
||||||
permissionDetailsContainer.appendChild(titleElement);
|
|
||||||
permissionDetailsContainer.appendChild(permissionElement);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// for admin users, the permissions are always the same
|
||||||
|
appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer);
|
||||||
|
appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer);
|
||||||
|
appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendPermissionInContainer(sectionTitle, permissionDisplay, permissionContainer) {
|
||||||
|
// Create new elements for the content
|
||||||
|
const titleElement = document.createElement("h4");
|
||||||
|
titleElement.textContent = sectionTitle;
|
||||||
|
titleElement.classList.add("text-primary", "margin-bottom-0");
|
||||||
|
|
||||||
|
const permissionElement = document.createElement("p");
|
||||||
|
permissionElement.textContent = permissionDisplay;
|
||||||
|
permissionElement.classList.add("margin-top-0");
|
||||||
|
|
||||||
|
// Append to the content container
|
||||||
|
permissionContainer.appendChild(titleElement);
|
||||||
|
permissionContainer.appendChild(permissionElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -149,18 +158,25 @@ export function initAddNewMemberPageListeners() {
|
||||||
let emailValue = document.getElementById('id_email').value;
|
let emailValue = document.getElementById('id_email').value;
|
||||||
document.getElementById('modalEmail').textContent = emailValue;
|
document.getElementById('modalEmail').textContent = emailValue;
|
||||||
|
|
||||||
// Get selected radio button for access level
|
// Get selected radio button for member access level
|
||||||
let selectedAccess = document.querySelector('input[name="role"]:checked');
|
let selectedAccess = document.querySelector('input[name="role"]:checked');
|
||||||
// Set the selected permission text to 'Basic' or 'Admin' (the value of the selected radio button)
|
// Map the access level values to user-friendly labels
|
||||||
// This value does not have the first letter capitalized so let's capitalize it
|
const accessLevelMapping = {
|
||||||
let accessText = selectedAccess ? capitalizeFirstLetter(selectedAccess.value) : "No access level selected";
|
organization_admin: "Admin",
|
||||||
|
organization_member: "Basic",
|
||||||
|
};
|
||||||
|
// Determine the access text based on the selected value
|
||||||
|
let accessText = selectedAccess
|
||||||
|
? accessLevelMapping[selectedAccess.value] || "Unknown access level"
|
||||||
|
: "No access level selected";
|
||||||
|
// Update the modal with the appropriate member access level text
|
||||||
document.getElementById('modalAccessLevel').textContent = accessText;
|
document.getElementById('modalAccessLevel').textContent = accessText;
|
||||||
|
|
||||||
// Populate permission details based on access level
|
// Populate permission details based on access level
|
||||||
if (selectedAccess && selectedAccess.value === 'organization_admin') {
|
if (selectedAccess && selectedAccess.value === 'organization_admin') {
|
||||||
populatePermissionDetails('new-member-admin-permissions');
|
populatePermissionDetails('admin');
|
||||||
} else {
|
} else {
|
||||||
populatePermissionDetails('new-member-basic-permissions');
|
populatePermissionDetails('member-basic-permissions');
|
||||||
}
|
}
|
||||||
|
|
||||||
//------- Show the modal
|
//------- Show the modal
|
||||||
|
@ -177,22 +193,14 @@ export function initPortfolioMemberPageRadio() {
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
let memberForm = document.getElementById("member_form");
|
let memberForm = document.getElementById("member_form");
|
||||||
let newMemberForm = document.getElementById("add_member_form")
|
let newMemberForm = document.getElementById("add_member_form")
|
||||||
if (memberForm) {
|
if (memberForm || newMemberForm) {
|
||||||
hookupRadioTogglerListener(
|
hookupRadioTogglerListener(
|
||||||
'role',
|
'role',
|
||||||
{
|
{
|
||||||
'organization_admin': 'member-admin-permissions',
|
'organization_admin': '',
|
||||||
'organization_member': 'member-basic-permissions'
|
'organization_member': 'member-basic-permissions'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}else if (newMemberForm){
|
|
||||||
hookupRadioTogglerListener(
|
|
||||||
'role',
|
|
||||||
{
|
|
||||||
'organization_admin': 'new-member-admin-permissions',
|
|
||||||
'organization_member': 'new-member-basic-permissions'
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,15 @@ export function handleRequestingEntityFieldset() {
|
||||||
const formPrefix = "portfolio_requesting_entity";
|
const formPrefix = "portfolio_requesting_entity";
|
||||||
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
const radioFieldset = document.getElementById(`id_${formPrefix}-requesting_entity_is_suborganization__fieldset`);
|
||||||
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
const radios = radioFieldset?.querySelectorAll(`input[name="${formPrefix}-requesting_entity_is_suborganization"]`);
|
||||||
const select = document.getElementById(`id_${formPrefix}-sub_organization`);
|
const input = document.getElementById(`id_${formPrefix}-sub_organization`);
|
||||||
const selectParent = select?.parentElement;
|
const inputGrandParent = input?.parentElement?.parentElement;
|
||||||
|
const select = input?.previousElementSibling;
|
||||||
const suborgContainer = document.getElementById("suborganization-container");
|
const suborgContainer = document.getElementById("suborganization-container");
|
||||||
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
const suborgDetailsContainer = document.getElementById("suborganization-container__details");
|
||||||
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
const suborgAddtlInstruction = document.getElementById("suborganization-addtl-instruction");
|
||||||
const subOrgCreateNewOption = document.getElementById("option-to-add-suborg")?.value;
|
|
||||||
// Make sure all crucial page elements exist before proceeding.
|
// Make sure all crucial page elements exist before proceeding.
|
||||||
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
// This more or less ensures that we are on the Requesting Entity page, and not elsewhere.
|
||||||
if (!radios || !select || !selectParent || !suborgContainer || !suborgDetailsContainer) return;
|
if (!radios || !input || !select || !inputGrandParent || !suborgContainer || !suborgDetailsContainer) return;
|
||||||
|
|
||||||
// requestingSuborganization: This just broadly determines if they're requesting a suborg at all
|
// requestingSuborganization: This just broadly determines if they're requesting a suborg at all
|
||||||
// requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not.
|
// requestingNewSuborganization: This variable determines if the user is trying to *create* a new suborganization or not.
|
||||||
|
@ -27,8 +27,8 @@ export function handleRequestingEntityFieldset() {
|
||||||
function toggleSuborganization(radio=null) {
|
function toggleSuborganization(radio=null) {
|
||||||
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
if (radio != null) requestingSuborganization = radio?.checked && radio.value === "True";
|
||||||
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
requestingSuborganization ? showElement(suborgContainer) : hideElement(suborgContainer);
|
||||||
if (select.options.length == 2) { // --Select-- and other are the only options
|
if (select.options.length == 1) { // other is the only option
|
||||||
hideElement(selectParent); // Hide the select drop down and indicate requesting new suborg
|
hideElement(inputGrandParent); // Hide the combo box and indicate requesting new suborg
|
||||||
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
hideElement(suborgAddtlInstruction); // Hide additional instruction related to the list
|
||||||
requestingNewSuborganization.value = "True";
|
requestingNewSuborganization.value = "True";
|
||||||
} else {
|
} else {
|
||||||
|
@ -37,11 +37,6 @@ export function handleRequestingEntityFieldset() {
|
||||||
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
requestingNewSuborganization.value === "True" ? showElement(suborgDetailsContainer) : hideElement(suborgDetailsContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add fake "other" option to sub_organization select
|
|
||||||
if (select && !Array.from(select.options).some(option => option.value === "other")) {
|
|
||||||
select.add(new Option(subOrgCreateNewOption, "other"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (requestingNewSuborganization.value === "True") {
|
if (requestingNewSuborganization.value === "True") {
|
||||||
select.value = "other";
|
select.value = "other";
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,6 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
||||||
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
<use xlink:href="/public/img/sprite.svg#delete"></use>
|
||||||
</svg>` : ''}
|
</svg>` : ''}
|
||||||
${modal_button_text}
|
${modal_button_text}
|
||||||
<span class="usa-sr-only">${screen_reader_text}</span>
|
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
@ -107,6 +106,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
||||||
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
class="usa-button usa-button--unstyled usa-button--with-icon usa-accordion__button usa-button--more-actions"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="more-actions-${unique_id}"
|
aria-controls="more-actions-${unique_id}"
|
||||||
|
aria-label="${screen_reader_text}"
|
||||||
>
|
>
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||||
|
@ -129,7 +129,7 @@ export class BaseTable {
|
||||||
this.displayName = itemName;
|
this.displayName = itemName;
|
||||||
this.sectionSelector = itemName + 's';
|
this.sectionSelector = itemName + 's';
|
||||||
this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`);
|
this.tableWrapper = document.getElementById(`${this.sectionSelector}__table-wrapper`);
|
||||||
this.tableHeaders = document.querySelectorAll(`#${this.sectionSelector} th[data-sortable]`);
|
this.tableHeaderSortButtons = document.querySelectorAll(`#${this.sectionSelector} th[data-sortable] button`);
|
||||||
this.currentSortBy = 'id';
|
this.currentSortBy = 'id';
|
||||||
this.currentOrder = 'asc';
|
this.currentOrder = 'asc';
|
||||||
this.currentStatus = [];
|
this.currentStatus = [];
|
||||||
|
@ -284,15 +284,18 @@ export class BaseTable {
|
||||||
showElement(dataWrapper);
|
showElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
showElement(noSearchResultsWrapper);
|
showElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noSearchResultsWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
showElement(noDataWrapper);
|
showElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noDataWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -300,13 +303,18 @@ export class BaseTable {
|
||||||
* A helper that resets sortable table headers
|
* A helper that resets sortable table headers
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
unsetHeader = (header) => {
|
unsetHeader = (headerSortButton) => {
|
||||||
|
let header = headerSortButton.closest('th');
|
||||||
|
if (header) {
|
||||||
header.removeAttribute('aria-sort');
|
header.removeAttribute('aria-sort');
|
||||||
let headerName = header.innerText;
|
let headerName = header.innerText;
|
||||||
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
const headerLabel = `${headerName}, sortable column, currently unsorted"`;
|
||||||
const headerButtonLabel = `Click to sort by ascending order.`;
|
const headerButtonLabel = `Click to sort by ascending order.`;
|
||||||
header.setAttribute("aria-label", headerLabel);
|
header.setAttribute("aria-label", headerLabel);
|
||||||
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
header.querySelector('.usa-table__header__button').setAttribute("title", headerButtonLabel);
|
||||||
|
} else {
|
||||||
|
console.warn('Issue with DOM');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -448,6 +456,7 @@ export class BaseTable {
|
||||||
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
||||||
if (!baseUrlValue) return;
|
if (!baseUrlValue) return;
|
||||||
|
|
||||||
|
this.tableAnnouncementRegion.innerHTML = '<p>Loading table.</p>';
|
||||||
let url = `${baseUrlValue}?${searchParams.toString()}`
|
let url = `${baseUrlValue}?${searchParams.toString()}`
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
|
@ -469,7 +478,6 @@ export class BaseTable {
|
||||||
|
|
||||||
let dataObjects = this.getDataObjects(data);
|
let dataObjects = this.getDataObjects(data);
|
||||||
let customTableOptions = this.customizeTable(data);
|
let customTableOptions = this.customizeTable(data);
|
||||||
|
|
||||||
dataObjects.forEach(dataObject => {
|
dataObjects.forEach(dataObject => {
|
||||||
this.addRow(dataObject, tbody, customTableOptions);
|
this.addRow(dataObject, tbody, customTableOptions);
|
||||||
});
|
});
|
||||||
|
@ -502,9 +510,10 @@ export class BaseTable {
|
||||||
|
|
||||||
// Add event listeners to table headers for sorting
|
// Add event listeners to table headers for sorting
|
||||||
initializeTableHeaders() {
|
initializeTableHeaders() {
|
||||||
this.tableHeaders.forEach(header => {
|
this.tableHeaderSortButtons.forEach(tableHeader => {
|
||||||
header.addEventListener('click', event => {
|
tableHeader.addEventListener('click', event => {
|
||||||
let button = header.querySelector('.usa-table__header__button')
|
let header = tableHeader.closest('th');
|
||||||
|
if (header) {
|
||||||
const sortBy = header.getAttribute('data-sortable');
|
const sortBy = header.getAttribute('data-sortable');
|
||||||
let order = 'asc';
|
let order = 'asc';
|
||||||
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
// sort order will be ascending, unless the currently sorted column is ascending, and the user
|
||||||
|
@ -514,12 +523,8 @@ export class BaseTable {
|
||||||
}
|
}
|
||||||
// load the results with the updated sort
|
// load the results with the updated sort
|
||||||
this.loadTable(1, sortBy, order);
|
this.loadTable(1, sortBy, order);
|
||||||
// If the click occurs outside of the button, need to simulate a button click in order
|
} else {
|
||||||
// for USWDS listener on the button to execute.
|
console.warn('Issue with DOM');
|
||||||
// Check first to see if click occurs outside of the button
|
|
||||||
if (!button.contains(event.target)) {
|
|
||||||
// Simulate a button click
|
|
||||||
button.click();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -584,9 +589,9 @@ export class BaseTable {
|
||||||
|
|
||||||
// Reset UI and accessibility
|
// Reset UI and accessibility
|
||||||
resetHeaders() {
|
resetHeaders() {
|
||||||
this.tableHeaders.forEach(header => {
|
this.tableHeaderSortButtons.forEach(headerSortButton => {
|
||||||
// Unset sort UI in headers
|
// Unset sort UI in headers
|
||||||
this.unsetHeader(header);
|
this.unsetHeader(headerSortButton);
|
||||||
});
|
});
|
||||||
// Reset the announcement region
|
// Reset the announcement region
|
||||||
this.tableAnnouncementRegion.innerHTML = '';
|
this.tableAnnouncementRegion.innerHTML = '';
|
||||||
|
|
|
@ -103,8 +103,9 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
disabled = true;
|
disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// uses margin-right-neg-5 as a hack to increase the text-wrapping width on this table
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td data-label="Selection" data-sort-value="0" class="padding-right-105">
|
<th scope="row" role="rowheader" data-label="Selection" data-sort-value="0" class="padding-right-105">
|
||||||
<div class="usa-checkbox">
|
<div class="usa-checkbox">
|
||||||
<input
|
<input
|
||||||
class="usa-checkbox__input"
|
class="usa-checkbox__input"
|
||||||
|
@ -112,6 +113,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
name="${domain.name}"
|
name="${domain.name}"
|
||||||
value="${domain.id}"
|
value="${domain.id}"
|
||||||
|
aria-label="${domain.name}"
|
||||||
${checked ? 'checked' : ''}
|
${checked ? 'checked' : ''}
|
||||||
${disabled ? 'disabled' : ''}
|
${disabled ? 'disabled' : ''}
|
||||||
/>
|
/>
|
||||||
|
@ -119,10 +121,10 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
<span class="sr-only">${domain.id}</span>
|
<span class="sr-only">${domain.id}</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</th>
|
||||||
<td data-label="Domain name">
|
<td data-label="Domain name">
|
||||||
${domain.name}
|
${domain.name}
|
||||||
${disabled ? '<span class="display-block margin-top-05 text-gray-50">Domains must have one domain manager. To unassign this member, the domain needs another domain manager.</span>' : ''}
|
${disabled ? '<span class="display-block margin-top-05 text-gray-50 margin-right-neg-5">Domains must have one domain manager. To unassign this member, the domain needs another domain manager.</span>' : ''}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
|
@ -235,7 +237,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Create unassigned domains list
|
// Create unassigned domains list
|
||||||
const unassignedDomainsList = document.createElement('ul');
|
const unassignedDomainsList = document.createElement('ul');
|
||||||
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
this.removedDomains.forEach(removedDomain => {
|
let removedDomainsCopy = [...this.removedDomains].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
removedDomainsCopy.forEach(removedDomain => {
|
||||||
const removedDomainListItem = document.createElement('li');
|
const removedDomainListItem = document.createElement('li');
|
||||||
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
|
removedDomainListItem.textContent = removedDomain.name; // Use textContent for security
|
||||||
unassignedDomainsList.appendChild(removedDomainListItem);
|
unassignedDomainsList.appendChild(removedDomainListItem);
|
||||||
|
@ -244,7 +247,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Create assigned domains list
|
// Create assigned domains list
|
||||||
const assignedDomainsList = document.createElement('ul');
|
const assignedDomainsList = document.createElement('ul');
|
||||||
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
assignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||||
this.addedDomains.forEach(addedDomain => {
|
let addedDomainsCopy = [...this.addedDomains].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
addedDomainsCopy.forEach(addedDomain => {
|
||||||
const addedDomainListItem = document.createElement('li');
|
const addedDomainListItem = document.createElement('li');
|
||||||
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
|
addedDomainListItem.textContent = addedDomain.name; // Use textContent for security
|
||||||
assignedDomainsList.appendChild(addedDomainListItem);
|
assignedDomainsList.appendChild(addedDomainListItem);
|
||||||
|
@ -259,7 +263,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append unassigned domains section
|
// Append unassigned domains section
|
||||||
if (this.removedDomains.length) {
|
if (this.removedDomains.length) {
|
||||||
const unassignedHeader = document.createElement('h3');
|
const unassignedHeader = document.createElement('h3');
|
||||||
unassignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
unassignedHeader.textContent = 'Unassigned domains';
|
unassignedHeader.textContent = 'Unassigned domains';
|
||||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||||
|
@ -268,7 +272,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
// Append assigned domains section
|
// Append assigned domains section
|
||||||
if (this.addedDomains.length) {
|
if (this.addedDomains.length) {
|
||||||
const assignedHeader = document.createElement('h3');
|
const assignedHeader = document.createElement('h3');
|
||||||
assignedHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
// Make this h3 look like a h4
|
||||||
|
assignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
assignedHeader.textContent = 'Assigned domains';
|
assignedHeader.textContent = 'Assigned domains';
|
||||||
domainAssignmentSummary.appendChild(assignedHeader);
|
domainAssignmentSummary.appendChild(assignedHeader);
|
||||||
domainAssignmentSummary.appendChild(assignedDomainsList);
|
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||||
|
@ -276,7 +281,8 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
|
|
||||||
// Append total assigned domains section
|
// Append total assigned domains section
|
||||||
const totalHeader = document.createElement('h3');
|
const totalHeader = document.createElement('h3');
|
||||||
totalHeader.classList.add('header--body', 'text-primary', 'margin-bottom-1');
|
// Make this h3 look like a h4
|
||||||
|
totalHeader.classList.add('margin-bottom-05', 'h4');
|
||||||
totalHeader.textContent = 'Total assigned domains';
|
totalHeader.textContent = 'Total assigned domains';
|
||||||
domainAssignmentSummary.appendChild(totalHeader);
|
domainAssignmentSummary.appendChild(totalHeader);
|
||||||
const totalCount = document.createElement('p');
|
const totalCount = document.createElement('p');
|
||||||
|
@ -289,6 +295,7 @@ export class EditMemberDomainsTable extends BaseTable {
|
||||||
this.updateReadonlyDisplay();
|
this.updateReadonlyDisplay();
|
||||||
hideElement(this.editModeContainer);
|
hideElement(this.editModeContainer);
|
||||||
showElement(this.readonlyModeContainer);
|
showElement(this.readonlyModeContainer);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
showEditMode() {
|
showEditMode() {
|
||||||
|
|
|
@ -19,9 +19,9 @@ export class MemberDomainsTable extends BaseTable {
|
||||||
const domain = dataObject;
|
const domain = dataObject;
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td scope="row" data-label="Domain name">
|
<th scope="row" role="rowheader" data-label="Domain name">
|
||||||
${domain.name}
|
${domain.name}
|
||||||
</td>
|
</th>
|
||||||
`;
|
`;
|
||||||
tbody.appendChild(row);
|
tbody.appendChild(row);
|
||||||
}
|
}
|
||||||
|
@ -35,16 +35,19 @@ export class MemberDomainsTable extends BaseTable {
|
||||||
showElement(dataWrapper);
|
showElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = '';
|
||||||
} else {
|
} else {
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
showElement(noSearchResultsWrapper);
|
showElement(noSearchResultsWrapper);
|
||||||
hideElement(noDataWrapper);
|
hideElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noSearchResultsWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hideElement(searchSection);
|
hideElement(searchSection);
|
||||||
hideElement(dataWrapper);
|
hideElement(dataWrapper);
|
||||||
hideElement(noSearchResultsWrapper);
|
hideElement(noSearchResultsWrapper);
|
||||||
showElement(noDataWrapper);
|
showElement(noDataWrapper);
|
||||||
|
this.tableAnnouncementRegion.innerHTML = this.noDataWrapper.innerHTML;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,13 +78,12 @@ export class MembersTable extends BaseTable {
|
||||||
const num_domains = member.domain_urls.length;
|
const num_domains = member.domain_urls.length;
|
||||||
const last_active = this.handleLastActive(member.last_active);
|
const last_active = this.handleLastActive(member.last_active);
|
||||||
let cancelInvitationButton = member.type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
let cancelInvitationButton = member.type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||||
const kebabHTML = customTableOptions.hasAdditionalActions ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `for ${member.name}`): '';
|
const kebabHTML = customTableOptions.hasAdditionalActions ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `Expand for more options for ${member.name}`): '';
|
||||||
|
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
let admin_tagHTML = ``;
|
let admin_tagHTML = ``;
|
||||||
if (member.is_admin)
|
if (member.is_admin)
|
||||||
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary">Admin</span>`
|
admin_tagHTML = `<span class="usa-tag margin-left-1 bg-primary-dark text-semibold">Admin</span>`
|
||||||
|
|
||||||
// generate html blocks for domains and permissions for the member
|
// generate html blocks for domains and permissions for the member
|
||||||
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
||||||
|
@ -99,7 +98,8 @@ export class MembersTable extends BaseTable {
|
||||||
type="button"
|
type="button"
|
||||||
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
class="usa-button--show-more-button usa-button usa-button--unstyled display-block margin-top-1"
|
||||||
data-for=${unique_id}
|
data-for=${unique_id}
|
||||||
aria-label="Expand for additional information"
|
aria-label="Expand for additional information for ${member.member_display}"
|
||||||
|
aria-label-placeholder="${member.member_display}"
|
||||||
>
|
>
|
||||||
<span>Expand</span>
|
<span>Expand</span>
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
@ -137,7 +137,7 @@ export class MembersTable extends BaseTable {
|
||||||
}
|
}
|
||||||
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
// This easter egg is only for fixtures that dont have names as we are displaying their emails
|
||||||
// All prod users will have emails linked to their account
|
// All prod users will have emails linked to their account
|
||||||
if (customTableOptions.hasAdditionalActions) MembersTable.addMemberDeleteModal(num_domains, member.email || "Samwise Gamgee", member_delete_url, unique_id, row);
|
if (customTableOptions.hasAdditionalActions) MembersTable.addMemberDeleteModal(num_domains, member.email || member.name || "Samwise Gamgee", member_delete_url, unique_id, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -166,13 +166,27 @@ export class MembersTable extends BaseTable {
|
||||||
spanElement.textContent = 'Close';
|
spanElement.textContent = 'Close';
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||||
buttonParentRow.classList.add('hide-td-borders');
|
buttonParentRow.classList.add('hide-td-borders');
|
||||||
toggleButton.setAttribute('aria-label', 'Close additional information');
|
|
||||||
|
let ariaLabelText = "Close additional information";
|
||||||
|
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||||
|
if (ariaLabelPlaceholder) {
|
||||||
|
ariaLabelText = `Close additional information for ${ariaLabelPlaceholder}`;
|
||||||
|
}
|
||||||
|
toggleButton.setAttribute('aria-label', ariaLabelText);
|
||||||
|
|
||||||
|
// Set tabindex for focusable elements in expanded content
|
||||||
} else {
|
} else {
|
||||||
hideElement(contentDiv);
|
hideElement(contentDiv);
|
||||||
spanElement.textContent = 'Expand';
|
spanElement.textContent = 'Expand';
|
||||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||||
buttonParentRow.classList.remove('hide-td-borders');
|
buttonParentRow.classList.remove('hide-td-borders');
|
||||||
toggleButton.setAttribute('aria-label', 'Expand for additional information');
|
|
||||||
|
let ariaLabelText = "Expand for additional information";
|
||||||
|
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||||
|
if (ariaLabelPlaceholder) {
|
||||||
|
ariaLabelText = `Expand for additional information for ${ariaLabelPlaceholder}`;
|
||||||
|
}
|
||||||
|
toggleButton.setAttribute('aria-label', ariaLabelText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,21 +259,19 @@ export class MembersTable extends BaseTable {
|
||||||
// Only generate HTML if the member has one or more assigned domains
|
// Only generate HTML if the member has one or more assigned domains
|
||||||
if (num_domains > 0) {
|
if (num_domains > 0) {
|
||||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||||
domainsHTML += "<h4 class='margin-y-0 text-primary'>Domains assigned</h4>";
|
domainsHTML += "<h4 class='font-body-xs margin-y-0'>Domains assigned</h4>";
|
||||||
domainsHTML += `<p class='margin-y-0'>This member is assigned to ${num_domains} domains:</p>`;
|
domainsHTML += `<p class='font-body-xs text-base-dark margin-y-0'>This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:</p>`;
|
||||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||||
|
|
||||||
// Display up to 6 domains with their URLs
|
// Display up to 6 domains with their URLs
|
||||||
for (let i = 0; i < num_domains && i < 6; i++) {
|
for (let i = 0; i < num_domains && i < 6; i++) {
|
||||||
domainsHTML += `<li><a href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
domainsHTML += `<li><a class="font-body-xs" href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
domainsHTML += "</ul>";
|
domainsHTML += "</ul>";
|
||||||
|
|
||||||
// If there are more than 6 domains, display a "View assigned domains" link
|
// If there are more than 6 domains, display a "View assigned domains" link
|
||||||
if (num_domains >= 6) {
|
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View assigned domains</a></p>`;
|
||||||
domainsHTML += `<p><a href="${action_url}/domains">View assigned domains</a></p>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
domainsHTML += "</div>";
|
domainsHTML += "</div>";
|
||||||
}
|
}
|
||||||
|
@ -378,34 +390,37 @@ export class MembersTable extends BaseTable {
|
||||||
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
||||||
let permissionsHTML = '';
|
let permissionsHTML = '';
|
||||||
|
|
||||||
|
// Define shared classes across elements for easier refactoring
|
||||||
|
let sharedParagraphClasses = "font-body-xs text-base-dark margin-top-1 p--blockquote";
|
||||||
|
|
||||||
// Check domain-related permissions
|
// Check domain-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can view all organization domains. Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domains:</strong> Can manage domains they are assigned to and edit information about the domain (including DNS settings).</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check request-related permissions
|
// Check request-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests:</strong> Can view all organization domain requests. Can create domain requests and modify their own requests.</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Domain requests (view-only):</strong> Can view all organization domain requests. Can't create or modify any domain requests.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check member-related permissions
|
// Check member-related permissions
|
||||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members:</strong> Can manage members including inviting new members, removing current members, and assigning domains to members.</p>`;
|
||||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific permissions are assigned, display a message indicating no additional permissions
|
// If no specific permissions are assigned, display a message indicating no additional permissions
|
||||||
if (!permissionsHTML) {
|
if (!permissionsHTML) {
|
||||||
permissionsHTML += "<p class='margin-top-1 p--blockquote'><b>No additional permissions:</b> There are no additional permissions for this member.</p>";
|
permissionsHTML += `<p class='${sharedParagraphClasses}'><b>No additional permissions:</b> There are no additional permissions for this member.</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a permissions header and wrap the entire output in a container
|
// Add a permissions header and wrap the entire output in a container
|
||||||
permissionsHTML = "<div class='desktop:grid-col-7'><h4 class='margin-y-0 text-primary'>Additional permissions for this member</h4>" + permissionsHTML + "</div>";
|
permissionsHTML = `<div class='desktop:grid-col-7'><h4 class='font-body-xs margin-y-0'>Additional permissions for this member</h4>${permissionsHTML}</div>`;
|
||||||
|
|
||||||
return permissionsHTML;
|
return permissionsHTML;
|
||||||
}
|
}
|
||||||
|
@ -423,7 +438,7 @@ export class MembersTable extends BaseTable {
|
||||||
let modalDescription = ``;
|
let modalDescription = ``;
|
||||||
|
|
||||||
if (num_domains >= 0){
|
if (num_domains >= 0){
|
||||||
modalHeading = `Are you sure you want to delete ${member_email}?`;
|
modalHeading = `Are you sure you want to remove ${member_email} from the organization?`;
|
||||||
modalDescription = `They will no longer be able to access this organization.
|
modalDescription = `They will no longer be able to access this organization.
|
||||||
This action cannot be undone.`;
|
This action cannot be undone.`;
|
||||||
if (num_domains >= 1)
|
if (num_domains >= 1)
|
||||||
|
|
|
@ -49,3 +49,30 @@ tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
||||||
bottom: -10px;
|
bottom: -10px;
|
||||||
right: 30px;
|
right: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A CSS only show-more/show-less based on usa-accordion
|
||||||
|
.usa-accordion--show-more {
|
||||||
|
width: auto;
|
||||||
|
.usa-accordion__button[aria-expanded=false],
|
||||||
|
.usa-accordion__button[aria-expanded=false]:hover,
|
||||||
|
.usa-accordion__button[aria-expanded=true],
|
||||||
|
.usa-accordion__button[aria-expanded=true]:hover {
|
||||||
|
background-image: none;
|
||||||
|
background-color: transparent;
|
||||||
|
padding-right: 0;
|
||||||
|
padding-left: 0;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
.usa-accordion__button[aria-expanded=true] .expand-more {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.usa-accordion__button[aria-expanded=true] .expand-less {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.usa-accordion__button[aria-expanded=false] .expand-more {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.usa-accordion__button[aria-expanded=false] .expand-less {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -188,7 +188,7 @@ html[data-theme="dark"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#branding h1,
|
#branding h1,
|
||||||
h1, h2, h3,
|
.dashboard h1, .dashboard h2, .dashboard h3,
|
||||||
.module h2 {
|
.module h2 {
|
||||||
font-weight: font-weight('bold');
|
font-weight: font-weight('bold');
|
||||||
}
|
}
|
||||||
|
@ -516,10 +516,6 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
|
||||||
max-width: 68ex;
|
max-width: 68ex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.usa-summary-box__dhs-color {
|
|
||||||
color: $dhs-blue-70;
|
|
||||||
}
|
|
||||||
|
|
||||||
details.dja-detail-table {
|
details.dja-detail-table {
|
||||||
display: inline-table;
|
display: inline-table;
|
||||||
background-color: var(--body-bg);
|
background-color: var(--body-bg);
|
||||||
|
@ -812,18 +808,6 @@ div.dja__model-description{
|
||||||
text-decoration: underline !important;
|
text-decoration: underline !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
//-- Override some styling for the USWDS summary box (per design quidance for ticket #2055
|
|
||||||
.usa-summary-box {
|
|
||||||
background: #{$dhs-blue-10};
|
|
||||||
border-color: #{$dhs-blue-30};
|
|
||||||
max-width: 72ex;
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usa-summary-box h3 {
|
|
||||||
color: #{$dhs-blue-60};
|
|
||||||
}
|
|
||||||
|
|
||||||
.module caption, .inline-group h2 {
|
.module caption, .inline-group h2 {
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
}
|
}
|
||||||
|
@ -929,14 +913,6 @@ ul.add-list-reset {
|
||||||
font-size: 14px;
|
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 {
|
.organization-admin-label {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: .8125rem;
|
font-size: .8125rem;
|
||||||
|
|
|
@ -59,7 +59,6 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: color('primary-dark');
|
|
||||||
margin-top: units(2);
|
margin-top: units(2);
|
||||||
margin-bottom: units(2);
|
margin-bottom: units(2);
|
||||||
}
|
}
|
||||||
|
@ -130,16 +129,6 @@ grid column to the max-width of the searchbar, which was calculated to be 33rem.
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotgov-status-box {
|
|
||||||
background-color: color('primary-lightest');
|
|
||||||
border-color: color('accent-cool-lighter');
|
|
||||||
}
|
|
||||||
|
|
||||||
.dotgov-status-box--action-need {
|
|
||||||
background-color: color('warning-lighter');
|
|
||||||
border-color: color('warning');
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
border-top: 1px solid color('primary-darker');
|
border-top: 1px solid color('primary-darker');
|
||||||
}
|
}
|
||||||
|
@ -228,14 +217,6 @@ abbr[title] {
|
||||||
max-width: 23ch;
|
max-width: 23ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ellipsis--30 {
|
|
||||||
max-width: 30ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ellipsis--50 {
|
|
||||||
max-width: 50ch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.vertical-align-middle {
|
.vertical-align-middle {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -272,6 +253,14 @@ abbr[title] {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.string-wrap {
|
||||||
|
white-space: normal;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: visible;
|
||||||
|
word-break: break-all;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
//Icon size adjustment used by buttons and form errors
|
//Icon size adjustment used by buttons and form errors
|
||||||
.usa-icon.usa-icon--large {
|
.usa-icon.usa-icon--large {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -236,13 +236,6 @@ a.withdraw_outline:active {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dotgov-table a
|
|
||||||
a .usa-icon,
|
|
||||||
.usa-button--with-icon .usa-icon {
|
|
||||||
height: 1.3em;
|
|
||||||
width: 1.3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Red, for delete buttons
|
// Red, for delete buttons
|
||||||
// Used on: All delete buttons
|
// Used on: All delete buttons
|
||||||
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
// Note: Can be simplified by adding text-secondary to delete anchors in tables
|
||||||
|
@ -253,6 +246,10 @@ a.text-secondary:hover {
|
||||||
color: $theme-color-error;
|
color: $theme-color-error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.usa-button.usa-button--secondary {
|
||||||
|
background-color: $theme-color-error;
|
||||||
|
}
|
||||||
|
|
||||||
.usa-button--show-more-button {
|
.usa-button--show-more-button {
|
||||||
font-size: size('ui', 'xs');
|
font-size: size('ui', 'xs');
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
@use "cisa_colors" as *;
|
@use "cisa_colors" as *;
|
||||||
@use "typography" as *;
|
|
||||||
|
|
||||||
|
// Normalize typography in forms
|
||||||
|
.usa-form,
|
||||||
|
.usa-form fieldset {
|
||||||
|
font-size: 1rem;
|
||||||
|
.usa-legend {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
.usa-form .usa-button {
|
.usa-form .usa-button {
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
}
|
}
|
||||||
|
@ -69,16 +76,6 @@ legend.float-left-tablet + button.float-right-tablet {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.read-only-label {
|
|
||||||
@extend .h4--sm-05;
|
|
||||||
font-weight: bold;
|
|
||||||
color: color('primary-dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-only-value {
|
|
||||||
margin-top: units(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.bg-gray-1 .usa-radio {
|
.bg-gray-1 .usa-radio {
|
||||||
background: color('gray-1');
|
background: color('gray-1');
|
||||||
}
|
}
|
||||||
|
|
5
src/registrar/assets/src/sass/_theme/_modals.scss
Normal file
5
src/registrar/assets/src/sass/_theme/_modals.scss
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-modal__main {
|
||||||
|
padding: 0 2rem 2rem;
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
@use "uswds-core" as *;
|
@use "uswds-core" as *;
|
||||||
@use "typography" as *;
|
|
||||||
|
|
||||||
.register-form-step > h1 {
|
.register-form-step > h1 {
|
||||||
//align to top of sidebar on first page of the form
|
//align to top of sidebar on first page of the form
|
||||||
|
@ -12,11 +11,7 @@
|
||||||
margin-top: units(1);
|
margin-top: units(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// header--body is used on the summary page and
|
.register-form-step h3:not(.margin-top-05) {
|
||||||
// should not be styled like the register form headers
|
|
||||||
.register-form-step h3 {
|
|
||||||
color: color('primary-dark');
|
|
||||||
letter-spacing: $letter-space--xs;
|
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
@ -64,26 +59,10 @@
|
||||||
margin-top: units(3);
|
margin-top: units(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary-item hr,
|
.summary-item hr,
|
||||||
.review__step hr {
|
.review__step hr {
|
||||||
border: none; //reset
|
border: none; //reset
|
||||||
border-top: 1px solid color('primary-dark');
|
border-top: 1px solid color('primary-dark');
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: units(0.5);
|
margin-bottom: units(0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.review__step__title a:visited {
|
|
||||||
color: color('primary');
|
|
||||||
}
|
|
||||||
|
|
||||||
.review__step__name {
|
|
||||||
color: color('primary-dark');
|
|
||||||
font-weight: font-weight('semibold');
|
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.review__step__subheading {
|
|
||||||
color: color('primary-dark');
|
|
||||||
font-weight: font-weight('semibold');
|
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
15
src/registrar/assets/src/sass/_theme/_summary-box.scss
Normal file
15
src/registrar/assets/src/sass/_theme/_summary-box.scss
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
@use "uswds-core" as *;
|
||||||
|
|
||||||
|
.usa-summary-box {
|
||||||
|
background-color: color('primary-lightest');
|
||||||
|
border-color: color('accent-cool-lighter');
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-summary-box--action-needed {
|
||||||
|
background-color: color('warning-lighter');
|
||||||
|
border-color: color('warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-summary-box__heading {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
|
@ -41,6 +41,13 @@ th {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The member table has an extra "expand" row, which looks like a single row.
|
||||||
|
// But the DOM disagrees - so we basically need to hide the border on both rows.
|
||||||
|
#members__table-wrapper .dotgov-table tr:nth-last-child(2) td,
|
||||||
|
#members__table-wrapper .dotgov-table tr:nth-last-child(2) th {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
.dotgov-table {
|
.dotgov-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
@ -56,11 +63,10 @@ th {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:not(.hide-td-borders) {
|
tr:not(.hide-td-borders):not(:last-child) td,
|
||||||
td, th {
|
tr:not(.hide-td-borders):not(:last-child) th {
|
||||||
border-bottom: 1px solid color('base-lighter');
|
border-bottom: 1px solid color('base-lighter');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
thead th {
|
thead th {
|
||||||
color: color('primary-darker');
|
color: color('primary-darker');
|
||||||
|
@ -99,3 +105,25 @@ th {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dotgov-table--cell-padding-2 {
|
||||||
|
td, th {
|
||||||
|
padding: units(2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-table--striped tbody tr:nth-child(odd) th,
|
||||||
|
.usa-table--striped tbody tr:nth-child(odd) td {
|
||||||
|
background-color: color('primary-lightest');
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-table--bg-transparent {
|
||||||
|
td, thead th {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.usa-table--full-borderless td,
|
||||||
|
.usa-table--full-borderless th {
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
3
src/registrar/assets/src/sass/_theme/_tags.scss
Normal file
3
src/registrar/assets/src/sass/_theme/_tags.scss
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.usa-tag {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
|
@ -10,44 +10,43 @@ address,
|
||||||
max-width: measure(5);
|
max-width: measure(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1:not(.usa-alert__heading),
|
||||||
|
// .module h2 excludes headers in DJA
|
||||||
|
h2:not(.usa-alert__heading, .module h2),
|
||||||
|
h3:not(.usa-alert__heading),
|
||||||
|
h4:not(.usa-alert__heading),
|
||||||
|
h5:not(.usa-alert__heading),
|
||||||
|
h6:not(.usa-alert__heading) {
|
||||||
|
color: color('primary-darker');
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, .h1 {
|
||||||
|
font-size: 2.125rem;
|
||||||
@include typeset('sans', '2xl', 2);
|
@include typeset('sans', '2xl', 2);
|
||||||
margin: 0 0 units(2);
|
margin: 0 0 units(2);
|
||||||
color: color('primary-darker');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2, .h2 {
|
||||||
font-weight: font-weight('semibold');
|
line-height: 1.3;
|
||||||
line-height: line-height('heading', 3);
|
|
||||||
margin: units(4) 0 units(1);
|
margin: units(4) 0 units(1);
|
||||||
color: color('primary-darker');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header--body {
|
h3, .h3 {
|
||||||
margin-top: units(2);
|
font-size: 1.25rem;
|
||||||
font-weight: font-weight('semibold');
|
font-weight: font-weight('semibold');
|
||||||
// The units mixin can only get us close, so it's between
|
|
||||||
// hardcoding the value and using in markup
|
|
||||||
font-size: 16.96px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h4--sm-05 {
|
h4, .h4 {
|
||||||
font-size: size('body', 'sm');
|
font-size: 1.125rem;
|
||||||
font-weight: normal;
|
line-height: 1.25;
|
||||||
color: color('primary');
|
font-weight: font-weight('semibold');
|
||||||
margin-bottom: units(0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize typography in forms
|
|
||||||
.usa-form,
|
|
||||||
.usa-form fieldset {
|
|
||||||
font-size: 1rem;
|
|
||||||
.usa-legend {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.p--blockquote {
|
.p--blockquote {
|
||||||
padding-left: units(1);
|
padding-left: units(1);
|
||||||
border-left: 2px solid color('base-lighter');
|
border-left: 2px solid color('base-lighter');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.font-body-1 {
|
||||||
|
font-size: size('body', 1);
|
||||||
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ in the form $setting: value,
|
||||||
/*---------------------------
|
/*---------------------------
|
||||||
## Font weights
|
## Font weights
|
||||||
----------------------------*/
|
----------------------------*/
|
||||||
|
$theme-font-weight-medium: 400,
|
||||||
$theme-font-weight-semibold: 600,
|
$theme-font-weight-semibold: 600,
|
||||||
|
|
||||||
/*---------------------------
|
/*---------------------------
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
@forward "forms";
|
@forward "forms";
|
||||||
@forward "search";
|
@forward "search";
|
||||||
@forward "tooltips";
|
@forward "tooltips";
|
||||||
|
@forward "summary-box";
|
||||||
@forward "fieldsets";
|
@forward "fieldsets";
|
||||||
@forward "alerts";
|
@forward "alerts";
|
||||||
@forward "tables";
|
@forward "tables";
|
||||||
|
@ -25,6 +26,8 @@
|
||||||
@forward "header";
|
@forward "header";
|
||||||
@forward "register-form";
|
@forward "register-form";
|
||||||
@forward "containers";
|
@forward "containers";
|
||||||
|
@forward "modals";
|
||||||
|
@forward "tags";
|
||||||
|
|
||||||
/*--------------------------------------------------
|
/*--------------------------------------------------
|
||||||
--- Admin ---------------------------------*/
|
--- Admin ---------------------------------*/
|
||||||
|
|
|
@ -25,6 +25,7 @@ from typing import Final
|
||||||
from botocore.config import Config
|
from botocore.config import Config
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import traceback
|
||||||
from django.utils.log import ServerFormatter
|
from django.utils.log import ServerFormatter
|
||||||
|
|
||||||
# # # ###
|
# # # ###
|
||||||
|
@ -471,7 +472,11 @@ class JsonFormatter(logging.Formatter):
|
||||||
"lineno": record.lineno,
|
"lineno": record.lineno,
|
||||||
"message": record.getMessage(),
|
"message": record.getMessage(),
|
||||||
}
|
}
|
||||||
return json.dumps(log_record)
|
# Capture exception info if it exists
|
||||||
|
if record.exc_info:
|
||||||
|
log_record["exception"] = "".join(traceback.format_exception(*record.exc_info))
|
||||||
|
|
||||||
|
return json.dumps(log_record, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
class JsonServerFormatter(ServerFormatter):
|
class JsonServerFormatter(ServerFormatter):
|
||||||
|
@ -520,7 +525,7 @@ LOGGING = {
|
||||||
"()": JsonFormatter,
|
"()": JsonFormatter,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
# define where log messages will be sent;
|
# define where log messages will be sent
|
||||||
# each logger can have one or more handlers
|
# each logger can have one or more handlers
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"console": {
|
"console": {
|
||||||
|
|
|
@ -323,9 +323,18 @@ class DomainRequestFixture:
|
||||||
cls._create_domain_requests(users)
|
cls._create_domain_requests(users)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create_domain_requests(cls, users):
|
def _create_domain_requests(cls, users): # noqa: C901
|
||||||
"""Creates DomainRequests given a list of users."""
|
"""Creates DomainRequests given a list of users."""
|
||||||
|
total_domain_requests_to_make = len(users) # 100000
|
||||||
|
|
||||||
|
# Check if the database is already populated with the desired
|
||||||
|
# number of entries.
|
||||||
|
# (Prevents re-adding more entries to an already populated database,
|
||||||
|
# which happens when restarting Docker src)
|
||||||
|
domain_requests_already_made = DomainRequest.objects.count()
|
||||||
|
|
||||||
domain_requests_to_create = []
|
domain_requests_to_create = []
|
||||||
|
if domain_requests_already_made < total_domain_requests_to_make:
|
||||||
for user in users:
|
for user in users:
|
||||||
for request_data in cls.DOMAINREQUESTS:
|
for request_data in cls.DOMAINREQUESTS:
|
||||||
# Prepare DomainRequest objects
|
# Prepare DomainRequest objects
|
||||||
|
@ -340,6 +349,25 @@ class DomainRequestFixture:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
|
|
||||||
|
num_additional_requests_to_make = (
|
||||||
|
total_domain_requests_to_make - domain_requests_already_made - len(domain_requests_to_create)
|
||||||
|
)
|
||||||
|
if num_additional_requests_to_make > 0:
|
||||||
|
for _ in range(num_additional_requests_to_make):
|
||||||
|
random_user = random.choice(users) # nosec
|
||||||
|
try:
|
||||||
|
random_request_type = random.choice(cls.DOMAINREQUESTS) # nosec
|
||||||
|
# Prepare DomainRequest objects
|
||||||
|
domain_request = DomainRequest(
|
||||||
|
creator=random_user,
|
||||||
|
organization_name=random_request_type["organization_name"],
|
||||||
|
)
|
||||||
|
cls._set_non_foreign_key_fields(domain_request, random_request_type)
|
||||||
|
cls._set_foreign_key_fields(domain_request, random_request_type, random_user)
|
||||||
|
domain_requests_to_create.append(domain_request)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Error creating random domain request: {e}")
|
||||||
|
|
||||||
# Bulk create domain requests
|
# Bulk create domain requests
|
||||||
cls._bulk_create_requests(domain_requests_to_create)
|
cls._bulk_create_requests(domain_requests_to_create)
|
||||||
|
|
||||||
|
|
|
@ -196,6 +196,7 @@ class UserFixture:
|
||||||
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
|
"username": "b6a15987-5c88-4e26-8de2-ca71a0bdb2cd",
|
||||||
"first_name": "Alysia-Analyst",
|
"first_name": "Alysia-Analyst",
|
||||||
"last_name": "Alysia-Analyst",
|
"last_name": "Alysia-Analyst",
|
||||||
|
"email": "abroddrick+1@truss.works",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
"username": "91a9b97c-bd0a-458d-9823-babfde7ebf44",
|
||||||
|
@ -351,32 +352,65 @@ class UserFixture:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_existing_users(users):
|
def _get_existing_users(users):
|
||||||
|
# if users match existing users in db by email address, update the users with the username
|
||||||
|
# from the db. this will prevent duplicate users (with same email) from being added to db.
|
||||||
|
# it is ok to keep the old username in the db because the username will be updated by oidc process during login
|
||||||
|
|
||||||
|
# Extract email addresses from users
|
||||||
|
emails = [user.get("email") for user in users]
|
||||||
|
|
||||||
|
# Fetch existing users by email
|
||||||
|
existing_users_by_email = User.objects.filter(email__in=emails).values_list("email", "username", "id")
|
||||||
|
|
||||||
|
# Create a dictionary to map emails to existing usernames
|
||||||
|
email_to_existing_user = {user[0]: user[1] for user in existing_users_by_email}
|
||||||
|
|
||||||
|
# Update the users list with the usernames from existing users by email
|
||||||
|
for user in users:
|
||||||
|
email = user.get("email")
|
||||||
|
if email and email in email_to_existing_user:
|
||||||
|
user["username"] = email_to_existing_user[email] # Update username with the existing one
|
||||||
|
|
||||||
|
# Get the user identifiers (username, id) for the existing users to query the database
|
||||||
user_identifiers = [(user.get("username"), user.get("id")) for user in users]
|
user_identifiers = [(user.get("username"), user.get("id")) for user in users]
|
||||||
|
|
||||||
|
# Fetch existing users by username or id
|
||||||
existing_users = User.objects.filter(
|
existing_users = User.objects.filter(
|
||||||
username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers]
|
username__in=[user[0] for user in user_identifiers] + [user[1] for user in user_identifiers]
|
||||||
).values_list("username", "id")
|
).values_list("username", "id")
|
||||||
|
|
||||||
|
# Create sets for usernames and ids that exist
|
||||||
existing_usernames = set(user[0] for user in existing_users)
|
existing_usernames = set(user[0] for user in existing_users)
|
||||||
existing_user_ids = set(user[1] for user in existing_users)
|
existing_user_ids = set(user[1] for user in existing_users)
|
||||||
|
|
||||||
return existing_usernames, existing_user_ids
|
return existing_usernames, existing_user_ids
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers):
|
def _prepare_new_users(users, existing_usernames, existing_user_ids, are_superusers):
|
||||||
return [
|
new_users = []
|
||||||
User(
|
for i, user_data in enumerate(users):
|
||||||
id=user_data.get("id"),
|
username = user_data.get("username")
|
||||||
first_name=user_data.get("first_name"),
|
id = user_data.get("id")
|
||||||
last_name=user_data.get("last_name"),
|
first_name = user_data.get("first_name", "Bob")
|
||||||
username=user_data.get("username"),
|
last_name = user_data.get("last_name", "Builder")
|
||||||
email=user_data.get("email", ""),
|
|
||||||
|
default_email = f"placeholder.{first_name.lower()}.{last_name.lower()}+{i}@igorville.gov"
|
||||||
|
email = user_data.get("email", default_email)
|
||||||
|
if username not in existing_usernames and id not in existing_user_ids:
|
||||||
|
user = User(
|
||||||
|
id=id,
|
||||||
|
first_name=first_name,
|
||||||
|
last_name=last_name,
|
||||||
|
username=username,
|
||||||
|
email=email,
|
||||||
title=user_data.get("title", "Peon"),
|
title=user_data.get("title", "Peon"),
|
||||||
phone=user_data.get("phone", "2022222222"),
|
phone=user_data.get("phone", "2022222222"),
|
||||||
is_active=user_data.get("is_active", True),
|
is_active=user_data.get("is_active", True),
|
||||||
is_staff=True,
|
is_staff=True,
|
||||||
is_superuser=are_superusers,
|
is_superuser=are_superusers,
|
||||||
)
|
)
|
||||||
for user_data in users
|
new_users.append(user)
|
||||||
if user_data.get("username") not in existing_usernames and user_data.get("id") not in existing_user_ids
|
return new_users
|
||||||
]
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _create_new_users(new_users):
|
def _create_new_users(new_users):
|
||||||
|
|
|
@ -4,6 +4,7 @@ import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
from django.core.validators import MinValueValidator, MaxValueValidator, RegexValidator, MaxLengthValidator
|
||||||
from django.forms import formset_factory
|
from django.forms import formset_factory
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import DomainRequest, FederalAgency
|
from registrar.models import DomainRequest, FederalAgency
|
||||||
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
from phonenumber_field.widgets import RegionalPhoneNumberWidget
|
||||||
from registrar.models.suborganization import Suborganization
|
from registrar.models.suborganization import Suborganization
|
||||||
|
@ -161,9 +162,10 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
"""Form for updating the suborganization"""
|
"""Form for updating the suborganization"""
|
||||||
|
|
||||||
sub_organization = forms.ModelChoiceField(
|
sub_organization = forms.ModelChoiceField(
|
||||||
|
label="Suborganization name",
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=forms.Select(),
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -178,20 +180,6 @@ class DomainSuborganizationForm(forms.ModelForm):
|
||||||
portfolio = self.instance.portfolio if self.instance else None
|
portfolio = self.instance.portfolio if self.instance else None
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
self.fields["sub_organization"].queryset = Suborganization.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
# Set initial value
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].initial = self.instance.sub_organization
|
|
||||||
|
|
||||||
# Set custom form label
|
|
||||||
self.fields["sub_organization"].label = "Suborganization name"
|
|
||||||
|
|
||||||
# Use the combobox rather than the regular select widget
|
|
||||||
self.fields["sub_organization"].widget.template_name = "django/forms/widgets/combobox.html"
|
|
||||||
|
|
||||||
# Set data-default-value attribute
|
|
||||||
if self.instance and self.instance.sub_organization:
|
|
||||||
self.fields["sub_organization"].widget.attrs["data-default-value"] = self.instance.sub_organization.pk
|
|
||||||
|
|
||||||
|
|
||||||
class BaseNameserverFormset(forms.BaseFormSet):
|
class BaseNameserverFormset(forms.BaseFormSet):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
|
@ -456,6 +444,13 @@ class DomainSecurityEmailForm(forms.Form):
|
||||||
class DomainOrgNameAddressForm(forms.ModelForm):
|
class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"""Form for updating the organization name and mailing address."""
|
"""Form for updating the organization name and mailing address."""
|
||||||
|
|
||||||
|
# for federal agencies we also want to know the top-level agency.
|
||||||
|
federal_agency = forms.ModelChoiceField(
|
||||||
|
label="Federal agency",
|
||||||
|
required=False,
|
||||||
|
queryset=FederalAgency.objects.all(),
|
||||||
|
widget=ComboboxWidget,
|
||||||
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
validators=[
|
validators=[
|
||||||
|
@ -469,6 +464,16 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = DomainInformation
|
model = DomainInformation
|
||||||
fields = [
|
fields = [
|
||||||
|
@ -486,25 +491,12 @@ class DomainOrgNameAddressForm(forms.ModelForm):
|
||||||
"organization_name": {"required": "Enter the name of your organization."},
|
"organization_name": {"required": "Enter the name of your organization."},
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"organization_name": forms.TextInput,
|
"organization_name": forms.TextInput,
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
"urbanization": forms.TextInput,
|
"urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django import forms
|
||||||
from django.core.validators import RegexValidator, MaxLengthValidator
|
from django.core.validators import RegexValidator, MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.forms.utility.wizard_form_helper import (
|
from registrar.forms.utility.wizard_form_helper import (
|
||||||
RegistrarForm,
|
RegistrarForm,
|
||||||
RegistrarFormSet,
|
RegistrarFormSet,
|
||||||
|
@ -43,7 +44,7 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
label="Suborganization name",
|
label="Suborganization name",
|
||||||
required=False,
|
required=False,
|
||||||
queryset=Suborganization.objects.none(),
|
queryset=Suborganization.objects.none(),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
requested_suborganization = forms.CharField(
|
requested_suborganization = forms.CharField(
|
||||||
label="Requested suborganization",
|
label="Requested suborganization",
|
||||||
|
@ -56,22 +57,44 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
suborganization_state_territory = forms.ChoiceField(
|
suborganization_state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
required=False,
|
required=False,
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Override of init to add the suborganization queryset"""
|
"""Override of init to add the suborganization queryset and 'other' option"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
if self.domain_request.portfolio:
|
if self.domain_request.portfolio:
|
||||||
self.fields["sub_organization"].queryset = Suborganization.objects.filter(
|
# Fetch the queryset for the portfolio
|
||||||
portfolio=self.domain_request.portfolio
|
queryset = Suborganization.objects.filter(portfolio=self.domain_request.portfolio)
|
||||||
)
|
# set the queryset appropriately so that post can validate against queryset
|
||||||
|
self.fields["sub_organization"].queryset = queryset
|
||||||
|
|
||||||
|
# Modify the choices to include "other" so that form can display options properly
|
||||||
|
self.fields["sub_organization"].choices = [(obj.id, str(obj)) for obj in queryset] + [
|
||||||
|
("other", "Other (enter your suborganization manually)")
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_database(cls, obj: DomainRequest | Contact | None):
|
||||||
|
"""Returns a dict of form field values gotten from `obj`.
|
||||||
|
Overrides RegistrarForm method in order to set sub_organization to 'other'
|
||||||
|
on GETs of the RequestingEntityForm."""
|
||||||
|
if obj is None:
|
||||||
|
return {}
|
||||||
|
# get the domain request as a dict, per usual method
|
||||||
|
domain_request_dict = {name: getattr(obj, name) for name in cls.declared_fields.keys()} # type: ignore
|
||||||
|
|
||||||
|
# set sub_organization to 'other' if is_requesting_new_suborganization is True
|
||||||
|
if isinstance(obj, DomainRequest) and obj.is_requesting_new_suborganization():
|
||||||
|
domain_request_dict["sub_organization"] = "other"
|
||||||
|
|
||||||
|
return domain_request_dict
|
||||||
|
|
||||||
def clean_sub_organization(self):
|
def clean_sub_organization(self):
|
||||||
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
"""On suborganization clean, set the suborganization value to None if the user is requesting
|
||||||
a custom suborganization (as it doesn't exist yet)"""
|
a custom suborganization (as it doesn't exist yet)"""
|
||||||
|
|
||||||
# If it's a new suborganization, return None (equivalent to selecting nothing)
|
# If it's a new suborganization, return None (equivalent to selecting nothing)
|
||||||
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
if self.cleaned_data.get("is_requesting_new_suborganization"):
|
||||||
return None
|
return None
|
||||||
|
@ -94,41 +117,60 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
return name
|
return name
|
||||||
|
|
||||||
def full_clean(self):
|
def full_clean(self):
|
||||||
"""Validation logic to remove the custom suborganization value before clean is triggered.
|
"""Validation logic to temporarily remove the custom suborganization value before clean is triggered.
|
||||||
Without this override, the form will throw an 'invalid option' error."""
|
Without this override, the form will throw an 'invalid option' error."""
|
||||||
# Remove the custom other field before cleaning
|
# Ensure self.data is not None before proceeding
|
||||||
data = self.data.copy() if self.data else None
|
if self.data:
|
||||||
|
# handle case where form has been submitted
|
||||||
|
# Create a copy of the data for manipulation
|
||||||
|
data = self.data.copy()
|
||||||
|
|
||||||
# Remove the 'other' value from suborganization if it exists.
|
# Retrieve sub_organization and store in _original_suborganization
|
||||||
# This is a special value that tracks if the user is requesting a new suborg.
|
suborganization = data.get("portfolio_requesting_entity-sub_organization")
|
||||||
suborganization = self.data.get("portfolio_requesting_entity-sub_organization")
|
self._original_suborganization = suborganization
|
||||||
if suborganization and "other" in suborganization:
|
# If the original value was "other", clear it for validation
|
||||||
|
if self._original_suborganization == "other":
|
||||||
data["portfolio_requesting_entity-sub_organization"] = ""
|
data["portfolio_requesting_entity-sub_organization"] = ""
|
||||||
|
|
||||||
# Set the modified data back to the form
|
# Set the modified data back to the form
|
||||||
self.data = data
|
self.data = data
|
||||||
|
else:
|
||||||
|
# handle case of a GET
|
||||||
|
suborganization = None
|
||||||
|
if self.initial and "sub_organization" in self.initial:
|
||||||
|
suborganization = self.initial["sub_organization"]
|
||||||
|
|
||||||
|
# Check if is_requesting_new_suborganization is True
|
||||||
|
is_requesting_new_suborganization = False
|
||||||
|
if self.initial and "is_requesting_new_suborganization" in self.initial:
|
||||||
|
# Call the method if it exists
|
||||||
|
is_requesting_new_suborganization = self.initial["is_requesting_new_suborganization"]()
|
||||||
|
|
||||||
|
# Determine if "other" should be set
|
||||||
|
if is_requesting_new_suborganization and suborganization is None:
|
||||||
|
self._original_suborganization = "other"
|
||||||
|
else:
|
||||||
|
self._original_suborganization = suborganization
|
||||||
|
|
||||||
# Call the parent's full_clean method
|
# Call the parent's full_clean method
|
||||||
super().full_clean()
|
super().full_clean()
|
||||||
|
|
||||||
|
# Restore "other" if there are errors
|
||||||
|
if self.errors:
|
||||||
|
self.data["portfolio_requesting_entity-sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Custom clean implementation to handle our desired logic flow for suborganization.
|
"""Custom clean implementation to handle our desired logic flow for suborganization."""
|
||||||
Given that these fields often rely on eachother, we need to do this in the parent function."""
|
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
# Do some custom error validation if the requesting entity is a suborg.
|
# Get the cleaned data
|
||||||
# Otherwise, just validate as normal.
|
suborganization = cleaned_data.get("sub_organization")
|
||||||
suborganization = self.cleaned_data.get("sub_organization")
|
is_requesting_new_suborganization = cleaned_data.get("is_requesting_new_suborganization")
|
||||||
is_requesting_new_suborganization = self.cleaned_data.get("is_requesting_new_suborganization")
|
|
||||||
|
|
||||||
# Get the value of the yes/no checkbox from RequestingEntityYesNoForm.
|
|
||||||
# Since self.data stores this as a string, we need to convert "True" => True.
|
|
||||||
requesting_entity_is_suborganization = self.data.get(
|
requesting_entity_is_suborganization = self.data.get(
|
||||||
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
"portfolio_requesting_entity-requesting_entity_is_suborganization"
|
||||||
)
|
)
|
||||||
if requesting_entity_is_suborganization == "True":
|
if requesting_entity_is_suborganization == "True":
|
||||||
if is_requesting_new_suborganization:
|
if is_requesting_new_suborganization:
|
||||||
# Validate custom suborganization fields
|
|
||||||
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
if not cleaned_data.get("requested_suborganization") and "requested_suborganization" not in self.errors:
|
||||||
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
self.add_error("requested_suborganization", "Enter the name of your suborganization.")
|
||||||
if not cleaned_data.get("suborganization_city"):
|
if not cleaned_data.get("suborganization_city"):
|
||||||
|
@ -141,6 +183,12 @@ class RequestingEntityForm(RegistrarForm):
|
||||||
elif not suborganization:
|
elif not suborganization:
|
||||||
self.add_error("sub_organization", "Suborganization is required.")
|
self.add_error("sub_organization", "Suborganization is required.")
|
||||||
|
|
||||||
|
# If there are errors, restore the "other" value for rendering
|
||||||
|
if self.errors and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
elif not self.data and getattr(self, "_original_suborganization", None) == "other":
|
||||||
|
self.cleaned_data["sub_organization"] = self._original_suborganization
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -274,7 +322,7 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
# uncomment to see if modelChoiceField can be an arg later
|
# uncomment to see if modelChoiceField can be an arg later
|
||||||
required=False,
|
required=False,
|
||||||
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
queryset=FederalAgency.objects.exclude(agency__in=excluded_agencies),
|
||||||
empty_label="--Select--",
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
organization_name = forms.CharField(
|
organization_name = forms.CharField(
|
||||||
label="Organization name",
|
label="Organization name",
|
||||||
|
@ -294,10 +342,11 @@ class OrganizationContactForm(RegistrarForm):
|
||||||
)
|
)
|
||||||
state_territory = forms.ChoiceField(
|
state_territory = forms.ChoiceField(
|
||||||
label="State, territory, or military post",
|
label="State, territory, or military post",
|
||||||
choices=[("", "--Select--")] + DomainRequest.StateTerritoryChoices.choices,
|
choices=DomainRequest.StateTerritoryChoices.choices,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": ("Select the state, territory, or military post where your organization is located.")
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
},
|
},
|
||||||
|
widget=ComboboxWidget,
|
||||||
)
|
)
|
||||||
zipcode = forms.CharField(
|
zipcode = forms.CharField(
|
||||||
label="Zip code",
|
label="Zip code",
|
||||||
|
@ -413,6 +462,7 @@ class CurrentSitesForm(RegistrarForm):
|
||||||
error_messages={
|
error_messages={
|
||||||
"invalid": ("Enter your organization's current website in the required format, like example.com.")
|
"invalid": ("Enter your organization's current website in the required format, like example.com.")
|
||||||
},
|
},
|
||||||
|
widget=forms.URLInput(attrs={"aria-labelledby": "id_current_sites_header id_current_sites_body"}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ import logging
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.core.validators import MaxLengthValidator
|
from django.core.validators import MaxLengthValidator
|
||||||
from django.utils.safestring import mark_safe
|
|
||||||
|
|
||||||
|
from registrar.forms.utility.combobox import ComboboxWidget
|
||||||
from registrar.models import (
|
from registrar.models import (
|
||||||
PortfolioInvitation,
|
PortfolioInvitation,
|
||||||
UserPortfolioPermission,
|
UserPortfolioPermission,
|
||||||
|
@ -33,6 +33,15 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
state_territory = forms.ChoiceField(
|
||||||
|
label="State, territory, or military post",
|
||||||
|
required=True,
|
||||||
|
choices=DomainInformation.StateTerritoryChoices.choices,
|
||||||
|
error_messages={
|
||||||
|
"required": ("Select the state, territory, or military post where your organization is located.")
|
||||||
|
},
|
||||||
|
widget=ComboboxWidget(attrs={"required": True}),
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Portfolio
|
model = Portfolio
|
||||||
|
@ -47,25 +56,12 @@ class PortfolioOrgAddressForm(forms.ModelForm):
|
||||||
error_messages = {
|
error_messages = {
|
||||||
"address_line1": {"required": "Enter the street address of your organization."},
|
"address_line1": {"required": "Enter the street address of your organization."},
|
||||||
"city": {"required": "Enter the city where your organization is located."},
|
"city": {"required": "Enter the city where your organization is located."},
|
||||||
"state_territory": {
|
|
||||||
"required": "Select the state, territory, or military post where your organization is located."
|
|
||||||
},
|
|
||||||
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
"zipcode": {"required": "Enter a 5-digit or 9-digit zip code, like 12345 or 12345-6789."},
|
||||||
}
|
}
|
||||||
widgets = {
|
widgets = {
|
||||||
# We need to set the required attributed for State/territory
|
|
||||||
# because for this fields we are creating an individual
|
|
||||||
# instance of the Select. For the other fields we use the for loop to set
|
|
||||||
# the class's required attribute to true.
|
|
||||||
"address_line1": forms.TextInput,
|
"address_line1": forms.TextInput,
|
||||||
"address_line2": forms.TextInput,
|
"address_line2": forms.TextInput,
|
||||||
"city": forms.TextInput,
|
"city": forms.TextInput,
|
||||||
"state_territory": forms.Select(
|
|
||||||
attrs={
|
|
||||||
"required": True,
|
|
||||||
},
|
|
||||||
choices=DomainInformation.StateTerritoryChoices.choices,
|
|
||||||
),
|
|
||||||
# "urbanization": forms.TextInput,
|
# "urbanization": forms.TextInput,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,47 +120,47 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
required=True,
|
required=True,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": "Member access level is required",
|
"required": "Select the level of access you would like to grant this member.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
domain_request_permission_admin = forms.ChoiceField(
|
domain_permissions = forms.ChoiceField(
|
||||||
label=mark_safe(f"Select permission {required_star}"), # nosec
|
|
||||||
choices=[
|
choices=[
|
||||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
|
(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"),
|
||||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
|
(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"),
|
||||||
],
|
],
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
required=False,
|
required=False,
|
||||||
|
initial=UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": "Admin domain request permission is required",
|
"required": "Domain permission is required.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
member_permission_admin = forms.ChoiceField(
|
domain_request_permissions = forms.ChoiceField(
|
||||||
label=mark_safe(f"Select permission {required_star}"), # nosec
|
|
||||||
choices=[
|
choices=[
|
||||||
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "View all members"),
|
|
||||||
(UserPortfolioPermissionChoices.EDIT_MEMBERS.value, "View all members plus manage members"),
|
|
||||||
],
|
|
||||||
widget=forms.RadioSelect,
|
|
||||||
required=False,
|
|
||||||
error_messages={
|
|
||||||
"required": "Admin member permission is required",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
domain_request_permission_member = forms.ChoiceField(
|
|
||||||
label=mark_safe(f"Select permission {required_star}"), # nosec
|
|
||||||
choices=[
|
|
||||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "View all requests"),
|
|
||||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "View all requests plus create requests"),
|
|
||||||
("no_access", "No access"),
|
("no_access", "No access"),
|
||||||
|
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"),
|
||||||
|
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"),
|
||||||
],
|
],
|
||||||
widget=forms.RadioSelect,
|
widget=forms.RadioSelect,
|
||||||
required=False,
|
required=False,
|
||||||
|
initial="no_access",
|
||||||
error_messages={
|
error_messages={
|
||||||
"required": "Basic member permission is required",
|
"required": "Domain request permission is required.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
member_permissions = forms.ChoiceField(
|
||||||
|
choices=[
|
||||||
|
("no_access", "No access"),
|
||||||
|
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"),
|
||||||
|
],
|
||||||
|
widget=forms.RadioSelect,
|
||||||
|
required=False,
|
||||||
|
initial="no_access",
|
||||||
|
error_messages={
|
||||||
|
"required": "Member permission is required.",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -172,12 +168,11 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
# All of the fields included here have "required=False" by default as they are conditionally required.
|
# All of the fields included here have "required=False" by default as they are conditionally required.
|
||||||
# see def clean() for more details.
|
# see def clean() for more details.
|
||||||
ROLE_REQUIRED_FIELDS = {
|
ROLE_REQUIRED_FIELDS = {
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [],
|
||||||
"domain_request_permission_admin",
|
|
||||||
"member_permission_admin",
|
|
||||||
],
|
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
"domain_request_permission_member",
|
"domain_permissions",
|
||||||
|
"member_permissions",
|
||||||
|
"domain_request_permissions",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,15 +188,24 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
Update field descriptions.
|
Update field descriptions.
|
||||||
"""
|
"""
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
# Adds a <p> description beneath each role option
|
|
||||||
self.fields["role"].descriptions = {
|
# Adds a <p> description beneath each option
|
||||||
"organization_admin": UserPortfolioRoleChoices.get_role_description(
|
self.fields["domain_permissions"].descriptions = {
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage",
|
||||||
),
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization",
|
||||||
"organization_member": UserPortfolioRoleChoices.get_role_description(
|
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
self.fields["domain_request_permissions"].descriptions = {
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS.value: (
|
||||||
|
"Can view all domain requests for the organization and create requests"
|
||||||
|
),
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization",
|
||||||
|
"no_access": "Cannot view or create domain requests",
|
||||||
|
}
|
||||||
|
self.fields["member_permissions"].descriptions = {
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions",
|
||||||
|
"no_access": "Cannot view member permissions",
|
||||||
|
}
|
||||||
|
|
||||||
# Map model instance values to custom form fields
|
# Map model instance values to custom form fields
|
||||||
if self.instance:
|
if self.instance:
|
||||||
self.map_instance_to_initial()
|
self.map_instance_to_initial()
|
||||||
|
@ -225,8 +229,12 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
self.add_error(field_name, self.fields.get(field_name).error_messages.get("required"))
|
self.add_error(field_name, self.fields.get(field_name).error_messages.get("required"))
|
||||||
|
|
||||||
# Edgecase: Member uses a special form value for None called "no_access".
|
# Edgecase: Member uses a special form value for None called "no_access".
|
||||||
if cleaned_data.get("domain_request_permission_member") == "no_access":
|
if cleaned_data.get("domain_request_permissions") == "no_access":
|
||||||
cleaned_data["domain_request_permission_member"] = None
|
cleaned_data["domain_request_permissions"] = None
|
||||||
|
|
||||||
|
# Edgecase: Member uses a special form value for None called "no_access".
|
||||||
|
if cleaned_data.get("member_permissions") == "no_access":
|
||||||
|
cleaned_data["member_permissions"] = None
|
||||||
|
|
||||||
# Handle roles
|
# Handle roles
|
||||||
cleaned_data["roles"] = [role]
|
cleaned_data["roles"] = [role]
|
||||||
|
@ -256,7 +264,7 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
"role": "organization_admin" or "organization_member",
|
"role": "organization_admin" or "organization_member",
|
||||||
"member_permission_admin": permission level if admin,
|
"member_permission_admin": permission level if admin,
|
||||||
"domain_request_permission_admin": permission level if admin,
|
"domain_request_permission_admin": permission level if admin,
|
||||||
"domain_request_permission_member": permission level if member
|
"domain_request_permissions": permission level if member
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
if self.initial is None:
|
if self.initial is None:
|
||||||
|
@ -270,12 +278,15 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER,
|
||||||
]
|
]
|
||||||
domain_perms = [
|
domain_request_perms = [
|
||||||
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
]
|
]
|
||||||
|
domain_perms = [
|
||||||
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
|
]
|
||||||
member_perms = [
|
member_perms = [
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -285,16 +296,21 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
||||||
roles = self.instance.roles or []
|
roles = self.instance.roles or []
|
||||||
selected_role = next((role for role in roles if role in roles), None)
|
selected_role = next((role for role in roles if role in roles), None)
|
||||||
self.initial["role"] = selected_role
|
self.initial["role"] = selected_role
|
||||||
is_admin = selected_role == UserPortfolioRoleChoices.ORGANIZATION_ADMIN
|
is_member = selected_role == UserPortfolioRoleChoices.ORGANIZATION_MEMBER
|
||||||
if is_admin:
|
if is_member:
|
||||||
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), None)
|
# Edgecase: Member and domain request use a special form value for None called "no_access".
|
||||||
selected_member_permission = next((perm for perm in member_perms if perm in perms), None)
|
# This ensures a form selection.
|
||||||
self.initial["domain_request_permission_admin"] = selected_domain_permission
|
selected_domain_permission = next(
|
||||||
self.initial["member_permission_admin"] = selected_member_permission
|
(perm for perm in domain_perms if perm in perms),
|
||||||
else:
|
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||||
# Edgecase: Member uses a special form value for None called "no_access". This ensures a form selection.
|
)
|
||||||
selected_domain_permission = next((perm for perm in domain_perms if perm in perms), "no_access")
|
selected_domain_request_permission = next(
|
||||||
self.initial["domain_request_permission_member"] = selected_domain_permission
|
(perm for perm in domain_request_perms if perm in perms), "no_access"
|
||||||
|
)
|
||||||
|
selected_member_permission = next((perm for perm in member_perms if perm in perms), "no_access")
|
||||||
|
self.initial["domain_request_permissions"] = selected_domain_request_permission
|
||||||
|
self.initial["domain_permissions"] = selected_domain_permission
|
||||||
|
self.initial["member_permissions"] = selected_member_permission
|
||||||
|
|
||||||
|
|
||||||
class PortfolioMemberForm(BasePortfolioMemberForm):
|
class PortfolioMemberForm(BasePortfolioMemberForm):
|
||||||
|
@ -323,7 +339,7 @@ class PortfolioNewMemberForm(BasePortfolioMemberForm):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
email = forms.EmailField(
|
email = forms.EmailField(
|
||||||
label="Enter the email of the member you'd like to invite",
|
label="Email",
|
||||||
max_length=None,
|
max_length=None,
|
||||||
error_messages={
|
error_messages={
|
||||||
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
"invalid": ("Enter an email address in the required format, like name@example.com."),
|
||||||
|
|
5
src/registrar/forms/utility/combobox.py
Normal file
5
src/registrar/forms/utility/combobox.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django.forms import Select
|
||||||
|
|
||||||
|
|
||||||
|
class ComboboxWidget(Select):
|
||||||
|
template_name = "django/forms/widgets/combobox.html"
|
|
@ -64,6 +64,11 @@ class Command(BaseCommand):
|
||||||
action=argparse.BooleanOptionalAction,
|
action=argparse.BooleanOptionalAction,
|
||||||
help="Adds portfolio to both requests and domains",
|
help="Adds portfolio to both requests and domains",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--skip_existing_portfolios",
|
||||||
|
action=argparse.BooleanOptionalAction,
|
||||||
|
help="Only add suborganizations to newly created portfolios, skip existing ones.",
|
||||||
|
)
|
||||||
|
|
||||||
def handle(self, **options):
|
def handle(self, **options):
|
||||||
agency_name = options.get("agency_name")
|
agency_name = options.get("agency_name")
|
||||||
|
@ -71,6 +76,7 @@ class Command(BaseCommand):
|
||||||
parse_requests = options.get("parse_requests")
|
parse_requests = options.get("parse_requests")
|
||||||
parse_domains = options.get("parse_domains")
|
parse_domains = options.get("parse_domains")
|
||||||
both = options.get("both")
|
both = options.get("both")
|
||||||
|
skip_existing_portfolios = options.get("skip_existing_portfolios")
|
||||||
|
|
||||||
if not both:
|
if not both:
|
||||||
if not parse_requests and not parse_domains:
|
if not parse_requests and not parse_domains:
|
||||||
|
@ -97,7 +103,9 @@ class Command(BaseCommand):
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
try:
|
try:
|
||||||
# C901 'Command.handle' is too complex (12)
|
# C901 'Command.handle' is too complex (12)
|
||||||
portfolio = self.handle_populate_portfolio(federal_agency, parse_domains, parse_requests, both)
|
portfolio = self.handle_populate_portfolio(
|
||||||
|
federal_agency, parse_domains, parse_requests, both, skip_existing_portfolios
|
||||||
|
)
|
||||||
portfolios.append(portfolio)
|
portfolios.append(portfolio)
|
||||||
except Exception as exec:
|
except Exception as exec:
|
||||||
self.failed_portfolios.add(federal_agency)
|
self.failed_portfolios.add(federal_agency)
|
||||||
|
@ -109,26 +117,33 @@ class Command(BaseCommand):
|
||||||
updated_suborg_count = self.post_process_all_suborganization_fields(agencies)
|
updated_suborg_count = self.post_process_all_suborganization_fields(agencies)
|
||||||
message = f"Added city and state_territory information to {updated_suborg_count} suborgs."
|
message = f"Added city and state_territory information to {updated_suborg_count} suborgs."
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, message)
|
||||||
|
|
||||||
TerminalHelper.log_script_run_summary(
|
TerminalHelper.log_script_run_summary(
|
||||||
self.updated_portfolios,
|
self.updated_portfolios,
|
||||||
self.failed_portfolios,
|
self.failed_portfolios,
|
||||||
self.skipped_portfolios,
|
self.skipped_portfolios,
|
||||||
debug=False,
|
debug=False,
|
||||||
skipped_header="----- SOME PORTFOLIOS WERENT CREATED -----",
|
log_header="============= FINISHED HANDLE PORTFOLIO STEP ===============",
|
||||||
|
skipped_header="----- SOME PORTFOLIOS WERENT CREATED (BUT OTHER RECORDS ARE STILL PROCESSED) -----",
|
||||||
display_as_str=True,
|
display_as_str=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name.
|
# POST PROCESSING STEP: Remove the federal agency if it matches the portfolio name.
|
||||||
# We only do this for started domain requests.
|
# We only do this for started domain requests.
|
||||||
if parse_requests or both:
|
if parse_requests or both:
|
||||||
|
prompt_message = (
|
||||||
|
"This action will update domain requests even if they aren't on a portfolio."
|
||||||
|
"\nNOTE: This will modify domain requests, even if no portfolios were created."
|
||||||
|
"\nIn the event no portfolios *are* created, then this step will target "
|
||||||
|
"the existing portfolios with your given params."
|
||||||
|
"\nThis step is entirely optional, and is just for extra data cleanup."
|
||||||
|
)
|
||||||
TerminalHelper.prompt_for_execution(
|
TerminalHelper.prompt_for_execution(
|
||||||
system_exit_on_terminate=True,
|
system_exit_on_terminate=True,
|
||||||
prompt_message="This action will update domain requests even if they aren't on a portfolio.",
|
prompt_message=prompt_message,
|
||||||
prompt_title=(
|
prompt_title=(
|
||||||
"POST PROCESS STEP: Do you want to clear federal agency on (related) started domain requests?"
|
"POST PROCESS STEP: Do you want to clear federal agency on (related) started domain requests?"
|
||||||
),
|
),
|
||||||
verify_message=None,
|
verify_message="*** THIS STEP IS OPTIONAL ***",
|
||||||
)
|
)
|
||||||
self.post_process_started_domain_requests(agencies, portfolios)
|
self.post_process_started_domain_requests(agencies, portfolios)
|
||||||
|
|
||||||
|
@ -151,6 +166,11 @@ class Command(BaseCommand):
|
||||||
status=DomainRequest.DomainRequestStatus.STARTED,
|
status=DomainRequest.DomainRequestStatus.STARTED,
|
||||||
organization_name__isnull=False,
|
organization_name__isnull=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if domain_requests_to_update.count() == 0:
|
||||||
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.MAGENTA, "No domain requests to update.")
|
||||||
|
return
|
||||||
|
|
||||||
portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio}
|
portfolio_set = {normalize_string(portfolio.organization_name) for portfolio in portfolios if portfolio}
|
||||||
|
|
||||||
# Update the request, assuming the given agency name matches the portfolio name
|
# Update the request, assuming the given agency name matches the portfolio name
|
||||||
|
@ -173,10 +193,19 @@ class Command(BaseCommand):
|
||||||
DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"])
|
DomainRequest.objects.bulk_update(updated_requests, ["federal_agency"])
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.")
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.OKBLUE, "Action completed successfully.")
|
||||||
|
|
||||||
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both):
|
def handle_populate_portfolio(self, federal_agency, parse_domains, parse_requests, both, skip_existing_portfolios):
|
||||||
"""Attempts to create a portfolio. If successful, this function will
|
"""Attempts to create a portfolio. If successful, this function will
|
||||||
also create new suborganizations"""
|
also create new suborganizations"""
|
||||||
portfolio, _ = self.create_portfolio(federal_agency)
|
portfolio, created = self.create_portfolio(federal_agency)
|
||||||
|
if skip_existing_portfolios and not created:
|
||||||
|
TerminalHelper.colorful_logger(
|
||||||
|
logger.warning,
|
||||||
|
TerminalColors.YELLOW,
|
||||||
|
"Skipping modifications to suborgs, domain requests, and "
|
||||||
|
"domains due to the --skip_existing_portfolios flag. Portfolio already exists.",
|
||||||
|
)
|
||||||
|
return portfolio
|
||||||
|
|
||||||
self.create_suborganizations(portfolio, federal_agency)
|
self.create_suborganizations(portfolio, federal_agency)
|
||||||
if parse_domains or both:
|
if parse_domains or both:
|
||||||
self.handle_portfolio_domains(portfolio, federal_agency)
|
self.handle_portfolio_domains(portfolio, federal_agency)
|
||||||
|
@ -283,15 +312,13 @@ class Command(BaseCommand):
|
||||||
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
DomainRequest.DomainRequestStatus.INELIGIBLE,
|
||||||
DomainRequest.DomainRequestStatus.REJECTED,
|
DomainRequest.DomainRequestStatus.REJECTED,
|
||||||
]
|
]
|
||||||
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
domain_requests = DomainRequest.objects.filter(federal_agency=federal_agency).exclude(status__in=invalid_states)
|
||||||
status__in=invalid_states
|
|
||||||
)
|
|
||||||
if not domain_requests.exists():
|
if not domain_requests.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolio '{portfolio}' not added to domain requests: no valid records found.
|
Portfolio '{portfolio}' not added to domain requests: no valid records found.
|
||||||
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
This means that a filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
Excluded statuses: STARTED, INELIGIBLE, REJECTED.
|
||||||
Filter info: DomainRequest.objects.filter(federal_agency=federal_agency, portfolio__isnull=True).exclude(
|
Filter info: DomainRequest.objects.filter(federal_agency=federal_agency).exclude(
|
||||||
status__in=invalid_states
|
status__in=invalid_states
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
|
@ -335,12 +362,12 @@ class Command(BaseCommand):
|
||||||
|
|
||||||
Returns a queryset of DomainInformation objects, or None if nothing changed.
|
Returns a queryset of DomainInformation objects, or None if nothing changed.
|
||||||
"""
|
"""
|
||||||
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
domain_infos = DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||||
if not domain_infos.exists():
|
if not domain_infos.exists():
|
||||||
message = f"""
|
message = f"""
|
||||||
Portfolio '{portfolio}' not added to domains: no valid records found.
|
Portfolio '{portfolio}' not added to domains: no valid records found.
|
||||||
The filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
The filter on DomainInformation for the federal_agency '{federal_agency}' returned no results.
|
||||||
Filter info: DomainInformation.objects.filter(federal_agency=federal_agency, portfolio__isnull=True)
|
Filter info: DomainInformation.objects.filter(federal_agency=federal_agency)
|
||||||
"""
|
"""
|
||||||
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
TerminalHelper.colorful_logger(logger.info, TerminalColors.YELLOW, message)
|
||||||
return None
|
return None
|
||||||
|
|
238
src/registrar/management/commands/remove_unused_portfolios.py
Normal file
238
src/registrar/management/commands/remove_unused_portfolios.py
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import IntegrityError
|
||||||
|
from django.db import transaction
|
||||||
|
from registrar.management.commands.utility.terminal_helper import (
|
||||||
|
TerminalColors,
|
||||||
|
TerminalHelper,
|
||||||
|
)
|
||||||
|
from registrar.models import (
|
||||||
|
Portfolio,
|
||||||
|
DomainGroup,
|
||||||
|
DomainInformation,
|
||||||
|
DomainRequest,
|
||||||
|
PortfolioInvitation,
|
||||||
|
Suborganization,
|
||||||
|
UserPortfolioPermission,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
ALLOWED_PORTFOLIOS = [
|
||||||
|
"Department of Veterans Affairs",
|
||||||
|
"Department of the Treasury",
|
||||||
|
"National Archives and Records Administration",
|
||||||
|
"Department of Defense",
|
||||||
|
"Office of Personnel Management",
|
||||||
|
"National Aeronautics and Space Administration",
|
||||||
|
"City and County of San Francisco",
|
||||||
|
"State of Arizona, Executive Branch",
|
||||||
|
"Department of the Interior",
|
||||||
|
"Department of State",
|
||||||
|
"Department of Justice",
|
||||||
|
"Capitol Police",
|
||||||
|
"Administrative Office of the Courts",
|
||||||
|
"Supreme Court of the United States",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Remove all Portfolio entries with names not in the allowed list."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
"""
|
||||||
|
OPTIONAL ARGUMENTS:
|
||||||
|
--debug
|
||||||
|
A boolean (default to true), which activates additional print statements
|
||||||
|
"""
|
||||||
|
parser.add_argument("--debug", action=argparse.BooleanOptionalAction)
|
||||||
|
|
||||||
|
def prompt_delete_entries(self, portfolios_to_delete, debug_on):
|
||||||
|
"""Brings up a prompt in the terminal asking
|
||||||
|
if the user wishes to delete data in the
|
||||||
|
Portfolio table. If the user confirms,
|
||||||
|
deletes the data in the Portfolio table"""
|
||||||
|
|
||||||
|
entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True))
|
||||||
|
formatted_entries = "\n\t\t".join(entries_to_remove_by_name)
|
||||||
|
confirm_delete = TerminalHelper.query_yes_no(
|
||||||
|
f"""
|
||||||
|
{TerminalColors.FAIL}
|
||||||
|
WARNING: You are about to delete the following portfolios:
|
||||||
|
|
||||||
|
{formatted_entries}
|
||||||
|
|
||||||
|
Are you sure you want to continue?{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
if confirm_delete:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.YELLOW}
|
||||||
|
----------Deleting entries----------
|
||||||
|
(please wait)
|
||||||
|
{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
self.delete_entries(portfolios_to_delete, debug_on)
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
----------No entries deleted----------
|
||||||
|
(exiting script)
|
||||||
|
{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete_entries(self, portfolios_to_delete, debug_on): # noqa: C901
|
||||||
|
# Log the number of entries being removed
|
||||||
|
count = portfolios_to_delete.count()
|
||||||
|
if count == 0:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
No entries to remove.
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
# If debug mode is on, print out entries being removed
|
||||||
|
if debug_on:
|
||||||
|
entries_to_remove_by_name = list(portfolios_to_delete.values_list("organization_name", flat=True))
|
||||||
|
formatted_entries = ", ".join(entries_to_remove_by_name)
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.YELLOW}
|
||||||
|
Entries to be removed: {formatted_entries}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for portfolios with non-empty related objects
|
||||||
|
# (These will throw integrity errors if they are not updated)
|
||||||
|
portfolios_with_assignments = []
|
||||||
|
for portfolio in portfolios_to_delete:
|
||||||
|
has_assignments = any(
|
||||||
|
[
|
||||||
|
DomainGroup.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
DomainInformation.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
DomainRequest.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
PortfolioInvitation.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
Suborganization.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
UserPortfolioPermission.objects.filter(portfolio=portfolio).exists(),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if has_assignments:
|
||||||
|
portfolios_with_assignments.append(portfolio)
|
||||||
|
|
||||||
|
if portfolios_with_assignments:
|
||||||
|
formatted_entries = "\n\t\t".join(
|
||||||
|
f"{portfolio.organization_name}" for portfolio in portfolios_with_assignments
|
||||||
|
)
|
||||||
|
confirm_cascade_delete = TerminalHelper.query_yes_no(
|
||||||
|
f"""
|
||||||
|
{TerminalColors.FAIL}
|
||||||
|
WARNING: these entries have related objects.
|
||||||
|
|
||||||
|
{formatted_entries}
|
||||||
|
|
||||||
|
Deleting them will update any associated domains / domain requests to have no portfolio
|
||||||
|
and will cascade delete any associated portfolio invitations, portfolio permissions, domain groups,
|
||||||
|
and suborganizations. Any suborganizations that get deleted will also orphan (not delete) their
|
||||||
|
associated domains / domain requests.
|
||||||
|
|
||||||
|
Are you sure you want to continue?{TerminalColors.ENDC}"""
|
||||||
|
)
|
||||||
|
if not confirm_cascade_delete:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
Operation canceled by the user.
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
# Try to delete the portfolios
|
||||||
|
try:
|
||||||
|
summary = []
|
||||||
|
for portfolio in portfolios_to_delete:
|
||||||
|
portfolio_summary = [f"---- CASCADE SUMMARY for {portfolio.organization_name} -----"]
|
||||||
|
if portfolio in portfolios_with_assignments:
|
||||||
|
domain_groups = DomainGroup.objects.filter(portfolio=portfolio)
|
||||||
|
domain_informations = DomainInformation.objects.filter(portfolio=portfolio)
|
||||||
|
domain_requests = DomainRequest.objects.filter(portfolio=portfolio)
|
||||||
|
portfolio_invitations = PortfolioInvitation.objects.filter(portfolio=portfolio)
|
||||||
|
suborganizations = Suborganization.objects.filter(portfolio=portfolio)
|
||||||
|
user_permissions = UserPortfolioPermission.objects.filter(portfolio=portfolio)
|
||||||
|
|
||||||
|
if domain_groups.exists():
|
||||||
|
formatted_groups = "\n".join([str(group) for group in domain_groups])
|
||||||
|
portfolio_summary.append(f"{len(domain_groups)} Deleted DomainGroups:\n{formatted_groups}")
|
||||||
|
domain_groups.delete()
|
||||||
|
|
||||||
|
if domain_informations.exists():
|
||||||
|
formatted_domain_infos = "\n".join([str(info) for info in domain_informations])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(domain_informations)} Orphaned DomainInformations:\n{formatted_domain_infos}"
|
||||||
|
)
|
||||||
|
domain_informations.update(portfolio=None)
|
||||||
|
|
||||||
|
if domain_requests.exists():
|
||||||
|
formatted_domain_reqs = "\n".join([str(req) for req in domain_requests])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(domain_requests)} Orphaned DomainRequests:\n{formatted_domain_reqs}"
|
||||||
|
)
|
||||||
|
domain_requests.update(portfolio=None)
|
||||||
|
|
||||||
|
if portfolio_invitations.exists():
|
||||||
|
formatted_portfolio_invitations = "\n".join([str(inv) for inv in portfolio_invitations])
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"{len(portfolio_invitations)} Deleted PortfolioInvitations:\n{formatted_portfolio_invitations}" # noqa
|
||||||
|
)
|
||||||
|
portfolio_invitations.delete()
|
||||||
|
|
||||||
|
if user_permissions.exists():
|
||||||
|
formatted_user_list = "\n".join(
|
||||||
|
[perm.user.get_formatted_name() for perm in user_permissions]
|
||||||
|
)
|
||||||
|
portfolio_summary.append(
|
||||||
|
f"Deleted UserPortfolioPermissions for the following users:\n{formatted_user_list}"
|
||||||
|
)
|
||||||
|
user_permissions.delete()
|
||||||
|
|
||||||
|
if suborganizations.exists():
|
||||||
|
portfolio_summary.append("Cascade Deleted Suborganizations:")
|
||||||
|
for suborg in suborganizations:
|
||||||
|
DomainInformation.objects.filter(sub_organization=suborg).update(sub_organization=None)
|
||||||
|
DomainRequest.objects.filter(sub_organization=suborg).update(sub_organization=None)
|
||||||
|
portfolio_summary.append(f"{suborg.name}")
|
||||||
|
suborg.delete()
|
||||||
|
|
||||||
|
portfolio.delete()
|
||||||
|
summary.append("\n\n".join(portfolio_summary))
|
||||||
|
summary_string = "\n\n".join(summary)
|
||||||
|
|
||||||
|
# Output a success message with detailed summary
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.OKCYAN}
|
||||||
|
Successfully removed {count} portfolios.
|
||||||
|
|
||||||
|
The following portfolio deletions had cascading effects;
|
||||||
|
|
||||||
|
{summary_string}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
except IntegrityError as e:
|
||||||
|
logger.info(
|
||||||
|
f"""{TerminalColors.FAIL}
|
||||||
|
Could not delete some portfolios due to integrity constraints:
|
||||||
|
{e}
|
||||||
|
{TerminalColors.ENDC}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
# Get all Portfolio entries not in the allowed portfolios list
|
||||||
|
portfolios_to_delete = Portfolio.objects.exclude(organization_name__in=ALLOWED_PORTFOLIOS)
|
||||||
|
|
||||||
|
self.prompt_delete_entries(portfolios_to_delete, options.get("debug"))
|
|
@ -4,9 +4,9 @@ import ipaddress
|
||||||
import re
|
import re
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from django.db import transaction
|
||||||
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
from django_fsm import FSMField, transition, TransitionNotAllowed # type: ignore
|
||||||
|
from django.db import models, IntegrityError
|
||||||
from django.db import models
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from registrar.models.host import Host
|
from registrar.models.host import Host
|
||||||
|
@ -1329,14 +1329,14 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
|
|
||||||
def get_default_administrative_contact(self):
|
def get_default_administrative_contact(self):
|
||||||
"""Gets the default administrative contact."""
|
"""Gets the default administrative contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding administrative security contact")
|
logger.info("get_default_administrative_contact() -> Adding default administrative contact")
|
||||||
contact = PublicContact.get_default_administrative()
|
contact = PublicContact.get_default_administrative()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
|
||||||
def get_default_technical_contact(self):
|
def get_default_technical_contact(self):
|
||||||
"""Gets the default technical contact."""
|
"""Gets the default technical contact."""
|
||||||
logger.info("get_default_security_contact() -> Adding technical security contact")
|
logger.info("get_default_security_contact() -> Adding default technical contact")
|
||||||
contact = PublicContact.get_default_technical()
|
contact = PublicContact.get_default_technical()
|
||||||
contact.domain = self
|
contact.domain = self
|
||||||
return contact
|
return contact
|
||||||
|
@ -1582,11 +1582,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if self.is_expired() and self.state != self.State.UNKNOWN:
|
if self.is_expired() and self.state != self.State.UNKNOWN:
|
||||||
# Given expired is not a physical state, but it is displayed as such,
|
# Given expired is not a physical state, but it is displayed as such,
|
||||||
# We need custom logic to determine this message.
|
# We need custom logic to determine this message.
|
||||||
help_text = (
|
help_text = "This domain has expired. Complete the online renewal process to maintain access."
|
||||||
"This domain has expired, but it is still online. " "To renew this domain, contact help@get.gov."
|
|
||||||
)
|
|
||||||
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
||||||
help_text = "This domain will expire soon. Contact one of the listed domain managers to renew the domain."
|
help_text = "This domain is expiring soon. Complete the online renewal process to maintain access."
|
||||||
else:
|
else:
|
||||||
help_text = Domain.State.get_help_text(self.state)
|
help_text = Domain.State.get_help_text(self.state)
|
||||||
|
|
||||||
|
@ -1678,9 +1676,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
for domainContact in contact_data:
|
for domainContact in contact_data:
|
||||||
req = commands.InfoContact(id=domainContact.contact)
|
req = commands.InfoContact(id=domainContact.contact)
|
||||||
data = registry.send(req, cleaned=True).res_data[0]
|
data = registry.send(req, cleaned=True).res_data[0]
|
||||||
|
logger.info(f"_fetch_contacts => this is the data: {data}")
|
||||||
|
|
||||||
# Map the object we recieved from EPP to a PublicContact
|
# Map the object we recieved from EPP to a PublicContact
|
||||||
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
mapped_object = self.map_epp_contact_to_public_contact(data, domainContact.contact, domainContact.type)
|
||||||
|
logger.info(f"_fetch_contacts => mapped_object: {mapped_object}")
|
||||||
|
|
||||||
# Find/create it in the DB
|
# Find/create it in the DB
|
||||||
in_db = self._get_or_create_public_contact(mapped_object)
|
in_db = self._get_or_create_public_contact(mapped_object)
|
||||||
|
@ -1871,8 +1871,9 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
missingSecurity = True
|
missingSecurity = True
|
||||||
missingTech = True
|
missingTech = True
|
||||||
|
|
||||||
if len(cleaned.get("_contacts")) < 3:
|
contacts = cleaned.get("_contacts", [])
|
||||||
for contact in cleaned.get("_contacts"):
|
if len(contacts) < 3:
|
||||||
|
for contact in contacts:
|
||||||
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
if contact.type == PublicContact.ContactTypeChoices.ADMINISTRATIVE:
|
||||||
missingAdmin = False
|
missingAdmin = False
|
||||||
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
if contact.type == PublicContact.ContactTypeChoices.SECURITY:
|
||||||
|
@ -1891,6 +1892,11 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
technical_contact = self.get_default_technical_contact()
|
technical_contact = self.get_default_technical_contact()
|
||||||
technical_contact.save()
|
technical_contact.save()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"_add_missing_contacts_if_unknown => Adding contacts. Values are "
|
||||||
|
f"missingAdmin: {missingAdmin}, missingSecurity: {missingSecurity}, missingTech: {missingTech}"
|
||||||
|
)
|
||||||
|
|
||||||
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
def _fetch_cache(self, fetch_hosts=False, fetch_contacts=False):
|
||||||
"""Contact registry for info about a domain."""
|
"""Contact registry for info about a domain."""
|
||||||
try:
|
try:
|
||||||
|
@ -2104,8 +2110,21 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
# Save to DB if it doesn't exist already.
|
# Save to DB if it doesn't exist already.
|
||||||
if db_contact.count() == 0:
|
if db_contact.count() == 0:
|
||||||
# Doesn't run custom save logic, just saves to DB
|
# Doesn't run custom save logic, just saves to DB
|
||||||
|
try:
|
||||||
|
with transaction.atomic():
|
||||||
public_contact.save(skip_epp_save=True)
|
public_contact.save(skip_epp_save=True)
|
||||||
logger.info(f"Created a new PublicContact: {public_contact}")
|
logger.info(f"Created a new PublicContact: {public_contact}")
|
||||||
|
except IntegrityError as err:
|
||||||
|
logger.error(
|
||||||
|
f"_get_or_create_public_contact() => tried to create a duplicate public contact: {err}",
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
return PublicContact.objects.get(
|
||||||
|
registry_id=public_contact.registry_id,
|
||||||
|
contact_type=public_contact.contact_type,
|
||||||
|
domain=self,
|
||||||
|
)
|
||||||
|
|
||||||
# Append the item we just created
|
# Append the item we just created
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
|
@ -2115,7 +2134,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
||||||
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
if existing_contact.email != public_contact.email or existing_contact.registry_id != public_contact.registry_id:
|
||||||
existing_contact.delete()
|
existing_contact.delete()
|
||||||
public_contact.save()
|
public_contact.save()
|
||||||
logger.warning("Requested PublicContact is out of sync " "with DB.")
|
logger.warning("Requested PublicContact is out of sync with DB.")
|
||||||
return public_contact
|
return public_contact
|
||||||
|
|
||||||
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
# If it already exists, we can assume that the DB instance was updated during set, so we should just use that.
|
||||||
|
|
|
@ -101,7 +101,6 @@ class DomainInformation(TimeStampedModel):
|
||||||
verbose_name="election office",
|
verbose_name="election office",
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO - Ticket #1911: stub this data from DomainRequest
|
|
||||||
organization_type = models.CharField(
|
organization_type = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
choices=DomainRequest.OrgChoicesElectionOffice.choices,
|
||||||
|
|
|
@ -9,6 +9,7 @@ from django.utils import timezone
|
||||||
from registrar.models.domain import Domain
|
from registrar.models.domain import Domain
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
from registrar.models.utility.generic_helper import CreateOrUpdateOrganizationTypeHelper
|
||||||
|
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices
|
||||||
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
from registrar.utility.errors import FSMDomainRequestError, FSMErrorCodes
|
||||||
from registrar.utility.constants import BranchChoices
|
from registrar.utility.constants import BranchChoices
|
||||||
from auditlog.models import LogEntry
|
from auditlog.models import LogEntry
|
||||||
|
@ -903,6 +904,7 @@ class DomainRequest(TimeStampedModel):
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
bcc_address="",
|
bcc_address="",
|
||||||
|
cc_addresses: list[str] = [],
|
||||||
context=None,
|
context=None,
|
||||||
send_email=True,
|
send_email=True,
|
||||||
wrap_email=False,
|
wrap_email=False,
|
||||||
|
@ -955,12 +957,20 @@ class DomainRequest(TimeStampedModel):
|
||||||
|
|
||||||
if custom_email_content:
|
if custom_email_content:
|
||||||
context["custom_email_content"] = custom_email_content
|
context["custom_email_content"] = custom_email_content
|
||||||
|
|
||||||
|
if self.requesting_entity_is_portfolio() or self.requesting_entity_is_suborganization():
|
||||||
|
portfolio_view_requests_users = self.portfolio.portfolio_users_with_permissions( # type: ignore
|
||||||
|
permissions=[UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS], include_admin=True
|
||||||
|
)
|
||||||
|
cc_addresses = list(portfolio_view_requests_users.values_list("email", flat=True))
|
||||||
|
|
||||||
send_templated_email(
|
send_templated_email(
|
||||||
email_template,
|
email_template,
|
||||||
email_template_subject,
|
email_template_subject,
|
||||||
recipient.email,
|
recipient.email,
|
||||||
context=context,
|
context=context,
|
||||||
bcc_address=bcc_address,
|
bcc_address=bcc_address,
|
||||||
|
cc_addresses=cc_addresses,
|
||||||
wrap_email=wrap_email,
|
wrap_email=wrap_email,
|
||||||
)
|
)
|
||||||
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
logger.info(f"The {new_status} email sent to: {recipient.email}")
|
||||||
|
|
|
@ -4,6 +4,7 @@ from registrar.models.domain_request import DomainRequest
|
||||||
from registrar.models.federal_agency import FederalAgency
|
from registrar.models.federal_agency import FederalAgency
|
||||||
from registrar.models.user import User
|
from registrar.models.user import User
|
||||||
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
from registrar.models.utility.portfolio_helper import UserPortfolioRoleChoices
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from .utility.time_stamped_model import TimeStampedModel
|
from .utility.time_stamped_model import TimeStampedModel
|
||||||
|
|
||||||
|
@ -122,6 +123,16 @@ class Portfolio(TimeStampedModel):
|
||||||
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
if self.state_territory != self.StateTerritoryChoices.PUERTO_RICO and self.urbanization:
|
||||||
self.urbanization = None
|
self.urbanization = None
|
||||||
|
|
||||||
|
# If the org type is federal, and org federal agency is not blank, and is a federal agency
|
||||||
|
# overwrite the organization name with the federal agency's agency
|
||||||
|
if (
|
||||||
|
self.organization_type == self.OrganizationChoices.FEDERAL
|
||||||
|
and self.federal_agency
|
||||||
|
and self.federal_agency != FederalAgency.get_non_federal_agency()
|
||||||
|
and self.federal_agency.agency
|
||||||
|
):
|
||||||
|
self.organization_name = self.federal_agency.agency
|
||||||
|
|
||||||
super().save(*args, **kwargs)
|
super().save(*args, **kwargs)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -144,6 +155,25 @@ class Portfolio(TimeStampedModel):
|
||||||
).values_list("user__id", flat=True)
|
).values_list("user__id", flat=True)
|
||||||
return User.objects.filter(id__in=admin_ids)
|
return User.objects.filter(id__in=admin_ids)
|
||||||
|
|
||||||
|
def portfolio_users_with_permissions(self, permissions=[], include_admin=False):
|
||||||
|
"""Gets all users with specified additional permissions for this particular portfolio.
|
||||||
|
Returns a queryset of User."""
|
||||||
|
portfolio_users = self.portfolio_users
|
||||||
|
if permissions:
|
||||||
|
if include_admin:
|
||||||
|
portfolio_users = portfolio_users.filter(
|
||||||
|
Q(additional_permissions__overlap=permissions)
|
||||||
|
| Q(
|
||||||
|
roles__overlap=[
|
||||||
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
portfolio_users = portfolio_users.filter(additional_permissions__overlap=permissions)
|
||||||
|
user_ids = portfolio_users.values_list("user__id", flat=True)
|
||||||
|
return User.objects.filter(id__in=user_ids)
|
||||||
|
|
||||||
# == Getters for domains == #
|
# == Getters for domains == #
|
||||||
def get_domains(self, order_by=None):
|
def get_domains(self, order_by=None):
|
||||||
"""Returns all DomainInformations associated with this portfolio"""
|
"""Returns all DomainInformations associated with this portfolio"""
|
||||||
|
|
|
@ -55,7 +55,9 @@ class SeniorOfficial(TimeStampedModel):
|
||||||
return " ".join(names) if names else "Unknown"
|
return " ".join(names) if names else "Unknown"
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.first_name or self.last_name:
|
if self.federal_agency and (self.first_name or self.last_name):
|
||||||
|
return self.get_formatted_name() + " of " + self.federal_agency.__str__()
|
||||||
|
elif self.first_name or self.last_name:
|
||||||
return self.get_formatted_name()
|
return self.get_formatted_name()
|
||||||
elif self.pk:
|
elif self.pk:
|
||||||
return str(self.pk)
|
return str(self.pk)
|
||||||
|
|
|
@ -171,11 +171,14 @@ class User(AbstractUser):
|
||||||
now = timezone.now().date()
|
now = timezone.now().date()
|
||||||
expiration_window = 60
|
expiration_window = 60
|
||||||
threshold_date = now + timedelta(days=expiration_window)
|
threshold_date = now + timedelta(days=expiration_window)
|
||||||
|
acceptable_statuses = [Domain.State.UNKNOWN, Domain.State.DNS_NEEDED, Domain.State.READY]
|
||||||
|
|
||||||
num_of_expiring_domains = Domain.objects.filter(
|
num_of_expiring_domains = Domain.objects.filter(
|
||||||
id__in=domain_ids,
|
id__in=domain_ids,
|
||||||
expiration_date__isnull=False,
|
expiration_date__isnull=False,
|
||||||
expiration_date__lte=threshold_date,
|
expiration_date__lte=threshold_date,
|
||||||
expiration_date__gt=now,
|
expiration_date__gt=now,
|
||||||
|
state__in=acceptable_statuses,
|
||||||
).count()
|
).count()
|
||||||
return num_of_expiring_domains
|
return num_of_expiring_domains
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,18 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
UserPortfolioRoleChoices.ORGANIZATION_ADMIN: [
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_REQUESTS,
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
||||||
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||||
# Domain: field specific permissions
|
|
||||||
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||||
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
|
# NOTE: Check FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS before adding roles here.
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
UserPortfolioPermissionChoices.VIEW_PORTFOLIO,
|
||||||
|
UserPortfolioPermissionChoices.VIEW_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +41,9 @@ class UserPortfolioPermission(TimeStampedModel):
|
||||||
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
|
# Used to throw a ValidationError on clean() for UserPortfolioPermission and PortfolioInvitation.
|
||||||
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
|
FORBIDDEN_PORTFOLIO_ROLE_PERMISSIONS = {
|
||||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
UserPortfolioRoleChoices.ORGANIZATION_MEMBER: [
|
||||||
UserPortfolioPermissionChoices.VIEW_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_PORTFOLIO,
|
||||||
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
UserPortfolioPermissionChoices.EDIT_MEMBERS,
|
||||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS,
|
UserPortfolioPermissionChoices.EDIT_SUBORGANIZATION,
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,11 @@ class DomainHelper:
|
||||||
|
|
||||||
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
# a domain name is alphanumeric or hyphen, up to 63 characters, doesn't
|
||||||
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
# begin or end with a hyphen, followed by a TLD of 2-6 alphabetic characters
|
||||||
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.[A-Za-z]{2,6}$")
|
DOMAIN_REGEX = re.compile(r"^(?!-)[A-Za-z0-9-]{1,200}(?<!-)\.[A-Za-z]{2,6}$")
|
||||||
|
|
||||||
# a domain can be no longer than 253 characters in total
|
# a domain can be no longer than 253 characters in total
|
||||||
|
# NOTE: the domain name is limited by the DOMAIN_REGEX above
|
||||||
|
# to 200 characters (not including the .gov at the end)
|
||||||
MAX_LENGTH = 253
|
MAX_LENGTH = 253
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -25,23 +25,6 @@ class UserPortfolioRoleChoices(models.TextChoices):
|
||||||
logger.warning(f"Invalid portfolio role: {user_portfolio_role}")
|
logger.warning(f"Invalid portfolio role: {user_portfolio_role}")
|
||||||
return f"Unknown ({user_portfolio_role})"
|
return f"Unknown ({user_portfolio_role})"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_role_description(cls, user_portfolio_role):
|
|
||||||
"""Returns a detailed description for a given role."""
|
|
||||||
descriptions = {
|
|
||||||
cls.ORGANIZATION_ADMIN: (
|
|
||||||
"Grants this member access to the organization-wide information "
|
|
||||||
"on domains, domain requests, and members. Domain management can be assigned separately."
|
|
||||||
),
|
|
||||||
cls.ORGANIZATION_MEMBER: (
|
|
||||||
"Grants this member access to the organization. They can be given extra permissions to view all "
|
|
||||||
"organization domain requests and submit domain requests on behalf of the organization. Basic access "
|
|
||||||
"members can’t view all members of an organization or manage them. "
|
|
||||||
"Domain management can be assigned separately."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
return descriptions.get(user_portfolio_role)
|
|
||||||
|
|
||||||
|
|
||||||
class UserPortfolioPermissionChoices(models.TextChoices):
|
class UserPortfolioPermissionChoices(models.TextChoices):
|
||||||
""" """
|
""" """
|
||||||
|
|
|
@ -154,7 +154,7 @@
|
||||||
<dd>{{ current_user.email }}</dd>
|
<dd>{{ current_user.email }}</dd>
|
||||||
<dt>Phone:</dt>
|
<dt>Phone:</dt>
|
||||||
<dd>{{ current_user.phone }}</dd>
|
<dd>{{ current_user.phone }}</dd>
|
||||||
<h3 class="font-heading-md" aria-label="Data that will added to:"> </h3>
|
<h3 class="font-heading-md" aria-label="Data that will be added to:"> </h3>
|
||||||
<dt>Domains:</dt>
|
<dt>Domains:</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if current_user_domains %}
|
{% if current_user_domains %}
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
aria-labelledby="summary-box-description"
|
aria-labelledby="summary-box-description"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
<h3 class="usa-summary-box__heading" id="summary-box-description">
|
||||||
When a domain is deleted:
|
When a domain is deleted:
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
aria-labelledby="summary-box-description"
|
aria-labelledby="summary-box-description"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<h3 class="usa-summary-box__heading usa-summary-box__dhs-color" id="summary-box-description">
|
<h3 class="usa-summary-box__heading">
|
||||||
When a domain is deleted:
|
When a domain is deleted:
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
<td>{{ member.user.phone }}</td>
|
<td>{{ member.user.phone }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% for role in member.user|portfolio_role_summary:original %}
|
{% for role in member.user|portfolio_role_summary:original %}
|
||||||
<span class="usa-tag">{{ role }}</span>
|
<span class="usa-tag bg-primary-dark text-semibold">{{ role }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td class="padding-left-1 text-size-small">
|
<td class="padding-left-1 text-size-small">
|
||||||
|
|
|
@ -11,6 +11,7 @@ for now we just carry the attribute to both the parent element and the select.
|
||||||
{{ name }}="{{ value }}"
|
{{ name }}="{{ value }}"
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
>
|
>
|
||||||
{% include "django/forms/widgets/select.html" %}
|
{% include "django/forms/widgets/select.html" with is_combobox=True %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% if field and field.field and field.field.descriptions %}
|
{% if field and field.field and field.field.descriptions %}
|
||||||
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
||||||
{% if description %}
|
{% if description %}
|
||||||
<p class="margin-0 margin-top-1">{{ description }}</p>
|
<p class="margin-0 margin-top-1 font-body-2xs">{{ description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
{# hint: spacing in the class string matters #}
|
{# hint: spacing in the class string matters #}
|
||||||
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
class="usa-select{% if classes %} {{ classes }}{% endif %}"
|
||||||
{% include "django/forms/widgets/attrs.html" %}
|
{% include "django/forms/widgets/attrs.html" %}
|
||||||
|
{% if is_combobox %}
|
||||||
|
data-default-value="{% for group_name, group_choices, group_index in widget.optgroups %}{% for option in group_choices %}{% if option.selected %}{{ option.value }}{% endif %}{% endfor %}{% endfor %}"
|
||||||
|
{% endif %}
|
||||||
>
|
>
|
||||||
{% for group, options, index in widget.optgroups %}
|
{% for group, options, index in widget.optgroups %}
|
||||||
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
{% if group %}<optgroup label="{{ group }}">{% endif %}
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
{% block title %}Add a domain manager | {% endblock %}
|
{% block title %}Add a domain manager | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -38,8 +41,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<h1>Add a domain manager</h1>
|
<h1>Add a domain manager</h1>
|
||||||
{% if has_organization_feature_flag %}
|
{% if has_organization_feature_flag %}
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
|
<div class="grid-row grid-gap {% if not is_widescreen_centered %}max-width--grid-container{% endif %}">
|
||||||
<div class="tablet:grid-col-3 ">
|
<div class="tablet:grid-col-3 ">
|
||||||
<p class="font-body-md margin-top-0 margin-bottom-2
|
<p class="font-body-md margin-top-0 margin-bottom-2
|
||||||
text-primary-darker text-semibold domain-name-wrap"
|
text-primary-darker text-semibold string-wrap"
|
||||||
>
|
>
|
||||||
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
<span class="usa-sr-only"> Domain name:</span> {{ domain.name }}
|
||||||
</p>
|
</p>
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
{% if not domain.domain_info %}
|
{% if not domain.domain_info %}
|
||||||
<div class="usa-alert usa-alert--error margin-bottom-2">
|
<div class="usa-alert usa-alert--error margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h4 class="usa-alert__heading larger-font-sizing">Domain missing domain information</h4>
|
<h4 class="usa-alert__heading">Domain missing domain information</h4>
|
||||||
<p class="usa-alert__text ">
|
<p class="usa-alert__text ">
|
||||||
You are attempting to manage a domain, {{ domain.name }}, which does not have a domain information object. Please correct this in the admin by editing the domain, and adding domain information, as appropriate.
|
You are attempting to manage a domain, {{ domain.name }}, which does not have a domain information object. Please correct this in the admin by editing the domain, and adding domain information, as appropriate.
|
||||||
</p>
|
</p>
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
{% if is_analyst_or_superuser and analyst_action == 'edit' and analyst_action_location == domain.pk %}
|
||||||
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
<div class="usa-alert usa-alert--warning margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
<h4 class="usa-alert__heading larger-font-sizing">Attention!</h4>
|
<h4 class="usa-alert__heading">Attention!</h4>
|
||||||
<p class="usa-alert__text ">
|
<p class="usa-alert__text ">
|
||||||
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
You are making changes to a registrant’s domain. When finished making changes, close this tab and inform the registrant of your updates.
|
||||||
</p>
|
</p>
|
||||||
|
@ -46,7 +46,7 @@
|
||||||
{# messages block is under the back breadcrumb link #}
|
{# messages block is under the back breadcrumb link #}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-3">
|
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -21,21 +21,17 @@
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<div class="margin-top-4 tablet:grid-col-10">
|
<div class="margin-top-4 tablet:grid-col-10">
|
||||||
<h2 class="text-bold text-primary-dark domain-name-wrap">{{ domain.name }}</h2>
|
<h2 class="string-wrap">{{ domain.name }}</h2>
|
||||||
<div
|
<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 %}"
|
class="usa-summary-box padding-y-2 margin-bottom-1"
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="summary-box-key-information"
|
aria-labelledby="summary-box-key-information"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<div class="usa-summary-box__text padding-top-0"
|
||||||
id="summary-box-key-information"
|
|
||||||
>
|
>
|
||||||
<span class="text-bold text-primary-darker">
|
<p class="font-sans-md margin-top-0 margin-bottom-05 text-primary-darker">
|
||||||
Status:
|
<strong>Status:</strong>
|
||||||
</span>
|
|
||||||
<span class="text-primary-darker">
|
|
||||||
|
|
||||||
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
||||||
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
||||||
Expired
|
Expired
|
||||||
|
@ -46,17 +42,18 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.state|title }}
|
{{ domain.state|title }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</p>
|
||||||
|
|
||||||
{% if domain.get_state_help_text %}
|
{% if domain.get_state_help_text %}
|
||||||
<div class="padding-top-1 text-primary-darker">
|
<p class="margin-y-0 text-primary-darker">
|
||||||
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
||||||
This domain has expired, but it is still online.
|
This domain has expired, but it is still online.
|
||||||
{% url 'domain-renewal' pk=domain.id as url %}
|
{% url 'domain-renewal' pk=domain.id as url %}
|
||||||
<a href="{{ url }}">Renew to maintain access.</a>
|
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||||
{% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
|
{% elif has_domain_renewal_flag and domain.is_expiring and is_domain_manager %}
|
||||||
This domain will expire soon.
|
This domain will expire soon.
|
||||||
{% url 'domain-renewal' pk=domain.id as url %}
|
{% url 'domain-renewal' pk=domain.id as url %}
|
||||||
<a href="{{ url }}">Renew to maintain access.</a>
|
<a href="{{ url }}" class="usa-link">Renew to maintain access.</a>
|
||||||
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
|
{% elif has_domain_renewal_flag and domain.is_expiring and is_portfolio_user %}
|
||||||
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
|
This domain will expire soon. Contact one of the listed domain managers to renew the domain.
|
||||||
{% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %}
|
{% elif has_domain_renewal_flag and domain.is_expired and is_portfolio_user %}
|
||||||
|
@ -64,13 +61,11 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ domain.get_state_help_text }}
|
{{ domain.get_state_help_text }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</p>
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
|
||||||
|
|
||||||
|
|
||||||
{% include "includes/domain_dates.html" %}
|
{% include "includes/domain_dates.html" %}
|
||||||
|
|
||||||
|
|
|
@ -35,15 +35,15 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if has_dnssec_records %}
|
{% if has_dnssec_records %}
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box padding-top-0"
|
class="usa-summary-box "
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="Important notes on disabling DNSSEC"
|
aria-labelledby="Important notes on disabling DNSSEC"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<h2 class="usa-summary-box__heading"
|
||||||
id="summary-box-key-information"
|
>To fully disable DNSSEC</h2>
|
||||||
>
|
|
||||||
<h2>To fully disable DNSSEC </h2>
|
<div class="usa-summary-box__text">
|
||||||
<ul class="usa-list">
|
<ul class="usa-list">
|
||||||
<li>Click “Disable DNSSEC” below.</li>
|
<li>Click “Disable DNSSEC” below.</li>
|
||||||
<li>Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.</li>
|
<li>Wait until the Time to Live (TTL) expires on your DNSSEC records managed by your DNS hosting provider. This is often less than 24 hours, but confirm with your provider.</li>
|
||||||
|
@ -51,6 +51,8 @@
|
||||||
</ul>
|
</ul>
|
||||||
<p><strong>Warning:</strong> If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.</p>
|
<p><strong>Warning:</strong> If you disable DNSSEC at your DNS hosting provider before TTL expiration, this may cause your domain to appear offline.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h2>DNSSEC is enabled on your domain</h2>
|
<h2>DNSSEC is enabled on your domain</h2>
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% for form in formset %}
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -38,10 +42,6 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% for form in formset %}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<h1 id="domain-dsdata">DS data</h1>
|
<h1 id="domain-dsdata">DS data</h1>
|
||||||
|
|
||||||
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
<p>In order to enable DNSSEC, you must first configure it with your DNS hosting service.</p>
|
||||||
|
|
|
@ -4,6 +4,12 @@
|
||||||
{% block title %}DNS name servers | {{ domain.name }} | {% endblock %}
|
{% block title %}DNS name servers | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{# this is right after the messages block in the parent template #}
|
||||||
|
{% for form in formset %}
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -26,11 +32,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{# this is right after the messages block in the parent template #}
|
|
||||||
{% for form in formset %}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<h1>DNS name servers</h1>
|
<h1>DNS name servers</h1>
|
||||||
|
|
||||||
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
<p>Before your domain can be used we’ll need information about your domain name servers. Name server records indicate which DNS server is authoritative for your domain.</p>
|
||||||
|
|
|
@ -37,12 +37,12 @@
|
||||||
|
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
<div class="margin-top-4 tablet:grid-col-10">
|
<div class="margin-top-4 tablet:grid-col-10">
|
||||||
<h2 class="text-bold text-primary-dark domain-name-wrap">Confirm the following information for accuracy</h2>
|
<h2 class="domain-name-wrap">Confirm the following information for accuracy</h2>
|
||||||
<p>Review these details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link">
|
<p>Review the details below. We <a href="https://get.gov/domains/requirements/#what-.gov-domain-registrants-must-do" class="usa-link" target="_blank">
|
||||||
require</a> that you maintain accurate information for the domain.
|
require</a> that you maintain accurate information for the domain.
|
||||||
The details you provide will only be used to support the administration of .gov and won't be made public.
|
The details you provide will only be used to support the administration of .gov and won't be made public.
|
||||||
</p>
|
</p>
|
||||||
<p>If you would like to retire your domain instead, please <a href="https://get.gov/contact/" class="usa-link">
|
<p>If you would like to retire your domain instead, please <a href="https://get.gov/contact/" class="usa-link" target="_blank">
|
||||||
contact us</a>. </p>
|
contact us</a>. </p>
|
||||||
<p><em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
<p><em>Required fields are marked with an asterisk (<abbr class="usa-hint usa-hint--required" title="required">*</abbr>).</em>
|
||||||
</p>
|
</p>
|
||||||
|
@ -98,7 +98,7 @@
|
||||||
{% if form.is_policy_acknowledged.errors %}
|
{% if form.is_policy_acknowledged.errors %}
|
||||||
{% for error in form.is_policy_acknowledged.errors %}
|
{% for error in form.is_policy_acknowledged.errors %}
|
||||||
<div class="usa-error-message display-flex" role="alert">
|
<div class="usa-error-message display-flex" role="alert">
|
||||||
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error">
|
<svg class="usa-icon usa-icon--large" focusable="true" role="img" aria-label="Error: Check the box if you read and agree to the requirements for operating a .gov domain.">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#error"></use>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="margin-left-05">{{ error }}</span>
|
<span class="margin-left-05">{{ error }}</span>
|
||||||
|
@ -119,10 +119,8 @@
|
||||||
>
|
>
|
||||||
<label class="usa-checkbox__label" for="renewal-checkbox">
|
<label class="usa-checkbox__label" for="renewal-checkbox">
|
||||||
I read and agree to the
|
I read and agree to the
|
||||||
<a href="https://get.gov/domains/requirements/" class="usa-link">
|
<a href="https://get.gov/domains/requirements/" class="usa-link" target="_blank">
|
||||||
requirements for operating a .gov domain
|
requirements for operating a .gov domain</a>.<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
||||||
</a>.
|
|
||||||
<abbr class="usa-hint usa-hint--required" title="required">*</abbr>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -131,7 +129,7 @@
|
||||||
name="submit_button"
|
name="submit_button"
|
||||||
value="next"
|
value="next"
|
||||||
class="usa-button margin-top-3"
|
class="usa-button margin-top-3"
|
||||||
> Submit
|
> Submit and renew
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
{% block form_instructions %}
|
{% block form_instructions %}
|
||||||
<p>We can better evaluate your request if we know about domains you’re already using.</p>
|
<p>We can better evaluate your request if we know about domains you’re already using.</p>
|
||||||
<h2>What are the current websites for your organization?</h2>
|
<h2 id="id_current_sites_header">What are the current websites for your organization?</h2>
|
||||||
<p>Enter your organization’s current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
|
<p id="id_current_sites_body">Enter your organization’s current public websites. If you already have a .gov domain, include that in your list. This question is optional.</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block form_required_fields_help_text %}
|
{% block form_required_fields_help_text %}
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--unstyled">
|
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--with-icon usa-button--unstyled">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another site</span>
|
</svg><span class="margin-left-05">Add another site</span>
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
<p id="domain_instructions" class="margin-top-05">After you enter your domain, we’ll make sure it’s available and that it meets some of our naming requirements. If your domain passes these initial checks, we’ll verify that it meets all our requirements after you complete the rest of this form.</p>
|
||||||
|
|
||||||
{% with attr_aria_describedby="domain_instructions domain_instructions2" %}
|
{% with attr_aria_labelledby="domain_instructions domain_instructions2" attr_aria_describedby="id_dotgov_domain-requested_domain--toast" %}
|
||||||
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
{# attr_validate / validate="domain" invokes code in getgov.min.js #}
|
||||||
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
{% with append_gov=True attr_validate="domain" add_label_class="usa-sr-only" %}
|
||||||
{% input_with_errors forms.0.requested_domain %}
|
{% input_with_errors forms.0.requested_domain %}
|
||||||
|
@ -67,18 +67,20 @@
|
||||||
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
<p id="alt_domain_instructions" class="margin-top-05">Are there other domains you’d like if we can’t give
|
||||||
you your first choice?</p>
|
you your first choice?</p>
|
||||||
|
|
||||||
{% with attr_aria_describedby="alt_domain_instructions" %}
|
{% with attr_aria_labelledby="alt_domain_instructions" %}
|
||||||
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
{# Will probably want to remove blank-ok and do related cleanup when we implement delete #}
|
||||||
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
{% with attr_validate="domain" append_gov=True add_label_class="usa-sr-only" add_class="blank-ok alternate-domain-input" %}
|
||||||
{% for form in forms.1 %}
|
{% for form in forms.1 %}
|
||||||
<div class="repeatable-form">
|
<div class="repeatable-form">
|
||||||
|
{% with attr_aria_describedby=form.alternative_domain.auto_id|stringformat:"s"|add:"--toast" %}
|
||||||
{% input_with_errors form.alternative_domain %}
|
{% input_with_errors form.alternative_domain %}
|
||||||
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
<button type="button" value="save" class="usa-button usa-button--unstyled" id="add-form">
|
<button type="button" value="save" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another alternative</span>
|
</svg><span class="margin-left-05">Add another alternative</span>
|
||||||
|
|
|
@ -31,13 +31,13 @@
|
||||||
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
<fieldset class="usa-fieldset repeatable-form padding-y-1">
|
||||||
|
|
||||||
<legend class="float-left-tablet">
|
<legend class="float-left-tablet">
|
||||||
<h2 class="margin-top-1">Organization contact {{ forloop.counter }}</h2>
|
<h3 class="margin-top-05">Organization contact {{ forloop.counter }}</h2>
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-bottom-2 text-secondary line-height-sans-5">
|
<button type="button" class="usa-button usa-button--unstyled display-block float-right-tablet delete-record margin-top-1 text-secondary line-height-sans-5 usa-button--with-icon">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#delete"></use>
|
||||||
</svg><span class="margin-left-05">Delete</span>
|
</svg>Delete
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@
|
||||||
</fieldset>
|
</fieldset>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<button type="button" class="usa-button usa-button--unstyled" id="add-form">
|
<button type="button" class="usa-button usa-button--unstyled usa-button--with-icon" id="add-form">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add another contact</span>
|
</svg><span class="margin-left-05">Add another contact</span>
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
{% block title %}Security email | {{ domain.name }} | {% endblock %}
|
{% block title %}Security email | {{ domain.name }} | {% endblock %}
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -23,8 +26,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
<h1>Security email</h1>
|
<h1>Security email</h1>
|
||||||
|
|
||||||
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/data/' %}">.gov domain data</a> we provide.</p>
|
<p>We strongly recommend that you provide a security email. This email will allow the public to report observed or suspected security issues on your domain. Security emails are made public and included in the <a class="usa-link" rel="noopener noreferrer" target="_blank" href="{% public_site_url 'about/data/' %}">.gov domain data</a> we provide.</p>
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
|
|
||||||
{% block domain_content %}
|
{% block domain_content %}
|
||||||
|
|
||||||
|
{% include "includes/form_errors.html" with form=form %}
|
||||||
|
|
||||||
{% block breadcrumb %}
|
{% block breadcrumb %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
<!-- Navigation breadcrumbs -->
|
<!-- Navigation breadcrumbs -->
|
||||||
|
@ -24,10 +26,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock breadcrumb %}
|
{% endblock breadcrumb %}
|
||||||
|
|
||||||
{# this is right after the messages block in the parent template #}
|
|
||||||
{% include "includes/form_errors.html" with form=form %}
|
|
||||||
|
|
||||||
|
|
||||||
<h1>Suborganization</h1>
|
<h1>Suborganization</h1>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
{% if domain_manager_roles %}
|
{% if domain_manager_roles %}
|
||||||
<section class="section-outlined" id="domain-managers">
|
<section class="section-outlined" id="domain-managers">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||||
<h2 class> Domain managers </h2>
|
<h2> Domain managers </h2>
|
||||||
<caption class="sr-only">Domain managers</caption>
|
<caption class="sr-only">Domain managers</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -65,11 +65,10 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
|
<th scope="row" role="rowheader" data-sort-value="{{ item.permission.user.email }}" data-label="Email">
|
||||||
{{ item.permission.user.email }}
|
{{ item.permission.user.email }}
|
||||||
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
{% if item.has_admin_flag %}<span class="usa-tag margin-left-1 primary-dark text-semibold">Admin</span>{% endif %}
|
||||||
</th>
|
</th>
|
||||||
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
{% if not portfolio %}<td data-label="Role">{{ item.permission.role|title }}</td>{% endif %}
|
||||||
<td>
|
<td>
|
||||||
{% if can_delete_users %}
|
|
||||||
<a
|
<a
|
||||||
id="button-toggle-user-alert-{{ forloop.counter }}"
|
id="button-toggle-user-alert-{{ forloop.counter }}"
|
||||||
href="#toggle-user-alert-{{ forloop.counter }}"
|
href="#toggle-user-alert-{{ forloop.counter }}"
|
||||||
|
@ -77,6 +76,7 @@
|
||||||
aria-controls="toggle-user-alert-{{ forloop.counter }}"
|
aria-controls="toggle-user-alert-{{ forloop.counter }}"
|
||||||
data-open-modal
|
data-open-modal
|
||||||
aria-disabled="false"
|
aria-disabled="false"
|
||||||
|
aria-label="Remove {{ item.permission.user.email }}""
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</a>
|
</a>
|
||||||
|
@ -112,18 +112,6 @@
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
class="usa-button--unstyled disabled-button usa-tooltip usa-tooltip--registrar"
|
|
||||||
value="Remove"
|
|
||||||
data-position="bottom"
|
|
||||||
title="Domains must have at least one domain manager"
|
|
||||||
data-tooltip="true"
|
|
||||||
aria-disabled="true"
|
|
||||||
role="button"
|
|
||||||
>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -135,7 +123,7 @@
|
||||||
></div>
|
></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<a class="usa-button usa-button--unstyled" href="{% url 'domain-users-add' pk=domain.id %}">
|
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||||
</svg><span class="margin-left-05">Add a domain manager</span>
|
</svg><span class="margin-left-05">Add a domain manager</span>
|
||||||
|
@ -160,7 +148,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
|
<th scope="row" role="rowheader" data-sort-value="{{ invitation.domain_invitation.user.email }}" data-label="Email">
|
||||||
{{ invitation.domain_invitation.email }}
|
{{ invitation.domain_invitation.email }}
|
||||||
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary">Admin</span>{% endif %}
|
{% if invitation.has_admin_flag %}<span class="usa-tag margin-left-1 bg-primary-dark text-semibold">Admin</span>{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
|
<td data-sort-value="{{ invitation.domain_invitation.created_at|date:"U" }}" data-label="Date created">{{ invitation.domain_invitation.created_at|date }} </td>
|
||||||
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
We've identified an action that you’ll need to complete before we continue reviewing your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Action needed
|
STATUS: Action needed
|
||||||
|
|
||||||
|
|
|
@ -2,27 +2,23 @@
|
||||||
Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %}
|
Hi,{% if domain_manager and domain_manager.first_name %} {{ domain_manager.first_name }}.{% endif %}
|
||||||
|
|
||||||
A domain manager was invited to {{ domain.name }}.
|
A domain manager was invited to {{ domain.name }}.
|
||||||
DOMAIN: {{ domain.name }}
|
|
||||||
INVITED BY: {{ requestor_email }}
|
INVITED BY: {{ requestor_email }}
|
||||||
INVITED ON: {{date}}
|
INVITED ON: {{date}}
|
||||||
MANAGER INVITED: {{ invited_email_address }}
|
MANAGER INVITED: {{ invited_email_address }}
|
||||||
|
|
||||||
|
|
||||||
----------------------------------------------------------------
|
----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
NEXT STEPS
|
NEXT STEPS
|
||||||
|
|
||||||
The person who received the invitation will become a domain manager once they log in to the
|
The person who received the invitation will become a domain manager once they log in to the
|
||||||
.gov registrar. They'll need to access the registrar using a Login.gov account that's
|
.gov registrar. They'll need to access the registrar using a Login.gov account that's
|
||||||
associated with the invited email address.
|
associated with the invited email address.
|
||||||
|
|
||||||
If you need to cancel this invitation or remove the domain manager (because they've already
|
If you need to cancel this invitation or remove the domain manager, you can do that by going to
|
||||||
logged in), you can do that by going to this domain in the .gov registrar <https://manage.get.gov/>.
|
this domain in the .gov registrar <https://manage.get.gov/>.
|
||||||
|
|
||||||
|
|
||||||
WHY DID YOU RECEIVE THIS EMAIL?
|
WHY DID YOU RECEIVE THIS EMAIL?
|
||||||
|
|
||||||
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever
|
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever
|
||||||
someone is invited to manage that domain.
|
someone is invited to manage that domain.
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
Your .gov domain request has been withdrawn and will not be reviewed by our team.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Withdrawn
|
STATUS: Withdrawn
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Congratulations! Your .gov domain request has been approved.
|
Congratulations! Your .gov domain request has been approved.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Approved
|
STATUS: Approved
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
Your .gov domain request has been rejected.
|
Your .gov domain request has been rejected.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Rejected
|
STATUS: Rejected
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Hi, {{ recipient.first_name }}.
|
||||||
We received your .gov domain request.
|
We received your .gov domain request.
|
||||||
|
|
||||||
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
DOMAIN REQUESTED: {{ domain_request.requested_domain.name }}
|
||||||
|
REQUESTED BY: {{ domain_request.creator.email }}
|
||||||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||||
STATUS: Submitted
|
STATUS: Submitted
|
||||||
|
|
||||||
|
@ -11,13 +12,15 @@ STATUS: Submitted
|
||||||
|
|
||||||
NEXT STEPS
|
NEXT STEPS
|
||||||
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
We’ll review your request. This review period can take 30 business days. Due to the volume of requests, the wait time is longer than usual. We appreciate your patience.
|
||||||
|
{% if is_org_user %}
|
||||||
|
During our review we’ll verify that your requested domain meets our naming requirements.
|
||||||
|
{% else %}
|
||||||
During our review, we’ll verify that:
|
During our review, we’ll verify that:
|
||||||
- Your organization is eligible for a .gov domain
|
- Your organization is eligible for a .gov domain
|
||||||
- You work at the organization and/or can make requests on its behalf
|
- You work at the organization and/or can make requests on its behalf
|
||||||
- Your requested domain meets our naming requirements
|
- Your requested domain meets our naming requirements
|
||||||
|
{% endif %}
|
||||||
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>
|
We’ll email you if we have questions. We’ll also email you as soon as we complete our review. You can check the status of your request at any time on the registrar. <https://manage.get.gov>.
|
||||||
|
|
||||||
|
|
||||||
NEED TO MAKE CHANGES?
|
NEED TO MAKE CHANGES?
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% if domain.expiration_date or domain.created_at %}
|
{% if domain.expiration_date or domain.created_at %}
|
||||||
<p class="margin-y-0">
|
<p>
|
||||||
{% if domain.expiration_date %}
|
{% if domain.expiration_date %}
|
||||||
<strong class="text-primary-dark">Expires:</strong>
|
<strong class="text-primary-dark">Date of expiration:</strong>
|
||||||
{{ domain.expiration_date|date }}
|
{{ domain.expiration_date|date }}
|
||||||
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
|
{% if domain.is_expired %} <span class="text-error"><strong>(expired)</strong></span>{% endif %}
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{% load url_helpers %}
|
{% load url_helpers %}
|
||||||
|
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold" >
|
<h2>
|
||||||
Next steps in this process
|
Next steps in this process
|
||||||
</h2>
|
</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>
|
<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>
|
||||||
|
|
||||||
{% if show_withdraw_text %}
|
{% if show_withdraw_text %}
|
||||||
<h2 class="margin-top-0 margin-bottom-2 text-primary-darker text-semibold">
|
<h2>
|
||||||
Need to make changes?
|
Need to make changes?
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<!----------------------------------------------------------------------
|
<!----------------------------------------------------------------------
|
||||||
This link is commented out because we intend to add it back in later.
|
This link is commented out because we intend to add it back in later.
|
||||||
------------------------------------------------------------------------->
|
------------------------------------------------------------------------->
|
||||||
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
<!-- <a href="{% url 'export_data_type_requests' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
|
@ -78,6 +78,7 @@
|
||||||
id="domain-requests__usa-button--filter"
|
id="domain-requests__usa-button--filter"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="filter-status"
|
aria-controls="filter-status"
|
||||||
|
aria-label="Status, list 7 items"
|
||||||
>
|
>
|
||||||
<span class="text-bold display-none" id="domain-requests__filter-indicator"></span> Status
|
<span class="text-bold display-none" id="domain-requests__filter-indicator"></span> Status
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
|
|
@ -10,14 +10,14 @@
|
||||||
|
|
||||||
<!-- Org model banner (org manager can view, domain manager can edit) -->
|
<!-- Org model banner (org manager can view, domain manager can edit) -->
|
||||||
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
|
{% if has_domain_renewal_flag and num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
|
||||||
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||||
<div class="usa-alert">
|
<div class="usa-alert">
|
||||||
<div class="usa-alert__body usa-alert__body--widescreen">
|
<div class="usa-alert__body">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
{% if num_expiring_domains == 1%}
|
{% if num_expiring_domains == 1%}
|
||||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domain.">Show expiring domain.</a>
|
||||||
{% else%}
|
{% else%}
|
||||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domains.">Show expiring domains.</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -64,7 +64,7 @@
|
||||||
{% if user_domain_count and user_domain_count > 0 %}
|
{% 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 %}">
|
<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">
|
<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 usa-button--justify-right" role="button">
|
<a href="{% url 'export_data_type_user' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
|
@ -76,14 +76,14 @@
|
||||||
|
|
||||||
<!-- Non org model banner -->
|
<!-- Non org model banner -->
|
||||||
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
|
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
|
||||||
<section class="usa-site-alert usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
<section class="usa-site-alert--slim usa-site-alert--info margin-bottom-2 {% if add_class %}{{ add_class }}{% endif %}" aria-label="Site alert">
|
||||||
<div class="usa-alert">
|
<div class="usa-alert">
|
||||||
<div class="usa-alert__body usa-alert__body--widescreen">
|
<div class="usa-alert__body">
|
||||||
<p class="usa-alert__text maxw-none">
|
<p class="usa-alert__text maxw-none">
|
||||||
{% if num_expiring_domains == 1%}
|
{% if num_expiring_domains == 1%}
|
||||||
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domain.</a>
|
One domain will expire soon. Go to "Manage" to renew the domain. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domain.">Show expiring domain.</a>
|
||||||
{% else%}
|
{% else%}
|
||||||
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link">Show expiring domains.</a>
|
Multiple domains will expire soon. Go to "Manage" to renew the domains. <a href="#" id="link-expiring-domains" class="usa-link" tabindex="0" aria-label="Show expiring domains. This will filter the Domains table to only show the expiring domains.">Show expiring domains.</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,6 +101,7 @@
|
||||||
id="domains__usa-button--filter"
|
id="domains__usa-button--filter"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-controls="filter-status"
|
aria-controls="filter-status"
|
||||||
|
aria-label="Status, list 5 items"
|
||||||
>
|
>
|
||||||
<span class="text-bold display-none" id="domains__filter-indicator"></span> Status
|
<span class="text-bold display-none" id="domains__filter-indicator"></span> Status
|
||||||
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon top-2px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
<div class="usa-alert usa-alert--{{ message.tags }} usa-alert--slim margin-bottom-2">
|
||||||
<div class="usa-alert__body">
|
<div class="usa-alert__body {% if no_max_width %} maxw-none {% endif %}">
|
||||||
{{ message }}
|
{{ message }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,7 +3,7 @@ Template include for read-only form fields
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
|
||||||
<h4 class="read-only-label">{{ field.label }}</h4>
|
<h4 class="margin-bottom-05">{{ field.label }}</h4>
|
||||||
{% if label_description %}
|
{% if label_description %}
|
||||||
<p class="usa-hint margin-top-0 margin-bottom-05">{{ label_description }}</p>
|
<p class="usa-hint margin-top-0 margin-bottom-05">{{ label_description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -11,4 +11,4 @@ Template include for read-only form fields
|
||||||
This allows us to customize the displayed value.
|
This allows us to customize the displayed value.
|
||||||
For instance, Select fields will display the id by default.
|
For instance, Select fields will display the id by default.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<p class="read-only-value">{{ value|default:field.value }}</p>
|
<p class="margin-top-0">{{ value|default:field.value }}</p>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% load field_helpers %}
|
||||||
|
<div id="member-basic-permissions" class="margin-top-2">
|
||||||
|
<h2>What permissions do you want to add?</h2>
|
||||||
|
<p>Configure the permissions for this member. Basic members cannot manage member permissions or organization metadata.</p>
|
||||||
|
|
||||||
|
<h3 class="margin-bottom-0">Domains <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||||
|
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors form.domain_permissions %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<h3 class="margin-bottom-0">Domain requests <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||||
|
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors form.domain_request_permissions %}
|
||||||
|
{% endwith %}
|
||||||
|
|
||||||
|
<h3 class="margin-bottom-0">Members <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||||
|
{% with group_classes="bg-gray-1 border-bottom-2px border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" %}
|
||||||
|
{% input_with_errors form.member_permissions %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
|
@ -1,4 +1,4 @@
|
||||||
<h4 class="margin-bottom-0 text-primary">Assigned domains</h4>
|
<h4 class="margin-bottom-0">Assigned domains</h4>
|
||||||
{% if domain_count > 0 %}
|
{% if domain_count > 0 %}
|
||||||
<p class="margin-top-0">{{domain_count}}</p>
|
<p class="margin-top-0">{{domain_count}}</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_member_domains_json' as url %}
|
{% url 'get_member_domains_json' as url %}
|
||||||
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined member-domains margin-top-0 section-outlined--border-base-light" id="edit-member-domains">
|
<section class="section-outlined member-domains margin-top-0 padding-bottom-0 section-outlined--border-base-light" id="edit-member-domains">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Edit domains assigned to
|
Edit domains assigned to
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Member domains search component" class="margin-top-2">
|
<section aria-label="Member domains search component">
|
||||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
||||||
|
@ -81,7 +81,7 @@
|
||||||
|
|
||||||
<!-- ---------- MAIN TABLE ---------- -->
|
<!-- ---------- MAIN TABLE ---------- -->
|
||||||
<div class="display-none margin-top-0" id="edit-member-domains__table-wrapper">
|
<div class="display-none margin-top-0" id="edit-member-domains__table-wrapper">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked margin-bottom-4">
|
||||||
<caption class="sr-only">member domains</caption>
|
<caption class="sr-only">member domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -99,10 +99,10 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="edit-member-domains__no-data">
|
<div class="display-none margin-bottom-4" id="edit-member-domains__no-data">
|
||||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="edit-member-domains__no-search-results">
|
<div class="display-none margin-bottom-4" id="edit-member-domains__no-search-results">
|
||||||
<p>No results found</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
|
||||||
{% url 'get_member_domains_json' as url %}
|
{% url 'get_member_domains_json' as url %}
|
||||||
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
<span id="get_member_domains_json_url" class="display-none">{{url}}</span>
|
||||||
<section class="section-outlined member-domains margin-top-0 section-outlined--border-base-light" id="member-domains">
|
<section class="section-outlined member-domains margin-top-0 padding-bottom-0 section-outlined--border-base-light" id="member-domains">
|
||||||
|
|
||||||
<h2>
|
<h2>
|
||||||
Domains assigned to
|
Domains assigned to
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
|
<div class="section-outlined__header margin-bottom-3 grid-row" id="edit-member-domains__search">
|
||||||
<!-- ---------- SEARCH ---------- -->
|
<!-- ---------- SEARCH ---------- -->
|
||||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||||
<section aria-label="Member domains search component" class="margin-top-2">
|
<section aria-label="Member domains search component">
|
||||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
||||||
|
@ -77,7 +77,7 @@
|
||||||
|
|
||||||
<!-- ---------- MAIN TABLE ---------- -->
|
<!-- ---------- MAIN TABLE ---------- -->
|
||||||
<div class="display-none margin-top-0" id="member-domains__table-wrapper">
|
<div class="display-none margin-top-0" id="member-domains__table-wrapper">
|
||||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked margin-bottom-4">
|
||||||
<caption class="sr-only">member domains</caption>
|
<caption class="sr-only">member domains</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -94,10 +94,10 @@
|
||||||
aria-live="polite"
|
aria-live="polite"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="member-domains__no-data">
|
<div class="display-none margin-bottom-4" id="member-domains__no-data">
|
||||||
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
<p>This member does not manage any domains. Click the Edit domain assignments buttons to assign domains.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="display-none" id="member-domains__no-search-results">
|
<div class="display-none margin-bottom-4" id="member-domains__no-search-results">
|
||||||
<p>No results found</p>
|
<p>No results found</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
<h4 class="margin-bottom-0 text-primary">Member access</h4>
|
|
||||||
{% if permissions.roles and 'organization_admin' in permissions.roles %}
|
|
||||||
<p class="margin-top-0">Admin access</p>
|
|
||||||
{% elif permissions.roles and 'organization_member' in permissions.roles %}
|
|
||||||
<p class="margin-top-0">Basic access</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="margin-top-0">⎯</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h4 class="margin-bottom-0 text-primary">Organization domain requests</h4>
|
|
||||||
{% if member_has_edit_request_portfolio_permission %}
|
|
||||||
<p class="margin-top-0">View all requests plus create requests</p>
|
|
||||||
{% elif member_has_view_all_requests_portfolio_permission %}
|
|
||||||
<p class="margin-top-0">View all requests</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="margin-top-0">No access</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h4 class="margin-bottom-0 text-primary">Organization members</h4>
|
|
||||||
{% if member_has_edit_members_portfolio_permission %}
|
|
||||||
<p class="margin-top-0">View all members plus manage members</p>
|
|
||||||
{% elif member_has_view_members_portfolio_permission %}
|
|
||||||
<p class="margin-top-0">View all members</p>
|
|
||||||
{% else %}
|
|
||||||
<p class="margin-top-0">No access</p>
|
|
||||||
{% endif %}
|
|
131
src/registrar/templates/includes/member_permissions_matrix.html
Normal file
131
src/registrar/templates/includes/member_permissions_matrix.html
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
<div class="usa-accordion usa-accordion--show-more">
|
||||||
|
<h4 class="usa-accordion__heading">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="usa-accordion__button"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-controls="admin-vs-basic-matrix"
|
||||||
|
>
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker text-middle" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#help_outline"></use>
|
||||||
|
</svg>
|
||||||
|
<span class="text-middle">
|
||||||
|
How are admins and basic members different?
|
||||||
|
</span>
|
||||||
|
<svg class="usa-icon font-body-xl text-primary text-middle expand-less" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_less"></use>
|
||||||
|
</svg>
|
||||||
|
<svg class="usa-icon font-body-xl text-primary text-middle expand-more" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#expand_more"></use>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</h4>
|
||||||
|
<div id="admin-vs-basic-matrix" class="usa-accordion__content bg-transparent padding-top-0 padding-left-0 padding-right-0">
|
||||||
|
<table class="usa-table dotgov-table dotgov-table--cell-padding-2 usa-table--bg-transparent usa-table--full-borderless usa-table--striped font-body-2xs line-height-sans-1 border-top-2px border-base-lighter">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col" role="columnheader">Member actions available</th>
|
||||||
|
<th scope="col" role="columnheader" class="text-center">Admin</th>
|
||||||
|
<th scope="col" role="columnheader" class="text-center">Basic</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">
|
||||||
|
View domains they manage
|
||||||
|
<svg
|
||||||
|
class="usa-icon usa-tooltip text-primary text-middle no-click-outline-and-cursor-help"
|
||||||
|
data-position="top"
|
||||||
|
title="Domains can be assigned after invitation."
|
||||||
|
focusable="true"
|
||||||
|
aria-label="Domains can be assigned after invitation."
|
||||||
|
role="tooltip"
|
||||||
|
>
|
||||||
|
<use aria-hidden="true" xlink:href="/public/img/sprite.svg#info_outline"></use>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">View all domains for the organization</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center text-middle">
|
||||||
|
<span class="font-body-1 text-primary-darker">Optional</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">View all domain requests</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center text-middle">
|
||||||
|
<span class="font-body-1 text-primary-darker">Optional</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">Create domain requests</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center text-middle">
|
||||||
|
<span class="font-body-1 text-primary-darker">Optional</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">View all member permissions</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center text-middle">
|
||||||
|
<span class="font-body-1 text-primary-darker">Optional</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">Manage member permissions</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#cancel"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row" class="text-middle">Manage organization metadata (address)</th>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#check"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
<td class="text-center">
|
||||||
|
<svg class="usa-icon font-body-xl text-primary-darker" aria-hidden="true" focusable="false" role="img">
|
||||||
|
<use xlink:href="/public/img/sprite.svg#cancel"></use>
|
||||||
|
</svg>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<h4 class="margin-bottom-0">Member access</h4>
|
||||||
|
{% if permissions.roles and 'organization_admin' in permissions.roles %}
|
||||||
|
<p class="margin-top-0">Admin</p>
|
||||||
|
{% elif permissions.roles and 'organization_member' in permissions.roles %}
|
||||||
|
<p class="margin-top-0">Basic</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-top-0">⎯</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h4 class="margin-bottom-0 text-primary">Domains</h4>
|
||||||
|
{% if member_has_view_all_domains_portfolio_permission %}
|
||||||
|
<p class="margin-top-0">Viewer, all</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-top-0">Viewer, limited</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h4 class="margin-bottom-0 text-primary">Domain requests</h4>
|
||||||
|
{% if member_has_edit_request_portfolio_permission %}
|
||||||
|
<p class="margin-top-0">Creator</p>
|
||||||
|
{% elif member_has_view_all_requests_portfolio_permission %}
|
||||||
|
<p class="margin-top-0">Viewer</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-top-0">No access</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<h4 class="margin-bottom-0 text-primary">Members</h4>
|
||||||
|
{% if member_has_edit_members_portfolio_permission %}
|
||||||
|
<p class="margin-top-0">Manager</p>
|
||||||
|
{% elif member_has_view_members_portfolio_permission %}
|
||||||
|
<p class="margin-top-0">Viewer</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="margin-top-0">No access</p>
|
||||||
|
{% endif %}
|
|
@ -38,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="section-outlined__utility-button mobile-lg:padding-right-105 {% if portfolio %} mobile:grid-col-12 desktop:grid-col-6 desktop:padding-left-3{% endif %}">
|
<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">
|
<section aria-label="Domains report component" class="margin-top-205">
|
||||||
<a href="{% url 'export_members_portfolio' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right" role="button">
|
<a href="{% url 'export_members_portfolio' %}" class="usa-button usa-button--unstyled usa-button--with-icon usa-button--justify-right">
|
||||||
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
<svg class="usa-icon usa-icon--large" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||||
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
<use xlink:href="{%static 'img/sprite.svg'%}#file_download"></use>
|
||||||
</svg>Export as CSV
|
</svg>Export as CSV
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th data-sortable="member" role="columnheader" id="header-member">Member</th>
|
<th data-sortable="member" role="columnheader" id="header-member">Member</th>
|
||||||
<th data-sortable="last_active" role="columnheader" id="header-last-active">Last Active</th>
|
<th data-sortable="last_active" role="columnheader" id="header-last-active">Last active</th>
|
||||||
<th
|
<th
|
||||||
role="columnheader"
|
role="columnheader"
|
||||||
id="header-action"
|
id="header-action"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
<h2 class="usa-modal__heading">
|
<h2 class="usa-modal__heading">
|
||||||
{{ modal_heading }}
|
{{ modal_heading }}
|
||||||
{%if domain_name_modal is not None %}
|
{%if domain_name_modal is not None %}
|
||||||
<span class="domain-name-wrap">
|
<span class="string-wrap">
|
||||||
{{ domain_name_modal }}
|
{{ domain_name_modal }}
|
||||||
</span>
|
</span>
|
||||||
{%endif%}
|
{%endif%}
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
<h4>Alternative domains</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
Your contact information
|
Your contact information
|
||||||
</h3>
|
</h3>
|
||||||
<div class="usa-summary-box__text">
|
<div class="usa-summary-box__text">
|
||||||
<ul>
|
<ul class="usa-list">
|
||||||
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
<li>Full name: <b>{{ user.get_formatted_name }}</b></li>
|
||||||
<li>Organization email: <b>{{ user.email }}</b></li>
|
<li>Organization email: <b>{{ user.email }}</b></li>
|
||||||
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
<li>Title or role in your organization: <b>{{ user.title }}</b></li>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if domain_request.alternative_domains.all %}
|
{% if domain_request.alternative_domains.all %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Alternative domains</h3>
|
<h4>Alternative domains</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for site in domain_request.alternative_domains.all %}
|
{% for site in domain_request.alternative_domains.all %}
|
||||||
<li>{{ site.website }}</li>
|
<li>{{ site.website }}</li>
|
||||||
|
@ -132,8 +132,8 @@
|
||||||
{% with title=form_titles|get_item:step %}
|
{% with title=form_titles|get_item:step %}
|
||||||
{% if domain_request.has_additional_details %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title="Additional Details" value=" " heading_level=heading_level editable=is_editable edit_link=domain_request_url %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
<h4 class="margin-bottom-0">CISA Regional Representative</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-05">
|
||||||
{% if domain_request.cisa_representative_first_name %}
|
{% if domain_request.cisa_representative_first_name %}
|
||||||
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
<li>{{domain_request.cisa_representative_first_name}} {{domain_request.cisa_representative_last_name}}</li>
|
||||||
{% if domain_request.cisa_representative_email %}
|
{% if domain_request.cisa_representative_email %}
|
||||||
|
@ -144,8 +144,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
<h4 class="margin-bottom-0">Anything else</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-05">
|
||||||
{% if domain_request.anything_else %}
|
{% if domain_request.anything_else %}
|
||||||
{{domain_request.anything_else}}
|
{{domain_request.anything_else}}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
|
@ -39,34 +39,32 @@
|
||||||
|
|
||||||
{% block status_summary %}
|
{% block status_summary %}
|
||||||
<div
|
<div
|
||||||
class="usa-summary-box dotgov-status-box margin-top-3 padding-left-2"
|
class="usa-summary-box margin-top-3 padding-y-2 margin-bottom-1"
|
||||||
role="region"
|
role="region"
|
||||||
aria-labelledby="summary-box-key-information"
|
aria-labelledby="summary-box-key-information"
|
||||||
>
|
>
|
||||||
<div class="usa-summary-box__body">
|
<div class="usa-summary-box__body">
|
||||||
<p class="usa-summary-box__heading font-sans-md margin-bottom-0"
|
<div class="usa-summary-box__text padding-top-0"
|
||||||
id="summary-box-key-information"
|
|
||||||
>
|
>
|
||||||
<span class="text-bold text-primary-darker">
|
<p class="font-sans-md margin-y-0 text-primary-darker">
|
||||||
Status:
|
<strong>Status:</strong>
|
||||||
</span>
|
|
||||||
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
{{ DomainRequest.get_status_display|default:"ERROR Please contact technical support/dev" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
</div>
|
||||||
{% endblock status_summary %}
|
{% endblock status_summary %}
|
||||||
|
|
||||||
{% block status_metadata %}
|
{% block status_metadata %}
|
||||||
|
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% if DomainRequest.creator %}
|
{% if DomainRequest.creator %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<p>
|
||||||
<b class="review__step__name">Created by:</b> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
<strong class="text-primary-dark">Created by:</strong> {{DomainRequest.creator.email|default:DomainRequest.creator.get_formatted_name }}
|
||||||
</p>
|
</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<p>
|
||||||
<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>.
|
<strong class="text-primary-dark">No creator found:</strong> this is an error, please email <a href="mailto:help@get.gov" class="usa-link">help@get.gov</a>.
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -77,49 +75,32 @@
|
||||||
There is some code repetition, but it gives us more flexibility rather than a dense reduction.
|
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.
|
Leave it this way until we've solidified our requirements.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
|
|
||||||
|
<p>
|
||||||
{% if DomainRequest.status == statuses.STARTED %}
|
{% if DomainRequest.status == statuses.STARTED %}
|
||||||
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
{% with first_started_date=DomainRequest.get_first_status_started_date|date:"F j, Y" %}
|
||||||
<p class="margin-top-1">
|
|
||||||
{% comment %}
|
{% comment %}
|
||||||
A newly created domain request will not have a value for last_status update.
|
A newly created domain request will not have a value for last_status update.
|
||||||
This is because the status never really updated.
|
This is because the status never really updated.
|
||||||
However, if this somehow goes back to started we can default to displaying that new date.
|
However, if this somehow goes back to started we can default to displaying that new date.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
<b class="review__step__name">Started on:</b> {{last_status_update|default:first_started_date}}
|
<strong class="text-primary-dark">Started on:</strong> {{last_status_update|default:first_started_date}}
|
||||||
</p>
|
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
{% elif DomainRequest.status == statuses.SUBMITTED %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
</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 %}
|
{% elif DomainRequest.status == statuses.ACTION_NEEDED %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
</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 %}
|
{% elif DomainRequest.status == statuses.REJECTED %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
<strong class="text-primary-dark">Rejected on:</strong> {{last_status_update}}
|
||||||
</p>
|
|
||||||
<p class="margin-top-1">
|
|
||||||
<b class="review__step__name">Rejected on:</b> {{last_status_update}}
|
|
||||||
</p>
|
|
||||||
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
{% elif DomainRequest.status == statuses.WITHDRAWN %}
|
||||||
<p class="margin-top-1 margin-bottom-1">
|
<strong class="text-primary-dark">Submitted on:</strong> {{last_submitted|default:first_submitted }}<br>
|
||||||
<b class="review__step__name">Submitted on:</b> {{last_submitted|default:first_submitted }}
|
<strong class="text-primary-dark">Withdrawn on:</strong> {{last_status_update}}
|
||||||
</p>
|
|
||||||
<p class="margin-top-1">
|
|
||||||
<b class="review__step__name">Withdrawn on:</b> {{last_status_update}}
|
|
||||||
</p>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
{% comment %} Shown for in_review, approved, ineligible {% endcomment %}
|
||||||
<p class="margin-top-1">
|
<strong class="text-primary-dark">Last updated on:</strong> {{DomainRequest.updated_at|date:"F j, Y"}}
|
||||||
<b class="review__step__name">Last updated on:</b> {{DomainRequest.updated_at|date:"F j, Y"}}
|
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
@ -127,7 +108,7 @@
|
||||||
|
|
||||||
{% block status_blurb %}
|
{% block status_blurb %}
|
||||||
{% if DomainRequest.is_awaiting_review %}
|
{% if DomainRequest.is_awaiting_review %}
|
||||||
<p>{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}</p>
|
{% include "includes/domain_request_awaiting_review.html" with show_withdraw_text=DomainRequest.is_withdrawable %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock status_blurb %}
|
{% endblock status_blurb %}
|
||||||
|
|
||||||
|
@ -142,20 +123,19 @@
|
||||||
|
|
||||||
<div class="grid-col maxw-fit-content desktop:grid-offset-2 ">
|
<div class="grid-col maxw-fit-content desktop:grid-offset-2 ">
|
||||||
{% block request_summary_header %}
|
{% block request_summary_header %}
|
||||||
<h2 class="text-primary-darker"> Summary of your domain request </h2>
|
<h2> Summary of your domain request </h2>
|
||||||
{% endblock request_summary_header%}
|
{% endblock request_summary_header%}
|
||||||
|
|
||||||
{% block request_summary %}
|
{% block request_summary %}
|
||||||
{% if portfolio %}
|
{% if portfolio %}
|
||||||
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
{% include "includes/portfolio_request_review_steps.html" with is_editable=False domain_request=DomainRequest %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with heading_level='h3' %}
|
|
||||||
{% with org_type=DomainRequest.get_generic_org_type_display %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Type of organization' value=org_type %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|
||||||
{% if DomainRequest.tribe_name %}
|
{% if DomainRequest.tribe_name %}
|
||||||
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Tribal government' value=DomainRequest.tribe_name %}
|
||||||
|
|
||||||
{% if DomainRequest.federally_recognized_tribe %}
|
{% if DomainRequest.federally_recognized_tribe %}
|
||||||
<p>Federally-recognized tribe</p>
|
<p>Federally-recognized tribe</p>
|
||||||
|
@ -168,56 +148,56 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.get_federal_type_display %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Federal government branch' value=DomainRequest.get_federal_type_display %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.is_election_board %}
|
{% if DomainRequest.is_election_board %}
|
||||||
{% with value=DomainRequest.is_election_board|yesno:"Yes,No,Incomplete" %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Election office' value=value %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.organization_name %}
|
{% if DomainRequest.organization_name %}
|
||||||
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Organization' value=DomainRequest address='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.about_your_organization %}
|
{% if DomainRequest.about_your_organization %}
|
||||||
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='About your organization' value=DomainRequest.about_your_organization %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.senior_official %}
|
{% if DomainRequest.senior_official %}
|
||||||
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Senior official' value=DomainRequest.senior_official contact='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.current_websites.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Current websites' value=DomainRequest.current_websites.all list='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.requested_domain %}
|
{% if DomainRequest.requested_domain %}
|
||||||
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='.gov domain' value=DomainRequest.requested_domain %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.alternative_domains.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Alternative domains' value=DomainRequest.alternative_domains.all list='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.purpose %}
|
{% if DomainRequest.purpose %}
|
||||||
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Purpose of your domain' value=DomainRequest.purpose %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.creator %}
|
{% if DomainRequest.creator %}
|
||||||
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Your contact information' value=DomainRequest.creator contact='true' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if DomainRequest.other_contacts.all %}
|
{% 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 %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.other_contacts.all contact='true' list='true' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale heading_level=heading_level %}
|
{% include "includes/summary_item.html" with title='Other employees from your organization' value=DomainRequest.no_other_contacts_rationale %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# We always show this field even if None #}
|
{# We always show this field even if None #}
|
||||||
{% if DomainRequest %}
|
{% if DomainRequest %}
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">CISA Regional Representative</h3>
|
<h4 class="margin-bottom-0">CISA Regional Representative</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.cisa_representative_first_name %}
|
{% if DomainRequest.cisa_representative_first_name %}
|
||||||
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
{{ DomainRequest.get_formatted_cisa_rep_name }}
|
||||||
|
@ -225,7 +205,7 @@
|
||||||
No
|
No
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Anything else</h3>
|
<h4 class="margin-bottom-0">Anything else</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% if DomainRequest.anything_else %}
|
{% if DomainRequest.anything_else %}
|
||||||
{{DomainRequest.anything_else}}
|
{{DomainRequest.anything_else}}
|
||||||
|
@ -234,7 +214,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock request_summary%}
|
{% endblock request_summary%}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,10 +9,7 @@
|
||||||
{% else %}
|
{% else %}
|
||||||
<h3
|
<h3
|
||||||
{% endif %}
|
{% endif %}
|
||||||
class="summary-item__title
|
class="margin-top-0 margin-bottom-05
|
||||||
font-sans-md
|
|
||||||
text-primary-dark text-semibold
|
|
||||||
margin-top-0 margin-bottom-05
|
|
||||||
padding-right-1"
|
padding-right-1"
|
||||||
>
|
>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
|
@ -22,10 +19,10 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub_header_text %}
|
{% if sub_header_text %}
|
||||||
<h4 class="header--body text-primary-dark margin-bottom-0">{{ sub_header_text }}</h4>
|
<h4 class="margin-bottom-0">{{ sub_header_text }}</h4>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if permissions %}
|
{% if permissions %}
|
||||||
{% include "includes/member_permissions.html" with permissions=value %}
|
{% include "includes/member_permissions_summary.html" with permissions=value %}
|
||||||
{% elif domain_mgmt %}
|
{% elif domain_mgmt %}
|
||||||
{% include "includes/member_domain_management.html" with domain_count=value %}
|
{% include "includes/member_domain_management.html" with domain_count=value %}
|
||||||
{% elif address %}
|
{% elif address %}
|
||||||
|
@ -40,9 +37,7 @@
|
||||||
{% for item in value %}
|
{% for item in value %}
|
||||||
<dt>
|
<dt>
|
||||||
|
|
||||||
<h4 class="summary-item__title
|
<h4 class="
|
||||||
font-sans-md
|
|
||||||
text-primary-dark text-semibold
|
|
||||||
margin-bottom-05
|
margin-bottom-05
|
||||||
padding-right-1">
|
padding-right-1">
|
||||||
Contact {{forloop.counter}}
|
Contact {{forloop.counter}}
|
||||||
|
@ -119,7 +114,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if value.invitations.all %}
|
{% if value.invitations.all %}
|
||||||
<h4 class="h4--sm-05">Invited domain managers</h4>
|
<h4 class="margin-bottom-05">Invited domain managers</h4>
|
||||||
<ul class="usa-list usa-list--unstyled margin-top-0">
|
<ul class="usa-list usa-list--unstyled margin-top-0">
|
||||||
{% for item in value.invitations.all %}
|
{% for item in value.invitations.all %}
|
||||||
<li>{{ item.email }}</li>
|
<li>{{ item.email }}</li>
|
||||||
|
@ -143,7 +138,7 @@
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a
|
<a
|
||||||
href="{{ edit_link }}"
|
href="{{ edit_link }}"
|
||||||
class="usa-link usa-link--icon font-sans-sm line-height-sans-5"
|
class="usa-link usa-link--icon font-sans-sm line-height-sans-4"
|
||||||
>
|
>
|
||||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||||
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif view_button %}visibility{% else %}edit{% endif %}"></use>
|
<use xlink:href="{% static 'img/sprite.svg' %}#{% if manage_button %}settings{% elif view_button %}visibility{% else %}edit{% endif %}"></use>
|
||||||
|
|
|
@ -16,7 +16,7 @@ Organization member
|
||||||
{% endblock messages%}
|
{% endblock messages%}
|
||||||
|
|
||||||
{% url 'members' as url %}
|
{% url 'members' as url %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -41,7 +41,7 @@ Organization member
|
||||||
{% if has_edit_members_portfolio_permission %}
|
{% if has_edit_members_portfolio_permission %}
|
||||||
{% if member %}
|
{% if member %}
|
||||||
<div id="wrapper-delete-action"
|
<div id="wrapper-delete-action"
|
||||||
data-member-name="{{ member.email }}"
|
data-member-name="{{ member.get_formatted_name }}"
|
||||||
data-member-type="member"
|
data-member-type="member"
|
||||||
data-member-id="{{ member.id }}"
|
data-member-id="{{ member.id }}"
|
||||||
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
|
data-num-domains="{{ portfolio_permission.get_managed_domains_count }}"
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
{% url 'invitedmember-domains-edit' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div class="grid-row grid-gap">
|
<div class="grid-row grid-gap">
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-7">
|
<div class="mobile:grid-col-12 tablet:grid-col-7">
|
||||||
<h1 class="margin-bottom-3">Domain assignments</h1>
|
<h1>Domain assignments</h1>
|
||||||
</div>
|
</div>
|
||||||
{% if has_edit_members_portfolio_permission %}
|
{% if has_edit_members_portfolio_permission %}
|
||||||
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
<div class="mobile:grid-col-12 tablet:grid-col-5">
|
||||||
|
@ -51,7 +51,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>
|
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||||
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
{% url 'invitedmember' pk=portfolio_invitation.id as url2 %}
|
||||||
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
{% url 'invitedmember-domains' pk=portfolio_invitation.id as url3 %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<nav class="usa-breadcrumb padding-top-0 margin-bottom-3" aria-label="Portfolio member breadcrumb">
|
<nav class="usa-breadcrumb padding-top-0" aria-label="Portfolio member breadcrumb">
|
||||||
<ol class="usa-breadcrumb__list">
|
<ol class="usa-breadcrumb__list">
|
||||||
<li class="usa-breadcrumb__list-item">
|
<li class="usa-breadcrumb__list-item">
|
||||||
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
<a href="{{ url }}" class="usa-breadcrumb__link"><span>Members</span></a>
|
||||||
|
@ -39,12 +39,10 @@
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<section id="domain-assignments-edit-view">
|
<section id="domain-assignments-edit-view">
|
||||||
<h1 class="margin-bottom-3">Edit domain assignments</h1>
|
<h1>Edit domain assignments</h1>
|
||||||
|
|
||||||
<p class="margin-bottom-0">
|
<p class="margin-top-0 margin-bottom-4 maxw-none">
|
||||||
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
A domain manager can be assigned to any domain across the organization. Domain managers can change domain information, adjust DNS settings, and invite or assign other domain managers to their assigned domains.
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
When you save this form the member will get an email to notify them of any changes.
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -76,7 +74,7 @@
|
||||||
<section id="domain-assignments-readonly-view" class="display-none">
|
<section id="domain-assignments-readonly-view" class="display-none">
|
||||||
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
<h1 class="margin-bottom-3">Review domain assignments</h1>
|
||||||
|
|
||||||
<h2 class="text-primary-dark">Would you like to continue with the following domain assignment changes for
|
<h2 class="margin-top-0">Would you like to continue with the following domain assignment changes for
|
||||||
{% if member %}
|
{% if member %}
|
||||||
{{ member.email }}
|
{{ member.email }}
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -84,17 +82,19 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<p>When you save this form the member will get an email to notify them of any changes.</p>
|
<p class="margin-bottom-4">
|
||||||
|
When you save this form the member will get an email to notify them of any changes.
|
||||||
|
</p>
|
||||||
|
|
||||||
<div id="domain-assignments-summary" class="margin-bottom-2">
|
<div id="domain-assignments-summary" class="margin-bottom-5">
|
||||||
<!-- AJAX will populate this summary -->
|
<!-- AJAX will populate this summary -->
|
||||||
<h3 class="header--body text-primary margin-bottom-1">Unassigned domains</h3>
|
<h3 class="margin-bottom-1 h4">Unassigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3 class="header--body text-primary-dark margin-bottom-0">Assigned domains</h3>
|
<h3 class="margin-bottom-0 h4">Assigned domains</h3>
|
||||||
<ul class="usa-list usa-list--unstyled">
|
<ul class="usa-list usa-list--unstyled">
|
||||||
<li>item1</li>
|
<li>item1</li>
|
||||||
<li>item2</li>
|
<li>item2</li>
|
||||||
|
|
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