Skip to content

Commit 224f0a4

Browse files
authored
Merge pull request #127 from bckohan/v2.0.3
fixes #124
2 parents c191c5d + bc2d7a3 commit 224f0a4

File tree

9 files changed

+203
-17
lines changed

9 files changed

+203
-17
lines changed

doc/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ sphinxcontrib-jsmath==1.0.1; python_version >= "3.5"
77
sphinxcontrib-qthelp==1.0.3; python_version >= "3.5"
88
sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.5"
99
sphinx-js==3.2.2; python_version >= "3.5"
10-
django-render-static==2.0.2
10+
django-render-static==2.0.3

doc/source/changelog.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22
Change Log
33
==========
44

5+
v2.0.3
6+
======
7+
* Fixed `Invalid URL generation for urls with default arguments. <https://github.com/bckohan/django-render-static/issues/124>`_
8+
9+
510
v2.0.2
611
======
712
* Fixed `Dependency bug, for python < 3.9 importlib_resource req should simply be >=1.3 <https://github.com/bckohan/django-render-static/issues/123>`_

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-render-static"
3-
version = "2.0.2"
3+
version = "2.0.3"
44
description = "Use Django's template engine to render static files at deployment or package time. Includes transpilers for extending Django's url reversal and enums to JavaScript."
55
authors = ["Brian Kohan <[email protected]>"]
66
license = "MIT"

render_static/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .transpilers.enums_to_js import EnumClassWriter
1515
from .transpilers.urls_to_js import ClassURLWriter, SimpleURLWriter
1616

17-
VERSION = (2, 0, 2)
17+
VERSION = (2, 0, 3)
1818

1919
__title__ = 'Django Render Static'
2020
__version__ = '.'.join(str(i) for i in VERSION)

render_static/tests/js_tests.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from datetime import date
1010
from enum import Enum
1111
from os import makedirs
12+
from pathlib import Path
1213
from time import perf_counter
1314

1415
import pytest
@@ -215,8 +216,8 @@ def get_members(cls):
215216
})
216217
class DefinesToJavascriptTest(StructureDiff, BaseTestCase):
217218

218-
def tearDown(self):
219-
pass
219+
# def tearDown(self):
220+
# pass
220221

221222
def test_classes_to_js(self):
222223
call_command('renderstatic', 'defines1.js')
@@ -560,8 +561,8 @@ def convert_idx_to_type(arr, idx, typ):
560561
})
561562
class URLSToJavascriptTest(URLJavascriptMixin, BaseTestCase):
562563

563-
def tearDown(self):
564-
pass
564+
# def tearDown(self):
565+
# pass
565566

566567
def setUp(self):
567568
self.clear_placeholder_registries()
@@ -3046,3 +3047,49 @@ def test_chained_enum_values(self):
30463047

30473048
#def tearDown(self):
30483049
# pass
3050+
3051+
3052+
@override_settings(
3053+
ROOT_URLCONF='render_static.tests.urls_default_args',
3054+
STATIC_TEMPLATES={
3055+
'ENGINES': [{
3056+
'BACKEND': 'render_static.backends.StaticDjangoTemplates',
3057+
'OPTIONS': {
3058+
'loaders': [
3059+
('render_static.loaders.StaticLocMemLoader', {
3060+
'urls.js': '{% urls_to_js %}'
3061+
})
3062+
],
3063+
'builtins': ['render_static.templatetags.render_static']
3064+
},
3065+
}]
3066+
}
3067+
)
3068+
class SitemapURLSToJavascriptTest(URLJavascriptMixin, BaseTestCase):
3069+
3070+
# def tearDown(self):
3071+
# pass
3072+
3073+
def setUp(self):
3074+
self.clear_placeholder_registries()
3075+
3076+
def test_sitemap_url_generation(self):
3077+
"""
3078+
Test class code with legacy arguments specified individually - may be deprecated in 2.0
3079+
"""
3080+
self.es6_mode = True
3081+
self.url_js = None
3082+
self.class_mode = ClassURLWriter.class_name_
3083+
call_command('renderstatic', 'urls.js')
3084+
self.assertEqual(self.get_url_from_js('sitemap'), reverse('sitemap'))
3085+
self.assertNotIn('sitemaps', Path(GLOBAL_STATIC_DIR / 'urls.js').read_text())
3086+
3087+
self.assertEqual(self.get_url_from_js('default', kwargs={'def': 'test'}), reverse('default', kwargs={'def': 'test'}))
3088+
3089+
self.assertIn('complex_default', Path(GLOBAL_STATIC_DIR / 'urls.js').read_text())
3090+
3091+
from render_static.tests.urls_default_args import Default
3092+
self.assertEqual(
3093+
self.get_url_from_js('default', kwargs={'def': 'blarg', 'complex_default': {'blog': Default}}),
3094+
reverse('default', kwargs={'def': 'blarg', 'complex_default': {'blog': Default}})
3095+
)

