styling in django admin
@@ -984,3 +992,7 @@ ul.add-list-reset {
}
}
+
+#result_list > tbody tr > th > a {
+ text-decoration: underline;
+}
diff --git a/src/registrar/fixtures/fixtures_domains.py b/src/registrar/fixtures/fixtures_domains.py
index 8855194f8..b960c9022 100644
--- a/src/registrar/fixtures/fixtures_domains.py
+++ b/src/registrar/fixtures/fixtures_domains.py
@@ -1,4 +1,5 @@
from datetime import timedelta
+from django.db import transaction, connection
from django.utils import timezone
import logging
import random
@@ -8,6 +9,8 @@
from registrar.fixtures.fixtures_users import UserFixture
from registrar.models import User, DomainRequest
from registrar.models.domain import Domain
+from registrar.models.domain_information import DomainInformation
+from registrar.models.draft_domain import DraftDomain
fake = Faker()
logger = logging.getLogger(__name__)
@@ -40,6 +43,37 @@ def load(cls):
# Approve each user associated with `in review` status domains
cls._approve_domain_requests(users)
+
+ # We need a hard-coded domain for our pa11y tests
+ mythical_creature = User.objects.filter(email="mythical.creature@igorville.gov").first()
+ mythical_request = DomainRequest.objects.filter(creator=mythical_creature, status=DomainRequest.DomainRequestStatus.APPROVED).first()
+ print(f"another mythical_request: {mythical_request}")
+ if mythical_request:
+ # Update the id of the domain request object
+ new_domain = mythical_request.approved_domain
+ print(f"new domain: {new_domain}")
+
+ if new_domain:
+ cls.update_domain_primary_key(new_domain.id, 9999)
+
+ @staticmethod
+ def update_domain_primary_key(old_id, new_id):
+ domain = Domain.objects.get(id=old_id)
+ with transaction.atomic():
+ with connection.cursor() as cursor:
+ cursor.execute("SET CONSTRAINTS ALL DEFERRED;")
+
+ for rel in domain._meta.related_objects:
+ if rel.one_to_many or rel.one_to_one:
+ related_model = rel.related_model
+ field_name = rel.field.name
+ filter_kwargs = {field_name: old_id}
+ update_kwargs = {field_name: new_id}
+
+ # Update all related records to point to the new primary key
+ related_model.objects.filter(**filter_kwargs).update(**update_kwargs)
+
+ Domain.objects.filter(id=old_id).update(id=new_id)
@staticmethod
def _generate_fake_expiration_date(days_in_future=365):
@@ -126,7 +160,7 @@ def _bulk_update_requests(cls, domain_requests_to_update):
"""Bulk update domain requests."""
if domain_requests_to_update:
try:
- DomainRequest.objects.bulk_update(domain_requests_to_update, ["status", "investigator"])
+ DomainRequest.objects.bulk_update(domain_requests_to_update, ["status", "investigator", "approved_domain"])
logger.info(f"Successfully updated {len(domain_requests_to_update)} requests.")
except Exception as e:
logger.error(f"Unexpected error during requests bulk update: {e}")
diff --git a/src/registrar/fixtures/fixtures_requests.py b/src/registrar/fixtures/fixtures_requests.py
index 6eee6438f..078343ba9 100644
--- a/src/registrar/fixtures/fixtures_requests.py
+++ b/src/registrar/fixtures/fixtures_requests.py
@@ -314,6 +314,31 @@ def load(cls):
cls._create_domain_requests(users)
+ # this is a request with a hard-coded id for our pa11y tests.
+ # Outside of the main for loop for maintability and also due to id conflicts
+ mythical_creature = User.objects.filter(email="mythical.creature@igorville.gov").first()
+ print(f"mythical_creature: {mythical_creature}")
+ if mythical_creature:
+ print("trying to create a specific request?")
+ random_request_type = random.choice(cls.DOMAINREQUESTS)
+ # Action needed rather than started because fixtures auto-approves started
+ request_data = {
+ "status": DomainRequest.DomainRequestStatus.ACTION_NEEDED,
+ "organization_name": "Candy Forest",
+ }
+ domain_request = DomainRequest(
+ created_at=datetime.now(),
+ id=9999,
+ creator=mythical_creature,
+ organization_name=random_request_type["organization_name"],
+ )
+ cls._set_non_foreign_key_fields(domain_request, request_data)
+ cls._set_foreign_key_fields(domain_request, request_data, mythical_creature)
+ domain_request.save()
+ cls._set_many_to_many_relations(domain_request, request_data)
+ else:
+ logger.error("Could not create hard-coded user for pa11y tests: user does not exist.")
+
@classmethod
def _create_domain_requests(cls, users): # noqa: C901
"""Creates DomainRequests given a list of users."""
@@ -335,6 +360,7 @@ def _create_domain_requests(cls, users): # noqa: C901
creator=user,
organization_name=request_data["organization_name"],
)
+
cls._set_non_foreign_key_fields(domain_request, request_data)
cls._set_foreign_key_fields(domain_request, request_data, user)
domain_requests_to_create.append(domain_request)
@@ -369,6 +395,12 @@ def _create_domain_requests(cls, users): # noqa: C901
cls._set_many_to_many_relations(domain_request, request_data)
except Exception as e:
logger.warning(e)
+
+
+ # mythical_request = DomainRequest.objects.filter(creator__email="mythical.creature@igorville.gov").first()
+ # if mythical_request and mythical_request.id != 9999:
+ # mythical_request.id = 9999
+ # mythical_request.save()
@classmethod
def _bulk_create_requests(cls, domain_requests_to_create):
diff --git a/src/registrar/fixtures/fixtures_users.py b/src/registrar/fixtures/fixtures_users.py
index fdaa1c135..081280693 100644
--- a/src/registrar/fixtures/fixtures_users.py
+++ b/src/registrar/fixtures/fixtures_users.py
@@ -297,6 +297,18 @@ class UserFixture:
"last_name": "Abbitt-Analyst",
"email": "kaitlin.abbitt@gwe.cisa.dhs.gov",
},
+ {
+ # This is a reserved user set at a specific id.
+ # We use this user for pa11y, since it is a static file.
+ # Respect the mythical creature, *do not* change the id!
+ "id": "9999",
+ "username": "10000000-0000-0000-0000-000000000001",
+ "first_name": "Mythical",
+ "last_name": "Creature",
+ "email": "mythical.creature@igorville.gov",
+ "title": "Reserved pa11y user",
+ "phone": "123-456-7890"
+ },
]
# Additional emails to add to the AllowedEmail whitelist.
diff --git a/src/registrar/management/commands/enable_org_waffle_flags.py b/src/registrar/management/commands/enable_org_waffle_flags.py
new file mode 100644
index 000000000..901667ca5
--- /dev/null
+++ b/src/registrar/management/commands/enable_org_waffle_flags.py
@@ -0,0 +1,27 @@
+"""Enables org-related waffle flags"""
+
+import logging
+from waffle.decorators import flag_is_active
+from waffle.models import get_waffle_flag_model
+from django.core.management import BaseCommand
+logger = logging.getLogger(__name__)
+
+
+class Command(BaseCommand):
+ help = "Runs the cat command on files from /tmp into the getgov directory."
+
+ def handle(self, **options):
+ # Check for each flag. This is essentially get_or_create, so we have a db reference.
+ added_flags = [
+ "organization_feature",
+ "organization_requests",
+ "organization_members"
+ ]
+ for flag_name in added_flags:
+ # We call flag_is_active first to auto-create the flag in the db.
+ flag_is_active(None, flag_name)
+ flag = get_waffle_flag_model().get(flag_name)
+ if not flag.everyone:
+ logger.info(f"Setting everyone on flag {flag_name} to True.")
+ flag.everyone = True
+ flag.save()
diff --git a/src/registrar/management/commands/generate_pa11y_config.py b/src/registrar/management/commands/generate_pa11y_config.py
new file mode 100644
index 000000000..62e4c6f3a
--- /dev/null
+++ b/src/registrar/management/commands/generate_pa11y_config.py
@@ -0,0 +1,173 @@
+import os
+import re
+import json
+from urllib.parse import urlparse
+from django.urls import URLPattern, URLResolver, get_resolver
+from django.core.management.base import BaseCommand
+
+BASE_URL = "http://localhost:8080/"
+class Command(BaseCommand):
+ """
+ Generate the .pa11yci configuration file with all URLs from Django's URLconf.
+ """
+ help = (
+ "Generates the .pa11yci file with all URLs found in the URLconf, "
+ "with dynamic parameters substituted with dummy values and some endpoints excluded."
+ )
+
+ def handle(self, *args, **options):
+ """
+ Generate and write the .pa11yci configuration file to the current working directory.
+ """
+ resolver = get_resolver()
+ urls = self.extract_urls(resolver.url_patterns)
+ config = {
+ "defaults": {
+ "concurrency": 1,
+ "timeout": 30000
+ },
+ "viewport": {
+ "width": 1920,
+ "height": 1080
+ },
+ "actions": [
+ "wait for url to be #"
+ ],
+ "urls": urls,
+ }
+ output_file = os.path.join(os.getcwd(), ".pa11yci")
+ with open(output_file, "w") as f:
+ json.dump(config, f, indent=4)
+ self.stdout.write(self.style.SUCCESS(f"Generated {output_file} with {len(urls)} URLs."))
+
+ def should_exclude(self, url: str) -> bool:
+ """
+ Checks whether a given URL should be excluded based on predefined patterns.
+
+ Args:
+ url (str): The full URL to test.
+ Returns:
+ bool: True if URL should be skipped; otherwise False.
+ """
+ exclude_segments = [
+ "__debug__",
+ "api",
+ "jsi18n",
+ "r",
+ "health",
+ "todo",
+ "autocomplete",
+ "openid",
+ "logout",
+ "login",
+ "password_change",
+ "reports",
+ "auditlog",
+ "password",
+ "import",
+ "export",
+ "process_import",
+ "process_export",
+ "waffleflag"
+ ]
+
+ # Specific endpoints to exclude
+ exclude_endpoints = {
+ "/get-domains-json/",
+ "/get-domain-requests-json/",
+ "/get-portfolio-members-json/",
+ "/get-member-domains-json/",
+ "/admin/analytics/export_data_type/",
+ "/admin/analytics/export_data_domain_requests_full/",
+ "/admin/analytics/export_data_full/",
+ "/admin/analytics/export_data_federal/",
+ "/admin/analytics/export_domains_growth/",
+ "/admin/analytics/export_requests_growth/",
+ "/admin/analytics/export_managed_domains/",
+ "/admin/analytics/export_unmanaged_domains/",
+ }
+
+ path = urlparse(url).path
+ if path in exclude_endpoints:
+ return True
+
+ path_segments = [seg for seg in path.split("/") if seg]
+ return any(segment in exclude_segments for segment in path_segments)
+
+ @staticmethod
+ def substitute_regex_params(route: str) -> str:
+ """
+ Replace regex named capture groups with dummy values.
+ Args:
+ route (str): The regex string.
+ Returns:
+ str: The regex string with named groups replaced by dummy values.
+ """
+ regex = r"\(\?P<(\w+)>([^)]+)\)"
+ if route == "(?P
{% url cl.opts|admin_urlname:'add' as add_url %}
-
+
+
-
- {{ total_websites.0 }}
-
-
+
{% 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 %}
-
-
+
+
{% endwith %}
{% endwith %}
diff --git a/src/registrar/templates/admin/fieldset.html b/src/registrar/templates/admin/fieldset.html
index 20b76217b..30528d77a 100644
--- a/src/registrar/templates/admin/fieldset.html
+++ b/src/registrar/templates/admin/fieldset.html
@@ -18,7 +18,17 @@
{% else %}
{{ fieldset.name }}
+
{% endif %}
+ {% else %}
+ {# Add hidden legend for unnamed fieldsets #}
+
{% endif %}
{# Customize the markup for the collapse toggle: Do not show a description for the collapse fieldsets, instead we're using the description as a screen reader only legend #}
diff --git a/src/registrar/templates/admin/import_export/change_list_export_item.html b/src/registrar/templates/admin/import_export/change_list_export_item.html
new file mode 100644
index 000000000..9678d224a
--- /dev/null
+++ b/src/registrar/templates/admin/import_export/change_list_export_item.html
@@ -0,0 +1,7 @@
+{% load i18n %}
+{% load admin_urls %}
+
+{% if has_export_permission %}
+{% comment %} Uses the initButtonLinks {% endcomment %}
+
+{% endif %}
diff --git a/src/registrar/templates/admin/import_export/change_list_import_item.html b/src/registrar/templates/admin/import_export/change_list_import_item.html
index 8255a8ba7..0f2d59421 100644
--- a/src/registrar/templates/admin/import_export/change_list_import_item.html
+++ b/src/registrar/templates/admin/import_export/change_list_import_item.html
@@ -3,6 +3,6 @@
{% if has_import_permission %}
{% if not IS_PRODUCTION %}
-User to receive data
+ Data that will be added to:
{% for website in total_websites %}
diff --git a/src/registrar/templates/django/admin/portfolio_change_form.html b/src/registrar/templates/django/admin/portfolio_change_form.html
index 8de6cd5eb..e7a89574e 100644
--- a/src/registrar/templates/django/admin/portfolio_change_form.html
+++ b/src/registrar/templates/django/admin/portfolio_change_form.html
@@ -1,15 +1,15 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load custom_filters %}
{% load i18n static %}
{% block content %}
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get-senior-official-from-federal-agency-json' as url %}
-
+
{% url 'get-federal-and-portfolio-types-from-federal-agency-json' as url %}
-
+
{% url "admin:registrar_seniorofficial_add" as url %}
-
+
{{ block.super }}
{% endblock content %}
diff --git a/src/registrar/templates/django/admin/portfolio_invitation_change_form.html b/src/registrar/templates/django/admin/portfolio_invitation_change_form.html
index 959e8f8bf..cbd237f6d 100644
--- a/src/registrar/templates/django/admin/portfolio_invitation_change_form.html
+++ b/src/registrar/templates/django/admin/portfolio_invitation_change_form.html
@@ -1,4 +1,4 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load custom_filters %}
{% load i18n static %}
diff --git a/src/registrar/templates/django/admin/suborg_change_form.html b/src/registrar/templates/django/admin/suborg_change_form.html
index 25fe5700d..7bcc1ad9d 100644
--- a/src/registrar/templates/django/admin/suborg_change_form.html
+++ b/src/registrar/templates/django/admin/suborg_change_form.html
@@ -1,4 +1,4 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load i18n static %}
{% block after_related_objects %}
diff --git a/src/registrar/templates/django/admin/user_change_form.html b/src/registrar/templates/django/admin/user_change_form.html
index c0ddd8caf..3470d15f5 100644
--- a/src/registrar/templates/django/admin/user_change_form.html
+++ b/src/registrar/templates/django/admin/user_change_form.html
@@ -1,4 +1,4 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load i18n static %}
diff --git a/src/registrar/templates/django/admin/user_domain_role_change_form.html b/src/registrar/templates/django/admin/user_domain_role_change_form.html
index d8c298bc1..16431dcfc 100644
--- a/src/registrar/templates/django/admin/user_domain_role_change_form.html
+++ b/src/registrar/templates/django/admin/user_domain_role_change_form.html
@@ -1,4 +1,4 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load custom_filters %}
{% load i18n static %}
diff --git a/src/registrar/templates/django/admin/user_portfolio_permission_change_form.html b/src/registrar/templates/django/admin/user_portfolio_permission_change_form.html
index 489d67bc5..fd8eceb7d 100644
--- a/src/registrar/templates/django/admin/user_portfolio_permission_change_form.html
+++ b/src/registrar/templates/django/admin/user_portfolio_permission_change_form.html
@@ -1,4 +1,4 @@
-{% extends 'django/admin/email_clipboard_change_form.html' %}
+{% extends 'django/admin/base_change_form.html' %}
{% load custom_filters %}
{% load i18n static %}
diff --git a/src/registrar/templates/includes/domain_requests_table.html b/src/registrar/templates/includes/domain_requests_table.html
index 8adc0929a..e7e721125 100644
--- a/src/registrar/templates/includes/domain_requests_table.html
+++ b/src/registrar/templates/includes/domain_requests_table.html
@@ -2,7 +2,7 @@
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_domain_requests_json' as url %}
-{{url}}
+
diff --git a/src/registrar/templates/includes/member_domains_table.html b/src/registrar/templates/includes/member_domains_table.html
index 4e63fdbc3..ca916ff9a 100644
--- a/src/registrar/templates/includes/member_domains_table.html
+++ b/src/registrar/templates/includes/member_domains_table.html
@@ -20,7 +20,7 @@
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_member_domains_json' as url %}
-{{url}}
+
diff --git a/src/registrar/templates/includes/members_table.html b/src/registrar/templates/includes/members_table.html
index be1715f30..dc6c95c1b 100644
--- a/src/registrar/templates/includes/members_table.html
+++ b/src/registrar/templates/includes/members_table.html
@@ -4,7 +4,7 @@
{% comment %} Stores the json endpoint in a url for easier access {% endcomment %}
{% url 'get_portfolio_members_json' as url %}
-{{url}}
+
)
+ text_pattern = r"<[^>]+>"
+ text_only = re.sub(text_pattern, "", content)
+ # Clean up any extra whitespace
+ return text_only.strip()
- return extracted_text
+ return ""
@register.filter
diff --git a/src/registrar/tests/common.py b/src/registrar/tests/common.py
index bb65ef6b1..acbd8b65f 100644
--- a/src/registrar/tests/common.py
+++ b/src/registrar/tests/common.py
@@ -234,11 +234,14 @@ def __call__(self, request):
if request.user.is_anonymous:
user = None
UserModel = get_user_model()
- username = "Testy"
+ # Corresponds to a special user in our pa11y tests at id 9999.
+ # See fixtures_users.py for more details.
+ username = "80000000-0000-0000-0000-00000000a09b"
args = {
UserModel.USERNAME_FIELD: username,
}
user, _ = UserModel.objects.get_or_create(**args)
+ user.is_superuser = True
user.is_staff = True
# Create or retrieve the group
group, _ = UserGroup.objects.get_or_create(name="full_access_group")