Skip to content

Commit 80d7816

Browse files
authored
Merge pull request #5 from bckohan/v1
Merge version 1 milestone updates
2 parents 25ab10b + 8f64df3 commit 80d7816

File tree

13 files changed

+1658
-489
lines changed

13 files changed

+1658
-489
lines changed

README.rst

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,8 @@ Your settings file might look like:
209209
'loaders': [
210210
('render_static.loaders.StaticLocMemLoader', {
211211
'urls.js': (
212-
'var urls = {\n
213-
{% urls_to_js exclude=exclude %}
214-
\n};'
212+
'{% urls_to_js visitor="render_static.ClassURLWriter" '
213+
'exclude=exclude %}'
215214
)
216215
})
217216
],
@@ -255,31 +254,49 @@ Then urls.js will look like this:
255254
256255
.. code:: javascript
257256
258-
var urls = {
259-
"simple": function(kwargs={}, args=[]) {
260-
if (Object.keys(kwargs).length === 0 && args.length === 0)
261-
return "/simple";
262-
if (
263-
Object.keys(kwargs).length === 1 &&
264-
['arg1'].every(value => kwargs.hasOwnProperty(value))
265-
)
266-
return `/simple/${kwargs["arg1"]}`;
267-
throw new TypeError("No reversal available for parameters at path: simple");
268-
},
269-
"different": function(kwargs={}, args=[]) {
270-
if (
271-
Object.keys(kwargs).length === 2 &&
272-
['arg1','arg2'].every(value => kwargs.hasOwnProperty(value))
273-
)
274-
return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`;
275-
throw new TypeError("No reversal available for parameters at path: different");
257+
class URLResolver {
258+
259+
match(kwargs, args, expected) {
260+
if (Array.isArray(expected)) {
261+
return Object.keys(kwargs).length === expected.length &&
262+
expected.every(value => kwargs.hasOwnProperty(value));
263+
} else if (expected) {
264+
return args.length === expected;
265+
} else {
266+
return Object.keys(kwargs).length === 0 && args.length === 0;
267+
}
276268
}
277-
}
269+
270+
reverse(qname, kwargs={}, args=[]) {
271+
let url = this.urls;
272+
for (const ns of qname.split(':')) {
273+
if (ns && url) { url = url.hasOwnProperty(ns) ? url[ns] : null; }
274+
}
275+
if (url) {
276+
let pth = url(kwargs, args);
277+
if (typeof pth === "string") { return pth; }
278+
}
279+
throw new TypeError(`No reversal available for parameters at path: ${qname}`);
280+
}
281+
282+
urls = {
283+
"simple": (kwargs={}, args=[]) => {
284+
if (this.match(kwargs, args)) { return "/simple/"; }
285+
if (this.match(kwargs, args, ['arg1'])) { return `/simple/${kwargs["arg1"]}`; }
286+
},
287+
"different": (kwargs={}, args=[]) => {
288+
if (this.match(kwargs, args, ['arg1','arg2'])) {
289+
return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`;
290+
}
291+
},
292+
}
293+
};
278294
279295
280296
So you can now fetch paths like this:
281297
282298
.. code:: javascript
283299
284300
// /different/143/emma
285-
urls.different({'arg1': 143, 'arg2': 'emma'});
301+
const urls = new URLResolver();
302+
urls.reverse('different', {'arg1': 143, 'arg2': 'emma'});

doc/source/changelog.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@ Change Log
33
==========
44

55

6+
v1.0.0
7+
====================
8+
9+
* New abstract visitor pattern allows customization of generated URL resolution javascript
10+
* A class generator is included which generates fully-fledged JavaScript class that includes a
11+
`reverse` function for urls that's directly analogous to Django's `reverse` function.
12+
* More common placeholders have been added as defaults that are always attempted if no
13+
registered placeholders are found to work, this should increase the success rate of
14+
out-of-the box URL generation.
15+
* Removed Jinja2 as a direct dependency - it is now in extras.
16+
* API is now considered production/stable.
17+
18+
619
v0.1.1
720
====================
821