render_static/tests/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
'django.contrib.sites',
5656
'django.contrib.messages',
5757
'django.contrib.staticfiles',
58+
'django.contrib.sitemaps',
5859
'django.contrib.admin',
5960
)
6061

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django.contrib.sitemaps import Sitemap
2+
from django.contrib.sitemaps.views import sitemap
3+
from django.urls import include, path
4+
from django.utils.timezone import now
5+
from render_static.tests.views import TestView
6+
7+
8+
class BlogSitemap(Sitemap):
9+
changefreq = "never"
10+
priority = 0.5
11+
12+
13+
class Default:
14+
pass
15+
16+
17+
urlpatterns = [
18+
path(
19+
"sitemap.xml",
20+
sitemap,
21+
{"sitemaps": {
22+
"blog": BlogSitemap(),
23+
}},
24+
name="sitemap",
25+
),
26+
path(
27+
'complex_default/<str:def>',
28+
TestView.as_view(),
29+
{'complex_default': {
30+
'blog': Default
31+
}},
32+
name='default'
33+
),
34+
path(
35+
'default/<str:def>',
36+
TestView.as_view(),
37+
name='default'
38+
),
39+
]

render_static/tests/views.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
1+
from django.core.serializers.json import DjangoJSONEncoder
12
from django.http import JsonResponse
23
from django.views import View
34

45

6+
class DefaultStrEncoder(DjangoJSONEncoder):
7+
8+
def default(self, obj):
9+
return str(obj)
10+
11+
512
class TestView(View):
613

714
def get(self, request, *args, **kwargs):
@@ -16,4 +23,4 @@ def get(self, request, *args, **kwargs):
1623
'args': list(args),
1724
'kwargs': {**kwargs},
1825
'query': query
19-
})
26+
}, encoder=DefaultStrEncoder)

