Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add frontend template debug view #5661

Merged
merged 5 commits into from
Apr 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cfgov/cfgov/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,11 +792,12 @@
# middleware. This list of regular expressions defines a set of URLs against
# which we don't want this logic to be run.
PARSE_LINKS_EXCLUSION_LIST = [
# Wagtail admin pages, except preview and draft views
# Wagtail admin pages, except preview, draft, and debug views
(
r"^/admin/(?!"
r"pages/\d+/(edit/preview|view_draft)/|"
r"mega_menu/menu/preview/\w+/"
r"mega_menu/menu/preview/\w+/|"
r"template_debug/"
chosak marked this conversation as resolved.
Show resolved Hide resolved
r")"
),
# Django admin pages
Expand Down
59 changes: 59 additions & 0 deletions cfgov/v1/jinja2/v1/template_debug.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
{% extends "base.html" %}

{% block title %}Debugging template {{ debug_template_name }}{% endblock %}

{% block css %}
{{ super() }}
<style>
.test-case {
margin-top: 20px;
margin-bottom: 20px;
}

.test-case_link {
display: none;
font-size: 18px;
}

.test-case h2:hover .test-case_link {
display: inline-block;
}

.test-case_render {
border: 1px #b4b5b6 dotted;
chosak marked this conversation as resolved.
Show resolved Hide resolved
}
</style>
{% endblock %}

{% block header %}{% endblock %}

{% block content %}
<div class="wrapper content_wrapper">
<div class="content_main">
<h1>{{ debug_template_name }}</h1>

{% if debug_test_cases %}
<ul>
{% for name in debug_test_cases.keys() %}
<li><a href="#{{ name | slugify }}">{{ name }}</a></li>
{% endfor %}
</ul>
{% endif %}

{% for name, rendered in debug_test_cases.items() %}
<div class="content_line"></div>
<div id="{{ name | slugify }}" class="test-case">
<h2>
{{ name }}
<a class="test-case_link" href="#{{ name | slugify }}">#</a>
</h2>
<div class="test-case_render">
{{ rendered | safe }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

{% block footer %}{% endblock %}
26 changes: 26 additions & 0 deletions cfgov/v1/template_debug/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from wagtail.core import hooks

from v1.views.template_debug import TemplateDebugView

from .notification import notification_test_cases # noqa 401


try:
from django.urls import re_path
except ImportError: # pragma: nocover
from django.conf.urls import url as re_path


def register_template_debug(app_name, url_path, template_name, test_cases):
@hooks.register('register_admin_urls')
def register_template_debug_url():
return [
re_path(
rf'^template_debug/{app_name}/{url_path}/',
TemplateDebugView.as_view(
debug_template_name=template_name,
debug_test_cases=test_cases
),
name=f'template_debug_{app_name}_{url_path}'
),
]
98 changes: 98 additions & 0 deletions cfgov/v1/template_debug/notification.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from django.utils.safestring import mark_safe


notification_defaults = {
'type': 'warning',
'is_visible': True,
'message': 'This is a test notification.',
'explanation': None,
'links': [],
}


message_with_html_tags = (
'Visit <a href="https://www.consumerfinance.gov/">'
'www.consumerfinance.gov</a>.'
)


consumer_tools_message = 'Consumer tools'


consumer_tools_explanation = (
'Wherever you are on your financial journey, you can prepare yourself '
'to make informed financial decisions with these resources.'
)


consumer_tools_links = [
{
'text': 'Auto loans',
'url': 'https://www.consumerfinance.gov/consumer-tools/auto-loans/',
},
{
'text': 'Bank accounts & services',
'url': 'https://www.consumerfinance.gov/consumer-tools/bank-accounts/',
},
{
'text': 'Credit cards',
'url': 'https://www.consumerfinance.gov/consumer-tools/credit-cards/',
},
]


notification_test_cases = {
'Warning message': {},

'Success message': {
'type': 'success',
},

'Error message': {
'type': 'error',
},

'Message including Unicode characters': {
'message': 'Use “curly quotes” instead of "straight quotes".',
},

'Message including raw HTML tags': {
'message': message_with_html_tags,
},

'Message including HTML tags passed through mark_safe': {
'message': mark_safe(message_with_html_tags),
},

'Long message': {
'message': (
'Some people, when confronted with a problem, think “I know, '
'I\'ll use regular expressions.” Now they have two problems.'
),
},

'Message with explanation': {
'message': consumer_tools_message,
'explanation': consumer_tools_explanation,
},

'Message with links': {
'message': consumer_tools_message,
'links': consumer_tools_links,
},

'Message with explanation and links': {
'message': consumer_tools_message,
'explanation': consumer_tools_explanation,
'links': consumer_tools_links,
},

'Invisible': {
'is_visible': False,
},
}


for test_case in notification_test_cases.values():
for k, v in notification_defaults.items():
test_case.setdefault(k, v)
51 changes: 51 additions & 0 deletions cfgov/v1/tests/views/test_template_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.core.exceptions import ImproperlyConfigured
from django.test import RequestFactory, SimpleTestCase

from v1.views.template_debug import TemplateDebugView


class TemplateDebugViewTests(SimpleTestCase):
def setUp(self):
self.request = RequestFactory().get('/')

def test_misconfigured_without_parameters(self):
with self.assertRaises(ImproperlyConfigured):
TemplateDebugView.as_view()(self.request)

def test_renders_debug_template_name(self):
debug_template_name = '_includes/atoms/hyperlink.html'

view = TemplateDebugView.as_view(
debug_template_name=debug_template_name,
debug_test_cases={}
)
response = view(self.request)

self.assertContains(response, debug_template_name)

def test_renders_debug_template_with_test_cases(self):
test_cases = {
'First': {
'url': 'https://example.com/first/',
'text': 'First test case to be rendered',
},
'Second': {
'url': 'https://example.com/second/',
'text': 'Second test case to be rendered',
},
}

view = TemplateDebugView.as_view(
debug_template_name='_includes/atoms/hyperlink.html',
debug_test_cases=test_cases
)

response = view(self.request)

# The view should render the provided template (the hyperlink atom)
# with each of the test cases.
for name, test_case in test_cases.items():
self.assertContains(
response,
f'href="{test_case["url"]}"'
)
chosak marked this conversation as resolved.
Show resolved Hide resolved
30 changes: 30 additions & 0 deletions cfgov/v1/views/template_debug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.core.exceptions import ImproperlyConfigured
from django.template import loader
from django.views.generic import TemplateView


class TemplateDebugView(TemplateView):
template_name = 'v1/template_debug.html'
debug_template_name = None
debug_test_cases = None

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)

if self.debug_template_name is None or self.debug_test_cases is None:
raise ImproperlyConfigured(
"TemplateDebugView requires definition of "
"debug_template_name and debug_test_cases"
)

template = loader.get_template(self.debug_template_name)

context.update({
'debug_template_name': self.debug_template_name,
'debug_test_cases': {
name: template.render({'value': data})
for name, data in self.debug_test_cases.items()
},
})

return context
9 changes: 9 additions & 0 deletions cfgov/v1/wagtail_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from v1.models.portal_topics import PortalCategory, PortalTopic
from v1.models.resources import Resource
from v1.models.snippets import Contact, RelatedResource, ReusableText
from v1.template_debug import notification_test_cases, register_template_debug
from v1.util import util


Expand Down Expand Up @@ -404,3 +405,11 @@ def add_export_feedback_permission_to_wagtail_admin_group_view():
content_type__app_label='v1',
codename='export_feedback'
)