doc/source/reference.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,28 @@ placeholders
8080
.. autofunction:: register_unnamed_placeholders
8181
.. autofunction:: resolve_placeholders
8282
.. autofunction:: resolve_unnamed_placeholders
83+
84+
85+
.. _javascript:
86+
87+
javascript
88+
----------------------------------------------------------------
89+
90+
.. automodule:: render_static.javascript
91+
92+
.. autoclass:: JavaScriptGenerator
93+
94+
95+
.. _url_tree:
96+
97+
url_tree
98+
----------------------------------------------------------------
99+
100+
.. automodule:: render_static.url_tree
101+
102+
.. autoclass:: URLTreeVisitor
103+
.. autoclass:: SimpleURLWriter
104+
.. autoclass:: ClassURLWriter
105+
.. autoclass:: Substitute
106+
.. autofunction:: normalize_ns
107+
.. autofunction:: build_tree

doc/source/templatetags.rst

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,35 @@ Tags
113113
``urls_to_js``
114114
~~~~~~~~~~~~~~
115115

116-
This tag spits out a JavaScript object that can be used in the same manner as Django's URL
117-
`reverse <https://docs.djangoproject.com/en/3.1/ref/urlresolvers/#reverse>`_ function.
116+
Often client side JavaScript needs to fetch site URLs asynchronously. These instances either
117+
necessitate using dynamic templating to reverse the url via the `url` tag or to hardcode the path
118+
into the JavaScript thereby violating the DRY principle. Frequently the need to generate these paths
119+
are the only thing driving the need to generate the JavaScript dynamically. But these paths might
120+
change only at deployment, not runtime, so the better approach is to generate JavaScript at
121+
deployment time and serve it statically. This tag makes that process even easier by automatically
122+
translating the site's url configuration into a JavaScript utility that can be used in the same
123+
manner as Django's URL `reverse <https://docs.djangoproject.com/en/3.1/ref/urlresolvers/#reverse>`_
124+
function.
118125

119126
It accepts a number of different parameters:
120127

128+
- **visitor** A string import path or a class that implements `URLTreeVisitor`. The visitor
129+
walks the URL tree and generates the JavaScript, users may customize the JavaScript generated
130+
by implementing their own visitor class. Two visitors are included. The default,
131+
`SimpleURLWriter`, spits out an object structure that indexes paths by their namespaces. The
132+
`ClassURLWriter`, spits out ES5 or 6 classes that provide a `reverse` function directly
133+
analogous to Django's reverse function.
121134
- **url_conf** The root url module to dump urls from. Can be an import string or an actual
122135
module type. default: settings.ROOT_URLCONF
123-
- **indent** String to use for indentation in javascript, default: '\\t'
136+
- **indent** String to use for indentation in javascript, default: '\\t', If None or the empty
137+
string is specified, the generated code will not contain newlines.
124138
- **depth** The starting indentation depth, default: 0
125139
- **include** A list of path names to include, namespaces without path names will be treated as
126140
every path under the namespace. Default: include everything
127141
- **exclude** A list of path names to exclude, namespaces without path names will be treated as
128142
every path under the namespace. Excludes override includes. Default: exclude nothing
143+
- **raise_on_not_found** If True (default), the generated JavaScript will raise a TypeError if
144+
asked to reverse an unrecognized URL name or set of arguments.
129145
- **es5** If True, dump es5 valid JavaScript, if False JavaScript will be es6, default: False
130146

131147
Includes and excludes are hierarchical strings that contain the fully qualified name of a namespace
@@ -136,6 +152,12 @@ not include paths directly under `namespace1`. Excludes always override includes
136152
path is included and no paths are excluded. If any includes are provided, then only those includes
137153
are included (everything else is by default excluded).
138154

155+
.. note::
156+
157+
When implementing custom URL visitors, any additional named arguments passed to the `urls_to_js`
158+
tag will be passed as kwargs to the URL visitor when this tag instantiates it. These parameters
159+
are meant to provide configuration toggles for the generated JavaScript.
160+
139161
.. warning::
140162