render_static/transpilers/urls_to_js.py

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
"""
77

88
import itertools
9+
import json
910
import re
1011
from abc import abstractmethod
1112
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Union
1213

1314
from django import VERSION as DJANGO_VERSION
1415
from django.conf import settings
16+
from django.core.serializers.json import DjangoJSONEncoder
1517
from django.urls import URLPattern, URLResolver, reverse
1618
from django.urls.exceptions import NoReverseMatch
1719
from django.urls.resolvers import RegexPattern, RoutePattern
@@ -386,7 +388,8 @@ def visit_pattern( # pylint: disable=R0914, R0915, R0912
386388
endpoint: URLPattern,
387389
qname: str,
388390
app_name: Optional[str],
389-
route: List[RoutePattern]
391+
route: List[RoutePattern],
392+
num_patterns: int
390393
) -> Generator[Optional[str], None, None]:
391394
"""
392395
Visit a pattern. Translates the pattern into a path component string
@@ -564,7 +567,7 @@ def get_params(pattern: Union[RoutePattern, RegexPattern]) -> Dict[
564567

565568
yield from self.visit_path(
566569
path, list(kwargs.keys()),
567-
endpoint.default_args
570+
endpoint.default_args if num_patterns > 1 else None
568571
)
569572

570573
else:
@@ -693,7 +696,7 @@ def visit_path_group(
693696
yield from self.enter_path_group(qname)
694697
for pattern in reversed(nodes):
695698
yield from self.visit_pattern(
696-
pattern, qname, app_name, route or []
699+
pattern, qname, app_name, route or [], num_patterns=len(nodes)
697700
)
698701
yield from self.exit_path_group(qname)
699702

@@ -945,7 +948,9 @@ def visit_path(
945948
yield f'return {quote}/{self.path_join(path).lstrip("/")}{quote};'
946949
self.outdent()
947950
else:
948-
opts_str = ",".join([f"'{param}'" for param in kwargs])
951+
opts_str = ",".join(
952+
[self.to_javascript(param) for param in kwargs]
953+
)
949954
yield (
950955
f'if (Object.keys(kwargs).length === {len(kwargs)} && '
951956
f'[{opts_str}].every(value => '
@@ -1093,6 +1098,71 @@ def reverse_jdoc(self) -> Generator[Optional[str], None, None]:
10931098
*/""".split('\n'):
10941099
yield comment_line[8:]
10951100

1101+
1102+
def deep_equal(self) -> Generator[Optional[str], None, None]:
1103+
"""
1104+
The recursive deepEqual function.
1105+
:yield: The JavaScript jdoc comment lines and deepEqual function.
1106+
"""
1107+
for comment_line in """
1108+
/**
1109+
* Given two values, do a deep equality comparison. If the values are
1110+
* objects, all keys and values are recursively compared.
1111+
*
1112+
* @param {Object} object1 - The first object to compare.
1113+
* @param {Object} object2 - The second object to compare.
1114+
*/""".split('\n'):
1115+
yield comment_line[8:]
1116+
yield 'deepEqual(object1, object2) {'
1117+
self.indent()
1118+
yield 'if (!(this.isObject(object1) && this.isObject(object2))) {'
1119+
self.indent()
1120+
yield 'return object1 === object2;'
1121+
self.outdent()
1122+
yield '}'
1123+
yield 'const keys1 = Object.keys(object1);'
1124+
yield 'const keys2 = Object.keys(object2);'
1125+
yield 'if (keys1.length !== keys2.length) {'
1126+
self.indent()
1127+
yield 'return false;'
1128+
self.outdent()
1129+
yield '}'
1130+
yield 'for (let key of keys1) {'
1131+
self.indent()
1132+
yield 'const val1 = object1[key];'
1133+
yield 'const val2 = object2[key];'
1134+
yield 'const areObjects = this.isObject(val1) && this.isObject(val2);'
1135+
yield 'if ('
1136+
self.indent()
1137+
yield '(areObjects && !deepEqual(val1, val2)) ||'
1138+
yield '(!areObjects && val1 !== val2)'
1139+
yield ') { return false; }'
1140+
self.outdent()
1141+
yield '}'
1142+
self.outdent()
1143+
yield 'return true;'
1144+
self.outdent()
1145+
yield '}'
1146+
1147+
def is_object(self) -> Generator[Optional[str], None, None]:
1148+
"""
1149+
The isObject() function.
1150+
:yield: The JavaScript jdoc comment lines and isObject function.
1151+
"""
1152+
for comment_line in """
1153+
/**
1154+
* Given a variable, return true if it is an object.
1155+
*
1156+
* @param {Object} object - The variable to check.
1157+
*/""".split('\n'):
1158+
yield comment_line[8:]
1159+
yield 'isObject(object) {'
1160+
self.indent()
1161+
yield 'return object != null && typeof object === "object";'
1162+
self.outdent()
1163+
yield '}'
1164+
1165+
10961166
def init_visit( # pylint: disable=R0915
10971167
self
10981168
) -> Generator[Optional[str], None, None]:
@@ -1159,7 +1229,7 @@ class code.
11591229
'{ return false; }'
11601230
)
11611231
else: # pragma: no cover
1162-
yield 'if (kwargs[key] !== val) { return false; }'
1232+
yield 'if (!this.deepEqual(kwargs[key], val)) { return false; }'
11631233
yield (
11641234
'if (!expected.includes(key)) '
11651235
'{ delete kwargs[key]; }'
@@ -1190,6 +1260,10 @@ class code.
11901260
self.outdent()
11911261
yield '}'
11921262
yield ''
1263+
yield from self.deep_equal()
1264+
yield ''
1265+
yield from self.is_object()
1266+
yield ''
11931267
yield from self.reverse_jdoc()
11941268
yield 'reverse(qname, options={}) {'
11951269
self.indent()
@@ -1331,9 +1405,20 @@ def visit_path(
13311405
:yield: The JavaScript lines of code
13321406
"""
13331407
quote = '`'
1408+
visitor = self
1409+
class ArgEncoder(DjangoJSONEncoder):
1410+
"""
1411+
An encoder that uses the configured to javascript function to
1412+
convert any unknown types to strings.
1413+
"""
1414+
1415+
def default(self, o):
1416+
return visitor.to_javascript(o).rstrip('"').lstrip('"')
1417+
1418+
defaults_str = json.dumps(defaults, cls=ArgEncoder)
13341419
if len(path) == 1: # there are no substitutions
13351420
if defaults:
1336-
yield f'if (this.#match(kwargs, args, [], {defaults})) ' \
1421+
yield f'if (this.#match(kwargs, args, [], {defaults_str})) ' \
13371422
f'{{ return "/{str(path[0]).lstrip("/")}"; }}'
13381423
else:
13391424
yield f'if (this.#match(kwargs, args)) ' \
@@ -1351,11 +1436,13 @@ def visit_path(
13511436
f'{quote}; }}'
13521437
)
13531438
else:
1354-
opts_str = ",".join([f"'{param}'" for param in kwargs])
1439+
opts_str = ",".join(
1440+
[self.to_javascript(param) for param in kwargs]
1441+
)
13551442
if defaults:
13561443
yield (
1357-
f'if (this.#match(kwargs, args, [{opts_str}], {defaults}))'
1358-
f' {{'
1444+
f'if (this.#match(kwargs, args, [{opts_str}], '
1445+
f'{defaults_str})) {{'
13591446
f' return {quote}/{self.path_join(path).lstrip("/")}'
13601447
f'{quote}; }}'
13611448
)

0 commit comments

Comments
 (0)