├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── jdj_tags ├── __init__.py └── extensions.py ├── setup.cfg ├── setup.py ├── tests.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | build/ 10 | develop-eggs/ 11 | dist/ 12 | eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | 22 | # Installer logs 23 | pip-log.txt 24 | pip-delete-this-directory.txt 25 | 26 | # Unit test / coverage reports 27 | .tox/ 28 | .coverage 29 | .cache 30 | nosetests.xml 31 | coverage.xml 32 | 33 | # Translations 34 | *.mo 35 | 36 | # Mr Developer 37 | .mr.developer.cfg 38 | .project 39 | .pydevproject 40 | 41 | # Rope 42 | .ropeproject 43 | 44 | # Django stuff: 45 | *.log 46 | *.pot 47 | 48 | # Sphinx documentation 49 | docs/_build/ 50 | 51 | # vim files 52 | .*.swp 53 | 54 | # sqlite db 55 | *.db 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Moritz Sichert 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | jinja2-django-tags 3 | ================== 4 | .. image:: https://img.shields.io/pypi/v/jinja2-django-tags.svg 5 | :alt: jinja2-django-tags on pypi 6 | :target: https://pypi.python.org/pypi/jinja2-django-tags 7 | 8 | This little library contains extensions for jinja2 that add template tags to 9 | jinja2 that you are used to from django templates. 10 | 11 | The following tags are included: 12 | 13 | - `csrf_token`_ 14 | - `trans and blocktrans`_ 15 | - `now`_ 16 | - `static`_ 17 | - `url`_ 18 | 19 | There is also an extension for `localizing template variables`_. 20 | 21 | .. _trans and blocktrans: trans-blocktrans_ 22 | .. _localizing template variables: Localization_ 23 | 24 | Requirements 25 | ============ 26 | 27 | This library requires at least Django 1.8 because there official jinja2 support 28 | was added. 29 | 30 | If you want to use jinja2 templates in older versions of Django, take a look 31 | at `django-jinja `_. 32 | 33 | This library has been tested on Python 2.7, 3.4 and 3.5, Jinja 2.7 and 2.8, and 34 | Django 1.8, 1.9 and 1.10. 35 | 36 | Usage 37 | ===== 38 | To use the tags, just run ``setup.py install`` from the base directory or 39 | ``pip install jinja2-django-tags`` and add the extensions to your ``TEMPLATES`` 40 | settings: 41 | 42 | .. code-block:: python 43 | 44 | TEMPLATES = [ 45 | { 46 | 'BACKEND': 'django.template.backends.jinja2.Jinja2', 47 | 'DIRS': [], 48 | 'APP_DIRS': True, 49 | 'OPTIONS': { 50 | 'extensions': [ 51 | 'jdj_tags.extensions.DjangoStatic', 52 | 'jdj_tags.extensions.DjangoI18n', 53 | ] 54 | }, 55 | }, 56 | } 57 | 58 | If you want all tags at once use ``jdj_tags.extensions.DjangoCompat`` in 59 | the ``extensions`` Option. 60 | 61 | Tags 62 | ==== 63 | 64 | csrf_token 65 | ---------- 66 | The ``{% csrf_token %}`` tag comes with ``jdj_tags.extensions.DjangoCsrf``. 67 | 68 | .. _trans-blocktrans: 69 | trans, blocktrans 70 | ----------------- 71 | The i18n tags are defined in ``jdj_tags.extensions.DjangoI18n``. 72 | The extension also tries to localize variables (such as dates and numbers) if 73 | ``USE_L10N`` is set in django settings. 74 | 75 | ``{% trans %}`` works as it does in django: 76 | 77 | .. code-block:: html+django/jinja 78 | 79 | Simple example: {% trans 'Hello World' %} 80 | 81 | {% trans "I was saved to a variable!" as translated_var %} 82 | Save to a variable: {{ translated_var }} 83 | 84 | Translation with context: 85 | {% trans 'Hello World' context 'second hello world example' %} 86 | 87 | Noop translation: {% trans "Please don't translate me!" noop %} 88 | 89 | 90 | ``{% blocktrans %}`` works as it does in django including ``with``, ``trimmed``, 91 | ``context``, ``count`` and ``asvar`` arguments: 92 | 93 | 94 | .. code-block:: html+django/jinja 95 | 96 | Simple example: {% blocktrans %}Hello World!{% endblocktrans %} 97 | 98 | Variables: 99 | {% url 'my_view' as my_url %} 100 | {% blocktrans with my_upper_url=my_url|upper %} 101 | Normal url: {{ my_url }} 102 | Upper url: {{ my_upper_url }} 103 | {% endblocktrans %} 104 | 105 | Trim whitespace and save to variable: 106 | {% blocktrans trimmed asvar translated_var %} 107 | Trim those 108 | pesky newlines. 109 | {% endblocktrans %} 110 | Translated text: {{ translated_var }} 111 | 112 | You can also use ``_``, ``gettext`` and ``pgettext`` directly: 113 | 114 | .. code-block:: html+django/jinja 115 | 116 | Simple example: {{ _('Hello World') }} 117 | More verbose: {{ gettext('Hello World') }} 118 | With context: {{ pgettext('Hello World', 'another example') }} 119 | 120 | 121 | now 122 | --- 123 | The ``{% now %}`` tag comes with ``jdj_tags.extensions.DjangoNow``. 124 | It works the same as in Django: 125 | 126 | .. code-block:: html+django/jinja 127 | 128 | Current year: {% now 'Y' %} 129 | 130 | {% now 'Y' as cur_year %} 131 | Copyright My Company, {{ cur_year }} 132 | 133 | 134 | static 135 | ------ 136 | The ``{% static %}`` tag comes with ``jdj_tags.extensions.DjangoStatic``. 137 | It works the same as in Django: 138 | 139 | .. code-block:: html+django/jinja 140 | 141 | My static file: {% static 'my/static.file' %} 142 | 143 | {% static 'my/static.file' as my_file %} 144 | My static file in a var: {{ my_file }} 145 | 146 | 147 | url 148 | --- 149 | The ``{% url %}`` tag is defined in ``jdj_tags.extensions.DjangoUrl``. 150 | It works as it does in django, therefore you can only specify either 151 | args or kwargs: 152 | 153 | .. code-block:: html+django/jinja 154 | Url with args: {% url 'my_view' arg1 "string arg2" %} 155 | Url with kwargs: {% url 'my_view' kwarg1=arg1 kwarg2="string arg2" %} 156 | 157 | Save to variable: 158 | {% url 'my_view' 'foo' 'bar' as my_url %} 159 | {{ my_url }} 160 | 161 | 162 | Localization 163 | ============ 164 | 165 | The ``jdj_tags.extensions.DjangoL10n`` extension implements localization of template variables 166 | with respect to ``USE_L10N`` and ``USE_TZ`` settings: 167 | 168 | .. code-block:: python 169 | 170 | >>> from datetime import datetime 171 | >>> from django.utils import timezone, translation 172 | >>> from jinja2 import Extension 173 | >>> env = Environment(extensions=[DjangoL10n]) 174 | >>> template = env.from_string("{{ a_number }} {{ a_date }}") 175 | >>> context = { 176 | ... 'a_number': 1.23, 177 | ... 'a_date': datetime(2000, 10, 1, 14, 10, 12, tzinfo=timezone.utc), 178 | ... } 179 | >>> translation.activate('en') 180 | >>> timezone.activate('America/Argentina/Buenos_Aires') 181 | >>> template.render(context) 182 | '1.23 Oct. 1, 2000, 11:10 a.m.' 183 | >>> translation.activate('de') 184 | >>> translation.activate('Europe/Berlin') 185 | >>> template.render(context) 186 | '1,23 1. Oktober 2000 16:10' 187 | -------------------------------------------------------------------------------- /jdj_tags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MoritzS/jinja2-django-tags/b1abd17423d81ad24cd93e948bec3d304dd44631/jdj_tags/__init__.py -------------------------------------------------------------------------------- /jdj_tags/extensions.py: -------------------------------------------------------------------------------- 1 | """ 2 | jinja2 extensions that add django tags. 3 | """ 4 | from __future__ import unicode_literals 5 | 6 | from datetime import datetime 7 | 8 | from django.conf import settings 9 | from django.templatetags.static import static as django_static 10 | from django.utils.encoding import force_text 11 | from django.utils.formats import date_format, localize 12 | from django.utils.timezone import get_current_timezone, template_localtime 13 | from django.utils.translation import npgettext, pgettext, ugettext, ungettext 14 | from jinja2 import lexer, nodes 15 | from jinja2.ext import Extension 16 | 17 | try: 18 | from django.urls import reverse 19 | except: 20 | from django.core.urlresolvers import reverse 21 | 22 | 23 | class DjangoCsrf(Extension): 24 | """ 25 | Implements django's `{% csrf_token %}` tag. 26 | """ 27 | tags = set(['csrf_token']) 28 | 29 | def parse(self, parser): 30 | lineno = parser.stream.expect('name:csrf_token').lineno 31 | call = self.call_method( 32 | '_csrf_token', 33 | [nodes.Name('csrf_token', 'load', lineno=lineno)], 34 | lineno=lineno 35 | ) 36 | return nodes.Output([nodes.MarkSafe(call)]) 37 | 38 | def _csrf_token(self, csrf_token): 39 | if not csrf_token or csrf_token == 'NOTPROVIDED': 40 | return '' 41 | else: 42 | return '' \ 43 | .format(csrf_token) 44 | 45 | 46 | class DjangoI18n(Extension): 47 | """ 48 | Implements django's `{% trans %}` and `{% blocktrans %}` tags. 49 | 50 | `{% trans %}` works as it does in django:: 51 | 52 | Simple example: {% trans 'Hello World' %} 53 | 54 | {% trans "I was saved to a variable!" as translated_var %} 55 | Save to a variable: {{ translated_var }} 56 | 57 | Translation with context: 58 | {% trans 'Hello World' context 'second hello world example' %} 59 | 60 | Noop translation: {% trans "Please don't translate me!" noop %} 61 | 62 | 63 | `{% blocktrans %}` works as it does in django including `with`, `trimmed`, 64 | `context`, `count` and `asvar` arguments:: 65 | 66 | Simple example: {% blocktrans %}Hello World!{% endblocktrans %} 67 | 68 | Variables: 69 | {% url 'my_view' as my_url %} 70 | {% blocktrans with my_upper_url=my_url|upper %} 71 | Normal url: {{ my_url }} 72 | Upper url: {{ my_upper_url }} 73 | {% endblocktrans %} 74 | 75 | Trim whitespace and save to variable: 76 | {% blocktrans trimmed asvar translated_var %} 77 | Trim those 78 | pesky newlines. 79 | {% endblocktrans %} 80 | Translated text: {{ translated_var }} 81 | 82 | You also can use `_`, `gettext` and `pgettext` directly:: 83 | 84 | Simple example: {{ _('Hello World') }} 85 | More verbose: {{ gettext('Hello World') }} 86 | With context: {{ pgettext('Hello World', 'another example') }} 87 | 88 | """ 89 | tags = set(['trans', 'blocktrans']) 90 | 91 | def __init__(self, environment): 92 | super(DjangoI18n, self).__init__(environment) 93 | environment.globals['_'] = ugettext 94 | environment.globals['gettext'] = ugettext 95 | environment.globals['pgettext'] = pgettext 96 | 97 | def _parse_trans(self, parser, lineno): 98 | string = parser.stream.expect(lexer.TOKEN_STRING) 99 | string = nodes.Const(string.value, lineno=string.lineno) 100 | is_noop = False 101 | context = None 102 | as_var = None 103 | for token in iter(lambda: parser.stream.next_if(lexer.TOKEN_NAME), None): 104 | if token.value == 'noop' and not is_noop: 105 | if context is not None: 106 | parser.fail("noop translation can't have context", lineno=token.lineno) 107 | is_noop = True 108 | elif token.value == 'context' and context is None: 109 | if is_noop: 110 | parser.fail("noop translation can't have context", lineno=token.lineno) 111 | context = parser.stream.expect(lexer.TOKEN_STRING) 112 | context = nodes.Const(context.value, lineno=context.lineno) 113 | elif token.value == 'as' and as_var is None: 114 | as_var = parser.stream.expect(lexer.TOKEN_NAME) 115 | as_var = nodes.Name(as_var.value, 'store', lineno=as_var.lineno) 116 | else: 117 | parser.fail("expected 'noop', 'context' or 'as'", lineno=token.lineno) 118 | if is_noop: 119 | output = string 120 | elif context is not None: 121 | func = nodes.Name('pgettext', 'load', lineno=lineno) 122 | output = nodes.Call(func, [context, string], [], None, None, lineno=lineno) 123 | else: 124 | func = nodes.Name('gettext', 'load') 125 | output = nodes.Call(func, [string], [], None, None, lineno=lineno) 126 | 127 | if as_var is None: 128 | return nodes.Output([output], lineno=lineno) 129 | else: 130 | return nodes.Assign(as_var, output, lineno=lineno) 131 | 132 | def _parse_blocktrans(self, parser, lineno): 133 | with_vars = {} 134 | count = None 135 | context = None 136 | trimmed = False 137 | as_var = None 138 | 139 | if parser.stream.skip_if('name:trimmed'): 140 | trimmed = True 141 | 142 | if parser.stream.skip_if('name:asvar'): 143 | as_var = parser.stream.expect(lexer.TOKEN_NAME) 144 | as_var = nodes.Name(as_var.value, 'store', lineno=as_var.lineno) 145 | 146 | if parser.stream.skip_if('name:with'): 147 | while parser.stream.look().type == lexer.TOKEN_ASSIGN: 148 | token = parser.stream.expect(lexer.TOKEN_NAME) 149 | key = token.value 150 | next(parser.stream) 151 | with_vars[key] = parser.parse_expression(False) 152 | 153 | if parser.stream.skip_if('name:count'): 154 | name = parser.stream.expect(lexer.TOKEN_NAME).value 155 | parser.stream.expect(lexer.TOKEN_ASSIGN) 156 | value = parser.parse_expression(False) 157 | count = (name, value) 158 | 159 | if parser.stream.skip_if('name:context'): 160 | context = parser.stream.expect(lexer.TOKEN_STRING).value 161 | 162 | parser.stream.expect(lexer.TOKEN_BLOCK_END) 163 | 164 | body_singular = None 165 | body = [] 166 | additional_vars = set() 167 | for token in parser.stream: 168 | if token is lexer.TOKEN_EOF: 169 | parser.fail('unexpected end of template, expected endblocktrans tag') 170 | if token.type is lexer.TOKEN_DATA: 171 | body.append(token.value) 172 | elif token.type is lexer.TOKEN_VARIABLE_BEGIN: 173 | name = parser.stream.expect(lexer.TOKEN_NAME).value 174 | if name not in with_vars and (count is None or count[0] != name): 175 | additional_vars.add(name) 176 | parser.stream.expect(lexer.TOKEN_VARIABLE_END) 177 | # django converts variables inside the blocktrans tag into 178 | # "%(var_name)s" format, so we do the same. 179 | body.append('%({})s'.format(name)) 180 | elif token.type is lexer.TOKEN_BLOCK_BEGIN: 181 | if body_singular is None and parser.stream.skip_if('name:plural'): 182 | if count is None: 183 | parser.fail('used plural without specifying count') 184 | parser.stream.expect(lexer.TOKEN_BLOCK_END) 185 | body_singular = body 186 | body = [] 187 | else: 188 | parser.stream.expect('name:endblocktrans') 189 | break 190 | 191 | if count is not None and body_singular is None: 192 | parser.fail('plural form not found') 193 | 194 | trans_vars = [ 195 | nodes.Pair(nodes.Const(key), val, lineno=lineno) 196 | for key, val in with_vars.items() 197 | ] 198 | 199 | if count is not None: 200 | trans_vars.append( 201 | nodes.Pair(nodes.Const(count[0]), count[1], lineno=lineno) 202 | ) 203 | 204 | trans_vars.extend( 205 | nodes.Pair( 206 | nodes.Const(key), 207 | nodes.Name(key, 'load', lineno=lineno), 208 | lineno=lineno 209 | ) 210 | for key in additional_vars 211 | ) 212 | 213 | kwargs = [ 214 | nodes.Keyword('trans_vars', nodes.Dict(trans_vars, lineno=lineno), lineno=lineno) 215 | ] 216 | 217 | if context is not None: 218 | kwargs.append( 219 | nodes.Keyword('context', nodes.Const(context, lineno=lineno), lineno=lineno) 220 | ) 221 | if count is not None: 222 | kwargs.append( 223 | nodes.Keyword('count_var', nodes.Const(count[0], lineno=lineno), lineno=lineno) 224 | ) 225 | 226 | body = ''.join(body) 227 | if trimmed: 228 | body = ' '.join(map(lambda s: s.strip(), body.strip().splitlines())) 229 | 230 | if body_singular is not None: 231 | body_singular = ''.join(body_singular) 232 | if trimmed: 233 | body_singular = ' '.join( 234 | map(lambda s: s.strip(), body_singular.strip().splitlines()) 235 | ) 236 | 237 | if body_singular is None: 238 | args = [] 239 | else: 240 | args = [nodes.TemplateData(body_singular, lineno=lineno)] 241 | args.append(nodes.TemplateData(body, lineno=lineno)) 242 | call = nodes.MarkSafe(self.call_method('_make_blocktrans', args, kwargs), lineno=lineno) 243 | 244 | if as_var is None: 245 | return nodes.Output([call], lineno=lineno) 246 | else: 247 | return nodes.Assign(as_var, call) 248 | 249 | def _make_blocktrans(self, singular, plural=None, context=None, trans_vars=None, 250 | count_var=None): 251 | if trans_vars is None: 252 | trans_vars = {} # pragma: no cover 253 | if self.environment.finalize: 254 | finalized_trans_vars = { 255 | key: self.environment.finalize(val) for key, val in trans_vars.items() 256 | } 257 | else: 258 | finalized_trans_vars = trans_vars 259 | if plural is None: 260 | if context is None: 261 | return ugettext(force_text(singular)) % finalized_trans_vars 262 | else: 263 | return pgettext(force_text(context), force_text(singular)) % finalized_trans_vars 264 | else: 265 | if context is None: 266 | return ungettext( 267 | force_text(singular), force_text(plural), trans_vars[count_var] 268 | ) % finalized_trans_vars 269 | else: 270 | return npgettext( 271 | force_text(context), force_text(singular), force_text(plural), 272 | trans_vars[count_var] 273 | ) % finalized_trans_vars 274 | 275 | def parse(self, parser): 276 | token = next(parser.stream) 277 | if token.value == 'blocktrans': 278 | return self._parse_blocktrans(parser, token.lineno) 279 | else: 280 | return self._parse_trans(parser, token.lineno) 281 | 282 | 283 | class DjangoL10n(Extension): 284 | """ 285 | Implements localization of template variables with respect to 286 | `USE_L10N` and `USE_TZ` settings:: 287 | 288 | >>> from datetime import datetime 289 | >>> from django.utils import timezone, translation 290 | >>> from jinja2 import Extension 291 | >>> env = Environment(extensions=[DjangoL10n]) 292 | >>> template = env.from_string("{{ a_number }} {{ a_date }}") 293 | >>> context = { 294 | ... 'a_number': 1.23, 295 | ... 'a_date': datetime(2000, 10, 1, 14, 10, 12, tzinfo=timezone.utc), 296 | ... } 297 | >>> translation.activate('en') 298 | >>> timezone.activate('America/Argentina/Buenos_Aires') 299 | >>> template.render(context) 300 | '1.23 Oct. 1, 2000, 11:10 a.m.' 301 | >>> translation.activate('de') 302 | >>> translation.activate('Europe/Berlin') 303 | >>> template.render(context) 304 | '1,23 1. Oktober 2000 16:10' 305 | 306 | """ 307 | 308 | def __init__(self, environment): 309 | super(DjangoL10n, self).__init__(environment) 310 | finalize = [] 311 | if settings.USE_TZ: 312 | finalize.append(template_localtime) 313 | if settings.USE_L10N: 314 | finalize.append(localize) 315 | 316 | if finalize: 317 | fns = iter(finalize) 318 | if environment.finalize is None: 319 | new_finalize = next(fns) 320 | else: 321 | new_finalize = environment.finalize 322 | for f in fns: 323 | new_finalize = self._compose(f, new_finalize) 324 | 325 | environment.finalize = new_finalize 326 | 327 | @staticmethod 328 | def _compose(f, g): 329 | return lambda var: f(g(var)) 330 | 331 | 332 | class DjangoStatic(Extension): 333 | """ 334 | Implements django's `{% static %}` tag:: 335 | 336 | My static file: {% static 'my/static.file' %} 337 | 338 | {% static 'my/static.file' as my_file %} 339 | My static file in a var: {{ my_file }} 340 | 341 | """ 342 | tags = set(['static']) 343 | 344 | def _static(self, path): 345 | return django_static(path) 346 | 347 | def parse(self, parser): 348 | lineno = next(parser.stream).lineno 349 | token = parser.stream.expect(lexer.TOKEN_STRING) 350 | path = nodes.Const(token.value) 351 | call = self.call_method('_static', [path], lineno=lineno) 352 | 353 | token = parser.stream.current 354 | if token.test('name:as'): 355 | next(parser.stream) 356 | as_var = parser.stream.expect(lexer.TOKEN_NAME) 357 | as_var = nodes.Name(as_var.value, 'store', lineno=as_var.lineno) 358 | return nodes.Assign(as_var, call, lineno=lineno) 359 | else: 360 | return nodes.Output([call], lineno=lineno) 361 | 362 | 363 | class DjangoNow(Extension): 364 | """ 365 | Implements django's `{% now %}` tag. 366 | """ 367 | tags = set(['now']) 368 | 369 | def _now(self, format_string): 370 | tzinfo = get_current_timezone() if settings.USE_TZ else None 371 | cur_datetime = datetime.now(tz=tzinfo) 372 | return date_format(cur_datetime, format_string) 373 | 374 | def parse(self, parser): 375 | lineno = next(parser.stream).lineno 376 | token = parser.stream.expect(lexer.TOKEN_STRING) 377 | format_string = nodes.Const(token.value) 378 | call = self.call_method('_now', [format_string], lineno=lineno) 379 | 380 | token = parser.stream.current 381 | if token.test('name:as'): 382 | next(parser.stream) 383 | as_var = parser.stream.expect(lexer.TOKEN_NAME) 384 | as_var = nodes.Name(as_var.value, 'store', lineno=as_var.lineno) 385 | return nodes.Assign(as_var, call, lineno=lineno) 386 | else: 387 | return nodes.Output([call], lineno=lineno) 388 | 389 | 390 | class DjangoUrl(Extension): 391 | """ 392 | Imlements django's `{% url %}` tag. 393 | It works as it does in django, therefore you can only specify either 394 | args or kwargs:: 395 | 396 | Url with args: {% url 'my_view' arg1 "string arg2" %} 397 | Url with kwargs: {% url 'my_view' kwarg1=arg1 kwarg2="string arg2" %} 398 | 399 | Save to variable: 400 | {% url 'my_view' 'foo' 'bar' as my_url %} 401 | {{ my_url }} 402 | """ 403 | tags = set(['url']) 404 | 405 | def _url_reverse(self, name, *args, **kwargs): 406 | return reverse(name, args=args, kwargs=kwargs) 407 | 408 | @staticmethod 409 | def parse_expression(parser): 410 | # Due to how the jinja2 parser works, it treats "foo" "bar" as a single 411 | # string literal as it is the case in python. 412 | # But the url tag in django supports multiple string arguments, e.g. 413 | # "{% url 'my_view' 'arg1' 'arg2' %}". 414 | # That's why we have to check if it's a string literal first. 415 | token = parser.stream.current 416 | if token.test(lexer.TOKEN_STRING): 417 | expr = nodes.Const(force_text(token.value), lineno=token.lineno) 418 | next(parser.stream) 419 | else: 420 | expr = parser.parse_expression(False) 421 | 422 | return expr 423 | 424 | def parse(self, parser): 425 | lineno = next(parser.stream).lineno 426 | view_name = parser.stream.expect(lexer.TOKEN_STRING) 427 | view_name = nodes.Const(view_name.value, lineno=view_name.lineno) 428 | 429 | args = None 430 | kwargs = None 431 | as_var = None 432 | 433 | while parser.stream.current.type != lexer.TOKEN_BLOCK_END: 434 | token = parser.stream.current 435 | if token.test('name:as'): 436 | next(parser.stream) 437 | token = parser.stream.expect(lexer.TOKEN_NAME) 438 | as_var = nodes.Name(token.value, 'store', lineno=token.lineno) 439 | break 440 | if args is not None: 441 | args.append(self.parse_expression(parser)) 442 | elif kwargs is not None: 443 | if token.type != lexer.TOKEN_NAME: 444 | parser.fail( 445 | "got '{}', expected name for keyword argument" 446 | "".format(lexer.describe_token(token)), 447 | lineno=token.lineno 448 | ) 449 | arg = token.value 450 | next(parser.stream) 451 | parser.stream.expect(lexer.TOKEN_ASSIGN) 452 | token = parser.stream.current 453 | kwargs[arg] = self.parse_expression(parser) 454 | else: 455 | if parser.stream.look().type == lexer.TOKEN_ASSIGN: 456 | kwargs = {} 457 | else: 458 | args = [] 459 | continue 460 | 461 | if args is None: 462 | args = [] 463 | args.insert(0, view_name) 464 | 465 | if kwargs is not None: 466 | kwargs = [nodes.Keyword(key, val) for key, val in kwargs.items()] 467 | 468 | call = self.call_method('_url_reverse', args, kwargs, lineno=lineno) 469 | if as_var is None: 470 | return nodes.Output([call], lineno=lineno) 471 | else: 472 | return nodes.Assign(as_var, call, lineno=lineno) 473 | 474 | 475 | class DjangoCompat(DjangoCsrf, DjangoI18n, DjangoL10n, DjangoNow, DjangoStatic, DjangoUrl): 476 | """ 477 | Combines all extensions to one, so you don't have to put all of them 478 | in the django settings. 479 | """ 480 | tags = set(['csrf_token', 'trans', 'blocktrans', 'now', 'static', 'url']) 481 | 482 | _tag_class = { 483 | 'csrf_token': DjangoCsrf, 484 | 'trans': DjangoI18n, 485 | 'blocktrans': DjangoI18n, 486 | 'now': DjangoNow, 487 | 'static': DjangoStatic, 488 | 'url': DjangoUrl, 489 | } 490 | 491 | def parse(self, parser): 492 | name = parser.stream.current.value 493 | cls = self._tag_class.get(name) 494 | if cls is None: 495 | parser.fail("got unexpected tag '{}'".format(name)) # pragma: no cover 496 | return cls.parse(self, parser) 497 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore = F812 6 | exclude = .git,build 7 | max-line-length = 100 8 | 9 | [isort] 10 | combine_as_imports = true 11 | line_length = 100 12 | known_third_party = django,jinja2 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name='jinja2-django-tags', 7 | version='0.5', 8 | author='Moritz Sichert', 9 | author_email='moritz.sichert@googlemail.com', 10 | url='https://github.com/MoritzS/jinja2-django-tags', 11 | description='jinja2 extensions that add django tags', 12 | license='BSD', 13 | packages=['jdj_tags'], 14 | install_requires=[ 15 | 'Django>=1.8', 16 | 'Jinja2>=2.7', 17 | ], 18 | classifiers=[ 19 | 'Framework :: Django', 20 | 'Environment :: Web Environment', 21 | 'Intended Audience :: Developers', 22 | 'License :: OSI Approved :: BSD License', 23 | 'Programming Language :: Python :: 2', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.4', 27 | 'Programming Language :: Python :: 3.5', 28 | ], 29 | keywords='jinja2 jinja django template tags', 30 | ) 31 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | from __future__ import unicode_literals 3 | 4 | import datetime 5 | 6 | from django.test import SimpleTestCase, override_settings 7 | from django.test.utils import requires_tz_support 8 | from django.utils import timezone, translation 9 | from jinja2 import Environment, TemplateSyntaxError 10 | from jinja2.ext import Extension 11 | 12 | from jdj_tags.extensions import (DjangoCompat, DjangoCsrf, DjangoI18n, DjangoL10n, DjangoNow, 13 | DjangoStatic, DjangoUrl) 14 | 15 | try: 16 | from unittest import mock 17 | except ImportError: 18 | import mock 19 | 20 | 21 | class DjangoCsrfTest(SimpleTestCase): 22 | def setUp(self): 23 | self.env = Environment(extensions=[DjangoCsrf]) 24 | self.template = self.env.from_string("{% csrf_token %}") 25 | 26 | def test_token(self): 27 | context = {'csrf_token': 'a_csrf_token'} 28 | expected = '' 29 | 30 | self.assertEqual(expected, self.template.render(context)) 31 | 32 | def test_empty_token(self): 33 | context1 = {} 34 | context2 = {'csrf_token': 'NOTPROVIDED'} 35 | 36 | self.assertEqual('', self.template.render(context1)) 37 | self.assertEqual('', self.template.render(context2)) 38 | 39 | 40 | class DjangoI18nTestBase(SimpleTestCase): 41 | @staticmethod 42 | def _gettext(message): 43 | return '{} - translated'.format(message) 44 | 45 | @staticmethod 46 | def _pgettext(context, message): 47 | return 'alt translated' 48 | 49 | @staticmethod 50 | def _ngettext(singular, plural, number): 51 | return 'count translated' 52 | 53 | @staticmethod 54 | def _npgettext(context, singular, plural, number): 55 | return 'count alt translated' 56 | 57 | def setUp(self): 58 | gettext_patcher = mock.patch('jdj_tags.extensions.ugettext', side_effect=self._gettext) 59 | pgettext_patcher = mock.patch('jdj_tags.extensions.pgettext', side_effect=self._pgettext) 60 | ngettext_patcher = mock.patch('jdj_tags.extensions.ungettext', side_effect=self._ngettext) 61 | npgettext_patcher = mock.patch('jdj_tags.extensions.npgettext', side_effect=self._npgettext) 62 | 63 | self.gettext = gettext_patcher.start() 64 | self.pgettext = pgettext_patcher.start() 65 | self.ngettext = ngettext_patcher.start() 66 | self.npgettext = npgettext_patcher.start() 67 | 68 | self.addCleanup(gettext_patcher.stop) 69 | self.addCleanup(pgettext_patcher.stop) 70 | self.addCleanup(ngettext_patcher.stop) 71 | self.addCleanup(npgettext_patcher.stop) 72 | 73 | self.env = Environment(extensions=[DjangoI18n]) 74 | 75 | 76 | class DjangoI18nTransTest(DjangoI18nTestBase): 77 | def test_simple_trans(self): 78 | template1 = self.env.from_string("{% trans 'Hello World' %}") 79 | template2 = self.env.from_string( 80 | "{% trans 'Hello World' context 'some context' %}" 81 | ) 82 | template3 = self.env.from_string("{{ _('Hello World') }}") 83 | template4 = self.env.from_string("{{ gettext('Hello World') }}") 84 | template5 = self.env.from_string("{{ pgettext('some context', 'Hello World') }}") 85 | 86 | self.assertEqual('Hello World - translated', template1.render()) 87 | self.gettext.assert_called_with('Hello World') 88 | self.assertEqual('alt translated', template2.render()) 89 | self.pgettext.assert_called_with('some context', 'Hello World') 90 | self.assertEqual('Hello World - translated', template3.render()) 91 | self.gettext.assert_called_with('Hello World') 92 | self.assertEqual('Hello World - translated', template4.render()) 93 | self.gettext.assert_called_with('Hello World') 94 | self.assertEqual('alt translated', template5.render()) 95 | self.pgettext.assert_called_with('some context', 'Hello World') 96 | 97 | def test_noop(self): 98 | template = self.env.from_string("{% trans 'Hello World' noop %}") 99 | 100 | self.assertEqual('Hello World', template.render()) 101 | self.gettext.assert_not_called() 102 | 103 | def test_as_var(self): 104 | template = self.env.from_string( 105 | "{% trans 'Hello World' as myvar %}My var is: {{ myvar }}!" 106 | ) 107 | 108 | self.assertEqual('My var is: Hello World - translated!', template.render()) 109 | self.gettext.assert_called_with('Hello World') 110 | 111 | def test_noop_as_var(self): 112 | template1 = self.env.from_string( 113 | "{% trans 'Hello World' noop as myvar %}My var is: {{ myvar }}!" 114 | ) 115 | template2 = self.env.from_string( 116 | "{% trans 'Hello World' as myvar noop %}My var is: {{ myvar }}!" 117 | ) 118 | 119 | expected_str = 'My var is: Hello World!' 120 | 121 | self.assertEqual(expected_str, template1.render()) 122 | self.gettext.assert_not_called() 123 | self.assertEqual(expected_str, template2.render()) 124 | self.gettext.assert_not_called() 125 | 126 | def test_errors(self): 127 | template1 = "{% trans 'Hello World' foo %}" 128 | template2 = "{% trans 'Hello World' noop context 'some context' %}" 129 | template3 = "{% trans 'Hello World' context 'some context' noop %}" 130 | 131 | error_messages = [ 132 | (template1, "expected 'noop', 'context' or 'as'"), 133 | (template2, "noop translation can't have context"), 134 | (template3, "noop translation can't have context"), 135 | ] 136 | 137 | for template, msg in error_messages: 138 | with self.assertRaisesMessage(TemplateSyntaxError, msg): 139 | self.env.from_string(template) 140 | 141 | 142 | class DjangoI18nBlocktransTest(DjangoI18nTestBase): 143 | def test_simple(self): 144 | template1 = self.env.from_string('{% blocktrans %}Translate me!{% endblocktrans %}') 145 | template2 = self.env.from_string( 146 | "{% blocktrans context 'foo' %}Translate me!{% endblocktrans %}" 147 | ) 148 | 149 | self.assertEqual('Translate me! - translated', template1.render()) 150 | self.gettext.assert_called_with('Translate me!') 151 | self.assertEqual('alt translated', template2.render()) 152 | self.pgettext.assert_called_with('foo', 'Translate me!') 153 | 154 | def test_trimmed(self): 155 | template = self.env.from_string("""{% blocktrans trimmed %} 156 | Translate 157 | me! 158 | {% endblocktrans %}""") 159 | 160 | self.assertEqual('Translate me! - translated', template.render()) 161 | self.gettext.assert_called_with('Translate me!') 162 | 163 | def test_with(self): 164 | template1 = self.env.from_string( 165 | "{% blocktrans with foo=bar %}Trans: {{ foo }}{% endblocktrans %}" 166 | ) 167 | template2 = self.env.from_string( 168 | "{% blocktrans with foo=bar spam=eggs %}Trans: {{ foo }} and " 169 | "{{ spam }}{% endblocktrans %}" 170 | ) 171 | template3 = self.env.from_string( 172 | "{{ foo }} {% blocktrans with foo=bar %}Trans: {{ foo }}" 173 | "{% endblocktrans %} {{ foo }}" 174 | ) 175 | template4 = self.env.from_string( 176 | "{% blocktrans with foo=bar|upper %}Trans: {{ foo }}{% endblocktrans %}" 177 | ) 178 | 179 | self.assertEqual( 180 | 'Trans: barvar - translated', 181 | template1.render({'bar': 'barvar'}) 182 | ) 183 | self.gettext.assert_called_with('Trans: %(foo)s') 184 | self.assertEqual( 185 | 'Trans: barvar and eggsvar - translated', 186 | template2.render({'bar': 'barvar', 'eggs': 'eggsvar'}) 187 | ) 188 | self.gettext.assert_called_with('Trans: %(foo)s and %(spam)s') 189 | self.assertEqual( 190 | 'foovar Trans: barvar - translated foovar', 191 | template3.render({'foo': 'foovar', 'bar': 'barvar'}) 192 | ) 193 | self.gettext.assert_called_with('Trans: %(foo)s') 194 | self.assertEqual( 195 | 'Trans: BARVAR - translated', 196 | template4.render({'bar': 'barvar'}) 197 | ) 198 | self.gettext.assert_called_with('Trans: %(foo)s') 199 | 200 | def test_global_var(self): 201 | template = self.env.from_string("{% blocktrans %}Trans: {{ foo }}{% endblocktrans %}") 202 | 203 | self.assertEqual( 204 | 'Trans: foovar - translated', 205 | template.render({'foo': 'foovar'}) 206 | ) 207 | self.gettext.assert_called_with('Trans: %(foo)s') 208 | 209 | def test_count(self): 210 | template1 = self.env.from_string( 211 | "{% blocktrans count counter=foo %}Singular{% plural %}" 212 | "Plural {{ counter }}{% endblocktrans %}" 213 | ) 214 | template2 = self.env.from_string( 215 | """{% blocktrans trimmed count counter=foo %} 216 | Singular 217 | {% plural %} 218 | Plural {{ counter }} 219 | {% endblocktrans %}""" 220 | ) 221 | template3 = self.env.from_string( 222 | "{% blocktrans count counter=foo context 'mycontext' %}" 223 | "Singular{% plural %}Plural {{ counter }}{% endblocktrans %}" 224 | ) 225 | 226 | self.assertEqual('count translated', template1.render({'foo': 123})) 227 | self.ngettext.assert_called_with('Singular', 'Plural %(counter)s', 123) 228 | self.assertEqual('count translated', template2.render({'foo': 123})) 229 | self.ngettext.assert_called_with('Singular', 'Plural %(counter)s', 123) 230 | self.assertEqual('count alt translated', template3.render({'foo': 123})) 231 | self.npgettext.assert_called_with('mycontext', 'Singular', 'Plural %(counter)s', 123) 232 | 233 | def test_count_finalize(self): 234 | def finalize(value): 235 | # force convert to string 236 | return '{}'.format(value) 237 | 238 | self.env.finalize = finalize 239 | 240 | template = self.env.from_string( 241 | "{% blocktrans count counter=foo %}Singular{% plural %}" 242 | "Plural {{ counter }}{% endblocktrans %}" 243 | ) 244 | self.assertEqual('count translated', template.render({'foo': 123})) 245 | self.ngettext.assert_called_with('Singular', 'Plural %(counter)s', 123) 246 | 247 | def test_count_override_counter(self): 248 | template = self.env.from_string( 249 | "{{ my_counter }} " 250 | "{% blocktrans count my_counter=foo %}Singular {{ my_counter }}" 251 | "{% plural %}Plural {{ my_counter }}{% endblocktrans %}" 252 | ) 253 | context = {'my_counter': 123, 'foo': 456} 254 | self.assertEqual('123 count translated', template.render(context)) 255 | self.ngettext.assert_called_with('Singular %(my_counter)s', 'Plural %(my_counter)s', 456) 256 | 257 | def test_as_var(self): 258 | template = self.env.from_string( 259 | "{% blocktrans asvar foo %}Translate me!{% endblocktrans %}" 260 | "Here comes the translation: {{ foo }}" 261 | ) 262 | self.assertEqual( 263 | 'Here comes the translation: Translate me! - translated', template.render() 264 | ) 265 | self.gettext.assert_called_with('Translate me!') 266 | 267 | @override_settings(USE_L10N=True) 268 | def test_finalize_vars(self): 269 | def finalize(s): 270 | if s == 123: 271 | return 'finalized 123' 272 | else: 273 | return s 274 | self.env.finalize = finalize 275 | template = self.env.from_string("{% blocktrans %}{{ foo }}{% endblocktrans %}") 276 | 277 | self.assertEqual('finalized 123 - translated', template.render({'foo': 123})) 278 | 279 | def test_errors(self): 280 | template1 = "{% blocktrans %}foo{% plural %}bar{% endblocktrans %}" 281 | template2 = "{% blocktrans count counter=10 %}foo{% endblocktrans %}" 282 | 283 | error_messages = [ 284 | (template1, 'used plural without specifying count'), 285 | (template2, 'plural form not found'), 286 | ] 287 | 288 | for template, msg in error_messages: 289 | with self.assertRaisesMessage(TemplateSyntaxError, msg): 290 | self.env.from_string(template) 291 | 292 | 293 | @override_settings(USE_L10N=True, USE_TZ=True) 294 | class DjangoL10nTest(SimpleTestCase): 295 | @requires_tz_support 296 | def test_localize(self): 297 | env = Environment(extensions=[DjangoL10n]) 298 | template = env.from_string("{{ foo }}") 299 | context1 = {'foo': 1.23} 300 | date = datetime.datetime(2000, 10, 1, 14, 10, 12, tzinfo=timezone.utc) 301 | context2 = {'foo': date} 302 | 303 | translation.activate('en') 304 | self.assertEqual('1.23', template.render(context1)) 305 | 306 | translation.activate('de') 307 | self.assertEqual('1,23', template.render(context1)) 308 | 309 | translation.activate('es') 310 | timezone.activate('America/Argentina/Buenos_Aires') 311 | self.assertEqual('1 de Octubre de 2000 a las 11:10', template.render(context2)) 312 | 313 | timezone.activate('Europe/Berlin') 314 | self.assertEqual('1 de Octubre de 2000 a las 16:10', template.render(context2)) 315 | 316 | translation.activate('de') 317 | self.assertEqual('1. Oktober 2000 16:10', template.render(context2)) 318 | 319 | timezone.activate('America/Argentina/Buenos_Aires') 320 | self.assertEqual('1. Oktober 2000 11:10', template.render(context2)) 321 | 322 | def test_existing_finalize(self): 323 | finalize_mock = mock.Mock(side_effect=lambda s: s) 324 | 325 | class TestExtension(Extension): 326 | def __init__(self, environment): 327 | environment.finalize = finalize_mock 328 | 329 | env = Environment(extensions=[TestExtension, DjangoL10n]) 330 | template = env.from_string("{{ foo }}") 331 | 332 | translation.activate('de') 333 | self.assertEqual('1,23', template.render({'foo': 1.23})) 334 | finalize_mock.assert_called_with(1.23) 335 | 336 | 337 | class DjangoStaticTest(SimpleTestCase): 338 | @staticmethod 339 | def _static(path): 340 | return 'Static: {}'.format(path) 341 | 342 | def setUp(self): 343 | patcher = mock.patch('jdj_tags.extensions.django_static', side_effect=self._static) 344 | self.static = patcher.start() 345 | self.addCleanup(patcher.stop) 346 | 347 | self.env = Environment(extensions=[DjangoStatic]) 348 | 349 | def test_simple(self): 350 | template = self.env.from_string("{% static 'static.png' %}") 351 | 352 | self.assertEqual('Static: static.png', template.render()) 353 | self.static.assert_called_with('static.png') 354 | 355 | def test_as_var(self): 356 | template = self.env.from_string( 357 | "{% static 'static.png' as my_url %}My url is: {{ my_url }}!" 358 | ) 359 | 360 | self.assertEqual('My url is: Static: static.png!', template.render()) 361 | self.static.assert_called_with('static.png') 362 | 363 | 364 | class DjangoNowTest(SimpleTestCase): 365 | @staticmethod 366 | def _now(tz=None): 367 | return datetime.datetime(2015, 7, 8, 10, 33, 25, tzinfo=tz) 368 | 369 | def setUp(self): 370 | patcher = mock.patch('jdj_tags.extensions.datetime') 371 | dt_mock = patcher.start() 372 | dt_mock.now.configure_mock(side_effect=self._now) 373 | self.addCleanup(patcher.stop) 374 | 375 | self.env = Environment(extensions=[DjangoNow]) 376 | 377 | def test_simple(self): 378 | template = self.env.from_string("{% now 'Y-m-d' %}") 379 | expected = self._now().strftime('%Y-%m-%d') 380 | 381 | self.assertEqual(expected, template.render()) 382 | 383 | def test_as_var(self): 384 | template = self.env.from_string( 385 | "{% now 'Y-m-d' as cur_date %}Current date is: {{ cur_date }}!" 386 | ) 387 | expected = "Current date is: %s!" % self._now().strftime('%Y-%m-%d') 388 | 389 | self.assertEqual(expected, template.render()) 390 | 391 | 392 | class DjangoUrlTest(SimpleTestCase): 393 | @staticmethod 394 | def _reverse(name, *args, **kwargs): 395 | return 'Url for: {}'.format(name) 396 | 397 | def setUp(self): 398 | patcher = mock.patch('jdj_tags.extensions.reverse', side_effect=self._reverse) 399 | self.reverse = patcher.start() 400 | self.addCleanup(patcher.stop) 401 | 402 | self.env = Environment(extensions=[DjangoUrl]) 403 | 404 | def test_simple(self): 405 | template = self.env.from_string("{% url 'my_view' %}") 406 | 407 | self.assertEqual('Url for: my_view', template.render()) 408 | self.reverse.assert_called_with('my_view', args=(), kwargs={}) 409 | 410 | def test_args(self): 411 | template1 = self.env.from_string("{% url 'my_view' 'foo' 'bar' %}") 412 | template2 = self.env.from_string("{% url 'my_view' arg1 'bar' %}") 413 | template3 = self.env.from_string("{% url 'my_view' arg1 arg2 %}") 414 | 415 | expected = 'Url for: my_view' 416 | call = mock.call('my_view', args=('foo', 'bar'), kwargs={}) 417 | 418 | self.assertEqual(expected, template1.render()) 419 | self.assertEqual(call, self.reverse.call_args) 420 | self.assertEqual(expected, template2.render({'arg1': 'foo'})) 421 | self.assertEqual(call, self.reverse.call_args) 422 | self.assertEqual(expected, template3.render({'arg1': 'foo', 'arg2': 'bar'})) 423 | self.assertEqual(call, self.reverse.call_args) 424 | 425 | def test_kwargs(self): 426 | template1 = self.env.from_string("{% url 'my_view' kw1='foo' kw2='bar' %}") 427 | template2 = self.env.from_string("{% url 'my_view' kw1=arg1 kw2='bar' %}") 428 | template3 = self.env.from_string("{% url 'my_view' kw1=arg1 kw2=arg2 %}") 429 | 430 | expected = 'Url for: my_view' 431 | call = mock.call('my_view', args=(), kwargs={'kw1': 'foo', 'kw2': 'bar'}) 432 | 433 | self.assertEqual(expected, template1.render()) 434 | self.assertEqual(call, self.reverse.call_args) 435 | self.assertEqual(expected, template2.render({'arg1': 'foo'})) 436 | self.assertEqual(call, self.reverse.call_args) 437 | self.assertEqual(expected, template3.render({'arg1': 'foo', 'arg2': 'bar'})) 438 | self.assertEqual(call, self.reverse.call_args) 439 | 440 | def test_dotted_expr(self): 441 | template1 = self.env.from_string("{% url 'my_view' foo.bar %}") 442 | template2 = self.env.from_string("{% url 'my_view' kw1=foo.bar %}") 443 | 444 | class Foo(object): 445 | pass 446 | 447 | foo = Foo() 448 | foo.bar = 'argument' 449 | 450 | self.assertEqual('Url for: my_view', template1.render({'foo': foo})) 451 | self.reverse.assert_called_with('my_view', args=('argument',), kwargs={}) 452 | self.assertEqual('Url for: my_view', template2.render({'foo': foo})) 453 | self.reverse.assert_called_with('my_view', args=(), kwargs={'kw1': 'argument'}) 454 | 455 | def test_as_var(self): 456 | template1 = self.env.from_string("{% url 'my_view' as my_url %}Url: {{ my_url }}") 457 | template2 = self.env.from_string( 458 | "{% url 'my_view' arg1 'bar' as my_url %}Url: {{ my_url }}" 459 | ) 460 | template3 = self.env.from_string( 461 | "{% url 'my_view' kw1=arg1 kw2='bar' as my_url %}Url: {{ my_url }}" 462 | ) 463 | 464 | expected = 'Url: Url for: my_view' 465 | 466 | self.assertEqual(expected, template1.render()) 467 | self.reverse.assert_called_with('my_view', args=(), kwargs={}) 468 | self.assertEqual(expected, template2.render({'arg1': 'foo'})) 469 | self.reverse.assert_called_with('my_view', args=('foo', 'bar'), kwargs={}) 470 | self.assertEqual(expected, template3.render({'arg1': 'foo'})) 471 | self.reverse.assert_called_with('my_view', args=(), kwargs={'kw1': 'foo', 'kw2': 'bar'}) 472 | 473 | def test_errors(self): 474 | template = "{% url 'my_view' kw1='foo' 123 %}" 475 | msg = "got 'integer', expected name for keyword argument" 476 | 477 | with self.assertRaisesMessage(TemplateSyntaxError, msg): 478 | self.env.from_string(template) 479 | 480 | 481 | class DjangoCompatTest(SimpleTestCase): 482 | classes = ['DjangoCsrf', 'DjangoI18n', 'DjangoStatic', 'DjangoNow', 'DjangoUrl'] 483 | 484 | class CalledParse(Exception): 485 | pass 486 | 487 | @classmethod 488 | def make_side_effect(cls, cls_name): 489 | def parse(self, parser): 490 | raise cls.CalledParse(cls_name) 491 | return parse 492 | 493 | def setUp(self): 494 | for class_name in self.classes: 495 | patcher = mock.patch( 496 | 'jdj_tags.extensions.{}.parse'.format(class_name), 497 | side_effect=self.make_side_effect(class_name) 498 | ) 499 | patcher.start() 500 | self.addCleanup(patcher.stop) 501 | 502 | self.env = Environment(extensions=[DjangoCompat]) 503 | 504 | def test_compat(self): 505 | tags = [ 506 | ('csrf_token', 'DjangoCsrf'), 507 | ('trans', 'DjangoI18n'), 508 | ('blocktrans', 'DjangoI18n'), 509 | ('static', 'DjangoStatic'), 510 | ('now', 'DjangoNow'), 511 | ('url', 'DjangoUrl'), 512 | ] 513 | 514 | for tag, class_name in tags: 515 | with self.assertRaisesMessage(self.CalledParse, class_name): 516 | self.env.from_string('{% ' + tag + ' %}') 517 | 518 | 519 | if __name__ == '__main__': 520 | import unittest 521 | from django.apps import apps 522 | from django.conf import settings 523 | settings.configure() 524 | apps.populate(settings.INSTALLED_APPS) 525 | 526 | unittest.main() 527 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{27,34,35}-django{18,19,110}-jinja{27,28} 3 | 4 | [testenv] 5 | deps = 6 | pytz 7 | py27: mock 8 | django18: Django>=1.8,<1.9 9 | django19: Django>=1.9,<1.10 10 | django110: Django>=1.10,<1.11 11 | jinja27: Jinja2>=2.7,<2.8 12 | jinja28: Jinja2>=2.8,<2.9 13 | passenv = PYTHONWARNINGS 14 | commands = {envpython} tests.py 15 | --------------------------------------------------------------------------------