mirror of
https://github.com/cisagov/manage.get.gov.git
synced 2025-07-23 11:16:07 +02:00
Merge branch 'main' into za/1612-modal-double-readout-attempt-2
This commit is contained in:
commit
a30c3a19d3
14 changed files with 195 additions and 24 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
|
||||
|
|
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.
|
|
@ -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('<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
|
||||
|
||||
|
|
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 <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
|
||||
|
@ -984,3 +992,7 @@ ul.add-list-reset {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
#result_list > tbody tr > th > a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
{% 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 %}
|
||||
|
@ -30,18 +30,18 @@
|
|||
{% 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="#">
|
||||
<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>
|
||||
|
|
|
@ -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 %}
|
|
@ -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
|
||||
|
|
|
@ -25,11 +25,15 @@ def extract_a_text(value):
|
|||
pattern = r"<a\b[^>]*>(.*?)</a>"
|
||||
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 <img>)
|
||||
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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue