├── .circleci
└── config.yml
├── .coveragerc
├── .gitignore
├── LICENSE
├── README.md
├── django_jsx
├── __init__.py
├── management
│ ├── __init__.py
│ └── commands
│ │ ├── __init__.py
│ │ └── compilejsx.py
└── templatetags
│ ├── __init__.py
│ └── jsx.py
├── runtests.py
├── setup.cfg
├── setup.py
├── tests
├── __init__.py
├── more_templates
│ └── another_template.html
├── templates
│ ├── test_dir_C
│ │ └── test_file_D.zip
│ ├── test_file_A.txt
│ └── test_file_B.html
├── test_compilejsx.py
├── test_jsx_tag.py
├── test_serialize.py
├── test_settings.py
└── test_template_files.py
└── tox.ini
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | working_directory: ~/app
5 | docker:
6 | # https://discuss.circleci.com/t/testing-in-different-environments/450/13
7 | - image: themattrix/tox
8 | steps:
9 | - checkout
10 | - restore_cache:
11 | key: tox-v1-{{ checksum "tox.ini" }}-{{ checksum "setup.py" }}
12 | - run: tox
13 | - save_cache:
14 | key: tox-v1-{{ checksum "tox.ini" }}-{{ checksum "setup.py" }}
15 | paths:
16 | - ~/app/.tox
17 |
--------------------------------------------------------------------------------
/.coveragerc:
--------------------------------------------------------------------------------
1 | [run]
2 | branch = true
3 | omit = */tests/*, .tox/*, runtests.py, setup.py
4 | source = .
5 |
6 | [report]
7 | show_missing = true
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[co]
2 | .project
3 | .pydevproject
4 | *~
5 | *.swp
6 | *.db
7 | *.orig
8 | *.DS_Store
9 | .coverage
10 | .tox
11 | *.egg-info/*
12 | docs/_build/*
13 | build
14 | dist/*
15 | *.mo
16 | node_modules/*
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012-2017, Caktus Consulting Group, LLC
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification,
5 | are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice,
8 | this list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # django-jsx [](https://circleci.com/gh/caktus/django-jsx)
2 |
3 | Django-JSX is a integration tool for Django projects using the excellent React
4 | UI library. It enables direct embedding of the JSX syntax into Django templates
5 | through a `{% jsx %}` template block. Your JSX will be extracted and compiled
6 | along with your other front-end assets and rendered into your page.
7 |
8 | Django-JSX makes React and Django work together.
9 |
10 | ## Features
11 |
12 | - Embed React's JSX syntax directly into your project's Django templates
13 | - Access the template's context variables inside your JSX attribute expressions
14 | - Expose your library of React components to your traditional template developers
15 |
16 |
17 | ## Installation and Use
18 |
19 | django-jsx requires Django >= 1.8 and Python >= 2.7 or >= 3.4.
20 |
21 | To install from PyPi:
22 |
23 | pip install django-jsx
24 |
25 | Add django-jsx to your Django settings' `INSTALLED_APPS`:
26 |
27 | INSTALLED_APPS = [
28 | ...
29 | 'django_jsx',
30 | ]
31 |
32 | Now, in your templates, you can use JSX and React components within a `{% jsx %}`
33 | block easily.
34 |
35 | {% load jsx %}
36 | {% jsx %}
37 |
38 | {% endjsx %}
39 |
40 | You'll need to include Django-JSX into your front-end build process. This includes
41 | two steps. First, you need to run the `compilejsx` management command, which will
42 | generate your "JSX Registry". This creates an ES6/JSX module you'll place with your
43 | other front-end assets.
44 |
45 | python manage.py compilejsx -o project/static/js/jsx_registry.js
46 |
47 | Now that all the inline JSX you used in your templates is extracted for your
48 | front-end to use, you'll import those JSX snippets and render them all. You are
49 | responsible for making all your React components available for this step in
50 | the process.
51 |
52 | import jsxRegistry from './jsx_registry.js'
53 | import DropdownWidget from './widgets/dropdown.js'
54 | import LoadingWidget from './widgets/loading.js'
55 |
56 | jsxRegistry.renderAllDjangoJSX({
57 | DropdownWidget,
58 | LoadingWidget
59 | })
60 |
61 |
62 | ## How it works
63 |
64 | * The `compilejsx` management command finds all the `jsx` blocks in the project's templates. It
65 | creates a jsx_registry.js file which has a snippet of javascript for
66 | each JSX block, which when called, will render the JSX.
67 | It also defines a `renderAllDjangoJSX` function which will do that for a given page.
68 |
69 | * Include the output file from `compilejsx` when bundling your JavaScript. It uses
70 | ECMAScript 2015 features and so might need some transpiling.
71 |
72 | * When rendered by Django, the `jsx` template tag is replaced by an empty script tag, whose purpose
73 | is to store (as a data attribute) a snapshot of the contents of the template context
74 | at that point in the template.
75 |
76 | * At load time in a page with jsx tags, the page should call `renderAllDjangoJSX`. It will
77 | iterate over all the `jsx` script tags in the page's HTML, and render the appropriate
78 | React component into each one using the template context from the script tag and the Javascript
79 | compiled from the original JSX.
80 |
81 | ## License
82 |
83 | django-jsx is released under the BSD License. See the
84 | [LICENSE](https://github.com/caktus/django-jsx/blob/master/LICENSE) file for more details.
85 |
86 |
87 | ### Contributing
88 |
89 | If you think you've found a bug or are interested in contributing to this project
90 | check out [django-jsx on Github](https://github.com/caktus/django-jsx).
91 |
92 | Development sponsored by [Caktus Consulting Group, LLC](http://www.caktusgroup.com/services).
93 |
--------------------------------------------------------------------------------
/django_jsx/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/django_jsx/__init__.py
--------------------------------------------------------------------------------
/django_jsx/management/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/django_jsx/management/__init__.py
--------------------------------------------------------------------------------
/django_jsx/management/commands/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/django_jsx/management/commands/__init__.py
--------------------------------------------------------------------------------
/django_jsx/management/commands/compilejsx.py:
--------------------------------------------------------------------------------
1 | from __future__ import print_function, unicode_literals
2 |
3 | import os
4 | import re
5 | import hashlib
6 |
7 | import django.template
8 | from django.template.backends.django import DjangoTemplates
9 | from django.core.management.base import BaseCommand
10 |
11 | # Regex matching all JSX blocks in a template
12 | R_JSX = re.compile(r'\{% *jsx *%\}(.*?)\{% *endjsx *%\}', re.DOTALL)
13 |
14 | # Regex to spot the beginning of an HTML element in JSX text
15 | R_COMPONENT = re.compile(r'<(\w+)')
16 |
17 | START_JS = """
18 | import React from 'react';
19 | import ReactDOM from 'react-dom';
20 | var jsx_registry = {};
21 | """
22 |
23 | # ' % \
147 | (sha1(self.jsx.encode('utf-8')).hexdigest(), escape(ctx))
148 |
--------------------------------------------------------------------------------
/runtests.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # https://docs.djangoproject.com/en/1.10/topics/testing/advanced/#using-the-django-test-runner-to-test-reusable-applications
3 | import os
4 | import sys
5 |
6 | import django
7 | from django.conf import settings
8 | from django.test.utils import get_runner
9 |
10 | if __name__ == "__main__":
11 | os.environ['DJANGO_SETTINGS_MODULE'] = 'tests.test_settings'
12 | django.setup()
13 | TestRunner = get_runner(settings)
14 | test_runner = TestRunner()
15 | test_modules = sys.argv[1:] if len(sys.argv) > 1 else ["tests"]
16 | failures = test_runner.run_tests(test_modules)
17 | sys.exit(bool(failures))
18 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.md
3 | [flake8]
4 | max-line-length=100
5 | exclude=migrations,.tox,node_modules
6 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name='django-jsx',
5 | version='0.4.0',
6 | author='Calvin Spealman',
7 | author_email='calvin@caktusgroup.com',
8 | packages=find_packages(exclude=['sample_project']),
9 | include_package_data=True,
10 | license='BSD',
11 | description='Integration library for React/JSX and Django',
12 | classifiers=[
13 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content',
14 | 'Intended Audience :: Developers',
15 | 'License :: OSI Approved :: BSD License',
16 | 'Programming Language :: Python',
17 | ],
18 | )
19 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/tests/__init__.py
--------------------------------------------------------------------------------
/tests/more_templates/another_template.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/tests/more_templates/another_template.html
--------------------------------------------------------------------------------
/tests/templates/test_dir_C/test_file_D.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/tests/templates/test_dir_C/test_file_D.zip
--------------------------------------------------------------------------------
/tests/templates/test_file_A.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/tests/templates/test_file_A.txt
--------------------------------------------------------------------------------
/tests/templates/test_file_B.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/caktus/django-jsx/ba8f77ca9b96c96328b44886b3c7389231570210/tests/templates/test_file_B.html
--------------------------------------------------------------------------------
/tests/test_compilejsx.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import hashlib
4 | import io
5 | import os
6 | import sys
7 | import tempfile
8 |
9 | from django.core.management import call_command
10 | from django.test import TestCase
11 |
12 | from django_jsx.management.commands.compilejsx import compile_templates, END_JS, START_JS
13 |
14 |
15 | class CompileJSXTest(TestCase):
16 | """
17 | Tests for the compilejsx management command, which looks at all the
18 | template files and emits a jsx_registry.jsx file with information about
19 | the JSX blocks in the templates, and some JavaScript code to make use of the
20 | information.
21 | """
22 | @classmethod
23 | def setUpClass(cls):
24 | cls.files_to_delete = []
25 |
26 | @classmethod
27 | def tearDownClass(cls):
28 | for fn in cls.files_to_delete:
29 | try:
30 | os.remove(fn)
31 | except Exception as e:
32 | print(e)
33 | delattr(cls, 'files_to_delete')
34 |
35 | @classmethod
36 | def make_testfile(cls):
37 | """Returns name of the test file"""
38 | (filehandle, filename) = tempfile.mkstemp()
39 | os.close(filehandle)
40 | cls.files_to_delete.append(filename)
41 | return filename
42 |
43 | def test_invoking_for_stdout(self):
44 | # Default output is to stdout
45 | out = io.StringIO()
46 | orig_out = sys.stdout
47 | try:
48 | sys.stdout = out
49 | call_command('compilejsx')
50 | self.assertIn(START_JS, out.getvalue())
51 | finally:
52 | sys.stdout = orig_out
53 |
54 | def test_invoking_to_output_file(self):
55 | # --output sends output to named file
56 | filename = type(self).make_testfile()
57 | call_command('compilejsx', output=filename)
58 | output = open(filename, "rb").read().decode('utf-8')
59 | self.assertIn(START_JS, output)
60 |
61 | def try_it(self, test_content, expected_result, raw=False):
62 | # Make template file containing a jsx block whose body is `test_content`, run
63 | # compilejsx, and make sure the output is `expected_result`. Or if `raw` is true,
64 | # then `test_content` is the entire content of the test file to compile.
65 | filename = type(self).make_testfile()
66 | expected_result = expected_result.replace('{filename}', filename)
67 | if raw:
68 | test_text = test_content
69 | else:
70 | test_text = "{% jsx %}" + test_content + "{% endjsx %}"
71 | with open(filename, "w") as f:
72 | f.write(test_text)
73 |
74 | # "Compile" it
75 | output = io.StringIO()
76 | compile_templates([filename], output)
77 |
78 | # Strip boilerplate to simplify checking
79 | start = len(START_JS) + 1
80 | end = 0 - (len(END_JS) + 1)
81 | result = output.getvalue()[start:end - 1]
82 |
83 | self.maxDiff = None
84 | self.assertEqual(expected_result, result)
85 |
86 | def test_empty_template(self):
87 | # If template is empty, output is just the boilerplate.
88 | # Make empty file
89 | filename = type(self).make_testfile()
90 | with open(filename, "w"):
91 | pass
92 | # "Compile" it
93 | output = io.StringIO()
94 | compile_templates([filename], output)
95 |
96 | # Check boilerplate
97 | self.assertTrue(output.getvalue().startswith(START_JS + "\n"))
98 | self.assertTrue(output.getvalue().endswith(END_JS + "\n"))
99 |
100 | # Strip boilerplate to simplify checking what's not boilerplate
101 | start = len(START_JS) + 1
102 | end = 0 - (len(END_JS) + 1)
103 | result = output.getvalue()[start:end - 1]
104 | self.assertEqual('', result)
105 |
106 | def test_template_with_empty_jsx_block(self):
107 | # If the block is empty, the output is pretty minimal
108 |
109 | test_content = ''
110 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
111 | expected = '''/* {filename} */
112 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
113 | return ();
114 | }''' % sha1
115 | self.try_it(test_content, expected)
116 |
117 | def test_template_with_minimal_component(self):
118 | # If the block just has a minimal React component, the output includes
119 | # a jsx_registry entry for it.
120 | test_content = ''
121 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
122 | expected = '''/* {filename} */
123 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
124 | if (Object.hasOwnProperty.call(COMPONENTS, 'NeatThing')) var {NeatThing} = COMPONENTS;
125 | return (%s);
126 | }''' % (sha1, test_content)
127 | self.try_it(test_content, expected)
128 |
129 | def test_template_with_component_with_static_property(self):
130 | # Static properties don't change the output
131 | test_content = ''
132 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
133 | expected = '''/* {filename} */
134 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
135 | if (Object.hasOwnProperty.call(COMPONENTS, 'NiftyFeature')) var {NiftyFeature} = COMPONENTS;
136 | return (%s);
137 | }''' % (sha1, test_content)
138 | self.try_it(test_content, expected)
139 |
140 | def test_template_with_component_with_variable_property(self):
141 | # Variable properties don't change the output
142 | test_content = ''
143 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
144 | expected = '''/* {filename} */
145 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
146 | if (Object.hasOwnProperty.call(COMPONENTS, 'WonderBar')) var {WonderBar} = COMPONENTS;
147 | return (%s);
148 | }''' % (sha1, test_content)
149 | self.try_it(test_content, expected)
150 |
151 | def test_template_with_component_with_expression_property(self):
152 | # Expressions in properties don't change the output
153 | test_content = ''
154 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
155 | expected = '''/* {filename} */
156 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
157 | if (Object.hasOwnProperty.call(COMPONENTS, 'Component')) var {Component} = COMPONENTS;
158 | return (%s);
159 | }''' % (sha1, test_content)
160 | self.try_it(test_content, expected)
161 |
162 | def test_template_with_component_with_deep_variable(self):
163 | # Variable properties don't change the output
164 | test_content = ''
165 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
166 | expected = '''/* {filename} */
167 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
168 | if (Object.hasOwnProperty.call(COMPONENTS, 'Component')) var {Component} = COMPONENTS;
169 | return (%s);
170 | }''' % (sha1, test_content)
171 | self.try_it(test_content, expected)
172 |
173 | def test_template_with_nested_html(self):
174 | # Each tag level contributes to the output. compilejsx doesn't know or care
175 | # which tags are React components.
176 | test_content = '''
177 |
185 |
'''
186 | sha1 = hashlib.sha1(test_content.encode('utf-8')).hexdigest()
187 | expected = '''/* {filename} */
188 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
189 | if (Object.hasOwnProperty.call(COMPONENTS, 'MobileModalSectionSearch')) var {MobileModalSectionSearch} = COMPONENTS;
190 | if (Object.hasOwnProperty.call(COMPONENTS, 'div')) var {div} = COMPONENTS;
191 | return (%s);
192 | }''' % (sha1, test_content) # noqa (long line hard to avoid here)
193 | self.try_it(test_content, expected)
194 |
195 | def test_duplicate_blocks_with_different_contexts(self):
196 | # compilejsx comes up with the same jsx_registry entry repeatedly if there are multiple
197 | # blocks with the same content but with different contexts. But this is okay, as
198 | # the rendered template will have a tag for each occurrence of the block, each
199 | # with its own unique context, and the JavaScript will render a component for
200 | # each one using that context.
201 | block_content = ''
202 | test_content = '''{% load jsx %}
203 | {% with foo=1 %}
204 | {% jsx %}BLOCK_CONTENT{% endjsx %}
205 | {% endwith %}
206 | {% with foo=2 %}
207 | {% jsx %}BLOCK_CONTENT{% endjsx %}
208 | {% endwith %}
209 | '''.replace("BLOCK_CONTENT", block_content)
210 | sha1 = hashlib.sha1(block_content.encode('utf-8')).hexdigest()
211 | expected = '''/* {filename} */
212 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
213 | if (Object.hasOwnProperty.call(COMPONENTS, 'Component')) var {Component} = COMPONENTS;
214 | return (%s);
215 | }
216 | jsx_registry["%s"] = (COMPONENTS, ctx) => {
217 | if (Object.hasOwnProperty.call(COMPONENTS, 'Component')) var {Component} = COMPONENTS;
218 | return (%s);
219 | }''' % (sha1, block_content, sha1, block_content)
220 | self.try_it(test_content, expected, raw=True)
221 |
--------------------------------------------------------------------------------
/tests/test_jsx_tag.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 |
3 | import hashlib
4 | import json
5 | import re
6 | from django.template import Context, Engine
7 | from django.template import TemplateSyntaxError
8 | from django.test import TestCase
9 |
10 | from django_jsx.templatetags.jsx import set_nested
11 |
12 |
13 | RESULT_REGEX = re.compile(
14 | r'')
16 |
17 | DEFAULT_CONTEXT = {
18 | 'False': False,
19 | 'True': True,
20 | 'None': None
21 | }
22 |
23 | ENGINE = Engine.get_default()
24 |
25 |
26 | def unescape(s):
27 | return s.replace('&', '&').replace('<', '<')\
28 | .replace('>', '>').replace('"', '"').replace(''', "'")
29 |
30 |
31 | class SetNestedTest(TestCase):
32 | def test_simple_key(self):
33 | d = {}
34 | set_nested(d, 'foo', 3)
35 | self.assertEqual({'foo': 3}, d)
36 |
37 | def test_one_level(self):
38 | d = {}
39 | set_nested(d, 'foo.bar', 3)
40 | self.assertEqual({'foo': {'bar': 3}}, d)
41 |
42 | def test_two_levels(self):
43 | d = {}
44 | set_nested(d, 'foo.bar.baz', 3)
45 | self.assertEqual({'foo': {'bar': {'baz': 3}}}, d)
46 |
47 | def test_with_existing_stuff(self):
48 | d = {'one': 1, 'foo': {'baz': 2}}
49 | set_nested(d, 'foo.bar', 3)
50 | self.assertEqual({'one': 1, 'foo': {'bar': 3, 'baz': 2}}, d)
51 |
52 | def test_with_existing_object(self):
53 | """
54 | If a top level item to be serialized is an object, we shouldn't fail at
55 | trying to set the lower level item.
56 | """
57 | d = {'foo': object()}
58 | set_nested(d, 'foo.bar', 3)
59 | self.assertEqual({'foo': {'bar': 3}}, d)
60 |
61 | def test_top_level_item_doesnt_clobber_nested(self):
62 | # foo.bar has previously been set
63 | d = {'foo': {'bar': 3}}
64 | # if we later try to set foo, we shouldn't clobber foo.bar
65 | set_nested(d, 'foo', object())
66 | self.assertEqual({'foo': {'bar': 3}}, d)
67 |
68 |
69 | class JsxTagTest(TestCase):
70 | def test_loading_tags(self):
71 | # We can `load` the tag library
72 | engine = Engine.get_default()
73 | template_object = engine.from_string("{% load jsx %}")
74 | result = template_object.render(Context({}))
75 | self.assertEqual("", result)
76 |
77 | def try_it(self, content, expected_ctx, raw=False, context=None):
78 | # Assert that if we do a {% jsx %} block with the given content, that we
79 | # get the standard empty script tag in the output, the data-sha1 attribute
80 | # has the sha1 digest of the content, and the `data-ctx` is
81 | # a JSON-encoding of `expected_ctx`.
82 | # If `raw` is True, use `content` as the entire template content, not just
83 | # the insides of a jsx block, and just return the result rather than trying
84 | # to validate it.
85 | # If `context` is given, use it as the template context.
86 | if raw:
87 | template_content = content
88 | else:
89 | template_content = "{% load jsx %}{% jsx %}" + content + "{% endjsx %}"
90 | template_object = ENGINE.from_string(template_content)
91 | result = template_object.render(Context(context or {}))
92 | if raw:
93 | return result
94 | else:
95 | m = RESULT_REGEX.match(result)
96 | self.assertEqual(m.group('sha1'), hashlib.sha1(content.encode('utf-8')).hexdigest())
97 | ctx = json.loads(unescape(m.group('ctx')))
98 | self.assertEqual(ctx, expected_ctx)
99 |
100 | def test_empty_tag(self):
101 | # No content -> no context
102 | expected_ctx = {}
103 | self.try_it('', expected_ctx)
104 |
105 | def test_empty_component(self):
106 | # No references to context -> no context
107 | content = ''
108 | expected_ctx = {}
109 | self.try_it(content, expected_ctx)
110 |
111 | def test_component_with_properties_using_context(self):
112 | # Refer to context -> context we used gets included
113 | content = ''''''
114 | expected_ctx = {'False': False, 'True': True}
115 | self.try_it(content, expected_ctx)
116 |
117 | def test_referring_to_elements_numerically(self):
118 | content = ''''''
119 | context = {'list': [1, 2, 3]}
120 | expected_ctx = {'list': {'0': 1}}
121 | self.try_it(content, expected_ctx, context=context)
122 |
123 | def test_missing_variables(self):
124 | "If variable is missing, set it to empty string (by default)."
125 | content = ''''''
126 | context = {}
127 | expected_ctx = {'does': {'not': {'exist': ''}}}
128 | self.try_it(content, expected_ctx, context=context)
129 |
130 | def test_missing_variables_with_string_if_invalid_set(self):
131 | "If variable is missing, use Engine's string_if_invalid value."
132 | ENGINE.string_if_invalid = 'hey, missing var -> %s'
133 | content = ''''''
134 | context = {}
135 | expected_ctx = {'does': {'not': {'exist': 'hey, missing var -> does.not.exist'}}}
136 | self.try_it(content, expected_ctx, context=context)
137 |
138 | # SHOULD NOT BE ALLOWED - compilejsx will reject
139 | # def test_two_identical_blocks_with_different_contexts(self):
140 | # # If an identical block is repeated with different context values, we
141 | # # can't handle it, so it should report an error and force the user to make
142 | # # the blocks different.
143 | # block_content = ''
144 | # test_content = '''{% spaceless %}
145 | # {% load jsx %}
146 | # {% with foo=1 %}
147 | # {% jsx %}BLOCK_CONTENT{% endjsx %}
148 | # {% endwith %}
149 | # {% with foo=2 %}
150 | # {% jsx %}BLOCK_CONTENT{% endjsx %}
151 | # {% endwith %}
152 | # {% endspaceless %}'''.replace("BLOCK_CONTENT", block_content)
153 | # result = self.try_it(test_content, None, raw=True)
154 | # sha1 = hashlib.sha1(block_content.encode('utf-8')).hexdigest()
155 | # expected_output = (
156 | # ''
157 | # ''
158 | # .replace('SHA1', sha1))
159 | # self.assertEqual(expected_output, unescape(result))
160 |
161 | def test_block_in_loop(self):
162 | # We can put a block in a loop, and we get an output tag for each loop iteration
163 | test_content = '''{% spaceless %}
164 | {% load jsx %}
165 | {% for i in values %}
166 | {% jsx %}{% endjsx %}
167 | {% endfor %}
168 | {% endspaceless %}'''
169 | result = self.try_it(test_content, None, raw=True, context={'values': [1, 2, 3]})
170 | sha1 = hashlib.sha1(''.encode('utf-8')).hexdigest()
171 | expected_output = (
172 | ''
173 | ''
174 | ''
175 | .replace('SHA1', sha1))
176 | self.assertEqual(expected_output, unescape(result))
177 |
178 | def test_nested_blocks(self):
179 | # Nested JSX blocks are not allowed
180 | test_content = '''
181 | {% load jsx %}
182 | {% jsx %}
183 |
184 | {% jsx %}{% endjsx %}
185 |
186 | {% endjsx %}'''
187 | with self.assertRaises(TemplateSyntaxError) as raise_context:
188 | self.try_it(test_content, None, raw=True,)
189 | exc = raise_context.exc
190 | self.assertIn('jsx blocks cannot be nested in a template', str(exc))
191 |
--------------------------------------------------------------------------------
/tests/test_serialize.py:
--------------------------------------------------------------------------------
1 | from __future__ import unicode_literals
2 | import json
3 |
4 | from django.template import Context
5 | from django.test import TestCase
6 |
7 | from django_jsx.templatetags.jsx import serialize_opportunistically
8 |
9 |
10 | class SerializeOpportunisticallyTest(TestCase):
11 | def test_no_expressions(self):
12 | obj = {
13 | 'a': 1,
14 | 'b': 2,
15 | 'c': 3
16 | }
17 | result = serialize_opportunistically(obj, [])
18 | self.assertEqual({}, json.loads(result))
19 |
20 | def test_simple_expressions(self):
21 | obj = {
22 | 'a': 1,
23 | 'b': 2,
24 | 'c': 3
25 | }
26 | result = serialize_opportunistically(obj, ['a', 'b'])
27 | expect = {
28 | 'a': 1,
29 | 'b': 2,
30 | }
31 | self.assertEqual(expect, json.loads(result))
32 |
33 | def test_missing_variable(self):
34 | """
35 | In Django templates, if a variable is not defined in the context, then
36 | Django sets that variable to the special value ``string_if_invalid``
37 | which is normally the empty string, but is customizable in Django
38 | settings TEMPLATES['OPTIONS']['string_if_invalid']. Django doesn't
39 | raise a user-visible error, so we should do the same thing in
40 | django_jsx.
41 | """
42 | result = serialize_opportunistically(Context(), ['no.such.variable'])
43 | expect = {
44 | 'no': {
45 | 'such': {
46 | 'variable': ''
47 | }
48 | }
49 | }
50 | self.assertEqual(expect, json.loads(result))
51 |
52 | def test_nested_expressions(self):
53 | obj = {
54 | 'a': {
55 | 'd': 4,
56 | 'f': {
57 | 'g': 'Hello'
58 | }
59 | },
60 | 'b': 2,
61 | 'c': {
62 | 'e': 5
63 | }
64 | }
65 | result = serialize_opportunistically(Context(obj), ['a.d', 'b', 'c.e', 'a.f', 'a.f.g'])
66 | expect = {
67 | 'a': {
68 |
69 | 'd': 4,
70 | 'f': {
71 | 'g': 'Hello'
72 | }
73 | },
74 | 'b': 2,
75 | 'c': {
76 | 'e': 5
77 | },
78 | }
79 | self.assertEqual(expect, json.loads(result))
80 |
81 | def test_top_level_object_doesnt_clobber_nested_expressions(self):
82 | # create an object with some attributes
83 | class Location(dict):
84 | full_name = 'New York'
85 | location = Location()
86 |
87 | # the order of expressions is important here
88 | expressions = [
89 | 'location.full_name', # set up the nested dict
90 | 'location', # this should not clobber the nested dict
91 | ]
92 | obj = {'location': location}
93 | result = serialize_opportunistically(Context(obj), expressions)
94 | expect = {
95 | 'location': {
96 | 'full_name': 'New York',
97 | }
98 | }
99 | self.assertEqual(expect, json.loads(result))
100 |
101 | def test_with_callable(self):
102 | def call1():
103 | return "called 1"
104 |
105 | def call2():
106 | return "called 2"
107 |
108 | obj = {
109 | 'call1': call1,
110 | 'a': {
111 | 'b': call2,
112 | 'c': call1,
113 | }
114 | }
115 | result = serialize_opportunistically(obj, ['call1', 'a.b'])
116 | expect = {
117 | 'call1': 'called 1',
118 | 'a': {
119 | 'b': 'called 2'
120 | }
121 | }
122 | self.assertEqual(expect, json.loads(result))
123 |
--------------------------------------------------------------------------------
/tests/test_settings.py:
--------------------------------------------------------------------------------
1 | # SETTINGS file for running tests on this pluggable application
2 |
3 | # https://docs.djangoproject.com/en/1.10/topics/testing/advanced/#using-the-django-test-runner-to-test-reusable-applications
4 |
5 | SECRET_KEY = 'fake-key'
6 | INSTALLED_APPS = [
7 | "tests",
8 | "django_jsx",
9 | ]
10 |
11 | # We need a database configuration or Django complains, but we're not
12 | # actually using the database. An in-memory Sqlite database should
13 | # be very light-weight and not leave anything behind after testing.
14 | DATABASES = {
15 | 'default': {
16 | 'ENGINE': 'django.db.backends.sqlite3',
17 | 'NAME': ':memory:',
18 | }
19 | }
20 |
21 |
22 | # COPIED from project template in Django 1.10
23 | TEMPLATES = [
24 | {
25 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
26 | 'DIRS': [],
27 | 'APP_DIRS': True,
28 | 'OPTIONS': {
29 | 'context_processors': [
30 | 'django.template.context_processors.debug',
31 | 'django.template.context_processors.request',
32 | 'django.contrib.auth.context_processors.auth',
33 | 'django.contrib.messages.context_processors.messages',
34 | ],
35 | },
36 | },
37 | ]
38 |
--------------------------------------------------------------------------------
/tests/test_template_files.py:
--------------------------------------------------------------------------------
1 | """Tests for finding all the template files"""
2 | from __future__ import unicode_literals
3 | import os.path
4 |
5 | from django.test import TestCase
6 | from django.test import override_settings
7 |
8 | from django_jsx.management.commands.compilejsx import list_template_files
9 |
10 |
11 | class TemplateFindingTest(TestCase):
12 | @override_settings(
13 | INSTALLED_APPS=['tests'],
14 | TEMPLATES=[
15 | {
16 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
17 | 'APP_DIRS': True,
18 | },
19 | ])
20 | def test_finding_templates_with_test_app(self):
21 | # If 'tests' is the only installed app, we find its templates
22 | this_dir = os.path.dirname(__file__)
23 | result = list_template_files()
24 | expected = [
25 | os.path.join(this_dir, "templates", filename)
26 | for filename in
27 | [
28 | "test_file_B.html",
29 | "test_file_A.txt",
30 | "test_dir_C/test_file_D.zip",
31 | ]
32 | ]
33 | self.assertEqual(set(expected), set(result))
34 |
35 | @override_settings(
36 | TEMPLATES=[
37 | {
38 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
39 | 'APP_DIRS': True,
40 | },
41 | ],
42 | INSTALLED_APPS=[])
43 | def test_finding_templates_without_test_app(self):
44 | # If there are no installed apps, we don't find any templates
45 | result = list_template_files()
46 | expected = []
47 | self.assertEqual(set(expected), set(result))
48 |
49 | @override_settings(
50 | INSTALLED_APPS=['tests'],
51 | TEMPLATES=[
52 | {
53 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
54 | 'DIRS': [os.path.join(os.path.dirname(__file__), "more_templates")],
55 | 'APP_DIRS': True,
56 | },
57 | ])
58 | def test_finding_templates_in_additional_dirs(self):
59 | # If the user configured additional dirs to find templates in, we spot them
60 | result = list_template_files()
61 | this_dir = os.path.dirname(__file__)
62 | self.assertEqual(
63 | set(result),
64 | {
65 | os.path.join(this_dir, "templates", filename)
66 | for filename in
67 | [
68 | "test_file_B.html",
69 | "test_file_A.txt",
70 | "test_dir_C/test_file_D.zip",
71 | ]
72 | } | {
73 | os.path.join(this_dir, "more_templates", "another_template.html")
74 | }
75 | )
76 |
77 | @override_settings(
78 | INSTALLED_APPS=['tests'],
79 | TEMPLATES=[
80 | {
81 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
82 | 'DIRS': [os.path.join(os.path.dirname(__file__), "more_templates")],
83 | 'APP_DIRS': False,
84 | },
85 | ])
86 | def test_finding_templates_with_app_dirs_false(self):
87 | this_dir = os.path.dirname(__file__)
88 | result = list_template_files()
89 | expected = [os.path.join(this_dir, "more_templates", "another_template.html")]
90 | self.assertEqual(set(expected), set(result))
91 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = {py27,py35,py36,py37}-dj111
3 | {py35,py36,py37}-dj20
4 | {py35,py36,py37}-dj21
5 | py37-flake8
6 | py37-coverage
7 |
8 | [testenv]
9 | basepython =
10 | py27: python2.7
11 | py35: python3.5
12 | py36: python3.6
13 | py37: python3.7
14 | deps =
15 | dj111: Django>=1.11,<2.0
16 | dj20: Django>=2.0,<2.1
17 | dj21: Django>=2.1,<2.2
18 | commands = {envpython} runtests.py
19 |
20 | [testenv:py37-flake8]
21 | deps = flake8>=3.4
22 | Django>=1.11,<2.0
23 | skip_install = true
24 | commands={envbindir}/flake8
25 |
26 | [testenv:py37-coverage]
27 | deps = coverage>=3.7
28 | Django>=1.11,<2.0
29 | commands = coverage run runtests.py
30 | coverage report -m --fail-under 90
31 |
--------------------------------------------------------------------------------