mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-30 06:26:34 +02:00
Merge remote-tracking branch 'origin/main' into ms/3212-FEB-purpose-questions
This commit is contained in:
commit
f218ca31ba
145 changed files with 5025 additions and 3402 deletions
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
2
.github/ISSUE_TEMPLATE/bug.yml
vendored
|
@ -1,6 +1,6 @@
|
|||
name: Bug
|
||||
description: Report a bug or problem with the application
|
||||
labels: ["bug"]
|
||||
labels: ["bug","dev"]
|
||||
|
||||
body:
|
||||
- type: markdown
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# This workflow can be run from the CLI
|
||||
# gh workflow run reset-db.yaml -f environment=ENVIRONMENT
|
||||
|
||||
name: Reset database
|
||||
run-name: Reset database for ${{ github.event.inputs.environment }}
|
||||
name: Delete and Recreate database
|
||||
run-name: Delete and Recreate for ${{ github.event.inputs.environment }}
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
@ -53,7 +53,7 @@ jobs:
|
|||
sudo apt-get update
|
||||
sudo apt-get install cf8-cli
|
||||
cf api api.fr.cloud.gov
|
||||
cf auth "$CF_USERNAME" "$CF_PASSWORD"
|
||||
cf auth "$cf_username" "$cf_password"
|
||||
cf target -o cisa-dotgov -s $DESTINATION_ENVIRONMENT
|
||||
|
||||
|
||||
|
|
102
docs/architecture/decisions/0027-ajax-for-dynamic-content.md
Normal file
102
docs/architecture/decisions/0027-ajax-for-dynamic-content.md
Normal file
|
@ -0,0 +1,102 @@
|
|||
# 26. Django Waffle library for Feature Flags
|
||||
|
||||
Date: 2024-05-22 (back dated)
|
||||
|
||||
## Status
|
||||
|
||||
Approved
|
||||
|
||||
## Context
|
||||
|
||||
When we decided to implement server-side rendering ([ADR#8 - Server-Side rendering](./0008-server-side-rendering.md)), we identified a potential risk: users and stakeholders might expect increasingly interactive experiences similar to those found in single-page applications (SPAs). Modern JavaScript frameworks such as React, Angular, and Vue enable rich interactivity by allowing applications to update portions of the page dynamically—often without requiring a full-page reload. These frameworks abstract AJAX and DOM manipulation, creating a high-level interface between JavaScript, HTML, and the browser.
|
||||
|
||||
Our decision to use Django for server-rendered pages allowed us to deliver an MVP quickly and facilitated easy onboarding for new developers. However, the anticipated risk materialized, and stakeholders now expect a more seamless, SPA-like experience.
|
||||
|
||||
We already leverage vanilla JavaScript for interactive components throughout the application. These implementations are neatly contained within Immediately Invoked Function Expressions (IIFEs) and are designed to extend specific components without interfering with Django’s server-rendered structure.
|
||||
|
||||
However, new components that require features like pagination, search, and filtering demand a more responsive, real-time user experience. This prompted an exploration of possible solutions.
|
||||
|
||||
## Considered Options
|
||||
|
||||
**Option 1:** Migrate to a Full SPA with Django as a Backend API
|
||||
This approach involves refactoring Django into a backend-only service and adopting a modern JavaScript framework for the frontend.
|
||||
|
||||
✅ Pros:
|
||||
- Future-proof solution that aligns with modern web development practices.
|
||||
- Enables highly interactive and dynamic UI.
|
||||
- Clean separation of concerns between frontend and backend.
|
||||
|
||||
❌ Cons:
|
||||
- Requires significant investment in development and infrastructure changes.
|
||||
- Major refactoring effort, delaying feature delivery.
|
||||
- Increased complexity for testing and deployment.
|
||||
|
||||
This approach was deemed too costly in terms of both time and resources.
|
||||
|
||||
---
|
||||
|
||||
**Option 2:** Adopt a Modern JS Framework for Select Parts of the Application
|
||||
Instead of a full migration, this approach involves integrating a modern JavaScript framework (e.g., React or Vue) only in areas that require high interactivity.
|
||||
|
||||
✅ Pros:
|
||||
- Avoids a complete rewrite, allowing incremental improvements.
|
||||
- More flexibility in choosing the level of interactivity per feature.
|
||||
|
||||
❌ Cons:
|
||||
- Introduces multiple frontend paradigms, increasing developer onboarding complexity.
|
||||
- Requires new deployment and build infrastructure.
|
||||
- Creates long-term technical debt if legacy Django templates and new JS-driven components coexist indefinitely.
|
||||
|
||||
This approach would still introduce diverging implementation stacks, leading to long-term maintenance challenges.
|
||||
|
||||
---
|
||||
|
||||
**Option 3:** Use a Lightweight JavaScript Framework (e.g., HTMX, HTMZ)
|
||||
Instead of React or Vue, this approach involves using a minimal JavaScript framework like HTMX or HTMZ to enhance interactivity while preserving Django’s server-rendered structure.
|
||||
|
||||
✅ Pros:
|
||||
- Reduces the need for a full rewrite.
|
||||
- Keeps Django templates largely intact.
|
||||
- Minimizes complexity compared to React or Vue.
|
||||
|
||||
❌ Cons:
|
||||
- Limited community support and long-term viability concerns.
|
||||
- Still introduces new technology and learning curves.
|
||||
- Unclear whether it fully meets our interactivity needs.
|
||||
|
||||
Ultimately, we determined that the benefits did not outweigh the potential downsides.
|
||||
|
||||
---
|
||||
|
||||
**Option 4:** Extend Vanilla JavaScript with AJAX (Selected Option)
|
||||
This approach involves incrementally enhancing Django’s server-rendered pages with AJAX while maintaining our existing architecture.
|
||||
|
||||
✅ Pros:
|
||||
Avoids expensive refactors and new dependencies.
|
||||
- Fully customized to our existing codebase.
|
||||
- Keeps Django templates intact while allowing dynamic updates.
|
||||
- No need for additional build tools or frontend frameworks.
|
||||
|
||||
❌ Cons:
|
||||
- Requires designing our own structured approach to AJAX.
|
||||
- Testing and maintainability must be carefully considered.
|
||||
|
||||
This approach aligns with our existing architecture and skill set while meeting stakeholder demands for interactivity.
|
||||
|
||||
## Decision
|
||||
We chose Option 4: Extending our use of vanilla JavaScript with AJAX.
|
||||
|
||||
## Consequences
|
||||
1. Ownership of Solution
|
||||
- We fully control the implementation without external dependencies.
|
||||
|
||||
2. Maintainability
|
||||
- Our AJAX implementation will follow an object-oriented approach, with a base class for components requiring pagination, search, and filtering.
|
||||
|
||||
3. Backend Considerations
|
||||
- Views handling AJAX responses will be explicitly designated as JSON views.
|
||||
|
||||
4. Scalability
|
||||
- While this approach works now, we may need to reassess in the future if interactivity demands continue to grow.
|
||||
|
||||
This decision allows us to enhance the application's responsiveness without disrupting existing architecture or delaying feature development.
|
1030
src/package-lock.json
generated
1030
src/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@uswds/uswds": "3.8.1",
|
||||
"pa11y-ci": "^3.0.1",
|
||||
"pa11y-ci": "^3.1.0",
|
||||
"sass": "^1.54.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -11,6 +11,7 @@ from django.db.models import (
|
|||
Value,
|
||||
When,
|
||||
)
|
||||
|
||||
from django.db.models.functions import Concat, Coalesce
|
||||
from django.http import HttpResponseRedirect
|
||||
from registrar.models.federal_agency import FederalAgency
|
||||
|
@ -24,7 +25,7 @@ from registrar.utility.admin_helpers import (
|
|||
from django.conf import settings
|
||||
from django.contrib.messages import get_messages
|
||||
from django.contrib.admin.helpers import AdminForm
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django_fsm import get_available_FIELD_transitions, FSMField
|
||||
from registrar.models import DomainInformation, Portfolio, UserPortfolioPermission, DomainInvitation
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
|
@ -162,6 +163,18 @@ class MyUserAdminForm(UserChangeForm):
|
|||
"user_permissions": NoAutocompleteFilteredSelectMultiple("user_permissions", False),
|
||||
}
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Custom init to modify the user form"""
|
||||
super(MyUserAdminForm, self).__init__(*args, **kwargs)
|
||||
|
@ -522,6 +535,18 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
|||
"user_url",
|
||||
]
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
# We name the custom prop 'resource' because linter
|
||||
# is not allowing a short_description attr on it
|
||||
# This gets around the linter limitation, for now.
|
||||
|
@ -541,13 +566,6 @@ class CustomLogEntryAdmin(LogEntryAdmin):
|
|||
change_form_template = "admin/change_form_no_submit.html"
|
||||
add_form_template = "admin/change_form_no_submit.html"
|
||||
|
||||
# Select log entry to change -> Log entries
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Log entries"
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
# #786: Skipping on updating audit log tab titles for now
|
||||
# def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
# if extra_context is None:
|
||||
|
@ -628,6 +646,18 @@ class AdminSortFields:
|
|||
class AuditedAdmin(admin.ModelAdmin):
|
||||
"""Custom admin to make auditing easier."""
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def history_view(self, request, object_id, extra_context=None):
|
||||
"""On clicking 'History', take admin to the auditlog view for an object."""
|
||||
return HttpResponseRedirect(
|
||||
|
@ -1028,6 +1058,18 @@ class MyUserAdmin(BaseUserAdmin, ImportExportModelAdmin):
|
|||
extra_context = {"domain_requests": domain_requests, "domains": domains, "portfolios": portfolios}
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select contact to change", display "Contacts")
|
||||
# see "base_site.html" for the <title> code.
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class HostIPInline(admin.StackedInline):
|
||||
"""Edit an ip address on the host page."""
|
||||
|
@ -1052,14 +1094,6 @@ class MyHostAdmin(AuditedAdmin, ImportExportModelAdmin):
|
|||
search_help_text = "Search by domain or host name."
|
||||
inlines = [HostIPInline]
|
||||
|
||||
# Select host to change -> Host
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Host"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class HostIpResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1075,14 +1109,6 @@ class HostIpAdmin(AuditedAdmin, ImportExportModelAdmin):
|
|||
resource_classes = [HostIpResource]
|
||||
model = models.HostIP
|
||||
|
||||
# Select host ip to change -> Host ip
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Host IP"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class ContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -1204,14 +1230,6 @@ class ContactAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
|
||||
return super().change_view(request, object_id, form_url, extra_context=extra_context)
|
||||
|
||||
# Select contact to change -> Contacts
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Contacts"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
# Clear warning messages before saving
|
||||
storage = messages.get_messages(request)
|
||||
|
@ -1326,6 +1344,7 @@ class UserPortfolioPermissionAdmin(ListHeaderAdmin):
|
|||
search_help_text = "Search by first name, last name, email, or portfolio."
|
||||
|
||||
change_form_template = "django/admin/user_portfolio_permission_change_form.html"
|
||||
delete_confirmation_template = "django/admin/user_portfolio_permission_delete_confirmation.html"
|
||||
|
||||
def get_roles(self, obj):
|
||||
readable_roles = obj.get_readable_roles()
|
||||
|
@ -1525,13 +1544,26 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
# Override for the delete confirmation page on the domain table (bulk delete action)
|
||||
delete_selected_confirmation_template = "django/admin/domain_invitation_delete_selected_confirmation.html"
|
||||
|
||||
# Select domain invitations to change -> Domain invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
def change_view(self, request, object_id, form_url="", extra_context=None):
|
||||
"""Override the change_view to add the invitation obj for the change_form_object_tools template"""
|
||||
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
# Get the domain invitation object
|
||||
invitation = get_object_or_404(DomainInvitation, id=object_id)
|
||||
extra_context["invitation"] = invitation
|
||||
|
||||
if request.method == "POST" and "cancel_invitation" in request.POST:
|
||||
if invitation.status == DomainInvitation.DomainInvitationStatus.INVITED:
|
||||
invitation.cancel_invitation()
|
||||
invitation.save(update_fields=["status"])
|
||||
messages.success(request, _("Invitation canceled successfully."))
|
||||
|
||||
# Redirect back to the change view
|
||||
return redirect(reverse("admin:registrar_domaininvitation_change", args=[object_id]))
|
||||
|
||||
return super().change_view(request, object_id, form_url, extra_context)
|
||||
|
||||
def delete_view(self, request, object_id, extra_context=None):
|
||||
"""
|
||||
|
@ -1551,6 +1583,7 @@ class DomainInvitationAdmin(BaseInvitationAdmin):
|
|||
which will be successful if a single User exists for that email; otherwise, will
|
||||
just continue to create the invitation.
|
||||
"""
|
||||
|
||||
if not change:
|
||||
domain = obj.domain
|
||||
domain_org = getattr(domain.domain_info, "portfolio", None)
|
||||
|
@ -1647,14 +1680,7 @@ class PortfolioInvitationAdmin(BaseInvitationAdmin):
|
|||
autocomplete_fields = ["portfolio"]
|
||||
|
||||
change_form_template = "django/admin/portfolio_invitation_change_form.html"
|
||||
|
||||
# Select portfolio invitations to change -> Portfolio invitations
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Portfolio invitations"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
delete_confirmation_template = "django/admin/portfolio_invitation_delete_confirmation.html"
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
|
@ -2045,14 +2071,6 @@ class DomainInformationAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
readonly_fields.extend([field for field in self.analyst_readonly_fields])
|
||||
return readonly_fields # Read-only fields for analysts
|
||||
|
||||
# Select domain information to change -> Domain information
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain information"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
def formfield_for_foreignkey(self, db_field, request, **kwargs):
|
||||
"""Customize the behavior of formfields with foreign key relationships. This will customize
|
||||
the behavior of selects. Customized behavior includes sorting of objects in list."""
|
||||
|
@ -2264,11 +2282,12 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
@admin.display(description=_("Requested Domain"))
|
||||
def custom_requested_domain(self, obj):
|
||||
# Example: Show different icons based on `status`
|
||||
url = reverse("admin:registrar_domainrequest_changelist") + f"{obj.id}"
|
||||
text = obj.requested_domain
|
||||
if obj.portfolio:
|
||||
return format_html('<a href="{}"><img src="/public/admin/img/icon-yes.svg"> {}</a>', url, text)
|
||||
return format_html('<a href="{}">{}</a>', url, text)
|
||||
return format_html(
|
||||
f'<img class="padding-right-05" src="/public/admin/img/icon-yes.svg" aria-hidden="true">{escape(text)}'
|
||||
)
|
||||
return text
|
||||
|
||||
custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore
|
||||
|
||||
|
@ -2872,11 +2891,6 @@ class DomainRequestAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
if next_char.isdigit():
|
||||
should_apply_default_filter = True
|
||||
|
||||
# Select domain request to change -> Domain requests
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Domain requests"
|
||||
|
||||
if should_apply_default_filter:
|
||||
# modify the GET of the request to set the selected filter
|
||||
modified_get = copy.deepcopy(request.GET)
|
||||
|
@ -3715,11 +3729,13 @@ class DomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# Using variables to get past the linter
|
||||
message1 = f"Cannot delete Domain when in state {obj.state}"
|
||||
message2 = f"This subdomain is being used as a hostname on another domain: {err.note}"
|
||||
message3 = f"Command failed with note: {err.note}"
|
||||
# Human-readable mappings of ErrorCodes. Can be expanded.
|
||||
error_messages = {
|
||||
# noqa on these items as black wants to reformat to an invalid length
|
||||
ErrorCode.OBJECT_STATUS_PROHIBITS_OPERATION: message1,
|
||||
ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION: message2,
|
||||
ErrorCode.COMMAND_FAILED: message3,
|
||||
}
|
||||
|
||||
message = "Cannot connect to the registry"
|
||||
|
@ -3931,14 +3947,6 @@ class DraftDomainAdmin(ListHeaderAdmin, ImportExportModelAdmin):
|
|||
# If no redirection is needed, return the original response
|
||||
return response
|
||||
|
||||
# Select draft domain to change -> Draft domains
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "Draft domains"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class PublicContactResource(resources.ModelResource):
|
||||
"""defines how each field in the referenced model should be mapped to the corresponding fields in the
|
||||
|
@ -4360,14 +4368,6 @@ class UserGroupAdmin(AuditedAdmin):
|
|||
def user_group(self, obj):
|
||||
return obj.name
|
||||
|
||||
# Select user groups to change -> User groups
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["tabtitle"] = "User groups"
|
||||
# Get the filtered values
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
class WaffleFlagAdmin(FlagAdmin):
|
||||
"""Custom admin implementation of django-waffle's Flag class"""
|
||||
|
@ -4384,6 +4384,13 @@ class WaffleFlagAdmin(FlagAdmin):
|
|||
if extra_context is None:
|
||||
extra_context = {}
|
||||
extra_context["dns_prototype_flag"] = flag_is_active_for_user(request.user, "dns_prototype_flag")
|
||||
|
||||
# Loads "tabtitle" for this admin page so that on render the <title>
|
||||
# element will only have the model name instead of
|
||||
# the default string loaded by native Django admin code.
|
||||
# (Eg. instead of "Select waffle flags to change", display "Waffle Flags")
|
||||
# see "base_site.html" for the <title> code.
|
||||
extra_context["tabtitle"] = str(self.opts.verbose_name_plural).title()
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
|
||||
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
/** An IIFE for admin in DjangoAdmin to listen to clicks on the growth report export button,
|
||||
* attach the seleted start and end dates to a url that'll trigger the view, and finally
|
||||
* redirect to that url.
|
||||
*
|
||||
* This function also sets the start and end dates to match the url params if they exist
|
||||
*/
|
||||
(function () {
|
||||
// Function to get URL parameter value by name
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
||||
// Get the current date in the format YYYY-MM-DD
|
||||
let currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Default the value of the start date input field to the current date
|
||||
let startDateInput = document.getElementById('start');
|
||||
|
||||
// Default the value of the end date input field to the current date
|
||||
let endDateInput = document.getElementById('end');
|
||||
|
||||
let exportButtons = document.querySelectorAll('.exportLink');
|
||||
|
||||
if (exportButtons.length > 0) {
|
||||
// Check if start and end dates are present in the URL
|
||||
let urlStartDate = getParameterByName('start_date');
|
||||
let urlEndDate = getParameterByName('end_date');
|
||||
|
||||
// Set input values based on URL parameters or current date
|
||||
startDateInput.value = urlStartDate || currentDate;
|
||||
endDateInput.value = urlEndDate || currentDate;
|
||||
|
||||
exportButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', function () {
|
||||
// Get the selected start and end dates
|
||||
let startDate = startDateInput.value;
|
||||
let endDate = endDateInput.value;
|
||||
let exportUrl = btn.dataset.exportUrl;
|
||||
|
||||
// Build the URL with parameters
|
||||
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
||||
|
||||
// Redirect to the export URL
|
||||
window.location.href = exportUrl;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
|
||||
/** An IIFE to initialize the analytics page
|
||||
*/
|
||||
(function () {
|
||||
function createComparativeColumnChart(canvasId, title, labelOne, labelTwo) {
|
||||
var canvas = document.getElementById(canvasId);
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
|
||||
var listOne = JSON.parse(canvas.getAttribute('data-list-one'));
|
||||
var listTwo = JSON.parse(canvas.getAttribute('data-list-two'));
|
||||
|
||||
var data = {
|
||||
labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"],
|
||||
datasets: [
|
||||
{
|
||||
label: labelOne,
|
||||
backgroundColor: "rgba(255, 99, 132, 0.2)",
|
||||
borderColor: "rgba(255, 99, 132, 1)",
|
||||
borderWidth: 1,
|
||||
data: listOne,
|
||||
},
|
||||
{
|
||||
label: labelTwo,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.2)",
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
borderWidth: 1,
|
||||
data: listTwo,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: title
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: data,
|
||||
options: options,
|
||||
});
|
||||
}
|
||||
|
||||
function initComparativeColumnCharts() {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
createComparativeColumnChart("myChart1", "Managed domains", "Start Date", "End Date");
|
||||
createComparativeColumnChart("myChart2", "Unmanaged domains", "Start Date", "End Date");
|
||||
createComparativeColumnChart("myChart3", "Deleted domains", "Start Date", "End Date");
|
||||
createComparativeColumnChart("myChart4", "Ready domains", "Start Date", "End Date");
|
||||
createComparativeColumnChart("myChart5", "Submitted requests", "Start Date", "End Date");
|
||||
createComparativeColumnChart("myChart6", "All requests", "Start Date", "End Date");
|
||||
});
|
||||
};
|
||||
|
||||
initComparativeColumnCharts();
|
||||
})();
|
|
@ -5695,19 +5695,35 @@ const createHeaderButton = (header, headerName) => {
|
|||
buttonEl.setAttribute("tabindex", "0");
|
||||
buttonEl.classList.add(SORT_BUTTON_CLASS);
|
||||
// ICON_SOURCE
|
||||
// ---- END DOTGOV EDIT
|
||||
// Change icons on sort, use source from arro_upward and arrow_downward
|
||||
// buttonEl.innerHTML = Sanitizer.escapeHTML`
|
||||
// <svg class="${PREFIX}-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
// <g class="descending" fill="transparent">
|
||||
// <path d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
// </g>
|
||||
// <g class="ascending" fill="transparent">
|
||||
// <path transform="rotate(180, 12, 12)" d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
// </g>
|
||||
// <g class="unsorted" fill="transparent">
|
||||
// <polygon points="15.17 15 13 17.17 13 6.83 15.17 9 16.58 7.59 12 3 7.41 7.59 8.83 9 11 6.83 11 17.17 8.83 15 7.42 16.41 12 21 16.59 16.41 15.17 15"/>
|
||||
// </g>
|
||||
// </svg>
|
||||
// `;
|
||||
buttonEl.innerHTML = Sanitizer.escapeHTML`
|
||||
<svg class="${PREFIX}-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g class="descending" fill="transparent">
|
||||
<path d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
<path d="m20 12-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>
|
||||
</g>
|
||||
<g class="ascending" fill="transparent">
|
||||
<path transform="rotate(180, 12, 12)" d="M17 17L15.59 15.59L12.9999 18.17V2H10.9999V18.17L8.41 15.58L7 17L11.9999 22L17 17Z" />
|
||||
<path d="m4 12 1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
|
||||
</g>
|
||||
<g class="unsorted" fill="transparent">
|
||||
<polygon points="15.17 15 13 17.17 13 6.83 15.17 9 16.58 7.59 12 3 7.41 7.59 8.83 9 11 6.83 11 17.17 8.83 15 7.42 16.41 12 21 16.59 16.41 15.17 15"/>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
// ---- END DOTGOV EDIT
|
||||
header.appendChild(buttonEl);
|
||||
updateSortLabel(header, headerName);
|
||||
};
|
||||
|
|
177
src/registrar/assets/src/js/getgov-admin/analytics.js
Normal file
177
src/registrar/assets/src/js/getgov-admin/analytics.js
Normal file
|
@ -0,0 +1,177 @@
|
|||
import { debounce } from '../getgov/helpers.js';
|
||||
import { getParameterByName } from './helpers-admin.js';
|
||||
|
||||
/** This function also sets the start and end dates to match the url params if they exist
|
||||
*/
|
||||
function initAnalyticsExportButtons() {
|
||||
// Get the current date in the format YYYY-MM-DD
|
||||
let currentDate = new Date().toISOString().split('T')[0];
|
||||
|
||||
// Default the value of the start date input field to the current date
|
||||
let startDateInput = document.getElementById('start');
|
||||
|
||||
// Default the value of the end date input field to the current date
|
||||
let endDateInput = document.getElementById('end');
|
||||
|
||||
let exportButtons = document.querySelectorAll('.exportLink');
|
||||
|
||||
if (exportButtons.length > 0) {
|
||||
// Check if start and end dates are present in the URL
|
||||
let urlStartDate = getParameterByName('start_date');
|
||||
let urlEndDate = getParameterByName('end_date');
|
||||
|
||||
// Set input values based on URL parameters or current date
|
||||
startDateInput.value = urlStartDate || currentDate;
|
||||
endDateInput.value = urlEndDate || currentDate;
|
||||
|
||||
exportButtons.forEach((btn) => {
|
||||
btn.addEventListener('click', function () {
|
||||
// Get the selected start and end dates
|
||||
let startDate = startDateInput.value;
|
||||
let endDate = endDateInput.value;
|
||||
let exportUrl = btn.dataset.exportUrl;
|
||||
|
||||
// Build the URL with parameters
|
||||
exportUrl += "?start_date=" + startDate + "&end_date=" + endDate;
|
||||
|
||||
// Redirect to the export URL
|
||||
window.location.href = exportUrl;
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a diagonal stripe pattern for chart.js
|
||||
* Inspired by https://stackoverflow.com/questions/28569667/fill-chart-js-bar-chart-with-diagonal-stripes-or-other-patterns
|
||||
* and https://github.com/ashiguruma/patternomaly
|
||||
* @param {string} backgroundColor - Background color of the pattern
|
||||
* @param {string} [lineColor="white"] - Color of the diagonal lines
|
||||
* @param {boolean} [rightToLeft=false] - Direction of the diagonal lines
|
||||
* @param {number} [lineGap=1] - Gap between lines
|
||||
* @returns {CanvasPattern} A canvas pattern object for use with backgroundColor
|
||||
*/
|
||||
function createDiagonalPattern(backgroundColor, lineColor, rightToLeft=false, lineGap=1) {
|
||||
// Define the canvas and the 2d context so we can draw on it
|
||||
let shape = document.createElement("canvas");
|
||||
shape.width = 20;
|
||||
shape.height = 20;
|
||||
let context = shape.getContext("2d");
|
||||
|
||||
// Fill with specified background color
|
||||
context.fillStyle = backgroundColor;
|
||||
context.fillRect(0, 0, shape.width, shape.height);
|
||||
|
||||
// Set stroke properties
|
||||
context.strokeStyle = lineColor;
|
||||
context.lineWidth = 2;
|
||||
|
||||
// Rotate canvas for a right-to-left pattern
|
||||
if (rightToLeft) {
|
||||
context.translate(shape.width, 0);
|
||||
context.rotate(90 * Math.PI / 180);
|
||||
};
|
||||
|
||||
// First diagonal line
|
||||
let halfSize = shape.width / 2;
|
||||
context.moveTo(halfSize - lineGap, -lineGap);
|
||||
context.lineTo(shape.width + lineGap, halfSize + lineGap);
|
||||
|
||||
// Second diagonal line (x,y are swapped)
|
||||
context.moveTo(-lineGap, halfSize - lineGap);
|
||||
context.lineTo(halfSize + lineGap, shape.width + lineGap);
|
||||
|
||||
context.stroke();
|
||||
return context.createPattern(shape, "repeat");
|
||||
}
|
||||
|
||||
function createComparativeColumnChart(id, title, labelOne, labelTwo) {
|
||||
var canvas = document.getElementById(id);
|
||||
if (!canvas) {
|
||||
return
|
||||
}
|
||||
|
||||
var ctx = canvas.getContext("2d");
|
||||
var listOne = JSON.parse(canvas.getAttribute('data-list-one'));
|
||||
var listTwo = JSON.parse(canvas.getAttribute('data-list-two'));
|
||||
|
||||
var data = {
|
||||
labels: ["Total", "Federal", "Interstate", "State/Territory", "Tribal", "County", "City", "Special District", "School District", "Election Board"],
|
||||
datasets: [
|
||||
{
|
||||
label: labelOne,
|
||||
backgroundColor: "rgba(255, 99, 132, 0.3)",
|
||||
borderColor: "rgba(255, 99, 132, 1)",
|
||||
borderWidth: 1,
|
||||
data: listOne,
|
||||
// Set this line style to be rightToLeft for visual distinction
|
||||
backgroundColor: createDiagonalPattern('rgba(255, 99, 132, 0.3)', 'white', true)
|
||||
},
|
||||
{
|
||||
label: labelTwo,
|
||||
backgroundColor: "rgba(75, 192, 192, 0.3)",
|
||||
borderColor: "rgba(75, 192, 192, 1)",
|
||||
borderWidth: 1,
|
||||
data: listTwo,
|
||||
backgroundColor: createDiagonalPattern('rgba(75, 192, 192, 0.3)', 'white')
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var options = {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: title
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
return new Chart(ctx, {
|
||||
type: "bar",
|
||||
data: data,
|
||||
options: options,
|
||||
});
|
||||
}
|
||||
|
||||
/** An IIFE to initialize the analytics page
|
||||
*/
|
||||
export function initAnalyticsDashboard() {
|
||||
const analyticsPageContainer = document.querySelector('.analytics-dashboard-charts');
|
||||
if (analyticsPageContainer) {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
initAnalyticsExportButtons();
|
||||
|
||||
// Create charts and store each instance of it
|
||||
const chartInstances = new Map();
|
||||
const charts = [
|
||||
{ id: "managed-domains-chart", title: "Managed domains" },
|
||||
{ id: "unmanaged-domains-chart", title: "Unmanaged domains" },
|
||||
{ id: "deleted-domains-chart", title: "Deleted domains" },
|
||||
{ id: "ready-domains-chart", title: "Ready domains" },
|
||||
{ id: "submitted-requests-chart", title: "Submitted requests" },
|
||||
{ id: "all-requests-chart", title: "All requests" }
|
||||
];
|
||||
charts.forEach(chart => {
|
||||
if (chartInstances.has(chart.id)) chartInstances.get(chart.id).destroy();
|
||||
chartInstances.set(chart.id, createComparativeColumnChart(chart.id, chart.title, "Start Date", "End Date"));
|
||||
});
|
||||
|
||||
// Add resize listener to each chart
|
||||
window.addEventListener("resize", debounce(() => {
|
||||
chartInstances.forEach((chart) => {
|
||||
if (chart?.canvas) chart.resize();
|
||||
});
|
||||
}, 200));
|
||||
});
|
||||
}
|
||||
};
|
15
src/registrar/assets/src/js/getgov-admin/button-utils.js
Normal file
15
src/registrar/assets/src/js/getgov-admin/button-utils.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Initializes buttons to behave like links by navigating to their data-url attribute
|
||||
* Example usage: <button class="use-button-as-link" data-url="/some/path">Click me</button>
|
||||
*/
|
||||
export function initButtonLinks() {
|
||||
document.querySelectorAll('button.use-button-as-link').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
// Equivalent to button.getAttribute("data-href")
|
||||
const href = this.dataset.href;
|
||||
if (href) {
|
||||
window.location.href = href;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -22,3 +22,13 @@ export function addOrRemoveSessionBoolean(name, add){
|
|||
sessionStorage.removeItem(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, '\\$&');
|
||||
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '));
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@ import { initDomainFormTargetBlankButtons } from './domain-form.js';
|
|||
import { initDynamicPortfolioFields } from './portfolio-form.js';
|
||||
import { initDynamicDomainInformationFields } from './domain-information-form.js';
|
||||
import { initDynamicDomainFields } from './domain-form.js';
|
||||
import { initAnalyticsDashboard } from './analytics.js';
|
||||
import { initButtonLinks } from './button-utils.js';
|
||||
|
||||
// General
|
||||
initModals();
|
||||
|
@ -22,6 +24,7 @@ initCopyToClipboard();
|
|||
initFilterHorizontalWidget();
|
||||
initDescriptions();
|
||||
initSubmitBar();
|
||||
initButtonLinks();
|
||||
|
||||
// Domain request
|
||||
initIneligibleModal();
|
||||
|
@ -41,3 +44,6 @@ initDynamicPortfolioFields();
|
|||
|
||||
// Domain information
|
||||
initDynamicDomainInformationFields();
|
||||
|
||||
// Analytics dashboard
|
||||
initAnalyticsDashboard();
|
||||
|
|
|
@ -2,11 +2,41 @@ import { submitForm } from './helpers.js';
|
|||
|
||||
export function initDomainRequestForm() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const button = document.getElementById("domain-request-form-submit-button");
|
||||
if (button) {
|
||||
button.addEventListener("click", function () {
|
||||
submitForm("submit-domain-request-form");
|
||||
});
|
||||
}
|
||||
// These are the request steps in DomainRequestWizard, such as current_websites or review
|
||||
initRequestStepCurrentWebsitesListener();
|
||||
initRequestStepReviewListener();
|
||||
});
|
||||
}
|
||||
|
||||
function initRequestStepReviewListener() {
|
||||
const button = document.getElementById("domain-request-form-submit-button");
|
||||
if (button) {
|
||||
button.addEventListener("click", function () {
|
||||
submitForm("submit-domain-request-form");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function initRequestStepCurrentWebsitesListener() {
|
||||
//register-form-step
|
||||
const addAnotherSiteButton = document.getElementById("submit-domain-request--site-button");
|
||||
if (addAnotherSiteButton) {
|
||||
// Check for focus state in sessionStorage
|
||||
const focusTarget = sessionStorage.getItem("lastFocusedElement");
|
||||
if (focusTarget) {
|
||||
document.querySelector(focusTarget)?.focus();
|
||||
}
|
||||
// Add form submit handler to store focus state
|
||||
const form = document.querySelector("form");
|
||||
if (form) {
|
||||
form.addEventListener("submit", () => {
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
sessionStorage.setItem("lastFocusedElement", "#" + activeElement.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
// We only want to do this action once, so we clear out the session
|
||||
sessionStorage.removeItem("lastFocusedElement");
|
||||
}
|
||||
}
|
|
@ -96,3 +96,14 @@ export function submitForm(form_id) {
|
|||
console.error("Form '" + form_id + "' not found.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to strip HTML tags
|
||||
* THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
*/
|
||||
export function unsafeStripHtmlTags(input) {
|
||||
const tempDiv = document.createElement("div");
|
||||
// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
tempDiv.innerHTML = input;
|
||||
return tempDiv.textContent || tempDiv.innerText || "";
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export function initPortfolioNewMemberPageToggle() {
|
|||
const unique_id = `${member_type}-${member_id}`;
|
||||
|
||||
let cancelInvitationButton = member_type === "invitedmember" ? "Cancel invitation" : "Remove member";
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`);
|
||||
wrapperDeleteAction.innerHTML = generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `More Options for ${member_name}`, "usa-icon--large");
|
||||
|
||||
// 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
|
||||
|
@ -100,8 +100,8 @@ export function initAddNewMemberPageListeners() {
|
|||
const permissionSections = document.querySelectorAll(`#${permission_details_div_id} > h3`);
|
||||
|
||||
permissionSections.forEach(section => {
|
||||
// Find the <h3> element text
|
||||
const sectionTitle = section.textContent;
|
||||
// Find the <h3> element text, strip out the '*'
|
||||
const sectionTitle = section.textContent.trim().replace(/\*$/, "") + ": ";
|
||||
|
||||
// Find the associated radio buttons container (next fieldset)
|
||||
const fieldset = section.nextElementSibling;
|
||||
|
@ -128,25 +128,29 @@ export function initAddNewMemberPageListeners() {
|
|||
});
|
||||
} else {
|
||||
// for admin users, the permissions are always the same
|
||||
appendPermissionInContainer('Domains', 'Viewer, all', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Domain requests', 'Creator', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Members', 'Manager', permissionDetailsContainer);
|
||||
appendPermissionInContainer('Domains: ', 'Viewer', 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 elementContainer = document.createElement("p");
|
||||
elementContainer.classList.add("margin-top-0", "margin-bottom-1");
|
||||
|
||||
const permissionElement = document.createElement("p");
|
||||
const titleElement = document.createElement("strong");
|
||||
titleElement.textContent = sectionTitle;
|
||||
titleElement.classList.add("text-primary-darker");
|
||||
|
||||
const permissionElement = document.createElement("span");
|
||||
permissionElement.textContent = permissionDisplay;
|
||||
permissionElement.classList.add("margin-top-0");
|
||||
|
||||
// Append to the content container
|
||||
permissionContainer.appendChild(titleElement);
|
||||
permissionContainer.appendChild(permissionElement);
|
||||
elementContainer.appendChild(titleElement);
|
||||
elementContainer.appendChild(permissionElement);
|
||||
|
||||
permissionContainer.appendChild(elementContainer);
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -79,13 +79,13 @@ export function addModal(id, ariaLabelledby, ariaDescribedby, modalHeading, moda
|
|||
* @param {string} modal_button_text - The action button's text
|
||||
* @param {string} screen_reader_text - A screen reader helper
|
||||
*/
|
||||
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text) {
|
||||
export function generateKebabHTML(action, unique_id, modal_button_text, screen_reader_text, icon_class) {
|
||||
const generateModalButton = (mobileOnly = false) => `
|
||||
<a
|
||||
role="button"
|
||||
id="button-trigger-${action}-${unique_id}"
|
||||
href="#toggle-${action}-${unique_id}"
|
||||
class="usa-button usa-button--unstyled text-no-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
|
||||
class="usa-button usa-button--unstyled text-underline late-loading-modal-trigger margin-top-2 line-height-sans-5 text-secondary ${mobileOnly ? 'visible-mobile-flex' : ''}"
|
||||
aria-controls="toggle-${action}-${unique_id}"
|
||||
data-open-modal
|
||||
>
|
||||
|
@ -99,7 +99,7 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
|||
// Main kebab structure
|
||||
const kebab = `
|
||||
${generateModalButton(true)} <!-- Mobile button -->
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 hidden-mobile-flex">
|
||||
<div class="usa-accordion usa-accordion--more-actions margin-right-2 margin-top-3px hidden-mobile-flex">
|
||||
<div class="usa-accordion__heading">
|
||||
<button
|
||||
type="button"
|
||||
|
@ -108,12 +108,12 @@ export function generateKebabHTML(action, unique_id, modal_button_text, screen_r
|
|||
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${icon_class ? " " + icon_class : ""}" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#more_vert"></use>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="more-actions-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-neg-1" hidden>
|
||||
<div id="more-actions-${unique_id}" class="usa-accordion__content usa-prose shadow-1 left-auto right-0${icon_class === 'usa-icon--large' ? ' top-28px' : ''}" hidden>
|
||||
<h2>More options</h2>
|
||||
${generateModalButton()} <!-- Desktop button -->
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { hideElement, showElement, getCsrfToken } from './helpers.js';
|
||||
import { hideElement, showElement, getCsrfToken, unsafeStripHtmlTags } from './helpers.js';
|
||||
import { uswdsInitializeModals, uswdsUnloadModals } from './helpers-uswds.js';
|
||||
|
||||
import { BaseTable, addModal, generateKebabHTML } from './table-base.js';
|
||||
|
@ -98,9 +98,10 @@ export class DomainRequestsTable extends BaseTable {
|
|||
|
||||
// Request is deletable, modal and modalTrigger are built. Now check if we are on the portfolio requests page (by seeing if there is a portfolio value) and enhance the modalTrigger accordingly
|
||||
if (this.portfolioValue) {
|
||||
|
||||
// NOTE: THIS IS NOT SUITABLE FOR SANITIZING DANGEROUS STRINGS
|
||||
const sanitizedDomainName = unsafeStripHtmlTags(domainName);
|
||||
// 2nd path (org model): Just a modal trigger on mobile for org users or kebab + accordion with nested modal trigger on desktop for org users
|
||||
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', domainName);
|
||||
modalTrigger = generateKebabHTML('delete-domain', request.id, 'Delete', sanitizedDomainName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,7 +118,7 @@ export class DomainRequestsTable extends BaseTable {
|
|||
${request.status}
|
||||
</td>
|
||||
<td class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-wrap">
|
||||
<div class="tablet:display-flex tablet:flex-row">
|
||||
<a href="${actionUrl}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#${request.svg_icon}"></use>
|
||||
|
|
|
@ -23,10 +23,13 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
this.readonlyModeContainer = document.getElementById('domain-assignments-readonly-view');
|
||||
this.reviewButton = document.getElementById('review-domain-assignments');
|
||||
this.backButton = document.getElementById('back-to-edit-domain-assignments');
|
||||
this.saveButton = document.getElementById('save-domain-assignments');
|
||||
this.initializeDomainAssignments();
|
||||
this.saveButton = document.getElementById('save-domain-assignments');
|
||||
}
|
||||
async init() {
|
||||
await this.initializeDomainAssignments();
|
||||
this.initCancelEditDomainAssignmentButton();
|
||||
this.initEventListeners();
|
||||
return this;
|
||||
}
|
||||
getBaseUrl() {
|
||||
return document.getElementById("get_member_domains_json_url");
|
||||
|
@ -134,27 +137,33 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
* member. It populates both initialDomainAssignments and initialDomainAssignmentsOnlyMember.
|
||||
* It is called once per page load, but not called with subsequent table changes.
|
||||
*/
|
||||
initializeDomainAssignments() {
|
||||
async initializeDomainAssignments() {
|
||||
const baseUrlValue = this.getBaseUrl()?.innerHTML ?? null;
|
||||
if (!baseUrlValue) return;
|
||||
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
|
||||
let url = baseUrlValue + "?" + searchParams.toString();
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
console.error('Error in AJAX call: ' + data.error);
|
||||
if (!baseUrlValue) {
|
||||
console.error("Base URL not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let searchParams = this.getDomainAssignmentSearchParams(this.portfolioValue);
|
||||
let url = baseUrlValue + "?" + searchParams.toString();
|
||||
|
||||
let response = await fetch(url);
|
||||
let data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
console.error("Error in AJAX call:", data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
let dataObjects = this.getDataObjects(data);
|
||||
// Map the id attributes of dataObjects to this.initialDomainAssignments
|
||||
this.initialDomainAssignments = dataObjects.map(obj => obj.id);
|
||||
this.initialDomainAssignmentsOnlyMember = dataObjects
|
||||
.filter(obj => obj.member_is_only_manager)
|
||||
.map(obj => obj.id);
|
||||
})
|
||||
.catch(error => console.error('Error fetching domain assignments:', error));
|
||||
.filter(obj => obj.member_is_only_manager)
|
||||
.map(obj => obj.id);
|
||||
} catch (error) {
|
||||
console.error("Error fetching domain assignments:", error);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Initializes listeners on checkboxes in the table. Checkbox listeners are used
|
||||
|
@ -232,8 +241,6 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
}
|
||||
|
||||
updateReadonlyDisplay() {
|
||||
let totalAssignedDomains = this.getCheckedDomains().length;
|
||||
|
||||
// Create unassigned domains list
|
||||
const unassignedDomainsList = document.createElement('ul');
|
||||
unassignedDomainsList.classList.add('usa-list', 'usa-list--unstyled');
|
||||
|
@ -260,35 +267,30 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
// Clear existing content
|
||||
domainAssignmentSummary.innerHTML = '';
|
||||
|
||||
// Append unassigned domains section
|
||||
if (this.removedDomains.length) {
|
||||
const unassignedHeader = document.createElement('h3');
|
||||
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
unassignedHeader.textContent = 'Unassigned domains';
|
||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||
}
|
||||
|
||||
// Append assigned domains section
|
||||
if (this.addedDomains.length) {
|
||||
const assignedHeader = document.createElement('h3');
|
||||
// Make this h3 look like a h4
|
||||
assignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
assignedHeader.textContent = 'Assigned domains';
|
||||
assignedHeader.textContent = `New assignments (${this.addedDomains.length})`;
|
||||
domainAssignmentSummary.appendChild(assignedHeader);
|
||||
domainAssignmentSummary.appendChild(assignedDomainsList);
|
||||
}
|
||||
|
||||
// Append total assigned domains section
|
||||
const totalHeader = document.createElement('h3');
|
||||
// Make this h3 look like a h4
|
||||
totalHeader.classList.add('margin-bottom-05', 'h4');
|
||||
totalHeader.textContent = 'Total assigned domains';
|
||||
domainAssignmentSummary.appendChild(totalHeader);
|
||||
const totalCount = document.createElement('p');
|
||||
totalCount.classList.add('margin-y-0');
|
||||
totalCount.textContent = totalAssignedDomains;
|
||||
domainAssignmentSummary.appendChild(totalCount);
|
||||
// Append unassigned domains section
|
||||
if (this.removedDomains.length) {
|
||||
const unassignedHeader = document.createElement('h3');
|
||||
unassignedHeader.classList.add('margin-bottom-05', 'h4');
|
||||
unassignedHeader.textContent =`Removed assignments (${this.removedDomains.length})`;
|
||||
domainAssignmentSummary.appendChild(unassignedHeader);
|
||||
domainAssignmentSummary.appendChild(unassignedDomainsList);
|
||||
}
|
||||
|
||||
if (!this.addedDomains.length && !this.removedDomains.length) {
|
||||
const noChangesParagraph = document.createElement('p');
|
||||
noChangesParagraph.textContent = "No changes were detected. Click the “Back” button to edit this member’s domain assignments.";
|
||||
domainAssignmentSummary.appendChild(noChangesParagraph);
|
||||
}
|
||||
}
|
||||
|
||||
showReadonlyMode() {
|
||||
|
@ -355,14 +357,14 @@ export class EditMemberDomainsTable extends BaseTable {
|
|||
}
|
||||
|
||||
export function initEditMemberDomainsTable() {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
|
||||
if (isEditMemberDomainsPage) {
|
||||
const editMemberDomainsTable = new EditMemberDomainsTable();
|
||||
if (editMemberDomainsTable.tableWrapper) {
|
||||
// Initial load
|
||||
editMemberDomainsTable.loadTable(1);
|
||||
}
|
||||
}
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
const isEditMemberDomainsPage = document.getElementById("edit-member-domains");
|
||||
if (!isEditMemberDomainsPage) return; // Exit if not on the right page
|
||||
|
||||
const editMemberDomainsTable = await new EditMemberDomainsTable().init();
|
||||
|
||||
if (editMemberDomainsTable.tableWrapper) {
|
||||
editMemberDomainsTable.loadTable(1); // Initial load
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -69,13 +69,14 @@ export class MembersTable extends BaseTable {
|
|||
const kebabHTML = customTableOptions.hasAdditionalActions ? generateKebabHTML('remove-member', unique_id, cancelInvitationButton, `Expand for more options for ${member.name}`): '';
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.classList.add('hide-td-borders');
|
||||
let admin_tagHTML = ``;
|
||||
if (member.is_admin)
|
||||
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
|
||||
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url);
|
||||
let permissionsHTML = this.generatePermissionsHTML(member.permissions, customTableOptions.UserPortfolioPermissionChoices);
|
||||
let domainsHTML = this.generateDomainsHTML(num_domains, member.domain_names, member.domain_urls, member.action_url, unique_id);
|
||||
let permissionsHTML = this.generatePermissionsHTML(member.is_admin, member.permissions, customTableOptions.UserPortfolioPermissionChoices, unique_id);
|
||||
|
||||
// domainsHTML block and permissionsHTML block need to be wrapped with hide/show toggle, Expand
|
||||
let showMoreButton = '';
|
||||
|
@ -96,28 +97,34 @@ export class MembersTable extends BaseTable {
|
|||
</button>
|
||||
`;
|
||||
|
||||
showMoreRow.innerHTML = `<td colspan='3' headers="header-member row-header-${unique_id}" class="padding-top-0"><div class='grid-row'>${domainsHTML} ${permissionsHTML}</div></td>`;
|
||||
showMoreRow.classList.add('show-more-content');
|
||||
showMoreRow.classList.add('display-none');
|
||||
showMoreRow.innerHTML = `
|
||||
<td colspan='4' headers="header-member row-header-${unique_id}" class="padding-top-0">
|
||||
${showMoreButton}
|
||||
<div class='grid-row grid-gap-2 show-more-content display-none'>
|
||||
${domainsHTML}
|
||||
${permissionsHTML}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
showMoreRow.id = unique_id;
|
||||
}
|
||||
|
||||
row.innerHTML = `
|
||||
<th role="rowheader" headers="header-member" data-label="member email" id='row-header-${unique_id}'>
|
||||
${member.member_display} ${admin_tagHTML} ${showMoreButton}
|
||||
<th class="padding-bottom-0" role="rowheader" headers="header-member" data-label="Member" id='row-header-${unique_id}'>
|
||||
${member.member_display} ${admin_tagHTML}
|
||||
</th>
|
||||
<td headers="header-last-active row-header-${unique_id}" data-sort-value="${last_active.sort_value}" data-label="last_active">
|
||||
<td class="padding-bottom-0" headers="header-last-active row-header-${unique_id}" data-sort-value="${last_active.sort_value}" data-label="Last active">
|
||||
${last_active.display_value}
|
||||
</td>
|
||||
<td headers="header-action row-header-${unique_id}" class="width--action-column">
|
||||
<td class="padding-bottom-0" headers="header-action row-header-${unique_id}" class="width--action-column">
|
||||
<div class="tablet:display-flex tablet:flex-row flex-align-center">
|
||||
<a href="${member.action_url}">
|
||||
<a href="${member.action_url}" ${customTableOptions.hasAdditionalActions ? "class='margin-right-2'" : ''}>
|
||||
<svg class="usa-icon top-1px" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="/public/img/sprite.svg#${member.svg_icon}"></use>
|
||||
</svg>
|
||||
${member.action_label} <span class="usa-sr-only">${member.name}</span>
|
||||
</a>
|
||||
<span class="padding-left-1">${customTableOptions.hasAdditionalActions ? kebabHTML : ''}</span>
|
||||
${customTableOptions.hasAdditionalActions ? kebabHTML : ''}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
@ -146,16 +153,15 @@ export class MembersTable extends BaseTable {
|
|||
*
|
||||
* @param {HTMLElement} toggleButton - The button that toggles the content visibility.
|
||||
* @param {HTMLElement} contentDiv - The content div whose visibility is toggled.
|
||||
* @param {HTMLElement} buttonParentRow - The parent row element containing the button.
|
||||
*/
|
||||
function toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow) {
|
||||
function toggleShowMoreButton(toggleButton, contentDiv) {
|
||||
const spanElement = toggleButton.querySelector('span');
|
||||
const useElement = toggleButton.querySelector('use');
|
||||
if (contentDiv.classList.contains('display-none')) {
|
||||
showElement(contentDiv);
|
||||
spanElement.textContent = 'Close';
|
||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_less');
|
||||
buttonParentRow.classList.add('hide-td-borders');
|
||||
toggleButton.classList.add('margin-bottom-2');
|
||||
|
||||
let ariaLabelText = "Close additional information";
|
||||
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||
|
@ -169,7 +175,7 @@ export class MembersTable extends BaseTable {
|
|||
hideElement(contentDiv);
|
||||
spanElement.textContent = 'Expand';
|
||||
useElement.setAttribute('xlink:href', '/public/img/sprite.svg#expand_more');
|
||||
buttonParentRow.classList.remove('hide-td-borders');
|
||||
toggleButton.classList.remove('margin-bottom-2');
|
||||
|
||||
let ariaLabelText = "Expand for additional information";
|
||||
let ariaLabelPlaceholder = toggleButton.getAttribute("aria-label-placeholder");
|
||||
|
@ -182,14 +188,11 @@ export class MembersTable extends BaseTable {
|
|||
|
||||
let toggleButtons = document.querySelectorAll('.usa-button--show-more-button');
|
||||
toggleButtons.forEach((toggleButton) => {
|
||||
|
||||
// get contentDiv for element specified in data-for attribute of toggleButton
|
||||
let dataFor = toggleButton.dataset.for;
|
||||
let contentDiv = document.getElementById(dataFor);
|
||||
let buttonParentRow = toggleButton.parentElement.parentElement;
|
||||
if (contentDiv && contentDiv.tagName.toLowerCase() === 'tr' && contentDiv.classList.contains('show-more-content') && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') {
|
||||
let contentDiv = buttonParentRow.querySelector(".show-more-content");
|
||||
if (contentDiv && buttonParentRow && buttonParentRow.tagName.toLowerCase() === 'tr') {
|
||||
toggleButton.addEventListener('click', function() {
|
||||
toggleShowMoreButton(toggleButton, contentDiv, buttonParentRow);
|
||||
toggleShowMoreButton(toggleButton, contentDiv);
|
||||
});
|
||||
} else {
|
||||
console.warn('Found a toggle button with no associated toggleable content or parent row');
|
||||
|
@ -240,32 +243,42 @@ export class MembersTable extends BaseTable {
|
|||
* @param {number} num_domains - The number of domains the member is assigned to.
|
||||
* @param {Array} domain_names - An array of domain names.
|
||||
* @param {Array} domain_urls - An array of corresponding domain URLs.
|
||||
* @param {Array} unique_id - A unique row id.
|
||||
* @returns {string} - A string of HTML displaying the domains assigned to the member.
|
||||
*/
|
||||
generateDomainsHTML(num_domains, domain_names, domain_urls, action_url) {
|
||||
generateDomainsHTML(num_domains, domain_names, domain_urls, action_url, unique_id) {
|
||||
// Initialize an empty string for the HTML
|
||||
let domainsHTML = '';
|
||||
|
||||
// Only generate HTML if the member has one or more assigned domains
|
||||
|
||||
domainsHTML += "<div class='desktop:grid-col-4 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||
domainsHTML += `<h4 id='domains-assigned--heading-${unique_id}' class='font-body-xs margin-y-0'>Domains assigned</h4>`;
|
||||
domainsHTML += `<section aria-labelledby='domains-assigned--heading-${unique_id}' tabindex='0'>`
|
||||
if (num_domains > 0) {
|
||||
domainsHTML += "<div class='desktop:grid-col-5 margin-bottom-2 desktop:margin-bottom-0'>";
|
||||
domainsHTML += "<h4 class='font-body-xs margin-y-0'>Domains assigned</h4>";
|
||||
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 += `<p class='font-body-xs text-base-darker margin-y-0'>This member is assigned to ${num_domains} domain${num_domains > 1 ? 's' : ''}:</p>`;
|
||||
if (num_domains > 1) {
|
||||
domainsHTML += "<ul class='usa-list usa-list--unstyled margin-y-0'>";
|
||||
|
||||
// Display up to 6 domains with their URLs
|
||||
for (let i = 0; i < num_domains && i < 6; i++) {
|
||||
domainsHTML += `<li><a class="font-body-xs" href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
||||
// Display up to 6 domains with their URLs
|
||||
for (let i = 0; i < num_domains && i < 6; i++) {
|
||||
domainsHTML += `<li><a class="font-body-xs" href="${domain_urls[i]}">${domain_names[i]}</a></li>`;
|
||||
}
|
||||
|
||||
domainsHTML += "</ul>";
|
||||
} else {
|
||||
// We don't display this in a list for better screenreader support, when only one item exists.
|
||||
domainsHTML += `<a class="font-body-xs" href="${domain_urls[0]}">${domain_names[0]}</a>`;
|
||||
}
|
||||
|
||||
domainsHTML += "</ul>";
|
||||
|
||||
// If there are more than 6 domains, display a "View assigned domains" link
|
||||
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View assigned domains</a></p>`;
|
||||
|
||||
domainsHTML += "</div>";
|
||||
} else {
|
||||
domainsHTML += `<p class='font-body-xs text-base-darker margin-y-0'>This member is assigned to 0 domains.</p>`;
|
||||
}
|
||||
|
||||
// If there are more than 6 domains, display a "View assigned domains" link
|
||||
domainsHTML += `<p class="font-body-xs"><a href="${action_url}/domains">View domain assignments</a></p>`;
|
||||
domainsHTML += "</section>"
|
||||
domainsHTML += "</div>";
|
||||
|
||||
return domainsHTML;
|
||||
}
|
||||
|
||||
|
@ -362,7 +375,7 @@ export class MembersTable extends BaseTable {
|
|||
* - VIEW_ALL_REQUESTS
|
||||
* - EDIT_MEMBERS
|
||||
* - VIEW_MEMBERS
|
||||
*
|
||||
* @param {String} unique_id
|
||||
* @returns {string} - A string of HTML representing the user's additional permissions.
|
||||
* If the user has no specific permissions, it returns a default message
|
||||
* indicating no additional permissions.
|
||||
|
@ -377,40 +390,51 @@ export class MembersTable extends BaseTable {
|
|||
* - If no relevant permissions are found, the function returns a message stating that the user has no additional permissions.
|
||||
* - The resulting HTML always includes a header "Additional permissions for this member" and appends the relevant permission descriptions.
|
||||
*/
|
||||
generatePermissionsHTML(member_permissions, UserPortfolioPermissionChoices) {
|
||||
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
|
||||
generatePermissionsHTML(is_admin, member_permissions, UserPortfolioPermissionChoices, unique_id) {
|
||||
// 1. Role
|
||||
const memberAccessValue = is_admin ? "Admin" : "Basic";
|
||||
|
||||
// 2. Domain access
|
||||
let domainValue = "No access";
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS)) {
|
||||
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>`;
|
||||
domainValue = "Viewer";
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS)) {
|
||||
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>`;
|
||||
domainValue = "Viewer, limited";
|
||||
}
|
||||
|
||||
// Check request-related permissions
|
||||
|
||||
// 3. Request access
|
||||
let requestValue = "No access";
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_REQUESTS)) {
|
||||
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>`;
|
||||
requestValue = "Creator";
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS)) {
|
||||
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>`;
|
||||
requestValue = "Viewer";
|
||||
}
|
||||
|
||||
// Check member-related permissions
|
||||
|
||||
// 4. Member access
|
||||
let memberValue = "No access";
|
||||
if (member_permissions.includes(UserPortfolioPermissionChoices.EDIT_MEMBERS)) {
|
||||
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>`;
|
||||
memberValue = "Manager";
|
||||
} else if (member_permissions.includes(UserPortfolioPermissionChoices.VIEW_MEMBERS)) {
|
||||
permissionsHTML += `<p class='${sharedParagraphClasses}'><strong class='text-base-dark'>Members (view-only):</strong> Can view all organizational members. Can't manage any members.</p>`;
|
||||
memberValue = "Viewer";
|
||||
}
|
||||
|
||||
// If no specific permissions are assigned, display a message indicating no additional permissions
|
||||
if (!permissionsHTML) {
|
||||
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
|
||||
permissionsHTML = `<div class='desktop:grid-col-7'><h4 class='font-body-xs margin-y-0'>Additional permissions for this member</h4>${permissionsHTML}</div>`;
|
||||
|
||||
// Helper function for faster element refactoring
|
||||
const createPermissionItem = (label, value) => {
|
||||
return `<p class="font-body-xs text-base-darker margin-top-1 p--blockquote">${label}: <strong>${value}</strong></p>`;
|
||||
};
|
||||
const permissionsHTML = `
|
||||
<div class="desktop:grid-col-8">
|
||||
<h4 id="member-access--heading-${unique_id}" class="font-body-xs margin-y-0">
|
||||
Member access and permissions
|
||||
</h4>
|
||||
<section aria-labelledby="member-access--heading-${unique_id}" tabindex="0">
|
||||
${createPermissionItem("Member access", memberAccessValue)}
|
||||
${createPermissionItem("Domains", domainValue)}
|
||||
${createPermissionItem("Domain requests", requestValue)}
|
||||
${createPermissionItem("Members", memberValue)}
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return permissionsHTML;
|
||||
}
|
||||
|
|
|
@ -5,17 +5,26 @@
|
|||
display: inline-block;
|
||||
width: auto;
|
||||
position: relative;
|
||||
.usa-accordion__button {
|
||||
border-radius: units(.5);
|
||||
}
|
||||
.usa-accordion__button:focus {
|
||||
outline-offset: 0;
|
||||
outline-width: 3px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.usa-accordion__button[aria-expanded=true] {
|
||||
background-color: color('primary-lighter');
|
||||
}
|
||||
.usa-accordion__content {
|
||||
// Note, width is determined by a custom width class on one of the children
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
border-radius: 4px;
|
||||
border: solid 1px color('base-lighter');
|
||||
padding: units(2) units(2) units(3) units(2);
|
||||
|
@ -32,22 +41,37 @@
|
|||
}
|
||||
}
|
||||
|
||||
// This will work in responsive tables if we overwrite the overflow value on the table container
|
||||
// Works with styles in _tables
|
||||
@include at-media(desktop) {
|
||||
.usa-accordion--more-actions .usa-accordion__content {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.usa-accordion--select .usa-accordion__content {
|
||||
top: 33.88px;
|
||||
}
|
||||
|
||||
.usa-accordion--more-actions .usa-accordion__content {
|
||||
top: 30px;
|
||||
// We need to match the height of the trigger button
|
||||
// to align the 'popup' underneath
|
||||
top: 20px;
|
||||
&.top-28px {
|
||||
top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
// Special positioning for the kabob menu popup in the last row on a given page
|
||||
// This won't work on the Members table rows because that table has show-more rows
|
||||
// Currently, that's not an issue since that Members table is not wrapped in the
|
||||
// reponsive wrapper.
|
||||
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
||||
top: auto;
|
||||
bottom: -10px;
|
||||
right: 30px;
|
||||
@include at-media-max("desktop") {
|
||||
tr:last-of-type .usa-accordion--more-actions .usa-accordion__content {
|
||||
top: auto;
|
||||
bottom: -10px;
|
||||
right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
// A CSS only show-more/show-less based on usa-accordion
|
||||
|
|
|
@ -498,15 +498,36 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too
|
|||
font-size: 13px;
|
||||
}
|
||||
|
||||
.module--custom {
|
||||
a {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
border: solid 1px var(--darkened-bg);
|
||||
background: var(--darkened-bg);
|
||||
.object-tools li button, button.addlink {
|
||||
font-family: Source Sans Pro Web, Helvetica Neue, Helvetica, Roboto, Arial, sans-serif;
|
||||
text-transform: none !important;
|
||||
font-size: 14px !important;
|
||||
display: block;
|
||||
float: left;
|
||||
padding: 3px 12px;
|
||||
background: var(--object-tools-bg) !important;
|
||||
color: var(--object-tools-fg);
|
||||
font-weight: 400;
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
border-radius: 15px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
line-height: 20px;
|
||||
&:focus, &:hover{
|
||||
background: var(--object-tools-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Mimic the style for <a>
|
||||
.object-tools > p > button.addlink {
|
||||
background-image: url(../admin/img/tooltag-add.svg) !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-position: right 7px center !important;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
.usa-modal--django-admin .usa-prose ul > li {
|
||||
list-style-type: inherit;
|
||||
// Styling based off of the <p> styling in django admin
|
||||
|
@ -536,13 +557,18 @@ details.dja-detail-table {
|
|||
background-color: transparent;
|
||||
}
|
||||
|
||||
thead tr {
|
||||
background-color: var(--darkened-bg);
|
||||
}
|
||||
|
||||
td, th {
|
||||
padding-left: 12px;
|
||||
border: none
|
||||
border: none;
|
||||
background-color: var(--darkened-bg);
|
||||
color: var(--body-quiet-color);
|
||||
}
|
||||
|
||||
thead > tr > th {
|
||||
border-radius: 4px;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
@ -812,6 +838,17 @@ div.dja__model-description{
|
|||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.module caption {
|
||||
// Match the old <h2> size for django admin
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
// text-bold doesn't work here due to style overrides, unfortunately.
|
||||
// This is a workaround.
|
||||
caption.text-bold {
|
||||
font-weight: font-weight('bold');
|
||||
}
|
||||
|
||||
.wrapped-button-group {
|
||||
// This button group has too many items
|
||||
flex-wrap: wrap;
|
||||
|
@ -924,3 +961,38 @@ ul.add-list-reset {
|
|||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1080px) {
|
||||
.analytics-dashboard-charts {
|
||||
// Desktop layout - charts in top row, details in bottom row
|
||||
display: grid;
|
||||
gap: 2rem;
|
||||
// Equal columns each gets 1/2 of the space
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
grid-template-areas:
|
||||
"chart1 chart2"
|
||||
"details1 details2"
|
||||
"chart3 chart4"
|
||||
"details3 details4"
|
||||
"chart5 chart6"
|
||||
"details5 details6";
|
||||
|
||||
.chart-1 { grid-area: chart1; }
|
||||
.chart-2 { grid-area: chart2; }
|
||||
.chart-3 { grid-area: chart3; }
|
||||
.chart-4 { grid-area: chart4; }
|
||||
.chart-5 { grid-area: chart5; }
|
||||
.chart-6 { grid-area: chart6; }
|
||||
.details-1 { grid-area: details1; }
|
||||
.details-2 { grid-area: details2; }
|
||||
.details-3 { grid-area: details3; }
|
||||
.details-4 { grid-area: details4; }
|
||||
.details-5 { grid-area: details5; }
|
||||
.details-6 { grid-area: details6; }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#result_list > tbody tr > th > a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ body {
|
|||
background-color: color('gray-1');
|
||||
}
|
||||
|
||||
|
||||
.section-outlined {
|
||||
background-color: color('white');
|
||||
border: 1px solid color('base-lighter');
|
||||
|
@ -227,11 +226,6 @@ abbr[title] {
|
|||
}
|
||||
}
|
||||
|
||||
// Boost this USWDS utility class for the accordions in the portfolio requests table
|
||||
.left-auto {
|
||||
left: auto!important;
|
||||
}
|
||||
|
||||
.usa-banner__inner--widescreen {
|
||||
max-width: $widescreen-max-width;
|
||||
}
|
||||
|
@ -276,6 +270,14 @@ abbr[title] {
|
|||
width: 25%;
|
||||
}
|
||||
|
||||
.margin-top-3px {
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.top-28px {
|
||||
top: 28px;
|
||||
}
|
||||
|
||||
/*
|
||||
NOTE: width: 3% basically forces a fit-content effect in the table.
|
||||
Fit-content itself does not work.
|
||||
|
|
|
@ -41,13 +41,8 @@ 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 allows us to customize .usa-table on the user-facing pages,
|
||||
// while leaving the default styles for use on the admin pages
|
||||
.dotgov-table {
|
||||
width: 100%;
|
||||
|
||||
|
@ -68,7 +63,8 @@ th {
|
|||
border-bottom: 1px solid color('base-lighter');
|
||||
}
|
||||
|
||||
thead th {
|
||||
thead th,
|
||||
thead th[aria-sort] {
|
||||
color: color('primary-darker');
|
||||
border-bottom: 2px solid color('base-light');
|
||||
}
|
||||
|
@ -93,17 +89,46 @@ th {
|
|||
}
|
||||
}
|
||||
|
||||
@include at-media(tablet-lg) {
|
||||
th[data-sortable] .usa-table__header__button {
|
||||
right: auto;
|
||||
|
||||
&[aria-sort=ascending],
|
||||
&[aria-sort=descending],
|
||||
&:not([aria-sort]) {
|
||||
right: auto;
|
||||
// Sortable headers
|
||||
th[data-sortable][aria-sort=ascending],
|
||||
th[data-sortable][aria-sort=descending] {
|
||||
background-color: transparent;
|
||||
.usa-table__header__button {
|
||||
background-color: color('accent-cool-lightest');
|
||||
border-radius: units(.5);
|
||||
color: color('primary-darker');
|
||||
&:hover {
|
||||
background-color: color('accent-cool-lightest');
|
||||
}
|
||||
}
|
||||
}
|
||||
@include at-media(tablet-lg) {
|
||||
th[data-sortable]:not(.left-align-sort-button) .usa-table__header__button {
|
||||
// position next to the copy
|
||||
right: auto;
|
||||
// slide left to mock a margin between the copy and the icon
|
||||
transform: translateX(units(1));
|
||||
// fix vertical alignment
|
||||
top: units(1.5);
|
||||
}
|
||||
th[data-sortable].left-align-sort-button .usa-table__header__button {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Currently the 'flash' when sort is clicked,
|
||||
// this will become persistent if the double-sort bug is fixed
|
||||
td[data-sort-active],
|
||||
th[data-sort-active] {
|
||||
background-color: color('primary-lightest');
|
||||
}
|
||||
}
|
||||
|
||||
// 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--cell-padding-2 {
|
||||
|
@ -118,7 +143,7 @@ th {
|
|||
}
|
||||
|
||||
.usa-table--bg-transparent {
|
||||
td, thead th {
|
||||
td, th, thead th {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
@ -127,3 +152,12 @@ th {
|
|||
.usa-table--full-borderless th {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
// This is an override to overflow on certain tables (note the custom class)
|
||||
// so that a popup menu can appear and starddle the edge of the table on large
|
||||
// screen sizes. Works with styles in _accordions
|
||||
@include at-media(desktop) {
|
||||
.usa-table-container--scrollable.usa-table-container--override-overflow {
|
||||
overflow-y: visible;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,7 @@ in the form $setting: value,
|
|||
----------------------------*/
|
||||
$theme-font-weight-medium: 400,
|
||||
$theme-font-weight-semibold: 600,
|
||||
$theme-font-weight-bold: 700,
|
||||
|
||||
/*---------------------------
|
||||
## Font roles
|
||||
|
|
|
@ -107,6 +107,7 @@ DEBUG = env_debug
|
|||
# Controls production specific feature toggles
|
||||
IS_PRODUCTION = env_is_production
|
||||
SECRET_ENCRYPT_METADATA = secret_encrypt_metadata
|
||||
BASE_URL = env_base_url
|
||||
|
||||
# Applications are modular pieces of code.
|
||||
# They are provided by Django, by third-parties, or by yourself.
|
||||
|
@ -200,6 +201,8 @@ MIDDLEWARE = [
|
|||
"waffle.middleware.WaffleMiddleware",
|
||||
"registrar.registrar_middleware.CheckUserProfileMiddleware",
|
||||
"registrar.registrar_middleware.CheckPortfolioMiddleware",
|
||||
# Restrict access using Opt-Out approach
|
||||
"registrar.registrar_middleware.RestrictAccessMiddleware",
|
||||
]
|
||||
|
||||
# application object used by Django's built-in servers (e.g. `runserver`)
|
||||
|
|
|
@ -68,7 +68,7 @@ for step, view in [
|
|||
(PortfolioDomainRequestStep.REQUESTING_ENTITY, views.RequestingEntity),
|
||||
(PortfolioDomainRequestStep.ADDITIONAL_DETAILS, views.PortfolioAdditionalDetails),
|
||||
]:
|
||||
domain_request_urls.append(path(f"<int:id>/{step}/", view.as_view(), name=step))
|
||||
domain_request_urls.append(path(f"<int:domain_request_pk>/{step}/", view.as_view(), name=step))
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -260,27 +260,27 @@ urlpatterns = [
|
|||
name="export_data_type_user",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:id>/edit/",
|
||||
"domain-request/<int:domain_request_pk>/edit/",
|
||||
views.DomainRequestWizard.as_view(),
|
||||
name=views.DomainRequestWizard.EDIT_URL_NAME,
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>",
|
||||
"domain-request/<int:domain_request_pk>",
|
||||
views.DomainRequestStatus.as_view(),
|
||||
name="domain-request-status",
|
||||
),
|
||||
path(
|
||||
"domain-request/viewonly/<int:pk>",
|
||||
"domain-request/viewonly/<int:domain_request_pk>",
|
||||
views.PortfolioDomainRequestStatusViewOnly.as_view(),
|
||||
name="domain-request-status-viewonly",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/withdraw",
|
||||
"domain-request/<int:domain_request_pk>/withdraw",
|
||||
views.DomainRequestWithdrawConfirmation.as_view(),
|
||||
name="domain-request-withdraw-confirmation",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/withdrawconfirmed",
|
||||
"domain-request/<int:domain_request_pk>/withdrawconfirmed",
|
||||
views.DomainRequestWithdrawn.as_view(),
|
||||
name="domain-request-withdrawn",
|
||||
),
|
||||
|
@ -296,56 +296,60 @@ urlpatterns = [
|
|||
lambda r: always_404(r, "We forgot to include this link, sorry."),
|
||||
name="todo",
|
||||
),
|
||||
path("domain/<int:pk>", views.DomainView.as_view(), name="domain"),
|
||||
path("domain/<int:pk>/prototype-dns", views.PrototypeDomainDNSRecordView.as_view(), name="prototype-domain-dns"),
|
||||
path("domain/<int:pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path("domain/<int:domain_pk>", views.DomainView.as_view(), name="domain"),
|
||||
path(
|
||||
"domain/<int:pk>/dns",
|
||||
"domain/<int:domain_pk>/prototype-dns",
|
||||
views.PrototypeDomainDNSRecordView.as_view(),
|
||||
name="prototype-domain-dns",
|
||||
),
|
||||
path("domain/<int:domain_pk>/users", views.DomainUsersView.as_view(), name="domain-users"),
|
||||
path(
|
||||
"domain/<int:domain_pk>/dns",
|
||||
views.DomainDNSView.as_view(),
|
||||
name="domain-dns",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/nameservers",
|
||||
"domain/<int:domain_pk>/dns/nameservers",
|
||||
views.DomainNameserversView.as_view(),
|
||||
name="domain-dns-nameservers",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec",
|
||||
"domain/<int:domain_pk>/dns/dnssec",
|
||||
views.DomainDNSSECView.as_view(),
|
||||
name="domain-dns-dnssec",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/dns/dnssec/dsdata",
|
||||
"domain/<int:domain_pk>/dns/dnssec/dsdata",
|
||||
views.DomainDsDataView.as_view(),
|
||||
name="domain-dns-dnssec-dsdata",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/org-name-address",
|
||||
"domain/<int:domain_pk>/org-name-address",
|
||||
views.DomainOrgNameAddressView.as_view(),
|
||||
name="domain-org-name-address",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/suborganization",
|
||||
"domain/<int:domain_pk>/suborganization",
|
||||
views.DomainSubOrganizationView.as_view(),
|
||||
name="domain-suborganization",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/senior-official",
|
||||
"domain/<int:domain_pk>/senior-official",
|
||||
views.DomainSeniorOfficialView.as_view(),
|
||||
name="domain-senior-official",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/security-email",
|
||||
"domain/<int:domain_pk>/security-email",
|
||||
views.DomainSecurityEmailView.as_view(),
|
||||
name="domain-security-email",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/renewal",
|
||||
"domain/<int:domain_pk>/renewal",
|
||||
views.DomainRenewalView.as_view(),
|
||||
name="domain-renewal",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/add",
|
||||
"domain/<int:domain_pk>/users/add",
|
||||
views.DomainAddUserView.as_view(),
|
||||
name="domain-users-add",
|
||||
),
|
||||
|
@ -360,17 +364,17 @@ urlpatterns = [
|
|||
name="user-profile",
|
||||
),
|
||||
path(
|
||||
"invitation/<int:pk>/cancel",
|
||||
"invitation/<int:domain_invitation_pk>/cancel",
|
||||
views.DomainInvitationCancelView.as_view(http_method_names=["post"]),
|
||||
name="invitation-cancel",
|
||||
),
|
||||
path(
|
||||
"domain-request/<int:pk>/delete",
|
||||
"domain-request/<int:domain_request_pk>/delete",
|
||||
views.DomainRequestDeleteView.as_view(http_method_names=["post"]),
|
||||
name="domain-request-delete",
|
||||
),
|
||||
path(
|
||||
"domain/<int:pk>/users/<int:user_pk>/delete",
|
||||
"domain/<int:domain_pk>/users/<int:user_pk>/delete",
|
||||
views.DomainDeleteUserView.as_view(http_method_names=["post"]),
|
||||
name="domain-user-delete",
|
||||
),
|
||||
|
@ -392,6 +396,7 @@ urlpatterns = [
|
|||
# This way, we can share a view for djangooidc, and other pages as we see fit.
|
||||
handler500 = "registrar.views.utility.error_views.custom_500_error_view"
|
||||
handler403 = "registrar.views.utility.error_views.custom_403_error_view"
|
||||
handler404 = "registrar.views.utility.error_views.custom_404_error_view"
|
||||
|
||||
# we normally would guard these with `if settings.DEBUG` but tests run with
|
||||
# DEBUG = False even when these apps have been loaded because settings.DEBUG
|
||||
|
|
|
@ -68,19 +68,9 @@ def portfolio_permissions(request):
|
|||
"has_organization_requests_flag": False,
|
||||
"has_organization_members_flag": False,
|
||||
"is_portfolio_admin": False,
|
||||
"has_domain_renewal_flag": False,
|
||||
}
|
||||
try:
|
||||
portfolio = request.session.get("portfolio")
|
||||
|
||||
# These feature flags will display and doesn't depend on portfolio
|
||||
portfolio_context.update(
|
||||
{
|
||||
"has_organization_feature_flag": True,
|
||||
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
|
||||
}
|
||||
)
|
||||
|
||||
if portfolio:
|
||||
return {
|
||||
"has_view_portfolio_permission": request.user.has_view_portfolio_permission(portfolio),
|
||||
|
@ -95,7 +85,6 @@ def portfolio_permissions(request):
|
|||
"has_organization_requests_flag": request.user.has_organization_requests_flag(),
|
||||
"has_organization_members_flag": request.user.has_organization_members_flag(),
|
||||
"is_portfolio_admin": request.user.is_portfolio_admin(portfolio),
|
||||
"has_domain_renewal_flag": request.user.has_domain_renewal_flag(),
|
||||
}
|
||||
return portfolio_context
|
||||
|
||||
|
|
300
src/registrar/decorators.py
Normal file
300
src/registrar/decorators.py
Normal file
|
@ -0,0 +1,300 @@
|
|||
import functools
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.decorators import method_decorator
|
||||
from registrar.models import Domain, DomainInformation, DomainInvitation, DomainRequest, UserDomainRole
|
||||
|
||||
# Constants for clarity
|
||||
ALL = "all"
|
||||
IS_STAFF = "is_staff"
|
||||
IS_DOMAIN_MANAGER = "is_domain_manager"
|
||||
IS_DOMAIN_REQUEST_CREATOR = "is_domain_request_creator"
|
||||
IS_STAFF_MANAGING_DOMAIN = "is_staff_managing_domain"
|
||||
IS_PORTFOLIO_MEMBER = "is_portfolio_member"
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER = "is_portfolio_member_and_domain_manager"
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER = "is_domain_manager_and_not_portfolio_member"
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM = "has_portfolio_domains_any_perm"
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL = "has_portfolio_domains_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM = "has_portfolio_domain_requests_any_perm"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL = "has_portfolio_domain_requests_view_all"
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT = "has_portfolio_domain_requests_edit"
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM = "has_portfolio_members_any_perm"
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT = "has_portfolio_members_edit"
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW = "has_portfolio_members_view"
|
||||
|
||||
|
||||
def grant_access(*rules):
|
||||
"""
|
||||
A decorator that enforces access control based on specified rules.
|
||||
|
||||
Usage:
|
||||
- Multiple rules in a single decorator:
|
||||
@grant_access(IS_STAFF, IS_SUPERUSER, IS_DOMAIN_MANAGER)
|
||||
|
||||
- Stacked decorators for separate rules:
|
||||
@grant_access(IS_SUPERUSER)
|
||||
@grant_access(IS_DOMAIN_MANAGER)
|
||||
|
||||
The decorator supports both function-based views (FBVs) and class-based views (CBVs).
|
||||
"""
|
||||
|
||||
def decorator(view):
|
||||
if isinstance(view, type): # Check if decorating a class-based view (CBV)
|
||||
original_dispatch = view.dispatch # Store the original dispatch method
|
||||
|
||||
@method_decorator(grant_access(*rules)) # Apply the decorator to dispatch
|
||||
def wrapped_dispatch(self, request, *args, **kwargs):
|
||||
if not _user_has_permission(request.user, request, rules, **kwargs):
|
||||
raise PermissionDenied # Deny access if the user lacks permission
|
||||
return original_dispatch(self, request, *args, **kwargs)
|
||||
|
||||
view.dispatch = wrapped_dispatch # Replace the dispatch method
|
||||
return view
|
||||
|
||||
else: # If decorating a function-based view (FBV)
|
||||
view.has_explicit_access = True # Mark the view as having explicit access control
|
||||
existing_rules = getattr(view, "_access_rules", set()) # Retrieve existing rules
|
||||
existing_rules.update(rules) # Merge with new rules
|
||||
view._access_rules = existing_rules # Store updated rules
|
||||
|
||||
@functools.wraps(view)
|
||||
def wrapper(request, *args, **kwargs):
|
||||
if not _user_has_permission(request.user, request, rules, **kwargs):
|
||||
raise PermissionDenied # Deny access if the user lacks permission
|
||||
return view(request, *args, **kwargs) # Proceed with the original view
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _user_has_permission(user, request, rules, **kwargs):
|
||||
"""
|
||||
Determines if the user meets the required permission rules.
|
||||
|
||||
This function evaluates a set of predefined permission rules to check whether a user has access
|
||||
to a specific view. It supports various access control conditions, including staff status,
|
||||
domain management roles, and portfolio-related permissions.
|
||||
|
||||
Parameters:
|
||||
- user: The user requesting access.
|
||||
- request: The HTTP request object.
|
||||
- rules: A set of access control rules to evaluate.
|
||||
- **kwargs: Additional keyword arguments used in specific permission checks.
|
||||
|
||||
Returns:
|
||||
- True if the user satisfies any of the specified rules.
|
||||
- False otherwise.
|
||||
"""
|
||||
|
||||
# Skip authentication if @login_not_required is applied
|
||||
if getattr(request, "login_not_required", False):
|
||||
return True
|
||||
|
||||
# Allow everyone if `ALL` is in rules
|
||||
if ALL in rules:
|
||||
return True
|
||||
|
||||
# Ensure user is authenticated and not restricted
|
||||
if not user.is_authenticated or user.is_restricted():
|
||||
return False
|
||||
|
||||
# Define permission checks
|
||||
permission_checks = [
|
||||
(IS_STAFF, lambda: user.is_staff),
|
||||
(IS_DOMAIN_MANAGER, lambda: _is_domain_manager(user, **kwargs)),
|
||||
(IS_STAFF_MANAGING_DOMAIN, lambda: _is_staff_managing_domain(request, **kwargs)),
|
||||
(IS_PORTFOLIO_MEMBER, lambda: user.is_org_user(request)),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_VIEW_ALL,
|
||||
lambda: _has_portfolio_view_all_domains(request, kwargs.get("domain_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAINS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_domains_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
IS_PORTFOLIO_MEMBER_AND_DOMAIN_MANAGER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_MANAGER_AND_NOT_PORTFOLIO_MEMBER,
|
||||
lambda: _is_domain_manager(user, **kwargs) and not _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
IS_DOMAIN_REQUEST_CREATOR,
|
||||
lambda: _is_domain_request_creator(user, kwargs.get("domain_request_pk"))
|
||||
and not _is_portfolio_member(request),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_any_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_VIEW_ALL,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_all_domain_requests_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_DOMAIN_REQUESTS_EDIT,
|
||||
lambda: _has_portfolio_domain_requests_edit(user, request, kwargs.get("domain_request_pk")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_ANY_PERM,
|
||||
lambda: user.is_org_user(request)
|
||||
and (
|
||||
user.has_view_members_portfolio_permission(request.session.get("portfolio"))
|
||||
or user.has_edit_members_portfolio_permission(request.session.get("portfolio"))
|
||||
),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_EDIT,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_edit_members_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
(
|
||||
HAS_PORTFOLIO_MEMBERS_VIEW,
|
||||
lambda: user.is_org_user(request)
|
||||
and user.has_view_members_portfolio_permission(request.session.get("portfolio")),
|
||||
),
|
||||
]
|
||||
|
||||
# Check conditions iteratively
|
||||
return any(check() for rule, check in permission_checks if rule in rules)
|
||||
|
||||
|
||||
def _has_portfolio_domain_requests_edit(user, request, domain_request_id):
|
||||
if domain_request_id and not _is_domain_request_creator(user, domain_request_id):
|
||||
return False
|
||||
return user.is_org_user(request) and user.has_edit_request_portfolio_permission(request.session.get("portfolio"))
|
||||
|
||||
|
||||
def _is_domain_manager(user, **kwargs):
|
||||
"""
|
||||
Determines if the given user is a domain manager for a specified domain.
|
||||
|
||||
- First, it checks if 'domain_pk' is present in the URL parameters.
|
||||
- If 'domain_pk' exists, it verifies if the user has a domain role for that domain.
|
||||
- If 'domain_pk' is absent, it checks for 'domain_invitation_pk' to determine if the user
|
||||
has domain permissions through an invitation.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is a domain manager, False otherwise.
|
||||
"""
|
||||
domain_id = kwargs.get("domain_pk")
|
||||
if domain_id:
|
||||
return UserDomainRole.objects.filter(user=user, domain_id=domain_id).exists()
|
||||
domain_invitation_id = kwargs.get("domain_invitation_pk")
|
||||
if domain_invitation_id:
|
||||
return DomainInvitation.objects.filter(id=domain_invitation_id, domain__permissions__user=user).exists()
|
||||
return False
|
||||
|
||||
|
||||
def _is_domain_request_creator(user, domain_request_pk):
|
||||
"""Checks to see if the user is the creator of a domain request
|
||||
with domain_request_pk."""
|
||||
if domain_request_pk:
|
||||
return DomainRequest.objects.filter(creator=user, id=domain_request_pk).exists()
|
||||
return True
|
||||
|
||||
|
||||
def _is_portfolio_member(request):
|
||||
"""Checks to see if the user in the request is a member of the
|
||||
portfolio in the request's session."""
|
||||
return request.user.is_org_user(request)
|
||||
|
||||
|
||||
def _is_staff_managing_domain(request, **kwargs):
|
||||
"""
|
||||
Determines whether a staff user (analyst or superuser) has permission to manage a domain
|
||||
that they did not create or were not invited to.
|
||||
|
||||
The function enforces:
|
||||
1. **User Authorization** - The user must have `analyst_access_permission` or `full_access_permission`.
|
||||
2. **Valid Session Context** - The user must have explicitly selected the domain for management
|
||||
via an 'analyst action' (e.g., by clicking 'Manage Domain' in the admin interface).
|
||||
3. **Domain Status Check** - Only domains in specific statuses (e.g., APPROVED, IN_REVIEW, etc.)
|
||||
can be managed, except in cases where the domain lacks a status due to errors.
|
||||
|
||||
Process:
|
||||
- First, the function retrieves the `domain_pk` from the URL parameters.
|
||||
- If `domain_pk` is not provided, it attempts to resolve the domain via `domain_invitation_pk`.
|
||||
- It checks if the user has the required permissions.
|
||||
- It verifies that the user has an active 'analyst action' session for the domain.
|
||||
- Finally, it ensures that the domain is in a status that allows management.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is allowed to manage the domain, False otherwise.
|
||||
"""
|
||||
|
||||
domain_id = kwargs.get("domain_pk")
|
||||
if not domain_id:
|
||||
domain_invitation_id = kwargs.get("domain_invitation_pk")
|
||||
domain_invitation = DomainInvitation.objects.filter(id=domain_invitation_id).first()
|
||||
if domain_invitation:
|
||||
domain_id = domain_invitation.domain_id
|
||||
|
||||
# Check if the request user is permissioned...
|
||||
user_is_analyst_or_superuser = request.user.has_perm(
|
||||
"registrar.analyst_access_permission"
|
||||
) or request.user.has_perm("registrar.full_access_permission")
|
||||
|
||||
if not user_is_analyst_or_superuser:
|
||||
return False
|
||||
|
||||
# Check if the user is attempting a valid edit action.
|
||||
# In other words, if the analyst/admin did not click
|
||||
# the 'Manage Domain' button in /admin,
|
||||
# then they cannot access this page.
|
||||
session = request.session
|
||||
can_do_action = (
|
||||
"analyst_action" in session
|
||||
and "analyst_action_location" in session
|
||||
and session["analyst_action_location"] == domain_id
|
||||
)
|
||||
|
||||
if not can_do_action:
|
||||
return False
|
||||
|
||||
# Analysts may manage domains, when they are in these statuses:
|
||||
valid_domain_statuses = [
|
||||
DomainRequest.DomainRequestStatus.APPROVED,
|
||||
DomainRequest.DomainRequestStatus.IN_REVIEW,
|
||||
DomainRequest.DomainRequestStatus.REJECTED,
|
||||
DomainRequest.DomainRequestStatus.ACTION_NEEDED,
|
||||
# Edge case - some domains do not have
|
||||
# a status or DomainInformation... aka a status of 'None'.
|
||||
# It is necessary to access those to correct errors.
|
||||
None,
|
||||
]
|
||||
|
||||
requested_domain = DomainInformation.objects.filter(domain_id=domain_id).first()
|
||||
|
||||
# if no domain information or domain request exist, the user
|
||||
# should be able to manage the domain; however, if domain information
|
||||
# and domain request exist, and domain request is not in valid status,
|
||||
# user should not be able to manage domain
|
||||
if (
|
||||
requested_domain
|
||||
and requested_domain.domain_request
|
||||
and requested_domain.domain_request.status not in valid_domain_statuses
|
||||
):
|
||||
return False
|
||||
|
||||
# Valid session keys exist,
|
||||
# the user is permissioned,
|
||||
# and it is in a valid status
|
||||
return True
|
||||
|
||||
|
||||
def _has_portfolio_view_all_domains(request, domain_pk):
|
||||
"""Returns whether the user in the request can access the domain
|
||||
via portfolio view all domains permission."""
|
||||
portfolio = request.session.get("portfolio")
|
||||
if request.user.has_view_all_domains_portfolio_permission(portfolio):
|
||||
if Domain.objects.filter(id=domain_pk).exists():
|
||||
domain = Domain.objects.get(id=domain_pk)
|
||||
if domain.domain_info.portfolio == portfolio:
|
||||
return True
|
||||
return False
|
|
@ -171,6 +171,13 @@ class UserFixture:
|
|||
"email": "gina.summers@ecstech.com",
|
||||
"title": "Scrum Master",
|
||||
},
|
||||
{
|
||||
"username": "89f2db87-87a2-4778-a5ea-5b27b585b131",
|
||||
"first_name": "Jaxon",
|
||||
"last_name": "Silva",
|
||||
"email": "jaxon.silva@cisa.dhs.gov",
|
||||
"title": "Designer",
|
||||
},
|
||||
]
|
||||
|
||||
STAFF = [
|
||||
|
|
|
@ -13,7 +13,16 @@ from registrar.models import (
|
|||
Portfolio,
|
||||
SeniorOfficial,
|
||||
)
|
||||
from registrar.models.utility.portfolio_helper import UserPortfolioPermissionChoices, UserPortfolioRoleChoices
|
||||
from registrar.models.utility.portfolio_helper import (
|
||||
UserPortfolioPermissionChoices,
|
||||
UserPortfolioRoleChoices,
|
||||
get_domain_requests_description_display,
|
||||
get_domain_requests_display,
|
||||
get_domains_description_display,
|
||||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -126,8 +135,16 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
domain_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
(UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value, "Viewer, limited"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value, "Viewer, all"),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value,
|
||||
get_domains_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value,
|
||||
get_domains_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -139,9 +156,19 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
domain_request_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
("no_access", "No access"),
|
||||
(UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value, "Viewer"),
|
||||
(UserPortfolioPermissionChoices.EDIT_REQUESTS.value, "Creator"),
|
||||
("no_access", get_domain_requests_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None)),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value,
|
||||
get_domain_requests_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]
|
||||
),
|
||||
),
|
||||
(
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS.value,
|
||||
get_domain_requests_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.EDIT_REQUESTS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -153,8 +180,13 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
member_permissions = forms.ChoiceField(
|
||||
choices=[
|
||||
("no_access", "No access"),
|
||||
(UserPortfolioPermissionChoices.VIEW_MEMBERS.value, "Viewer"),
|
||||
("no_access", get_members_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None)),
|
||||
(
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value,
|
||||
get_members_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_MEMBERS]
|
||||
),
|
||||
),
|
||||
],
|
||||
widget=forms.RadioSelect,
|
||||
required=False,
|
||||
|
@ -191,19 +223,31 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
|
||||
# Adds a <p> description beneath each option
|
||||
self.fields["domain_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: "Can view only the domains they manage",
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: "Can view all domains for the organization",
|
||||
UserPortfolioPermissionChoices.VIEW_MANAGED_DOMAINS.value: get_domains_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None
|
||||
),
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS.value: get_domains_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS]
|
||||
),
|
||||
}
|
||||
self.fields["domain_request_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.EDIT_REQUESTS.value: (
|
||||
"Can view all domain requests for the organization and create requests"
|
||||
get_domain_requests_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.EDIT_REQUESTS]
|
||||
)
|
||||
),
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: "Can view all domain requests for the organization",
|
||||
"no_access": "Cannot view or create domain requests",
|
||||
UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS.value: (
|
||||
get_domain_requests_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS]
|
||||
)
|
||||
),
|
||||
"no_access": get_domain_requests_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
}
|
||||
self.fields["member_permissions"].descriptions = {
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: "Can view all members permissions",
|
||||
"no_access": "Cannot view member permissions",
|
||||
UserPortfolioPermissionChoices.VIEW_MEMBERS.value: get_members_description_display(
|
||||
UserPortfolioRoleChoices.ORGANIZATION_MEMBER, [UserPortfolioPermissionChoices.VIEW_MEMBERS]
|
||||
),
|
||||
"no_access": get_members_description_display(UserPortfolioRoleChoices.ORGANIZATION_MEMBER, None),
|
||||
}
|
||||
|
||||
# Map model instance values to custom form fields
|
||||
|
@ -218,6 +262,9 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
cleaned_data = super().clean()
|
||||
role = cleaned_data.get("role")
|
||||
|
||||
# handle role
|
||||
cleaned_data["roles"] = [role] if role else []
|
||||
|
||||
# Get required fields for the selected role. Then validate all required fields for the role.
|
||||
required_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||
for field_name in required_fields:
|
||||
|
@ -236,9 +283,6 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
if cleaned_data.get("member_permissions") == "no_access":
|
||||
cleaned_data["member_permissions"] = None
|
||||
|
||||
# Handle roles
|
||||
cleaned_data["roles"] = [role]
|
||||
|
||||
# Handle additional_permissions
|
||||
valid_fields = self.ROLE_REQUIRED_FIELDS.get(role, [])
|
||||
additional_permissions = {cleaned_data.get(field) for field in valid_fields if cleaned_data.get(field)}
|
||||
|
@ -338,6 +382,24 @@ class BasePortfolioMemberForm(forms.ModelForm):
|
|||
and UserPortfolioRoleChoices.ORGANIZATION_ADMIN not in new_roles
|
||||
)
|
||||
|
||||
def is_change(self) -> bool:
|
||||
"""
|
||||
Determines if the form has changed by comparing the initial data
|
||||
with the submitted cleaned data.
|
||||
|
||||
Returns:
|
||||
bool: True if the form has changed, False otherwise.
|
||||
"""
|
||||
# Compare role values
|
||||
previous_roles = set(self.initial.get("roles", []))
|
||||
new_roles = set(self.cleaned_data.get("roles", []))
|
||||
|
||||
# Compare additional permissions values
|
||||
previous_permissions = set(self.initial.get("additional_permissions") or [])
|
||||
new_permissions = set(self.cleaned_data.get("additional_permissions") or [])
|
||||
|
||||
return previous_roles != new_roles or previous_permissions != new_permissions
|
||||
|
||||
|
||||
class PortfolioMemberForm(BasePortfolioMemberForm):
|
||||
"""
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""""
|
||||
""" "
|
||||
Converts all ready and DNS needed domains with a non-default public contact
|
||||
to disclose their public contact. Created for Issue#1535 to resolve
|
||||
disclose issue of domains with missing security emails.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
"""Data migration:
|
||||
1 - generates a report of data integrity across all
|
||||
transition domain related tables
|
||||
2 - allows users to run all migration scripts for
|
||||
transition domain data
|
||||
1 - generates a report of data integrity across all
|
||||
transition domain related tables
|
||||
2 - allows users to run all migration scripts for
|
||||
transition domain data
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
""""
|
||||
""" "
|
||||
Data migration: Renaming deprecated Federal Agencies to
|
||||
their new updated names ie (U.S. Peace Corps to Peace Corps)
|
||||
within Domain Information and Domain Requests
|
||||
|
|
|
@ -2,6 +2,7 @@ from itertools import zip_longest
|
|||
import logging
|
||||
import ipaddress
|
||||
import re
|
||||
import time
|
||||
from datetime import date, timedelta
|
||||
from typing import Optional
|
||||
from django.db import transaction
|
||||
|
@ -41,7 +42,6 @@ from .utility.time_stamped_model import TimeStampedModel
|
|||
from .public_contact import PublicContact
|
||||
|
||||
from .user_domain_role import UserDomainRole
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -751,11 +751,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
|
||||
successTotalNameservers = len(oldNameservers) - deleteCount + addToDomainCount
|
||||
|
||||
try:
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
except Exception as e:
|
||||
# we don't need this part to succeed in order to continue.
|
||||
logger.error("Failed to delete nameserver hosts: %s", e)
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
|
||||
if successTotalNameservers < 2:
|
||||
try:
|
||||
|
@ -1039,13 +1035,13 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
logger.error(f"registry error removing client hold: {err}")
|
||||
raise (err)
|
||||
|
||||
def _delete_domain(self):
|
||||
def _delete_domain(self): # noqa
|
||||
"""This domain should be deleted from the registry
|
||||
may raises RegistryError, should be caught or handled correctly by caller"""
|
||||
|
||||
logger.info("Deleting subdomains for %s", self.name)
|
||||
# check if any subdomains are in use by another domain
|
||||
hosts = Host.objects.filter(name__regex=r".+{}".format(self.name))
|
||||
hosts = Host.objects.filter(name__regex=r".+\.{}".format(self.name))
|
||||
for host in hosts:
|
||||
if host.domain != self:
|
||||
logger.error("Unable to delete host: %s is in use by another domain: %s", host.name, host.domain)
|
||||
|
@ -1053,38 +1049,119 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
code=ErrorCode.OBJECT_ASSOCIATION_PROHIBITS_OPERATION,
|
||||
note=f"Host {host.name} is in use by {host.domain}",
|
||||
)
|
||||
try:
|
||||
# set hosts to empty list so nameservers are deleted
|
||||
(
|
||||
deleted_values,
|
||||
updated_values,
|
||||
new_values,
|
||||
oldNameservers,
|
||||
) = self.getNameserverChanges(hosts=[])
|
||||
|
||||
(
|
||||
deleted_values,
|
||||
updated_values,
|
||||
new_values,
|
||||
oldNameservers,
|
||||
) = self.getNameserverChanges(hosts=[])
|
||||
|
||||
_ = self._update_host_values(updated_values, oldNameservers) # returns nothing, just need to be run and errors
|
||||
addToDomainList, _ = self.createNewHostList(new_values)
|
||||
deleteHostList, _ = self.createDeleteHostList(deleted_values)
|
||||
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
|
||||
|
||||
# update the hosts
|
||||
_ = self._update_host_values(
|
||||
updated_values, oldNameservers
|
||||
) # returns nothing, just need to be run and errors
|
||||
addToDomainList, _ = self.createNewHostList(new_values)
|
||||
deleteHostList, _ = self.createDeleteHostList(deleted_values)
|
||||
responseCode = self.addAndRemoveHostsFromDomain(hostsToAdd=addToDomainList, hostsToDelete=deleteHostList)
|
||||
except RegistryError as e:
|
||||
logger.error(f"Error trying to delete hosts from domain {self}: {e}")
|
||||
raise e
|
||||
# if unable to update domain raise error and stop
|
||||
if responseCode != ErrorCode.COMMAND_COMPLETED_SUCCESSFULLY:
|
||||
raise NameserverError(code=nsErrorCodes.BAD_DATA)
|
||||
|
||||
logger.info("Finished removing nameservers from domain")
|
||||
|
||||
# addAndRemoveHostsFromDomain removes the hosts from the domain object,
|
||||
# but we still need to delete the object themselves
|
||||
self._delete_hosts_if_not_used(hostsToDelete=deleted_values)
|
||||
logger.info("Finished _delete_hosts_if_not_used inside _delete_domain()")
|
||||
|
||||
# delete the non-registrant contacts
|
||||
logger.debug("Deleting non-registrant contacts for %s", self.name)
|
||||
contacts = PublicContact.objects.filter(domain=self)
|
||||
for contact in contacts:
|
||||
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
|
||||
self._update_domain_with_contact(contact, rem=True)
|
||||
request = commands.DeleteContact(contact.registry_id)
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info(f"retrieved contacts for domain: {contacts}")
|
||||
|
||||
logger.info("Deleting domain %s", self.name)
|
||||
for contact in contacts:
|
||||
try:
|
||||
if contact.contact_type != PublicContact.ContactTypeChoices.REGISTRANT:
|
||||
logger.info(f"Deleting contact: {contact}")
|
||||
try:
|
||||
self._update_domain_with_contact(contact, rem=True)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while updating domain with contact: {contact}, e: {e}", exc_info=True)
|
||||
request = commands.DeleteContact(contact.registry_id)
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info(f"sent DeleteContact for {contact}")
|
||||
except RegistryError as e:
|
||||
logger.error(f"Error deleting contact: {contact}, {e}", exc_info=True)
|
||||
|
||||
logger.info(f"Finished deleting contacts for {self.name}")
|
||||
|
||||
# delete ds data if it exists
|
||||
if self.dnssecdata:
|
||||
logger.debug("Deleting ds data for %s", self.name)
|
||||
try:
|
||||
# set and unset client hold to be able to change ds data
|
||||
logger.info("removing client hold")
|
||||
self._remove_client_hold()
|
||||
self.dnssecdata = None
|
||||
logger.info("placing client hold")
|
||||
self._place_client_hold()
|
||||
except RegistryError as e:
|
||||
logger.error("Error deleting ds data for %s: %s", self.name, e)
|
||||
e.note = "Error deleting ds data for %s" % self.name
|
||||
raise e
|
||||
|
||||
# check if the domain can be deleted
|
||||
if not self._domain_can_be_deleted():
|
||||
note = "Domain has associated objects that prevent deletion."
|
||||
raise RegistryError(code=ErrorCode.COMMAND_FAILED, note=note)
|
||||
|
||||
# delete the domain
|
||||
request = commands.DeleteDomain(name=self.name)
|
||||
registry.send(request, cleaned=True)
|
||||
try:
|
||||
registry.send(request, cleaned=True)
|
||||
logger.info("Domain %s deleted successfully.", self.name)
|
||||
except RegistryError as e:
|
||||
logger.error("Error deleting domain %s: %s", self.name, e)
|
||||
raise e
|
||||
|
||||
def _domain_can_be_deleted(self, max_attempts=5, wait_interval=2) -> bool:
|
||||
"""
|
||||
Polls the registry using InfoDomain calls to confirm that the domain can be deleted.
|
||||
Returns True if the domain can be deleted, False otherwise. Includes a retry mechanism
|
||||
using wait_interval and max_attempts, which may be necessary if subdomains and other
|
||||
associated objects were only recently deleted as the registry may not be immediately updated.
|
||||
"""
|
||||
logger.info("Polling registry to confirm deletion pre-conditions for %s", self.name)
|
||||
last_info_error = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
info_response = registry.send(commands.InfoDomain(name=self.name), cleaned=True)
|
||||
domain_info = info_response.res_data[0]
|
||||
hosts_associated = getattr(domain_info, "hosts", None)
|
||||
if hosts_associated is None or len(hosts_associated) == 0:
|
||||
logger.info("InfoDomain reports no associated hosts for %s. Proceeding with deletion.", self.name)
|
||||
return True
|
||||
else:
|
||||
logger.info("Attempt %d: Domain %s still has hosts: %s", attempt + 1, self.name, hosts_associated)
|
||||
except RegistryError as info_e:
|
||||
# If the domain is already gone, we can assume deletion already occurred.
|
||||
if info_e.code == ErrorCode.OBJECT_DOES_NOT_EXIST:
|
||||
logger.info("InfoDomain check indicates domain %s no longer exists.", self.name)
|
||||
raise info_e
|
||||
logger.warning("Attempt %d: Error during InfoDomain check: %s", attempt + 1, info_e)
|
||||
time.sleep(wait_interval)
|
||||
else:
|
||||
logger.error(
|
||||
"Exceeded max attempts waiting for domain %s to clear associated objects; last error: %s",
|
||||
self.name,
|
||||
last_info_error,
|
||||
)
|
||||
return False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
@ -1172,7 +1249,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
"""Return the display status of the domain."""
|
||||
if self.is_expired() and (self.state != self.State.UNKNOWN):
|
||||
return "Expired"
|
||||
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
||||
elif self.is_expiring():
|
||||
return "Expiring soon"
|
||||
elif self.state == self.State.UNKNOWN or self.state == self.State.DNS_NEEDED:
|
||||
return "DNS needed"
|
||||
|
@ -1588,7 +1665,7 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
# Given expired is not a physical state, but it is displayed as such,
|
||||
# We need custom logic to determine this message.
|
||||
help_text = "This domain has expired. Complete the online renewal process to maintain access."
|
||||
elif flag_is_active(request, "domain_renewal") and self.is_expiring():
|
||||
elif self.is_expiring():
|
||||
help_text = "This domain is expiring soon. Complete the online renewal process to maintain access."
|
||||
else:
|
||||
help_text = Domain.State.get_help_text(self.state)
|
||||
|
@ -1841,8 +1918,6 @@ class Domain(TimeStampedModel, DomainHelper):
|
|||
else:
|
||||
logger.error("Error _delete_hosts_if_not_used, code was %s error was %s" % (e.code, e))
|
||||
|
||||
raise e
|
||||
|
||||
def _fix_unknown_state(self, cleaned):
|
||||
"""
|
||||
_fix_unknown_state: Calls _add_missing_contacts_if_unknown
|
||||
|
|
|
@ -9,6 +9,13 @@ from .utility.portfolio_helper import (
|
|||
UserPortfolioPermissionChoices,
|
||||
UserPortfolioRoleChoices,
|
||||
cleanup_after_portfolio_member_deletion,
|
||||
get_domain_requests_description_display,
|
||||
get_domain_requests_display,
|
||||
get_domains_description_display,
|
||||
get_domains_display,
|
||||
get_members_description_display,
|
||||
get_members_display,
|
||||
get_role_display,
|
||||
validate_portfolio_invitation,
|
||||
) # type: ignore
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
@ -85,6 +92,90 @@ class PortfolioInvitation(TimeStampedModel):
|
|||
"""
|
||||
return UserPortfolioPermission.get_portfolio_permissions(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def role_display(self):
|
||||
"""
|
||||
Returns a human-readable display name for the user's role.
|
||||
|
||||
Uses the `get_role_display` function to determine if the user is an "Admin",
|
||||
"Basic" member, or has no role assigned.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's role.
|
||||
"""
|
||||
return get_role_display(self.roles)
|
||||
|
||||
@property
|
||||
def domains_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's domain access level.
|
||||
|
||||
Uses the `get_domains_display` function to determine whether the user has
|
||||
"Viewer" access (can view all domains) or "Viewer, limited" access.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions.
|
||||
"""
|
||||
return get_domains_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domains_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's domain access level.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions description.
|
||||
"""
|
||||
return get_domains_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's access to domain requests.
|
||||
|
||||
Uses the `get_domain_requests_display` function to determine if the user
|
||||
is a "Creator" (can create and edit requests), a "Viewer" (can only view requests),
|
||||
or has "No access" to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions.
|
||||
"""
|
||||
return get_domain_requests_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions description.
|
||||
"""
|
||||
return get_domain_requests_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's access to managing members.
|
||||
|
||||
Uses the `get_members_display` function to determine if the user is a
|
||||
"Manager" (can edit members), a "Viewer" (can view members), or has "No access"
|
||||
to member management.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions.
|
||||
"""
|
||||
return get_members_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to managing members.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions description.
|
||||
"""
|
||||
return get_members_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@transition(field="status", source=PortfolioInvitationStatus.INVITED, target=PortfolioInvitationStatus.RETRIEVED)
|
||||
def retrieve(self):
|
||||
"""When an invitation is retrieved, create the corresponding permission.
|
||||
|
|
|
@ -269,10 +269,7 @@ class User(AbstractUser):
|
|||
return self._has_portfolio_permission(portfolio, UserPortfolioPermissionChoices.EDIT_REQUESTS)
|
||||
|
||||
def is_portfolio_admin(self, portfolio):
|
||||
return "Admin" in self.portfolio_role_summary(portfolio)
|
||||
|
||||
def has_domain_renewal_flag(self):
|
||||
return flag_is_active_for_user(self, "domain_renewal")
|
||||
return self.has_edit_portfolio_permission(portfolio)
|
||||
|
||||
def get_first_portfolio(self):
|
||||
permission = self.portfolio_permissions.first()
|
||||
|
@ -280,49 +277,6 @@ class User(AbstractUser):
|
|||
return permission.portfolio
|
||||
return None
|
||||
|
||||
def portfolio_role_summary(self, portfolio):
|
||||
"""Returns a list of roles based on the user's permissions."""
|
||||
roles = []
|
||||
|
||||
# Define the conditions and their corresponding roles
|
||||
conditions_roles = [
|
||||
(self.has_edit_portfolio_permission(portfolio), ["Admin"]),
|
||||
(
|
||||
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and self.has_any_requests_portfolio_permission(portfolio)
|
||||
and self.has_edit_request_portfolio_permission(portfolio),
|
||||
["View-only admin", "Domain requestor"],
|
||||
),
|
||||
(
|
||||
self.has_view_all_domains_portfolio_permission(portfolio)
|
||||
and self.has_any_requests_portfolio_permission(portfolio),
|
||||
["View-only admin"],
|
||||
),
|
||||
(
|
||||
self.has_view_portfolio_permission(portfolio)
|
||||
and self.has_edit_request_portfolio_permission(portfolio)
|
||||
and self.has_any_domains_portfolio_permission(portfolio),
|
||||
["Domain requestor", "Domain manager"],
|
||||
),
|
||||
(
|
||||
self.has_view_portfolio_permission(portfolio) and self.has_edit_request_portfolio_permission(portfolio),
|
||||
["Domain requestor"],
|
||||
),
|
||||
(
|
||||
self.has_view_portfolio_permission(portfolio) and self.has_any_domains_portfolio_permission(portfolio),
|
||||
["Domain manager"],
|
||||
),
|
||||
(self.has_view_portfolio_permission(portfolio), ["Member"]),
|
||||
]
|
||||
|
||||
# Evaluate conditions and add roles
|
||||
for condition, role_list in conditions_roles:
|
||||
if condition:
|
||||
roles.extend(role_list)
|
||||
break
|
||||
|
||||
return roles
|
||||
|
||||
def get_portfolios(self):
|
||||
return self.portfolio_permissions.all()
|
||||
|
||||
|
|
|
@ -6,6 +6,13 @@ from registrar.models.utility.portfolio_helper import (
|
|||
DomainRequestPermissionDisplay,
|
||||
MemberPermissionDisplay,
|
||||
cleanup_after_portfolio_member_deletion,
|
||||
get_domain_requests_display,
|
||||
get_domain_requests_description_display,
|
||||
get_domains_display,
|
||||
get_domains_description_display,
|
||||
get_members_display,
|
||||
get_members_description_display,
|
||||
get_role_display,
|
||||
validate_user_portfolio_permission,
|
||||
)
|
||||
from .utility.time_stamped_model import TimeStampedModel
|
||||
|
@ -181,6 +188,90 @@ class UserPortfolioPermission(TimeStampedModel):
|
|||
# This is the same as portfolio_permissions & common_forbidden_perms.
|
||||
return portfolio_permissions.intersection(common_forbidden_perms)
|
||||
|
||||
@property
|
||||
def role_display(self):
|
||||
"""
|
||||
Returns a human-readable display name for the user's role.
|
||||
|
||||
Uses the `get_role_display` function to determine if the user is an "Admin",
|
||||
"Basic" member, or has no role assigned.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's role.
|
||||
"""
|
||||
return get_role_display(self.roles)
|
||||
|
||||
@property
|
||||
def domains_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's domain access level.
|
||||
|
||||
Uses the `get_domains_display` function to determine whether the user has
|
||||
"Viewer" access (can view all domains) or "Viewer, limited" access.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions.
|
||||
"""
|
||||
return get_domains_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domains_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's domain access level.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain permissions description.
|
||||
"""
|
||||
return get_domains_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's access to domain requests.
|
||||
|
||||
Uses the `get_domain_requests_display` function to determine if the user
|
||||
is a "Creator" (can create and edit requests), a "Viewer" (can only view requests),
|
||||
or has "No access" to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions.
|
||||
"""
|
||||
return get_domain_requests_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def domain_requests_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to domain requests.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's domain request permissions description.
|
||||
"""
|
||||
return get_domain_requests_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_display(self):
|
||||
"""
|
||||
Returns a string representation of the user's access to managing members.
|
||||
|
||||
Uses the `get_members_display` function to determine if the user is a
|
||||
"Manager" (can edit members), a "Viewer" (can view members), or has "No access"
|
||||
to member management.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions.
|
||||
"""
|
||||
return get_members_display(self.roles, self.additional_permissions)
|
||||
|
||||
@property
|
||||
def members_description_display(self):
|
||||
"""
|
||||
Returns a string description of the user's access to managing members.
|
||||
|
||||
Returns:
|
||||
str: The display name of the user's member management permissions description.
|
||||
"""
|
||||
return get_members_description_display(self.roles, self.additional_permissions)
|
||||
|
||||
def clean(self):
|
||||
"""Extends clean method to perform additional validation, which can raise errors in django admin."""
|
||||
super().clean()
|
||||
|
|
|
@ -79,6 +79,161 @@ class MemberPermissionDisplay(StrEnum):
|
|||
NONE = "None"
|
||||
|
||||
|
||||
def get_role_display(roles):
|
||||
"""
|
||||
Returns a user-friendly display name for a given list of user roles.
|
||||
|
||||
- If the user has the ORGANIZATION_ADMIN role, return "Admin".
|
||||
- If the user has the ORGANIZATION_MEMBER role, return "Basic".
|
||||
- If the user has neither role, return "-".
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: The display name for the highest applicable role.
|
||||
"""
|
||||
if UserPortfolioRoleChoices.ORGANIZATION_ADMIN in roles:
|
||||
return "Admin"
|
||||
elif UserPortfolioRoleChoices.ORGANIZATION_MEMBER in roles:
|
||||
return "Basic"
|
||||
else:
|
||||
return "-"
|
||||
|
||||
|
||||
def get_domains_display(roles, permissions):
|
||||
"""
|
||||
Determines the display name for a user's domain viewing permissions.
|
||||
|
||||
- If the user has the VIEW_ALL_DOMAINS permission, return "Viewer".
|
||||
- Otherwise, return "Viewer, limited".
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain viewing access.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions:
|
||||
return "Viewer"
|
||||
else:
|
||||
return "Viewer, limited"
|
||||
|
||||
|
||||
def get_domains_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's domain viewing permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain viewing access description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.VIEW_ALL_DOMAINS in all_permissions:
|
||||
return "Can view all domains for the organization"
|
||||
else:
|
||||
return "Can view only the domains they manage"
|
||||
|
||||
|
||||
def get_domain_requests_display(roles, permissions):
|
||||
"""
|
||||
Determines the display name for a user's domain request permissions.
|
||||
|
||||
- If the user has the EDIT_REQUESTS permission, return "Creator".
|
||||
- If the user has the VIEW_ALL_REQUESTS permission, return "Viewer".
|
||||
- Otherwise, return "No access".
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain request access level.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions:
|
||||
return "Creator"
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
|
||||
return "Viewer"
|
||||
else:
|
||||
return "No access"
|
||||
|
||||
|
||||
def get_domain_requests_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's domain request permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's domain request access level description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_REQUESTS in all_permissions:
|
||||
return "Can view all domain requests for the organization and create requests"
|
||||
elif UserPortfolioPermissionChoices.VIEW_ALL_REQUESTS in all_permissions:
|
||||
return "Can view all domain requests for the organization"
|
||||
else:
|
||||
return "Cannot view or create domain requests"
|
||||
|
||||
|
||||
def get_members_display(roles, permissions):
|
||||
"""
|
||||
Determines the display name for a user's member management permissions.
|
||||
|
||||
- If the user has the EDIT_MEMBERS permission, return "Manager".
|
||||
- If the user has the VIEW_MEMBERS permission, return "Viewer".
|
||||
- Otherwise, return "No access".
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's member management access level.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions:
|
||||
return "Manager"
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
|
||||
return "Viewer"
|
||||
else:
|
||||
return "No access"
|
||||
|
||||
|
||||
def get_members_description_display(roles, permissions):
|
||||
"""
|
||||
Determines the display description for a user's member management permissions.
|
||||
|
||||
Args:
|
||||
roles (list): A list of role strings assigned to the user.
|
||||
permissions (list): A list of additional permissions assigned to the user.
|
||||
|
||||
Returns:
|
||||
str: A string representing the user's member management access level description.
|
||||
"""
|
||||
UserPortfolioPermission = apps.get_model("registrar.UserPortfolioPermission")
|
||||
all_permissions = UserPortfolioPermission.get_portfolio_permissions(roles, permissions)
|
||||
if UserPortfolioPermissionChoices.EDIT_MEMBERS in all_permissions:
|
||||
return "Can view and manage all member permissions"
|
||||
elif UserPortfolioPermissionChoices.VIEW_MEMBERS in all_permissions:
|
||||
return "Can view all member permissions"
|
||||
else:
|
||||
return "Cannot view member permissions"
|
||||
|
||||
|
||||
def validate_user_portfolio_permission(user_portfolio_permission):
|
||||
"""
|
||||
Validates a UserPortfolioPermission instance. Located in portfolio_helper to avoid circular imports
|
||||
|
|
|
@ -3,9 +3,13 @@ Contains middleware used in settings.py
|
|||
"""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from urllib.parse import parse_qs
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.urls import reverse
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.urls import resolve
|
||||
from registrar.models import User
|
||||
from waffle.decorators import flag_is_active
|
||||
|
||||
|
@ -170,3 +174,51 @@ class CheckPortfolioMiddleware:
|
|||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
else:
|
||||
request.session["portfolio"] = request.user.get_first_portfolio()
|
||||
|
||||
|
||||
class RestrictAccessMiddleware:
|
||||
"""
|
||||
Middleware that blocks access to all views unless explicitly permitted.
|
||||
|
||||
This middleware enforces authentication by default. Views must explicitly allow access
|
||||
using access control mechanisms such as the `@grant_access` decorator. Exceptions are made
|
||||
for Django admin views, explicitly ignored paths, and views that opt out of login requirements.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
# Compile regex patterns from settings to identify paths that bypass login requirements
|
||||
self.ignored_paths = [re.compile(pattern) for pattern in getattr(settings, "LOGIN_REQUIRED_IGNORE_PATHS", [])]
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
# Allow requests to Django Debug Toolbar
|
||||
if request.path.startswith("/__debug__/"):
|
||||
return self.get_response(request)
|
||||
|
||||
# Allow requests matching configured ignored paths
|
||||
if any(pattern.match(request.path) for pattern in self.ignored_paths):
|
||||
return self.get_response(request)
|
||||
|
||||
# Attempt to resolve the request path to a view function
|
||||
try:
|
||||
resolver_match = resolve(request.path_info)
|
||||
view_func = resolver_match.func
|
||||
app_name = resolver_match.app_name # Get the app name of the resolved view
|
||||
except Exception:
|
||||
# If resolution fails, allow the request to proceed (avoid blocking non-view routes)
|
||||
return self.get_response(request)
|
||||
|
||||
# Automatically allow access to Django's built-in admin views (excluding custom /admin/* views)
|
||||
if app_name == "admin":
|
||||
return self.get_response(request)
|
||||
|
||||
# Allow access if the view explicitly opts out of login requirements
|
||||
if getattr(view_func, "login_required", True) is False:
|
||||
return self.get_response(request)
|
||||
|
||||
# Restrict access to views that do not explicitly declare access rules
|
||||
if not getattr(view_func, "has_explicit_access", False):
|
||||
raise PermissionDenied # Deny access if the view lacks explicit permission handling
|
||||
|
||||
return self.get_response(request)
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}
|
||||
Registrar Analytics | Django admin
|
||||
{% endblock %}
|
||||
|
||||
{% block content_title %}<h1>Registrar Analytics</h1>{% endblock %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
|
@ -18,7 +22,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
|||
|
||||
{% block content %}
|
||||
|
||||
<div id="content-main" class="custom-admin-template">
|
||||
<div id="content-main" class="custom-admin-template analytics-dashboard">
|
||||
|
||||
<div class="grid-row grid-gap-2">
|
||||
<div class="tablet:grid-col-6 margin-top-2">
|
||||
|
@ -95,7 +99,7 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
|||
<input type="date" id="end" name="end" value="2023-12-01" min="2023-12-01" />
|
||||
</div>
|
||||
</div>
|
||||
<ul class="usa-button-group">
|
||||
<ul class="usa-button-group flex-wrap">
|
||||
<li class="usa-button-group__item">
|
||||
<button class="usa-button usa-button--dja exportLink" data-export-url="{% url 'export_domains_growth' %}" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
|
@ -133,80 +137,127 @@ https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="grid-row grid-gap-2 margin-y-2">
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart1" width="400" height="200"
|
||||
aria-label="Chart: {{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.managed_domains_sliced_at_start_date}}"
|
||||
data-list-two="{{data.managed_domains_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: Managed domains</h2>
|
||||
<p>{{ data.managed_domains_sliced_at_end_date.0 }} managed domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart2" width="400" height="200"
|
||||
aria-label="Chart: {{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.unmanaged_domains_sliced_at_start_date}}"
|
||||
data-list-two="{{data.unmanaged_domains_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: Unmanaged domains</h2>
|
||||
<p>{{ data.unmanaged_domains_sliced_at_end_date.0 }} unmanaged domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="analytics-dashboard-charts margin-top-2">
|
||||
{% comment %} Managed/Unmanaged domains {% endcomment %}
|
||||
<div class="chart-1 grid-col">
|
||||
<canvas id="managed-domains-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.managed_domains.start_date_count }}"
|
||||
data-list-two="{{ data.managed_domains.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: Managed domains</h2>
|
||||
<p>{{ data.managed_domains.end_date_count.0 }} managed domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-1 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for managed domains</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="managed_domains" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="chart-2 grid-col">
|
||||
<canvas id="unmanaged-domains-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.unmanaged_domains.start_date_count }}"
|
||||
data-list-two="{{ data.unmanaged_domains.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: Unmanaged domains</h2>
|
||||
<p>{{ data.unmanaged_domains.end_date_count.0 }} unmanaged domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-2 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for unmanaged domains</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="unmanaged_domains" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="grid-row grid-gap-2 margin-y-2">
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart3" width="400" height="200"
|
||||
aria-label="Chart: {{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.deleted_domains_sliced_at_start_date}}"
|
||||
data-list-two="{{data.deleted_domains_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: Deleted domains</h2>
|
||||
<p>{{ data.deleted_domains_sliced_at_end_date.0 }} deleted domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart4" width="400" height="200"
|
||||
aria-label="Chart: {{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.ready_domains_sliced_at_start_date}}"
|
||||
data-list-two="{{data.ready_domains_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: Ready domains</h2>
|
||||
<p>{{ data.ready_domains_sliced_at_end_date.0 }} ready domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} Deleted/Ready domains {% endcomment %}
|
||||
<div class="chart-3 grid-col">
|
||||
<canvas id="deleted-domains-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.deleted_domains.start_date_count }}"
|
||||
data-list-two="{{ data.deleted_domains.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: Deleted domains</h2>
|
||||
<p>{{ data.deleted_domains.end_date_count.0 }} deleted domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-3 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for deleted domains</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="deleted_domains" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="chart-4 grid-col">
|
||||
<canvas id="ready-domains-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.ready_domains.start_date_count }}"
|
||||
data-list-two="{{ data.ready_domains.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: Ready domains</h2>
|
||||
<p>{{ data.ready_domains.end_date_count.0 }} ready domains for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-4 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for ready domains</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="ready_domains" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="grid-row grid-gap-2 margin-y-2">
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart5" width="400" height="200"
|
||||
aria-label="Chart: {{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.submitted_requests_sliced_at_start_date}}"
|
||||
data-list-two="{{data.submitted_requests_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: Submitted requests</h2>
|
||||
<p>{{ data.submitted_requests_sliced_at_end_date.0 }} submitted requests for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="grid-col">
|
||||
<canvas id="myChart6" width="400" height="200"
|
||||
aria-label="Chart: {{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{data.requests_sliced_at_start_date}}"
|
||||
data-list-two="{{data.requests_sliced_at_end_date}}"
|
||||
>
|
||||
<h2>Chart: All requests</h2>
|
||||
<p>{{ data.requests_sliced_at_end_date.0 }} requests for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
</div>
|
||||
{% comment %} Requests {% endcomment %}
|
||||
<div class="chart-5 grid-col">
|
||||
<canvas id="submitted-requests-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.submitted_requests.start_date_count }}"
|
||||
data-list-two="{{ data.submitted_requests.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: Submitted requests</h2>
|
||||
<p>{{ data.submitted_requests.end_date_count.0 }} submitted requests for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-5 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for submitted requests</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="submitted_requests" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="chart-6 grid-col">
|
||||
<canvas id="all-requests-chart" width="400" height="200"
|
||||
aria-label="Chart: {{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}"
|
||||
role="img"
|
||||
data-list-one="{{ data.requests.start_date_count }}"
|
||||
data-list-two="{{ data.requests.end_date_count }}"
|
||||
>
|
||||
<h2>Chart: All requests</h2>
|
||||
<p>{{ data.requests.end_date_count.0 }} requests for {{ data.end_date }}</p>
|
||||
</canvas>
|
||||
</div>
|
||||
<div class="details-6 grid-col margin-bottom-2">
|
||||
<details class="dja-detail-table" aria-role="button" closed>
|
||||
<summary class="dja-details-summary">Details for all requests</summary>
|
||||
<div class="grid-container margin-left-0 padding-left-0 padding-right-0 dja-details-contents">
|
||||
{% include "admin/analytics_graph_table.html" with data=data property_name="requests" %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
26
src/registrar/templates/admin/analytics_graph_table.html
Normal file
26
src/registrar/templates/admin/analytics_graph_table.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
<table class="usa-table usa-table--borderless">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Start date {{ data.start_date }}</th>
|
||||
<th scope="col">End date {{ data.end_date }} </th>
|
||||
<tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% comment %}
|
||||
This ugly notation is equivalent to data.property_name.start_date_count.index.
|
||||
Or represented in the pure python way: data[property_name]["start_date_count"][index]
|
||||
{% endcomment %}
|
||||
{% with start_counts=data|get_item:property_name|get_item:"start_date_count" end_counts=data|get_item:property_name|get_item:"end_date_count" %}
|
||||
{% for org_count_type in data.org_count_types %}
|
||||
{% with index=forloop.counter %}
|
||||
<tr>
|
||||
<th class="padding-left-1" scope="row">{{ org_count_type }}</th>
|
||||
<td class="padding-left-1">{{ start_counts|slice:index|last }}</td>
|
||||
<td class="padding-left-1">{{ end_counts|slice:index|last }}</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
</tbody>
|
||||
</table>
|
|
@ -4,24 +4,22 @@
|
|||
{% for app in app_list %}
|
||||
<div class="app-{{ app.app_label }} module{% if app.app_url in request.path|urlencode %} current-app{% endif %}">
|
||||
<table>
|
||||
|
||||
{# .gov override: add headers #}
|
||||
{% if show_changelinks %}
|
||||
<colgroup span="3"></colgroup>
|
||||
{% else %}
|
||||
<colgroup span="2"></colgroup>
|
||||
{% endif %}
|
||||
{# .gov override: display the app name as a caption rather than a table header #}
|
||||
<caption class="text-bold">{{ app.name }}</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
{% if show_changelinks %}
|
||||
<th colspan="3" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% else %}
|
||||
<th colspan="2" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% endif %}
|
||||
{# .gov override: hide headers #}
|
||||
{% comment %}
|
||||
{% if show_changelinks %}
|
||||
<th colspan="3" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% else %}
|
||||
<th colspan="2" class="primary-th" scope="colgroup">
|
||||
{{ app.name }}
|
||||
</th>
|
||||
{% endif %}
|
||||
{% endcomment %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="col">Model</th>
|
||||
|
@ -45,16 +43,17 @@
|
|||
{% endif %}
|
||||
|
||||
{% if model.add_url %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink">{% translate 'Add' %}</a></td>
|
||||
{% comment %} Remove the 's' from the end of the string to avoid text like "Add domain requests" {% endcomment %}
|
||||
<td><a href="{{ model.add_url }}" class="addlink" aria-label="Add {{ model.name|slice:":-1" }}">{% translate 'Add' %}</a></td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
{% if model.admin_url and show_changelinks %}
|
||||
{% if model.view_only %}
|
||||
<td><a href="{{ model.admin_url }}" class="viewlink">{% translate 'View' %}</a></td>
|
||||
<td><a href="{{ model.admin_url }}" class="viewlink" aria-label="View {{ model.name }}">{% translate 'View' %}</a></td>
|
||||
{% else %}
|
||||
<td><a href="{{ model.admin_url }}" class="changelink">{% translate 'Change' %}</a></td>
|
||||
<td><a href="{{ model.admin_url }}" class="changelink" aria-label="Change {{ model.name }}">{% translate 'Change' %}</a></td>
|
||||
{% endif %}
|
||||
{% elif show_changelinks %}
|
||||
<td></td>
|
||||
|
@ -64,9 +63,20 @@
|
|||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="module module--custom">
|
||||
<h2>Analytics</h2>
|
||||
<a class="display-block padding-y-1 padding-x-1" href="{% url 'analytics' %}">Dashboard</a>
|
||||
<div class="module">
|
||||
<table class="width-full">
|
||||
<caption class="text-bold">Analytics</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Reports</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row"><a href="{% url 'analytics' %}">Dashboard</a></th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>{% translate 'You don’t have permission to view or edit anything.' %}</p>
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
<script src="{% static 'js/uswds.min.js' %}" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script type="application/javascript" src="{% static 'js/getgov-admin.min.js' %}" defer></script>
|
||||
<script type="application/javascript" src="{% static 'js/get-gov-reports.js' %}" defer></script>
|
||||
<script type="application/javascript" src="{% static 'js/dja-collapse.js' %}" defer></script>
|
||||
{% endblock %}
|
||||
|
||||
|
@ -34,8 +33,8 @@
|
|||
{{ tabtitle }} |
|
||||
{% else %}
|
||||
{{ title }} |
|
||||
{% endif %}
|
||||
{{ site_title|default:_('Django site admin') }}
|
||||
{% endif %}
|
||||
Django admin
|
||||
{% endblock %}
|
||||
|
||||
{% block extrastyle %}{{ block.super }}
|
||||
|
@ -49,6 +48,10 @@
|
|||
{% endwith %}
|
||||
{% endif %}
|
||||
|
||||
{% if opts.model_name %}
|
||||
<a class="usa-skipnav" href="#changelist-filter" aria-label="Skip to the filters section">Skip to filters</a>
|
||||
{% endif %}
|
||||
|
||||
{# Djando update: this div will change to header #}
|
||||
<div id="header">
|
||||
<div id="branding">
|
||||
|
|
|
@ -7,29 +7,43 @@
|
|||
{% if has_absolute_url %}
|
||||
<ul>
|
||||
<li>
|
||||
<a href="{% add_preserved_filters history_url %}" class="historylink">{% translate "History" %}</a>
|
||||
<button data-href="{% add_preserved_filters history_url %}" class="historylink use-button-as-link">{% translate "History" %}</button>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ absolute_url }}" class="viewsitelink">{% translate "View on site" %}</a>
|
||||
<button data-href="{{ absolute_url }}" class="viewsitelink use-button-as-link">{% translate "View on site" %}</button>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% if opts.model_name == 'domaininvitation' %}
|
||||
{% if invitation.status == invitation.DomainInvitationStatus.INVITED %}
|
||||
<li>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="cancel_invitation" value="true">
|
||||
<button type="submit" class="usa-button--dja">
|
||||
Cancel invitation
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<li>
|
||||
<a href="{% add_preserved_filters history_url %}">{% translate "History" %}</a>
|
||||
<button data-href="{% add_preserved_filters history_url %}" class="historylink use-button-as-link">{% translate "History" %}</button>
|
||||
</li>
|
||||
|
||||
{% if opts.model_name == 'domainrequest' %}
|
||||
<li>
|
||||
<a id="id-copy-to-clipboard-summary" class="usa-button--dja" type="button" href="#">
|
||||
<svg class="usa-icon" >
|
||||
<button id="id-copy-to-clipboard-summary" class="usa-button--dja">
|
||||
<svg class="usa-icon">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#content_copy"></use>
|
||||
</svg>
|
||||
<!-- the span is targeted in JS, do not remove -->
|
||||
<span>{% translate "Copy request summary" %}</span>
|
||||
</a>
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends "admin/change_list.html" %}
|
||||
{% load i18n admin_urls static admin_list %}
|
||||
|
||||
{% block content_title %}
|
||||
<h1>{{ title }}</h1>
|
||||
|
@ -37,6 +38,7 @@
|
|||
for {{ search_query }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% comment %} Replace the Django ul markup with a div. We'll replace the li with a p in change_list_object_tools {% endcomment %}
|
||||
|
@ -46,4 +48,25 @@
|
|||
{{ block.super }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% comment %} Replace the Django header markup for clearing all filters with a div. {% endcomment %}
|
||||
{% block filters %}
|
||||
{% if cl.has_filters %}
|
||||
<nav id="changelist-filter" aria-labelledby="changelist-filter-header">
|
||||
<h2 id="changelist-filter-header">{% translate 'Filter' %}</h2>
|
||||
{% if cl.is_facets_optional or cl.has_active_filters %}<div id="changelist-filter-extra-actions">
|
||||
{% if cl.is_facets_optional %}<h3>
|
||||
{% if cl.add_facets %}<a href="{{ cl.remove_facet_link }}" class="hidelink">{% translate "Hide counts" %}</a>
|
||||
{% else %}<a href="{{ cl.add_facet_link }}" class="viewlink">{% translate "Show counts" %}</a>{% endif %}
|
||||
</h3>{% endif %}
|
||||
{% if cl.has_active_filters %}<div class="margin-2">
|
||||
<a href="{{ cl.clear_all_filters_qs }}" role="link">✖ {% translate "Clear all filters" %}</a>
|
||||
</div>{% endif %}
|
||||
</div>{% endif %}
|
||||
{% for spec in cl.filter_specs %}{% admin_list_filter cl spec %}{% endfor %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
{% if has_add_permission %}
|
||||
<p class="margin-0 padding-0">
|
||||
{% url cl.opts|admin_urlname:'add' as add_url %}
|
||||
<a href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink">
|
||||
<button data-href="{% add_preserved_filters add_url is_popup to_field %}" class="addlink use-button-as-link">
|
||||
{% blocktranslate with cl.opts.verbose_name as name %}Add {{ name }}{% endblocktranslate %}
|
||||
</a>
|
||||
</button>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -19,11 +19,11 @@ Load our custom filters to extract info from the django generated markup.
|
|||
|
||||
{% if results.0|contains_checkbox %}
|
||||
{# .gov - hardcode the select all checkbox #}
|
||||
<th scope="col" class="action-checkbox-column" title="Toggle all">
|
||||
<th scope="col" class="action-checkbox-column" title="Toggle">
|
||||
<div class="text">
|
||||
<span>
|
||||
<input type="checkbox" id="action-toggle">
|
||||
<label for="action-toggle" class="usa-sr-only">Toggle all</label>
|
||||
<input type="checkbox" id="action-toggle">
|
||||
</span>
|
||||
</div>
|
||||
<div class="clear"></div>
|
||||
|
@ -34,9 +34,9 @@ Load our custom filters to extract info from the django generated markup.
|
|||
{% if header.sortable %}
|
||||
{% if header.sort_priority > 0 %}
|
||||
<div class="sortoptions">
|
||||
<a class="sortremove" href="{{ header.url_remove }}" title="{% translate "Remove from sorting" %}"></a>
|
||||
<a class="sortremove" href="{{ header.url_remove }}" aria-label="{{ header.text }}" title="{% translate "Remove from sorting" %}"></a>
|
||||
{% if num_sorted_fields > 1 %}<span class="sortpriority" title="{% blocktranslate with priority_number=header.sort_priority %}Sorting priority: {{ priority_number }}{% endblocktranslate %}">{{ header.sort_priority }}</span>{% endif %}
|
||||
<a href="{{ header.url_toggle }}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
|
||||
<a href="{{ header.url_toggle }}" aria-label="{{ header.text }} sorting {% if header.ascending %}ascending{% else %}descending{% endif %}" class="toggle {% if header.ascending %}ascending{% else %}descending{% endif %}" title="{% translate "Toggle sorting" %}"></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
@ -61,10 +61,10 @@ Load our custom filters to extract info from the django generated markup.
|
|||
{% endif %}
|
||||
<tr>
|
||||
{% with result_value=result.0|extract_value %}
|
||||
{% with result_label=result.1|extract_a_text %}
|
||||
{% with result_label=result.1|extract_a_text checkbox_id="select-"|add:result_value %}
|
||||
<td>
|
||||
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}" class="action-select">
|
||||
<label class="usa-sr-only" for="{{ result_value|default:'value' }}-{{ result_label|default:'label' }}">{{ result_label|default:'label' }}</label>
|
||||
<label class="usa-sr-only" for="{{ checkbox_id }}">Select row {{ result_label|default:'label' }}</label>
|
||||
<input type="checkbox" name="_selected_action" value="{{ result_value|default:'value' }}" id="{{ checkbox_id }}" class="action-select">
|
||||
</td>
|
||||
{% endwith %}
|
||||
{% endwith %}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
{% load i18n %}
|
||||
{% load admin_urls %}
|
||||
|
||||
{% if has_export_permission %}
|
||||
{% comment %} Uses the initButtonLinks {% endcomment %}
|
||||
<li><button class="export_link use-button-as-link" data-href="{% url opts|admin_urlname:"export" %}">{% trans "Export" %}</button></li>
|
||||
{% endif %}
|
|
@ -3,6 +3,6 @@
|
|||
|
||||
{% if has_import_permission %}
|
||||
{% if not IS_PRODUCTION %}
|
||||
<li><a href='{% url opts|admin_urlname:"import" %}' class="import_link">{% trans "Import" %}</a></li>
|
||||
<li><button class="import_link use-button-as-link" data-href="{% url opts|admin_urlname:"import" %}">{% trans "Import" %}</button></li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
|
26
src/registrar/templates/admin/search_form.html
Normal file
26
src/registrar/templates/admin/search_form.html
Normal file
|
@ -0,0 +1,26 @@
|
|||
{% comment %} This is an override of the django search bar to add better accessibility compliance.
|
||||
There are no blocks defined here, so we had to copy the code.
|
||||
https://github.com/django/django/blob/main/django/contrib/admin/templates/admin/search_form.html
|
||||
{% endcomment %}
|
||||
{% load i18n static %}
|
||||
{% if cl.search_fields %}
|
||||
<div id="toolbar"><form id="changelist-search" method="get" role="search">
|
||||
<div><!-- DIV needed for valid HTML -->
|
||||
{% comment %} .gov override - removed for="searchbar" {% endcomment %}
|
||||
<label><img src="{% static "admin/img/search.svg" %}" alt="Search"></label>
|
||||
<input type="text" size="40" name="{{ search_var }}" value="{{ cl.query }}" id="searchbar"{% if cl.search_help_text %} aria-describedby="searchbar_helptext"{% endif %}>
|
||||
<input type="submit" value="{% translate 'Search' %}">
|
||||
{% if show_result_count %}
|
||||
<span class="small quiet">{% blocktranslate count counter=cl.result_count %}{{ counter }} result{% plural %}{{ counter }} results{% endblocktranslate %} (<a href="?{% if cl.is_popup %}{{ is_popup_var }}=1{% if cl.add_facets %}&{% endif %}{% endif %}{% if cl.add_facets %}{{ is_facets_var }}{% endif %}">{% if cl.show_full_result_count %}{% blocktranslate with full_result_count=cl.full_result_count %}{{ full_result_count }} total{% endblocktranslate %}{% else %}{% translate "Show all" %}{% endif %}</a>)</span>
|
||||
{% endif %}
|
||||
{% for pair in cl.params.items %}
|
||||
{% if pair.0 != search_var %}<input type="hidden" name="{{ pair.0 }}" value="{{ pair.1 }}">{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if cl.search_help_text %}
|
||||
<br class="clear">
|
||||
{% comment %} .gov override - added for="searchbar" {% endcomment %}
|
||||
<label class="help" id="searchbar_helptext" for="searchbar">{{ cl.search_help_text }}</label>
|
||||
{% endif %}
|
||||
</form></div>
|
||||
{% endif %}
|
|
@ -11,4 +11,4 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
|
@ -10,7 +10,6 @@
|
|||
<th>Title</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Roles</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -28,11 +27,6 @@
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ member.user.phone }}</td>
|
||||
<td>
|
||||
{% for role in member.user|portfolio_role_summary:original %}
|
||||
<span class="usa-tag bg-primary-dark text-semibold">{{ role }}</span>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="padding-left-1 text-size-small">
|
||||
{% if member.user.email %}
|
||||
<input aria-hidden="true" class="display-none" value="{{ member.user.email }}" />
|
||||
|
|
|
@ -1,37 +1,41 @@
|
|||
{% load i18n %}
|
||||
{% load static field_helpers url_helpers %}
|
||||
|
||||
<details data-filter-title="{{ title }}" open="">
|
||||
<summary aria-label="Show/hide {{ title }} filters" role="button">
|
||||
{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}
|
||||
</summary>
|
||||
<ul class="mulitple-choice">
|
||||
{% for choice in choices %}
|
||||
{% if choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<h3>{% blocktrans with filter_title=title %} By {{ filter_title }} {% endblocktrans %}</h3>
|
||||
<ul class="mulitple-choice">
|
||||
{% for choice in choices %}
|
||||
{% if choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
<a href="{{ choice.query_string|iriencode }}" title="{{ choice.display }}">{{ choice.display }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for choice in choices %}
|
||||
{% if not choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
{% if choice.selected and choice.exclude_query_string %}
|
||||
<a class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
<svg class="usa-icon position-absolute z-100 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not choice.selected and choice.include_query_string %}
|
||||
<a class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for choice in choices %}
|
||||
{% if not choice.reset %}
|
||||
<li{% if choice.selected %} class="selected"{% endif %}">
|
||||
{% if choice.selected and choice.exclude_query_string %}
|
||||
<a role="menuitemcheckbox" class="choice-filter choice-filter--checked" href="{{ choice.exclude_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
<svg class="usa-icon position-absolute z-100 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not choice.selected and choice.include_query_string %}
|
||||
<a role="menuitemcheckbox" class="choice-filter" href="{{ choice.include_query_string|iriencode }}">{{ choice.display }}
|
||||
<svg class="usa-icon position-absolute z-0 left-0" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#check_box_outline_blank"></use>
|
||||
</svg>
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</details>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "admin/delete_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you cancel the portfolio invitation here, it won't trigger any emails. It also won't remove the user's
|
||||
portfolio access if they already logged in. Go to the
|
||||
<a href="{% url 'admin:registrar_userportfoliopermission_changelist' %}">
|
||||
User Portfolio Permissions
|
||||
</a>
|
||||
table if you want to remove the user from a portfolio.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -0,0 +1,12 @@
|
|||
{% extends "admin/delete_confirmation.html" %}
|
||||
|
||||
{% block content_subtitle %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body margin-left-1 maxw-none">
|
||||
<p class="usa-alert__text maxw-none">
|
||||
If you remove someone from a portfolio here, it will not send any emails when you click "Save".
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{{ block.super }}
|
||||
{% endblock %}
|
|
@ -21,7 +21,7 @@
|
|||
{% if field and field.field and field.field.descriptions %}
|
||||
{% with description=field.field.descriptions|get_dict_value:option.value %}
|
||||
{% if description %}
|
||||
<p class="margin-0 margin-top-1 font-body-2xs">{{ description }}</p>
|
||||
<p class="margin-0 font-body-2xs">{{ description }}</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
|
|
|
@ -16,10 +16,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-users' pk=domain.id %}" class="usa-breadcrumb__link"><span>Domain managers</span></a>
|
||||
<a href="{% url 'domain-users' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>Domain managers</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Add a domain manager</span>
|
||||
|
@ -27,7 +27,7 @@
|
|||
</ol>
|
||||
</nav>
|
||||
{% else %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
<nav class="usa-breadcrumb padding-top-0" aria-label="Domain manager breadcrumb">
|
||||
<ol class="usa-breadcrumb__list">
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
|
@ -42,17 +42,18 @@
|
|||
{% endblock breadcrumb %}
|
||||
|
||||
<h1>Add a domain manager</h1>
|
||||
{% if has_organization_feature_flag %}
|
||||
{% if portfolio %}
|
||||
<p>
|
||||
Provide an email address for the domain manager you’d like to add.
|
||||
They’ll need to access the registrar using a Login.gov account that’s associated with this email address.
|
||||
Domain managers can be a member of only one .gov organization.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You can add another user to help manage your domain. Users can only be a member of one .gov organization,
|
||||
and they'll need to sign in with their Login.gov account.
|
||||
Provide an email address for the domain manager you’d like to add.
|
||||
They’ll need to access the registrar using a Login.gov account that’s associated with this email address.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
You can add another user to help manage your domain. They will need to sign in to the .gov registrar with
|
||||
their Login.gov account.
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<form class="usa-form usa-form--large" method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
{# UNKNOWN domains would not have an expiration date and thus would show 'Expired' #}
|
||||
{% if domain.is_expired and domain.state != domain.State.UNKNOWN %}
|
||||
Expired
|
||||
{% elif has_domain_renewal_flag and domain.is_expiring %}
|
||||
{% elif domain.is_expiring %}
|
||||
Expiring soon
|
||||
{% elif domain.state == domain.State.UNKNOWN or domain.state == domain.State.DNS_NEEDED %}
|
||||
DNS needed
|
||||
|
@ -46,17 +46,17 @@
|
|||
|
||||
{% if domain.get_state_help_text %}
|
||||
<p class="margin-y-0 text-primary-darker">
|
||||
{% if has_domain_renewal_flag and domain.is_expired and is_domain_manager %}
|
||||
{% if domain.is_expired and is_domain_manager %}
|
||||
This domain has expired, but it is still online.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
{% url 'domain-renewal' domain_pk=domain.id as url %}
|
||||
<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 domain.is_expiring and is_domain_manager %}
|
||||
This domain will expire soon.
|
||||
{% url 'domain-renewal' pk=domain.id as url %}
|
||||
{% url 'domain-renewal' domain_pk=domain.id as url %}
|
||||
<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 domain.is_expiring and is_portfolio_user %}
|
||||
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 domain.is_expired and is_portfolio_user %}
|
||||
This domain has expired, but it is still online. Contact one of the listed domain managers to renew the domain.
|
||||
{% else %}
|
||||
{{ domain.get_state_help_text }}
|
||||
|
@ -82,7 +82,7 @@
|
|||
{% endif %}
|
||||
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
{% if domain.nameservers|length > 0 %}
|
||||
{% include "includes/summary_item.html" with title='DNS name servers' domains='true' value=domain.nameservers list='true' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -95,7 +95,7 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
{% if domain.dnssecdata is not None %}
|
||||
{% include "includes/summary_item.html" with title='DNSSEC' value='Enabled' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -104,26 +104,26 @@
|
|||
|
||||
{% if portfolio %}
|
||||
{% if has_any_domains_portfolio_permission and has_edit_portfolio_permission %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% url 'domain-suborganization' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_edit_portfolio_permission %}
|
||||
{% elif has_any_domains_portfolio_permission and has_view_portfolio_permission %}
|
||||
{% url 'domain-suborganization' pk=domain.id as url %}
|
||||
{% url 'domain-suborganization' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Suborganization' value=domain.domain_info.sub_organization edit_link=url editable=is_editable|and:has_view_portfolio_permission view_button=True %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% url 'domain-org-name-address' pk=domain.id as url %}
|
||||
{% url 'domain-org-name-address' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Organization' value=domain.domain_info address='true' edit_link=url editable=is_editable %}
|
||||
{% url 'domain-senior-official' pk=domain.id as url %}
|
||||
{% url 'domain-senior-official' domain_pk=domain.id as url %}
|
||||
{% include "includes/summary_item.html" with title='Senior official' value=domain.domain_info.senior_official contact='true' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% url 'domain-security-email' domain_pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNS</span>
|
||||
|
@ -30,14 +30,14 @@
|
|||
<p>You can enter your name servers, as well as other DNS-related information, in the following sections:</p>
|
||||
|
||||
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
<ul class="usa-list">
|
||||
<li><a href="{{ url }}">Name servers</a></li>
|
||||
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
<li><a href="{{ url }}">DNSSEC</a></li>
|
||||
{% if dns_prototype_flag and is_valid_domain %}
|
||||
<li><a href="{% url 'prototype-domain-dns' pk=domain.id %}">Prototype DNS record creator</a></li>
|
||||
<li><a href="{% url 'prototype-domain-dns' domain_pk=domain.id %}">Prototype DNS record creator</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNSSEC</span>
|
||||
|
@ -69,7 +69,7 @@
|
|||
<p class="margin-y-0">It is strongly recommended that you only enable DNSSEC if you know how to set it up properly at your hosting service. If you make a mistake, it could cause your domain name to stop working.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||
<a href="{% url 'domain-dns-dnssec-dsdata' domain_pk=domain.id %}" class="usa-button">Enable DNSSEC</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns-dnssec' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNSSEC</span></a>
|
||||
<a href="{% url 'domain-dns-dnssec' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNSSEC</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DS data</span>
|
||||
|
|
|
@ -19,10 +19,10 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain-dns' pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
<a href="{% url 'domain-dns' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>DNS</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>DNS name servers</span>
|
||||
|
|
|
@ -29,7 +29,10 @@
|
|||
{% csrf_token %}
|
||||
|
||||
{% if domain.domain_info.generic_org_type == 'federal' %}
|
||||
{% input_with_errors form.federal_agency %}
|
||||
<h4 class="margin-bottom-05">Federal Agency</h4>
|
||||
<p class="margin-top-0">
|
||||
{{ domain.domain_info.federal_agency }}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% input_with_errors form.organization_name %}
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{domain.name}}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{domain.name}}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Renewal Form</span>
|
||||
|
@ -63,14 +63,14 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-security-email' pk=domain.id as url %}
|
||||
{% url 'domain-security-email' domain_pk=domain.id as url %}
|
||||
{% if security_email is not None and security_email not in hidden_security_emails%}
|
||||
{% include "includes/summary_item.html" with title='Security email' value=security_email custom_text_for_value_none='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.' edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
{% include "includes/summary_item.html" with title='Security email' value='None provided' custom_text_for_value_none='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.' edit_link=url editable=is_editable %}
|
||||
{% endif %}
|
||||
|
||||
{% url 'domain-users' pk=domain.id as url %}
|
||||
{% url 'domain-users' domain_pk=domain.id as url %}
|
||||
{% if portfolio %}
|
||||
{% include "includes/summary_item.html" with title='Domain managers' domain_permissions=True value=domain edit_link=url editable=is_editable %}
|
||||
{% else %}
|
||||
|
@ -91,7 +91,7 @@
|
|||
Acknowledgement of .gov domain requirements </h3>
|
||||
</legend>
|
||||
|
||||
<form method="post" action="{% url 'domain-renewal' pk=domain.id %}">
|
||||
<form method="post" action="{% url 'domain-renewal' domain_pk=domain.id %}">
|
||||
{% csrf_token %}
|
||||
<div class="usa-checkbox">
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
{% endwith %}
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" name="submit_button" value="save" class="usa-button usa-button--with-icon usa-button--unstyled">
|
||||
<button id="submit-domain-request--site-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">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#add_circle"></use>
|
||||
</svg><span class="margin-left-05">Add another site</span>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
</ol>
|
||||
</nav>
|
||||
{% elif steps.prev %}
|
||||
<a href="{% namespaced_url 'domain-request' steps.prev id=domain_request_id %}" class="breadcrumb__back">
|
||||
<a href="{% namespaced_url 'domain-request' steps.prev domain_request_pk=domain_request_id %}" class="breadcrumb__back">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24" height="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#arrow_back"></use>
|
||||
</svg><span class="margin-left-05">Previous step</span>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
</svg>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<a href="{% namespaced_url 'domain-request' this_step id=domain_request_id %}"
|
||||
<a href="{% namespaced_url 'domain-request' this_step domain_request_pk=domain_request_id %}"
|
||||
{% if this_step == steps.current %}
|
||||
class="usa-current"
|
||||
{% else %}
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
|
||||
<p>If you withdraw your request, we won't review it. Once you withdraw your request, you can edit it and submit it again. </p>
|
||||
|
||||
<p><a href="{% url 'domain-request-withdrawn' DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'domain-request-status' DomainRequest.id %}">Cancel</a></p>
|
||||
<p><a href="{% url 'domain-request-withdrawn' domain_request_pk=DomainRequest.id %}" class="usa-button withdraw">Withdraw request</a>
|
||||
<a href="{% url 'domain-request-status' domain_request_pk=DomainRequest.id %}">Cancel</a></p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Security email</span>
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
{% endif %}
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns' pk=domain.id as url %}
|
||||
{% url 'domain-dns' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}" {% if request.path|startswith:url %}class="usa-current"{% endif %}">
|
||||
DNS
|
||||
</a>
|
||||
{% if request.path|startswith:url %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-nameservers' pk=domain.id as url %}
|
||||
{% url 'domain-dns-nameservers' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
@ -33,7 +33,7 @@
|
|||
</li>
|
||||
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path|startswith:url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
@ -43,7 +43,7 @@
|
|||
{% if domain.dnssecdata is not None or request.path|startswith:url and request.path|endswith:'dsdata' %}
|
||||
<ul class="usa-sidenav__sublist">
|
||||
<li class="usa-sidenav__item">
|
||||
{% url 'domain-dns-dnssec-dsdata' pk=domain.id as url %}
|
||||
{% url 'domain-dns-dnssec-dsdata' domain_pk=domain.id as url %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
>
|
||||
|
@ -81,7 +81,7 @@
|
|||
{% endwith %}
|
||||
|
||||
|
||||
{% if has_domain_renewal_flag and is_domain_manager%}
|
||||
{% if is_domain_manager%}
|
||||
{% if domain.is_expiring or domain.is_expired %}
|
||||
{% with url_name="domain-renewal" %}
|
||||
{% include "includes/domain_sidenav_item.html" with item_text="Renewal form" %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Suborganization</span>
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
<a href="{% url 'domains' %}" class="usa-breadcrumb__link"><span>Domains</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item">
|
||||
<a href="{% url 'domain' pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
<a href="{% url 'domain' domain_pk=domain.id %}" class="usa-breadcrumb__link"><span>{{ domain.name }}</span></a>
|
||||
</li>
|
||||
<li class="usa-breadcrumb__list-item usa-current" aria-current="page">
|
||||
<span>Domain managers</span>
|
||||
|
@ -25,29 +25,25 @@
|
|||
|
||||
<h1>Domain managers</h1>
|
||||
|
||||
{% comment %}Copy below differs depending on whether view is in portfolio mode.{% endcomment %}
|
||||
{% if not portfolio %}
|
||||
<p>
|
||||
Domain managers can update all information related to a domain within the
|
||||
.gov registrar, including security email and DNS name servers.
|
||||
Domain managers can update information related to this domain, including security email and DNS name servers.
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
Domain managers can update all information related to a domain within the
|
||||
.gov registrar, including contact details, senior official, security email, and DNS name servers.
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<ul class="usa-list">
|
||||
<li>There is no limit to the number of domain managers you can add.</li>
|
||||
<li>After adding a domain manager, an email invitation will be sent to that user with
|
||||
instructions on how to set up an account.</li>
|
||||
<li>There is no limit on the number of domain managers you can add.</li>
|
||||
<li>All domain managers must keep their contact information updated and be responsive if contacted by the .gov team.</li>
|
||||
{% if not portfolio %}<li>All domain managers will be notified when updates are made to this domain.</li>{% endif %}
|
||||
<li>Domains must have at least one domain manager. You can’t remove yourself as a domain manager if you’re the only one assigned to this domain.
|
||||
{% if portfolio %} Add another domain manager before you remove yourself from this domain.{% endif %}</li>
|
||||
<li>All domain managers will be notified when updates are made to this domain and when managers are added or removed.</li>
|
||||
<li>Domains must have at least one manager. You can’t remove yourself if you’re the only one assigned to this domain.</li>
|
||||
</ul>
|
||||
|
||||
{% if domain_manager_roles and domain_manager_roles|length == 1 %}
|
||||
<div class="usa-alert usa-alert--info usa-alert--slim">
|
||||
<div class="usa-alert__body">
|
||||
This domain has only one manager. Consider adding another manager to ensure the domain has continuous oversight and support.
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if domain_manager_roles %}
|
||||
<section class="section-outlined" id="domain-managers">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table--stacked dotgov-table">
|
||||
|
@ -93,7 +89,7 @@
|
|||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove yourself as a domain manager?" modal_description="You will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove myself" modal_button_class="usa-button--secondary" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}" >
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" domain_pk=domain.id user_pk=item.permission.user.id %}" >
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% else %}
|
||||
|
@ -108,7 +104,7 @@
|
|||
{% include 'includes/modal.html' with modal_heading="Are you sure you want to remove " heading_value=email|add:"?" modal_description="<strong>"|add:email|add:"</strong> will no longer be able to manage the domain <strong>"|add:domain_name|add:"</strong>."|safe modal_button_id="user-delete-button-"|add:counter_str|safe modal_button_text="Yes, remove domain manager" modal_button_class="usa-button--secondary" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" pk=domain.id user_pk=item.permission.user.id %}">
|
||||
<form method="POST" id="user-delete-form-{{ forloop.counter }}" action="{% url "domain-user-delete" domain_pk=domain.id user_pk=item.permission.user.id %}">
|
||||
{% csrf_token %}
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -123,7 +119,7 @@
|
|||
></div>
|
||||
{% endif %}
|
||||
|
||||
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' pk=domain.id %}">
|
||||
<a class="usa-button usa-button--unstyled usa-button--with-icon" href="{% url 'domain-users-add' domain_pk=domain.id %}">
|
||||
<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>
|
||||
</svg><span class="margin-left-05">Add a domain manager</span>
|
||||
|
@ -154,7 +150,7 @@
|
|||
{% if not portfolio %}<td data-label="Status">{{ invitation.domain_invitation.status|title }}</td>{% endif %}
|
||||
<td>
|
||||
{% if invitation.domain_invitation.status == invitation.domain_invitation.DomainInvitationStatus.INVITED %}
|
||||
<form method="POST" action="{% url "invitation-cancel" pk=invitation.domain_invitation.id %}">
|
||||
<form method="POST" action="{% url "invitation-cancel" domain_invitation_pk=invitation.domain_invitation.id %}">
|
||||
{% csrf_token %}<input type="submit" class="usa-button--unstyled text-no-underline cursor-pointer" value="Cancel">
|
||||
</form>
|
||||
{% endif %}
|
||||
|
|
|
@ -17,7 +17,7 @@ Domains should uniquely identify a government organization and be clear to the g
|
|||
|
||||
|
||||
ACTION NEEDED
|
||||
First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <https://manage.get.gov/> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
First, we need you to identify a new domain name that meets our naming requirements for your type of organization. Then, log in to the registrar and update the name in your domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
|
||||
If you have questions or want to discuss potential domain names, reply to this email.
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ We expect a senior official to be someone in a role of significant, executive re
|
|||
ACTION NEEDED
|
||||
Reply to this email with a justification for naming {{ domain_request.senior_official.get_formatted_name }} as the senior official. If you have questions or comments, include those in your reply.
|
||||
|
||||
Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <https://manage.get.gov/> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
Alternatively, you can log in to the registrar and enter a different senior official for this domain request. <{{ manage_url }}> Once you submit your updated request, we’ll resume the adjudication process.
|
||||
|
||||
|
||||
THANK YOU
|
||||
|
|
|
@ -4,11 +4,10 @@ Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first
|
|||
{{ requestor_email }} has invited you to manage:
|
||||
{% for domain in domains %}{{ domain.name }}
|
||||
{% endfor %}
|
||||
To manage domain information, visit the .gov registrar <https://manage.get.gov>.
|
||||
To manage domain information, visit the .gov registrar <{{ manage_url }}>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
{% if not requested_user %}
|
||||
|
||||
YOU NEED A LOGIN.GOV ACCOUNT
|
||||
You’ll need a Login.gov account to access the .gov registrar. That account needs to be
|
||||
associated with the following email address: {{ invitee_email_address }}
|
||||
|
@ -17,8 +16,6 @@ Login.gov provides a simple and secure process for signing in to many government
|
|||
services with one account. If you don’t already have one, follow these steps to create
|
||||
your Login.gov account <https://login.gov/help/get-started/create-your-account/>.
|
||||
{% endif %}
|
||||
|
||||
|
||||
DOMAIN MANAGEMENT
|
||||
As a .gov domain manager, you can add or update information like name servers. You’ll
|
||||
also serve as a contact for the domains you manage. Please keep your contact
|
||||
|
|
|
@ -11,6 +11,7 @@ MANAGER REMOVED: {{ manager_removed.email }}
|
|||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
You’re listed as a domain manager for {{ domain.name }}, so you’ll receive a notification whenever a domain manager is removed from that domain.
|
||||
|
||||
If you have questions or concerns, reach out to the person who removed the domain manager or reply to this email.
|
||||
|
||||
THANK YOU
|
||||
|
|
|
@ -15,7 +15,7 @@ The person who received the invitation will become a domain manager once they lo
|
|||
associated with the invited email address.
|
||||
|
||||
If you need to cancel this invitation or remove the domain manager, you can do that by going to
|
||||
this domain in the .gov registrar <https://manage.get.gov/>.
|
||||
this domain in the .gov registrar <{{ manage_url }}>.
|
||||
|
||||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
|
|
|
@ -11,7 +11,7 @@ STATUS: Withdrawn
|
|||
----------------------------------------------------------------
|
||||
|
||||
YOU CAN EDIT YOUR WITHDRAWN REQUEST
|
||||
You can edit and resubmit this request by signing in to the registrar <https://manage.get.gov/>.
|
||||
You can edit and resubmit this request by signing in to the registrar <{{ manage_url }}>.
|
||||
|
||||
|
||||
SOMETHING WRONG?
|
||||
|
|
|
@ -16,7 +16,7 @@ The person who received the invitation will become an admin once they log in to
|
|||
associated with the invited email address.
|
||||
|
||||
If you need to cancel this invitation or remove the admin, you can do that by going to
|
||||
the Members section for your organization <https://manage.get.gov/>.
|
||||
the Members section for your organization <{{ manage_url }}>.
|
||||
|
||||
|
||||
WHY DID YOU RECEIVE THIS EMAIL?
|
||||
|
|
|
@ -8,7 +8,7 @@ REMOVED BY: {{ requestor_email }}
|
|||
REMOVED ON: {{date}}
|
||||
ADMIN REMOVED: {{ removed_email_address }}
|
||||
|
||||
You can view this update by going to the Members section for your .gov organization <https://manage.get.gov/>.
|
||||
You can view this update by going to the Members section for your .gov organization <{{ manage_url }}>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ Hi.
|
|||
|
||||
{{ requestor_email }} has invited you to {{ portfolio.organization_name }}.
|
||||
|
||||
You can view this organization on the .gov registrar <https://manage.get.gov>.
|
||||
You can view this organization on the .gov registrar <{{ manage_url }}>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
|
21
src/registrar/templates/emails/portfolio_removal.txt
Normal file
21
src/registrar/templates/emails/portfolio_removal.txt
Normal file
|
@ -0,0 +1,21 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
|
||||
|
||||
{{ requestor_email }} has removed you from {{ portfolio.organization_name }}.
|
||||
|
||||
You can no longer view this organization or its related domains within the .gov registrar.
|
||||
|
||||
|
||||
SOMETHING WRONG?
|
||||
If you have questions or concerns, reach out to the person who removed you from the
|
||||
organization, or reply to this email.
|
||||
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
You've been removed from a .gov organization
|
35
src/registrar/templates/emails/portfolio_update.txt
Normal file
35
src/registrar/templates/emails/portfolio_update.txt
Normal file
|
@ -0,0 +1,35 @@
|
|||
{% autoescape off %}{# In a text file, we don't want to have HTML entities escaped #}
|
||||
Hi,{% if requested_user and requested_user.first_name %} {{ requested_user.first_name }}.{% endif %}
|
||||
|
||||
Your permissions were updated in the .gov registrar.
|
||||
|
||||
ORGANIZATION: {{ portfolio.organization_name }}
|
||||
UPDATED BY: {{ requestor_email }}
|
||||
UPDATED ON: {{ date }}
|
||||
YOUR PERMISSIONS: {{ permissions.role_display }}
|
||||
Domains - {{ permissions.domains_display }}
|
||||
Domain requests - {{ permissions.domain_requests_display }}
|
||||
Members - {{ permissions.members_display }}
|
||||
|
||||
Your updated permissions are now active in the .gov registrar <https://manage.get.gov>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
SOMETHING WRONG?
|
||||
If you have questions or concerns, reach out to the person who updated your
|
||||
permissions, or reply to this email.
|
||||
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for using a .gov
|
||||
domain.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
Contact us: <https://get.gov/contact/>
|
||||
Learn about .gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency
|
||||
(CISA) <https://cisa.gov/>
|
||||
{% endautoescape %}
|
|
@ -0,0 +1 @@
|
|||
Your permissions were updated in the .gov registrar
|
|
@ -8,7 +8,7 @@ REQUESTED BY: {{ domain_request.creator.email }}
|
|||
REQUEST RECEIVED ON: {{ domain_request.last_submitted_date|date }}
|
||||
STATUS: Approved
|
||||
|
||||
You can manage your approved domain on the .gov registrar <https://manage.get.gov>.
|
||||
You can manage your approved domain on the .gov registrar <{{ manage_url }}>.
|
||||
|
||||
----------------------------------------------------------------
|
||||
|
||||
|
|
|
@ -68,10 +68,12 @@ Learn more about:
|
|||
NEED ASSISTANCE?
|
||||
If you have questions about this domain request or need help choosing a new domain name, reply to this email.
|
||||
{% endif %}
|
||||
{% if reason != domain_request.RejectionReasons.REQUESTOR_NOT_ELIGIBLE and reason != domain_request.RejectionReasons.ORG_NOT_ELIGIBLE %}
|
||||
|
||||
THANK YOU
|
||||
.Gov helps the public identify official, trusted information. Thank you for requesting a .gov domain.
|
||||
|
||||
{% endif %}
|
||||
----------------------------------------------------------------
|
||||
|
||||
The .gov team
|
||||
|
|
|
@ -20,7 +20,7 @@ During our review, we’ll verify that:
|
|||
- You work at the organization and/or can make requests on its behalf
|
||||
- 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. <{{ manage_url }}>.
|
||||
|
||||
|
||||
NEED TO MAKE CHANGES?
|
||||
|
|
|
@ -31,7 +31,7 @@ CHECK YOUR .GOV DOMAIN CONTACTS
|
|||
|
||||
This is a good time to check who has access to your .gov domain{% if domains|length > 1 %}s{% endif %}. The admin, technical, and billing contacts listed for your domain{% if domains|length > 1 %}s{% endif %} in our old system also received this email. In our new registrar, these contacts are all considered “domain managers.” We no longer have the admin, technical, and billing roles, and you aren’t limited to three domain managers like in the old system.
|
||||
|
||||
1. Once you have your Login.gov account, sign in to the new registrar at <https://manage.get.gov>.
|
||||
1. Once you have your Login.gov account, sign in to the new registrar at <{{ manage_url }}>.
|
||||
2. Click the “Manage” link next to your .gov domain, then click on “Domain managers” to see who has access to your domain.
|
||||
3. If any of these users should not have access to your domain, let us know in a reply to this email.
|
||||
|
||||
|
@ -57,7 +57,7 @@ THANK YOU
|
|||
The .gov team
|
||||
|
||||
.Gov blog <https://get.gov/updates/>
|
||||
Domain management <https://manage.get.gov>
|
||||
Domain management <{{ manage_url }}}>
|
||||
Get.gov <https://get.gov>
|
||||
|
||||
The .gov registry is a part of the Cybersecurity and Infrastructure Security Agency (CISA) <https://cisa.gov/>
|
||||
|
|
|
@ -8,7 +8,7 @@ UPDATED BY: {{user}}
|
|||
UPDATED ON: {{date}}
|
||||
INFORMATION UPDATED: {{changes}}
|
||||
|
||||
You can view this update in the .gov registrar <https://manage.get.gov/>.
|
||||
You can view this update in the .gov registrar <{{ manage_url }}>.
|
||||
|
||||
Get help with managing your .gov domain <https://get.gov/help/domain-management/>.
|
||||
|
||||
|
|
|
@ -163,7 +163,7 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domain-requests__table-wrapper">
|
||||
<div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domain-requests__table-wrapper">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
<caption class="sr-only">Your domain requests</caption>
|
||||
<thead>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<li class="usa-sidenav__item">
|
||||
{% if url_name %}
|
||||
{% url url_name pk=domain.id as url %}
|
||||
{% url url_name domain_pk=domain.id as url %}
|
||||
{% endif %}
|
||||
<a href="{{ url }}"
|
||||
{% if request.path == url %}class="usa-current"{% endif %}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<span id="get_domains_json_url" class="display-none">{{url}}</span>
|
||||
|
||||
<!-- 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 num_expiring_domains > 0 and has_any_domains_portfolio_permission %}
|
||||
<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__body">
|
||||
|
@ -75,7 +75,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Non org model banner -->
|
||||
{% if has_domain_renewal_flag and num_expiring_domains > 0 and not portfolio %}
|
||||
{% if num_expiring_domains > 0 and not portfolio %}
|
||||
<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__body">
|
||||
|
@ -173,7 +173,6 @@
|
|||
>Deleted</label
|
||||
>
|
||||
</div>
|
||||
{% if has_domain_renewal_flag %}
|
||||
<div class="usa-checkbox">
|
||||
<input
|
||||
class="usa-checkbox__input"
|
||||
|
@ -185,7 +184,6 @@
|
|||
<label class="usa-checkbox__label" for="filter-status-expiring"
|
||||
>Expiring soon</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -200,7 +198,7 @@
|
|||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="display-none usa-table-container--scrollable margin-top-0" tabindex="0" id="domains__table-wrapper">
|
||||
<div class="display-none usa-table-container--scrollable usa-table-container--override-overflow margin-top-0" tabindex="0" id="domains__table-wrapper">
|
||||
<table class="usa-table usa-table--borderless usa-table--stacked dotgov-table dotgov-table--stacked">
|
||||
<caption class="sr-only">Your registered domains</caption>
|
||||
<thead>
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
{% load field_helpers %}
|
||||
<div id="member-basic-permissions" class="margin-top-2">
|
||||
<h2>What permissions do you want to add?</h2>
|
||||
<h2>Member permissions</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" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-4">Domains <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% 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" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-2">Domain requests <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% 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" %}
|
||||
<h3 class="margin-bottom-neg-1 margin-top-2">Members <abbr class="usa-hint usa-hint--required" title="required">*</abbr></h3>
|
||||
{% with group_classes="bg-gray-1 border-base-lighter padding-bottom-2 margin-top-0" add_legend_class="usa-sr-only" label_classes="margin-top-1" %}
|
||||
{% input_with_errors form.member_permissions %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h4 class="margin-bottom-0">Assigned domains</h4>
|
||||
{% if domain_count > 0 %}
|
||||
<h4 class="margin-bottom-0">Domains assigned</h4>
|
||||
<p class="margin-top-0">{{domain_count}}</p>
|
||||
{% else %}
|
||||
<p class="margin-top-0">This member does not manage any domains.{% if manage_button %} To assign this member a domain, click "Manage".{% endif %}</p>
|
||||
<p class="margin-top-0">This member does not manage any domains.{% if manage_button %} To assign this member a domain, click "Edit".{% endif %}</p>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{% load static %}
|
||||
|
||||
{% if member %}
|
||||
<span
|
||||
id="portfolio-js-value"
|
||||
|
@ -36,47 +34,9 @@
|
|||
|
||||
<div class="section-outlined__header margin-bottom-3 grid-row">
|
||||
<!-- ---------- SEARCH ---------- -->
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||
<section aria-label="Member domains search component">
|
||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<label class="usa-label display-block margin-bottom-05" for="edit-member-domains__search-field">
|
||||
{% if has_edit_members_portfolio_permission %}
|
||||
Search all domains
|
||||
{% else %}
|
||||
Search domains assigned to
|
||||
{% if member %}
|
||||
{{ member.email }}
|
||||
{% else %}
|
||||
{{ portfolio_invitation.email }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper">
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="edit-member-domains__reset-search" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="edit-member-domains__search-field"
|
||||
type="search"
|
||||
name="member-domains-search"
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="edit-member-domains__search-field-submit">
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% with label_text="Search all domains" item_name="edit-member-domains" aria_label_text="Member domains search component" %}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
|
@ -85,7 +45,7 @@
|
|||
<caption class="sr-only">member domains</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-sortable="checked" scope="col" role="columnheader" class="padding-right-105"><span class="sr-only">Assigned domains</span></th>
|
||||
<th data-sortable="checked" scope="col" role="columnheader" class="padding-right-105 width-6 left-align-sort-button"><span class="sr-only">Assigned domains</span></th>
|
||||
<!-- We override default sort to be name/ascending in the JSON endpoint. We add the correct aria-sort attribute here to reflect that in the UI -->
|
||||
<th data-sortable="name" scope="col" role="columnheader" aria-sort="descending">Domains</th>
|
||||
</tr>
|
||||
|
@ -100,7 +60,7 @@
|
|||
></div>
|
||||
</div>
|
||||
<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.</p>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="edit-member-domains__no-search-results">
|
||||
<p>No results found</p>
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
{% load static %}
|
||||
|
||||
{% if member %}
|
||||
<span
|
||||
id="portfolio-js-value"
|
||||
|
@ -34,45 +32,19 @@
|
|||
{% endif %}
|
||||
</h2>
|
||||
|
||||
<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 ---------- -->
|
||||
<div class="section-outlined__search mobile:grid-col-12 desktop:grid-col-9">
|
||||
<section aria-label="Member domains search component">
|
||||
<form class="usa-search usa-search--show-label" method="POST" role="search">
|
||||
{% csrf_token %}
|
||||
<label class="usa-label display-block margin-bottom-05" for="member-domains__search-field">
|
||||
Search domains assigned to
|
||||
{% if member %}
|
||||
{{ member.email }}
|
||||
{% else %}
|
||||
{{ portfolio_invitation.email }}
|
||||
{% endif %}
|
||||
</label>
|
||||
<div class="usa-search--show-label__input-wrapper">
|
||||
<button class="usa-button usa-button--unstyled margin-right-3 display-none" id="member-domains__reset-search" type="button">
|
||||
<svg class="usa-icon" aria-hidden="true" focusable="false" role="img" width="24">
|
||||
<use xlink:href="{%static 'img/sprite.svg'%}#close"></use>
|
||||
</svg>
|
||||
Reset
|
||||
</button>
|
||||
<input
|
||||
class="usa-input"
|
||||
id="member-domains__search-field"
|
||||
type="search"
|
||||
name="member-domains-search"
|
||||
/>
|
||||
<button class="usa-button" type="submit" id="member-domains__search-field-submit">
|
||||
<span class="usa-search__submit-text">Search </span>
|
||||
<img
|
||||
src="{% static 'img/usa-icons-bg/search--white.svg' %}"
|
||||
class="usa-search__submit-icon"
|
||||
alt="Search"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% with label_text="Domains assigned to " %}
|
||||
{% if member %}
|
||||
{% with label_text=label_text|add:member.email item_name="member-domains" aria_label_text="Member domains search component" %}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
{% with label_text=label_text|add:portfolio_invitation.email item_name="member-domains" aria_label_text="Member domains search component" %}
|
||||
{% include "includes/search.html" %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
|
||||
<!-- ---------- MAIN TABLE ---------- -->
|
||||
|
@ -95,7 +67,7 @@
|
|||
></div>
|
||||
</div>
|
||||
<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.</p>
|
||||
</div>
|
||||
<div class="display-none margin-bottom-4" id="member-domains__no-search-results">
|
||||
<p>No results found</p>
|
||||
|
|
|
@ -1,33 +1,11 @@
|
|||
<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 %}
|
||||
<p class="margin-top-0">{{ permissions.role_display }}</p>
|
||||
|
||||
<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 %}
|
||||
<p class="margin-top-0">{{ permissions.domains_display }}: {{ permissions.domains_description_display }}</p>
|
||||
|
||||
<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 %}
|
||||
<p class="margin-top-0">{{ permissions.domain_requests_display }}: {{ permissions.domain_requests_description_display }}</p>
|
||||
|
||||
<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 %}
|
||||
<p class="margin-top-0">{{ permissions.members_display }}: {{ permissions.members_description_display }}</p>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
||||
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.REQUESTING_ENTITY %}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% for step in steps %}
|
||||
<section class="summary-item margin-top-3">
|
||||
{% if is_editable %}
|
||||
{% namespaced_url 'domain-request' step id=domain_request_id as domain_request_url %}
|
||||
{% namespaced_url 'domain-request' step domain_request_pk=domain_request_id as domain_request_url %}
|
||||
{% endif %}
|
||||
|
||||
{% if step == Step.ORGANIZATION_TYPE %}
|
||||
|
|
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