Skip to content

Add render_patterns management command for static export and automated render check (#17, #18) #123

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

Merged
merged 10 commits into from
Nov 2, 2020
Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,4 @@ local.py
# -------------------------------------------------
# Your own project's ignores
# -------------------------------------------------
dpl-rendered-patterns
5 changes: 4 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ Then you can install the dependencies and run the test app:

```sh
poetry install
poetry run 'django-admin runserver --settings=tests.settings --pythonpath=.'
# Start the server for testing:
poetry run django-admin runserver --settings=tests.settings --pythonpath=.
# Or to try out the render_patterns command:
poetry run django-admin render_patterns --settings=tests.settings --pythonpath=. --dry-run --verbosity 2
```

### Run a local build with docker
Expand Down
Empty file.
Empty file.
115 changes: 115 additions & 0 deletions pattern_library/management/commands/render_patterns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
from pathlib import Path

from django.core.management.base import BaseCommand
from django.template.loader import render_to_string
from django.test.client import RequestFactory

from pattern_library import (
get_base_template_names, get_pattern_base_template_name
)
from pattern_library.utils import (
get_pattern_context, get_pattern_templates, get_template_ancestors,
render_pattern
)


class Command(BaseCommand):
help = "Renders all django-pattern-library patterns to HTML files, in a directory structure."

def add_arguments(self, parser):
super().add_arguments(parser)
parser.add_argument(
'--output',
'-o',
action='store',
dest='output_dir',
default='dpl-rendered-patterns',
help='Directory where to render your patterns',
type=str,
)
parser.add_argument(
'--dry-run',
action='store_true',
help="Render the patterns without writing them to disk.",
)
parser.add_argument(
'--wrap-fragments',
action='store_true',
help="Render fragment patterns wrapped in the base template.",
)

def handle(self, **options):
self.verbosity = options['verbosity']
self.dry_run = options['dry_run']
self.wrap_fragments = options['wrap_fragments']
self.output_dir = options['output_dir']

templates = get_pattern_templates()

factory = RequestFactory()
request = factory.get('/')

if self.verbosity >= 2:
if self.dry_run:
self.stderr.write(f'Target directory: {self.output_dir}. Dry run, not writing files to disk')
else:
self.stderr.write(f'Target directory: {self.output_dir}')

if self.wrap_fragments:
self.stderr.write('Writing fragment patterns wrapped in base template')

# Resolve the output dir according to the directory the command is run from.
parent_dir = Path.cwd().joinpath(self.output_dir)

if not self.dry_run:
parent_dir.mkdir(exist_ok=True)

self.render_group(request, parent_dir, templates)

def render_group(self, request, parent_dir: Path, pattern_templates):
for template in pattern_templates['templates_stored']:
if self.verbosity >= 2:
self.stderr.write(f'Pattern: {template.pattern_name}')
if self.verbosity >= 1:
self.stderr.write(template.origin.template_name)

render_path = parent_dir.joinpath(template.pattern_name)
rendered_pattern = self.render_pattern(request, template.origin.template_name)

if self.dry_run:
if self.verbosity >= 2:
self.stdout.write(rendered_pattern)
else:
render_path.write_text(rendered_pattern)

if not pattern_templates['template_groups']:
return

for pattern_type_group, pattern_templates in pattern_templates['template_groups'].items():
if self.verbosity >= 2:
self.stderr.write(f'Group: {pattern_type_group}')
group_parent = parent_dir.joinpath(pattern_type_group)
if not self.dry_run:
group_parent.mkdir(exist_ok=True)
self.render_group(request, group_parent, pattern_templates)

def render_pattern(self, request, pattern_template_name):
rendered_pattern = render_pattern(request, pattern_template_name)

# If we don’t wrap fragments in the base template, we can simply render the pattern and return as-is.
if not self.wrap_fragments:
return rendered_pattern

pattern_template_ancestors = get_template_ancestors(
pattern_template_name,
context=get_pattern_context(pattern_template_name),
)
pattern_is_fragment = set(pattern_template_ancestors).isdisjoint(set(get_base_template_names()))

if pattern_is_fragment:
base_template = get_pattern_base_template_name()
context = get_pattern_context(base_template)
context['pattern_library_rendered_pattern'] = rendered_pattern
return render_to_string(base_template, request=request, context=context)
else:
return rendered_pattern
2 changes: 2 additions & 0 deletions tests/templates/patterns/atoms/icons/icon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
context:
name: close
92 changes: 92 additions & 0 deletions tests/tests/test_commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import io
import shutil
import tempfile
from pathlib import Path

from django.core.management import call_command
from django.test import SimpleTestCase


class RenderPatternsTests(SimpleTestCase):
"""Tests of the render_pattern command’s output, based on the test project’s templates"""

def test_displays_patterns(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=True, stdout=stdout, stderr=stderr)
self.assertIn("""patterns/atoms/tags_test_atom/tags_test_atom.html
patterns/atoms/test_atom/test_atom.html
""", stderr.getvalue())

def test_verbose_output(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=True, stdout=stdout, stderr=stderr, verbosity=2)
self.assertIn("""Target directory: dpl-rendered-patterns. Dry run, not writing files to disk
Group: atoms
Group: icons
Pattern: icon.html
patterns/atoms/icons/icon.html
""", stderr.getvalue())
self.assertIn("""<svg class="icon icon--close" aria-hidden="true" focusable="false">
<use xlink:href="#close"></use>
</svg>""", stdout.getvalue())

def test_quiet_output(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=True, stdout=stdout, stderr=stderr, verbosity=0)
self.assertEqual(stdout.getvalue(), '')
self.assertEqual(stderr.getvalue(), '')

def test_shows_output_folder(self):
stdout = io.StringIO()
stderr = io.StringIO()
temp = tempfile.gettempdir()
call_command('render_patterns', dry_run=True, stdout=stdout, stderr=stderr, output=temp, verbosity=2)
self.assertIn(temp, stderr.getvalue())

def test_shows_wrap_fragment(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=True, wrap_fragments=True, stdout=stdout, stderr=stderr, verbosity=2)
self.assertIn('Writing fragment patterns wrapped in base template', stderr.getvalue())
# Only testing a small subset of the output just to show patterns are wrapped.
self.assertIn("""<svg class="icon icon--close" aria-hidden="true" focusable="false">
<use xlink:href="#close"></use>
</svg>

<script src="/static/main.js"></script>
</body>
</html>""", stdout.getvalue())


class RenderPatternsFileSystemTests(SimpleTestCase):
"""Tests of the render_pattern command’s file system changes, based on the test project’s templates"""

def setUp(self):
self.output = tempfile.mkdtemp()

def tearDown(self):
shutil.rmtree(self.output)

def test_uses_output(self):
stdout = io.StringIO()
stderr = io.StringIO()
modification_time_before = Path(self.output).stat().st_mtime
call_command('render_patterns', dry_run=False, stdout=stdout, stderr=stderr, output=self.output)
self.assertNotEqual(Path(self.output).stat().st_mtime, modification_time_before)

def test_uses_subfolders(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=False, stdout=stdout, stderr=stderr, output=self.output)
subfolders = Path(self.output).iterdir()
self.assertIn('atoms', [p.name for p in subfolders])

def test_outputs_html(self):
stdout = io.StringIO()
stderr = io.StringIO()
call_command('render_patterns', dry_run=False, stdout=stdout, stderr=stderr, output=self.output)
html_files = Path(self.output).glob('**/*.html')
self.assertIn('test_atom.html', [p.name for p in html_files])
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ install_command =
./tox_install.sh {packages}
commands =
poetry run ./runtests.py
poetry run django-admin render_patterns --settings=tests.settings --pythonpath=. --dry-run
deps =
dj22: Django>=2.2,<2.3
dj30: Django>=3.0,<3.1
Expand Down