141163
All the URLs embedded in JavaScript are exposed client side. Its never a good idea to have site
@@ -240,3 +262,60 @@ Paths with unnamed arguments are also supported, but be kind to yourself and don
240262
Any number of placeholders may be registered against any number of variable/app_name combinations.
241263
When `urls_to_js` is run it won't give up until its tried all placeholders that might potentially
242264
match the path.
265+
266+
`ClassURLWriter`
267+
****************
268+
269+
A visitor class that produces ES5/6 JavaScript class is now included. This class is not used by
270+
default, but should be the preferred visitor for larger, more complex URL trees - mostly because
271+
it minifies better than the default `SimpleURLWriter`. To use the class writer::
272+
273+
{% urls_to_js visitor='render_static.ClassURLWriter' class_name='URLReverser' %}
274+
275+
This will generate an ES6 class by default::
276+
277+
class URLReverser {
278+
279+
match(kwargs, args, expected) {
280+
if (Array.isArray(expected)) {
281+
return Object.keys(kwargs).length === expected.length &&
282+
expected.every(value => kwargs.hasOwnProperty(value));
283+
} else if (expected) {
284+
return args.length === expected;
285+
} else {
286+
return Object.keys(kwargs).length === 0 && args.length === 0;
287+
}
288+
}
289+
290+
reverse(qname, kwargs={}, args=[]) {
291+
let url = this.urls;
292+
for (const ns of qname.split(':')) {
293+
if (ns && url) { url = url.hasOwnProperty(ns) ? url[ns] : null; }
294+
}
295+
if (url) {
296+
let pth = url(kwargs, args);
297+
if (typeof pth === "string") { return pth; }
298+
}
299+
throw new TypeError(`No reversal available for parameters at path: ${qname}`);
300+
}
301+
302+
urls = {
303+
"simple": (kwargs={}, args=[]) => {
304+
if (this.match(kwargs, args)) { return "/simple/"; }
305+
if (this.match(kwargs, args, ['arg1'])) { return `/simple/${kwargs["arg1"]}`; }
306+
},
307+
"different": (kwargs={}, args=[]) => {
308+
if (this.match(kwargs, args, ['arg1','arg2'])) {
309+
return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`;
310+
}
311+
},
312+
}
313+
};
314+
315+
Which can be used as::
316+
317+
// /different/143/emma
318+
const urls = new URLReverser();
319+
urls.reverse('different', {'arg1': 143, 'arg2': 'emma'});
320+
321+
The default `class_name` is URLResolver. Reverse should behave exactly as Django's `reverse`.

doc/source/tldr.rst

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
TL/DR
55
=====
66

7-
First go back to the install page and install `django-render-static` you lazy bum.
7+
First go back to the install page and install `django-render-static` if you haven't!
88

99
Generating Javascript Defines
1010
-----------------------------
@@ -110,9 +110,8 @@ Your settings file might look like::
110110
'loaders': [
111111
('render_static.loaders.StaticLocMemLoader', {
112112
'urls.js': (
113-
'var urls = {\n
114-
{% urls_to_js exclude=exclude %}
115-
\n};'
113+
'{% urls_to_js visitor="render_static.ClassURLWriter" '
114+
'exclude=exclude %}'
116115
)
117116
})
118117
],
@@ -150,34 +149,57 @@ If your root urls.py looks like this::
150149

151150
Then urls.js will look like this::
152151

153-
var urls = {
154-
"simple": function(kwargs={}, args=[]) {
155-
if (Object.keys(kwargs).length === 0 && args.length === 0)
156-
return "/simple";
157-
if (
158-
Object.keys(kwargs).length === 1 &&
159-
['arg1'].every(value => kwargs.hasOwnProperty(value))
160-
)
161-
return `/simple/${kwargs["arg1"]}`;
162-
throw new TypeError("No reversal available for parameters at path: simple");
163-
},
164-
"different": function(kwargs={}, args=[]) {
165-
if (
166-
Object.keys(kwargs).length === 2 &&
167-
['arg1','arg2'].every(value => kwargs.hasOwnProperty(value))
168-
)
169-
return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`;
170-
throw new TypeError("No reversal available for parameters at path: different");
152+
class URLResolver {
153+
154+
match(kwargs, args, expected) {
155+
if (Array.isArray(expected)) {
156+
return Object.keys(kwargs).length === expected.length &&
157+
expected.every(value => kwargs.hasOwnProperty(value));
158+
} else if (expected) {
159+
return args.length === expected;
160+
} else {
161+
return Object.keys(kwargs).length === 0 && args.length === 0;
162+
}
171163
}
172-
}
164+
165+
reverse(qname, kwargs={}, args=[]) {
166+
let url = this.urls;
167+
for (const ns of qname.split(':')) {
168+
if (ns && url) { url = url.hasOwnProperty(ns) ? url[ns] : null; }
169+
}
170+
if (url) {
171+
let pth = url(kwargs, args);
172+
if (typeof pth === "string") { return pth; }
173+
}
174+
throw new TypeError(`No reversal available for parameters at path: ${qname}`);
175+
}
176+
177+
urls = {
178+
"simple": (kwargs={}, args=[]) => {
179+
if (this.match(kwargs, args)) { return "/simple/"; }
180+
if (this.match(kwargs, args, ['arg1'])) { return `/simple/${kwargs["arg1"]}`; }
181+
},
182+
"different": (kwargs={}, args=[]) => {
183+
if (this.match(kwargs, args, ['arg1','arg2'])) {
184+
return `/different/${kwargs["arg1"]}/${kwargs["arg2"]}`;
185+
}
186+
},
187+
}
188+
};
173189

174190
So you can now fetch paths like this::
175191

176192
// /different/143/emma
177-
urls.different({'arg1': 143, 'arg2': 'emma'});
193+
const urls = new URLResolver();
194+
urls.reverse('different', {'arg1': 143, 'arg2': 'emma'});
178195

179196

180-
.. note::
197+
.. warning::
181198

182199
If you get an exception when you run render_static that originated from a PlaceholderNotFound
183200
exception, you need to register some :ref:`placeholders` before calling :ref:`urls_to_js`.
201+
202+
.. note::
203+
The JavaScript URL resolution is guaranteed to produce the same paths as Django's reversal
204+
mechanism. If it does not, this is a bug and we kindly ask
205+
`you to report it <https://github.com/bckohan/django-render-static/issues>`_.

pyproject.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-render-static"
3-
version = "0.1.1"
3+
version = "1.0.0"
44
description = "Use Django's template engine to render static files at deployment time. Extend Django's url reverse mechanism to JavaScript."
55
authors = ["Brian Kohan <[email protected]>"]
66
license = "MIT"
@@ -13,7 +13,7 @@ classifiers = [
1313
"Framework :: Django",
1414
"Operating System :: OS Independent",
1515
"Topic :: Software Development :: Libraries :: Python Modules",
16-
"Development Status :: 4 - Beta",
16+
"Development Status :: 5 - Production/Stable",
1717
"Framework :: Django :: 2.2",
1818
"Framework :: Django :: 3.0",
1919
"Framework :: Django :: 3.1",
@@ -41,8 +41,8 @@ exclude = ["render_static/tests"]
4141

4242
[tool.poetry.dependencies]
4343
python = "^3.6"
44-
Jinja2 = "^2.9"
4544
Django = "^2.2||^3.0"
45+
Jinja2 = { version = "^2.9", optional = true }
4646

4747
[tool.poetry.dev-dependencies]
4848
pytest = "^5.2"

render_static/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
/_/ |_|\___/_/ /_/\__,_/\___/_/ /____/\__/\__,_/\__/_/\___/
77
88
"""
9+
from .url_tree import ClassURLWriter, SimpleURLWriter, URLTreeVisitor
910

1011
__title__ = 'Django Render Static'
11-
__version__ = '0.1.1'
12+
__version__ = '1.0.0'
1213
__author__ = 'Brian Kohan'
1314
__license__ = 'MIT'
1415
__copyright__ = 'Copyright 2021 Brian Kohan'

0 commit comments

Comments
 (0)