diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 559be3fca..f29dacc43 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,6 +1,6 @@ name: Bug description: Report a bug or problem with the application -labels: ["bug"] +labels: ["bug","dev"] body: - type: markdown diff --git a/docs/architecture/decisions/0027-ajax-for-dynamic-content.md b/docs/architecture/decisions/0027-ajax-for-dynamic-content.md new file mode 100644 index 000000000..6c4b750b1 --- /dev/null +++ b/docs/architecture/decisions/0027-ajax-for-dynamic-content.md @@ -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. diff --git a/src/registrar/admin.py b/src/registrar/admin.py index d097c900e..4b05bbb6d 100644 --- a/src/registrar/admin.py +++ b/src/registrar/admin.py @@ -2289,11 +2289,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(' {}', url, text) - return format_html('{}', url, text) + return format_html( + f'{escape(text)}' + ) + return text custom_requested_domain.admin_order_field = "requested_domain__name" # type: ignore diff --git a/src/registrar/assets/src/js/getgov-admin/button-utils.js b/src/registrar/assets/src/js/getgov-admin/button-utils.js new file mode 100644 index 000000000..e3746d289 --- /dev/null +++ b/src/registrar/assets/src/js/getgov-admin/button-utils.js @@ -0,0 +1,15 @@ +/** + * Initializes buttons to behave like links by navigating to their data-url attribute + * Example usage: + */ +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; + } + }); + }); +} diff --git a/src/registrar/assets/src/js/getgov-admin/main.js b/src/registrar/assets/src/js/getgov-admin/main.js index 5c6de20ab..7eb1fc8cd 100644 --- a/src/registrar/assets/src/js/getgov-admin/main.js +++ b/src/registrar/assets/src/js/getgov-admin/main.js @@ -16,6 +16,7 @@ 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(); @@ -23,6 +24,7 @@ initCopyToClipboard(); initFilterHorizontalWidget(); initDescriptions(); initSubmitBar(); +initButtonLinks(); // Domain request initIneligibleModal(); diff --git a/src/registrar/assets/src/sass/_theme/_admin.scss b/src/registrar/assets/src/sass/_theme/_admin.scss index 9a00cf022..bd55bbfcb 100644 --- a/src/registrar/assets/src/sass/_theme/_admin.scss +++ b/src/registrar/assets/src/sass/_theme/_admin.scss @@ -498,7 +498,7 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too font-size: 13px; } -.object-tools li button { +.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; @@ -520,6 +520,14 @@ input[type=submit].button--dja-toolbar:focus, input[type=submit].button--dja-too } } +// Mimic the style for +.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

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/templates/admin/change_form_object_tools.html b/src/registrar/templates/admin/change_form_object_tools.html index 2f3d282ea..d2ec555e2 100644 --- a/src/registrar/templates/admin/change_form_object_tools.html +++ b/src/registrar/templates/admin/change_form_object_tools.html @@ -7,10 +7,10 @@ {% if has_absolute_url %}

{% else %} @@ -30,18 +30,18 @@ {% endif %}
  • - {% translate "History" %} +
  • {% if opts.model_name == 'domainrequest' %}
  • - +
  • {% endif %} diff --git a/src/registrar/templates/admin/change_list_object_tools.html b/src/registrar/templates/admin/change_list_object_tools.html index 9a046b4bb..5ba88aa3a 100644 --- a/src/registrar/templates/admin/change_list_object_tools.html +++ b/src/registrar/templates/admin/change_list_object_tools.html @@ -5,9 +5,9 @@ {% if has_add_permission %}

    {% url cl.opts|admin_urlname:'add' as add_url %} - +

    {% endif %} {% endblock %} \ No newline at end of file diff --git a/src/registrar/templates/admin/change_list_results.html b/src/registrar/templates/admin/change_list_results.html index 5e4f37711..c5be04133 100644 --- a/src/registrar/templates/admin/change_list_results.html +++ b/src/registrar/templates/admin/change_list_results.html @@ -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 #} - +
    - +
    @@ -34,9 +34,9 @@ Load our custom filters to extract info from the django generated markup. {% if header.sortable %} {% if header.sort_priority > 0 %}
    - + {% if num_sorted_fields > 1 %}{{ header.sort_priority }}{% endif %} - +
    {% endif %} {% endif %} @@ -61,10 +61,10 @@ Load our custom filters to extract info from the django generated markup. {% endif %} {% 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/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 %} -
  • {% trans "Import" %}
  • +
  • {% endif %} {% endif %} diff --git a/src/registrar/templates/admin/search_form.html b/src/registrar/templates/admin/search_form.html new file mode 100644 index 000000000..c5fcf31f8 --- /dev/null +++ b/src/registrar/templates/admin/search_form.html @@ -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 %} +
    +{% endif %} \ No newline at end of file diff --git a/src/registrar/templates/emails/status_change_rejected.txt b/src/registrar/templates/emails/status_change_rejected.txt index e56d46a1f..e865031fa 100644 --- a/src/registrar/templates/emails/status_change_rejected.txt +++ b/src/registrar/templates/emails/status_change_rejected.txt @@ -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 diff --git a/src/registrar/templatetags/custom_filters.py b/src/registrar/templatetags/custom_filters.py index ff73e6dc1..e02a29e73 100644 --- a/src/registrar/templatetags/custom_filters.py +++ b/src/registrar/templatetags/custom_filters.py @@ -25,11 +25,15 @@ def extract_a_text(value): pattern = r"]*>(.*?)" match = re.search(pattern, value) if match: - extracted_text = match.group(1) - else: - extracted_text = "" + # Get the content and strip any nested HTML tags + content = match.group(1) + # Remove any nested HTML tags (like ) + 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