register_template_debug(
'v1',
'notification',
'_includes/molecules/notification.html',
notification_test_cases
)
59 changes: 59 additions & 0 deletions docs/debugging-templates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Debugging Templates

Module templates can be debugged visually through use of a `TemplateDebugView`
that renders a single module with a series of test cases.

Consider an example module that renders a simple hyperlink. This module
requires a target URL and also accepts optional link text. A simple Jinja
template for this module might look like this:

```html
<a href="{{ value.url }}">
chosak marked this conversation as resolved.
Show resolved Hide resolved
{{ value.text | default( value.url, true ) }}
</a>
```

## Defining template test cases

Next, define test cases for the module that cover all supported input
configurations. For this simple link module, there are only a few useful
test cases, but more complicated modules might have many more.

Test cases should be defined as a Python dict, where the key is a string name
of the test case and the value is a dict that will be passed to the module
template.

```py
# myapp/template_debug.py
Copy link
Member

@anselmbradford anselmbradford Apr 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it matter that you have myapp/template_debug.py here but the actual file path is myapp/views/template_debug.py?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the actual view itself lives in the v1 app, because it has to live somewhere. This is the single Django view used to render any templates form anywhere. Individual apps (including v1) also need a place to define the test data used when debugging templates, and I propose to use a convention of myapp.template_debug for this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't quite call the test data module that in this PR, either though. As discussed in chat, I would suggest renaming test_cases.py to notifications.py and use a different module for each new set of test data. Otherwise this file gon' get MASSIVE. Then update this line as such:

Suggested change
# myapp/template_debug.py
# myapp/template_debug/link.py

link_test_cases = {
'Link without text': {
'url': 'https://www.consumerfinance.gov',
},

'Link with empty text': {
'url': 'https://www.consumerfinance.gov',
'text': '',
},

'Link with text': {
'url': 'https://www.consumerfinance.gov',
'text': 'Visit our website',
},
}
```

## Registering the template debug view

The next step is to register the template debug view with Django so that it can
be loaded in a browser.

```py
# in myapp/wagtail_hooks.py
from myapp.template_debug import link_test_cases
chosak marked this conversation as resolved.
Show resolved Hide resolved
from v1.template_debug import register_template_debug

register_template_debug('myapp', 'link', 'myapp/link.html', link_test_cases)
```

Once logged into the Wagtail admin, the template debug view for this module
will now be available at the `/admin/template_debug/myapp/link/` URL.
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ nav:
- Pages and Components:
- Atomic Structure and Design: atomic-structure.md
- Creating and Editing Components: editing-components.md
- Debugging Templates: debugging-templates.md
- Django and Wagtail Migrations: migrations.md
- Forms in Wagtail Page Context: forms-in-wagtail.md
- Wagtail Pages: wagtail-pages.md
Expand Down