Skip to content

Commit

Permalink
Merge pull request #66 from anentropic/distill-file-template-string
Browse files Browse the repository at this point in the history
Allow to customise output filename of urls with params
  • Loading branch information
meeb authored Dec 9, 2022
2 parents 185ab4e + 9240ea3 commit 0e6a436
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 21 deletions.
39 changes: 25 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,21 @@ argument.

Assuming you have an existing Django project, edit a `urls.py` to include the
`distill_path` function which replaces Django's standard `path` function and
supports the new keyword arguments `distill_func` and `distill_file`. The
`distill_func` argument should be provided with a function or callable class
that returns an iterable or None. The `distill_file` argument is entirely
optional and allows you to override the URL that would otherwise be generated
from the reverse of the URL regex. This allows you to rename URLs like
`/example` to any other name like `example.html`. As of v0.8 any URIs ending
in a slash `/` are automatically modified to end in `/index.html`. An example
distill setup for a theoretical blogging app would be:
supports the new keyword arguments `distill_func` and `distill_file`.

The `distill_func` argument should be provided with a function or callable
class that returns an iterable or `None`.

The `distill_file` argument is entirely optional and allows you to override the
URL that would otherwise be generated from the reverse of the URL regex. This
allows you to rename URLs like `/example` to any other name like
`example.html`. As of v0.8 any URIs ending in a slash `/` are automatically
modified to end in `/index.html`. You can use format string parameters in the
`distill_file` to customise the file name, arg values from the URL will be
substituted in, for example `{}` for positional args or `{param_name}` for
named args.

An example distill setup for a theoretical blogging app would be:

```python
# Replaces the standard django.conf.path, identical syntax
Expand Down Expand Up @@ -132,15 +139,19 @@ urlpatterns = (
# Note that for paths which have no paramters
# distill_func is optional
distill_func=get_index,
# / is not a valid file name! override it to index.html
# '' is not a valid file name! override it to index.html
distill_file='index.html'),
# e.g. /post/123-some-post-title using named parameters
distill_path('post/<int:blog_id>-<slug:blog_title>',
PostView.as_view(),
name='blog-post',
distill_func=get_all_blogposts),
distill_func=get_all_blogposts,
# url does not end in / nor has any file extension
# distill_file with format params is used here
distill_file="post/{blog_id}-{blog_title}.html"),
# e.g. /posts-by-year/2015 using positional parameters
distill_path('posts-by-year/<int:year>',
# url ends in / so file path will have /index.html appended
distill_path('posts-by-year/<int:year>/',
PostYear.as_view(),
name='blog-year',
distill_func=get_years),
Expand All @@ -153,9 +164,9 @@ passed back to Django for normal processing. This has no runtime performance
impact as this happens only once upon starting the application.

If your path has no URI paramters, such as `/` or `/some-static-url` you do
not have to specify the `distill_func` paramter if you don't want to. As for
paths with no paramters the `distill_func` always returns `None` this has is
set as the default behaviour for `distill_func`s.
not have to specify the `distill_func` parameter if you don't want to. As for
paths with no parameters the `distill_func` always returns `None`, this is set
as the default behaviour for `distill_func`s.

You can use the `distill_re_path` function as well, which replaces the default
`django.urls.re_path` function. Its usage is identical to the above:
Expand Down
18 changes: 12 additions & 6 deletions django_distill/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def render_file(self, view_name, status_codes, view_args, view_kwargs):
uri = self.generate_uri(url, view_name, view_args)
args = view_args
render = self.render_view(uri, status_codes, args, a)
file_name = self._get_filename(file_name, uri)
file_name = self._get_filename(file_name, uri, args)
return uri, file_name, render

def render_all_urls(self):
Expand All @@ -186,10 +186,10 @@ def render_all_urls(self):
if not param_set:
param_set = ()
elif self._is_str(param_set):
param_set = param_set,
param_set = (param_set,)
uri = self.generate_uri(url, view_name, param_set)
render = self.render_view(uri, status_codes, param_set, a)
file_name = self._get_filename(file_name_base, uri)
file_name = self._get_filename(file_name_base, uri, param_set)
yield uri, file_name, render

def render(self, view_name=None, status_codes=None, view_args=None, view_kwargs=None):
Expand All @@ -204,13 +204,19 @@ def render(self, view_name=None, status_codes=None, view_args=None, view_kwargs=
else:
return self.render_all_urls()

def _get_filename(self, file_name, uri):
if file_name is None and uri.endswith('/'):
def _get_filename(self, file_name, uri, param_set):
if file_name is not None:
if isinstance(param_set, dict):
return file_name.format(**param_set)
else:
return file_name.format(*param_set)
elif uri.endswith('/'):
# rewrite URIs ending with a slash to ../index.html
if uri.startswith('/'):
uri = uri[1:]
return uri + 'index.html'
return file_name
else:
return None

def _is_str(self, s):
return isinstance(s, str)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ def _blackhole(_):
('test',),
('re_path', '12345'),
('re_path', 'test'),
('re_path', 'x', '12345.html'),
('re_path', 'x', 'test.html'),
)
with tempfile.TemporaryDirectory() as tmpdirname:
with self.assertRaises(DistillError):
Expand Down Expand Up @@ -238,7 +240,7 @@ def _blackhole(_):
for expected_file in expected_files:
filepath = os.path.join(tmpdirname, *expected_file)
self.assertIn(filepath, written_files)
self.assertEqual(render_view_spy.call_count, 9)
self.assertEqual(render_view_spy.call_count, 12)

def test_sessions_are_ignored(self):
if settings.HAS_PATH:
Expand Down
20 changes: 20 additions & 0 deletions tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,20 @@ def test_flatpages_func():
test_positional_param_view,
name='re_path-positional-param',
distill_func=test_positional_param_func),
distill_re_path(r'^re_path/x/([\d]+)$',
test_positional_param_view,
name='re_path-positional-param-custom',
distill_func=test_positional_param_func,
distill_file="re_path/x/{}.html"),
distill_re_path(r'^re_path/(?P<param>[\w]+)$',
test_named_param_view,
name='re_path-named-param',
distill_func=test_named_param_func),
distill_re_path(r'^re_path/x/(?P<param>[\w]+)$',
test_named_param_view,
name='re_path-named-param-custom',
distill_func=test_named_param_func,
distill_file="re_path/x/{param}.html"),
distill_re_path(r'^re_path/broken$',
test_broken_view,
name='re_path-broken',
Expand Down Expand Up @@ -146,10 +156,20 @@ def test_flatpages_func():
test_positional_param_view,
name='path-positional-param',
distill_func=test_positional_param_func),
distill_path('path/x/<int>',
test_positional_param_view,
name='path-positional-param-custom',
distill_func=test_positional_param_func,
distill_file="path/x/{}.html"),
distill_path('path/<str:param>',
test_named_param_view,
name='path-named-param',
distill_func=test_named_param_func),
distill_path('path/x/<str:param>',
test_named_param_view,
name='path-named-param-custom',
distill_func=test_named_param_func,
distill_file="path/x/{param}.html"),
distill_path('path/broken',
test_broken_view,
name='path-broken',
Expand Down

0 comments on commit 0e6a436

Please sign in to comment.