├── .coveragerc ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .gitignore ├── .isort.cfg ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── SECURITY.md ├── django_react_templatetags ├── __init__.py ├── context_processors.py ├── encoders.py ├── mixins.py ├── ssr │ ├── __init__.py │ ├── default.py │ └── hypernova.py ├── templates │ └── react_print.html ├── templatetags │ ├── __init__.py │ └── react.py └── tests │ ├── __init__.py │ ├── demosite │ ├── __init__.py │ ├── models.py │ ├── settings.py │ ├── templates │ │ └── static-react.html │ ├── urls.py │ └── views.py │ ├── mock_response.py │ ├── test_filters.py │ ├── test_manager.py │ ├── test_representation_mixin.py │ ├── test_ssr.py │ ├── test_ssr_custom_service.py │ ├── test_ssr_default_service.py │ └── test_ssr_hypernova_service.py ├── docs ├── django-react-templatetags-logo.monopic ├── example-multiple-components.md ├── example-single-component.md ├── examples.md ├── faq.md ├── getting-started.md ├── server-side-rendering.md ├── settings.md ├── templatetags-params.md └── working-with-models.md ├── example_django_react_templatetags ├── Dockerfile ├── docker-compose.yml ├── docker-entrypoint.sh ├── examplesite │ ├── __init__.py │ ├── settings.py │ ├── templates │ │ └── examplesite │ │ │ └── index.html │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── manage.py ├── requirements.txt └── runtests.py ├── img └── django-react-templatetags-logo.png ├── pyproject.toml ├── requirements ├── dev.txt └── tests.txt ├── runtests.py └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | omit = 4 | */tests/* 5 | */migrations/* 6 | */urls.py 7 | */settings/* 8 | */wsgi.py 9 | example_django_react_templatetags/* 10 | venv/* 11 | manage.py 12 | runtests.py 13 | setup.py 14 | source = . 15 | 16 | [report] 17 | show_missing = true 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{py,ini,js,html,scss,css,json}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{yml,yaml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | target-branch: develop 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Run tests and lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 12 | django: ["3.2", "4.2.8", "5.0"] 13 | exclude: 14 | - python-version: "3.12" 15 | django: "3.2" 16 | - python-version: "3.8" 17 | django: "5.0" 18 | - python-version: "3.9" 19 | django: "5.0" 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install -q Django==${{ matrix.django }} requests mock hypernova 30 | - name: Test 31 | run: | 32 | python runtests.py 33 | 34 | lint-black: 35 | runs-on: ubuntu-latest 36 | needs: test 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: psf/black@stable 40 | with: 41 | options: "--check --verbose" 42 | src: "." 43 | version: "22.3.0" 44 | 45 | lint-isort: 46 | runs-on: ubuntu-latest 47 | needs: test 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-python@v2 51 | with: 52 | python-version: 3.8 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | pip install isort 57 | isort . --check-only 58 | 59 | lint-ruff: 60 | runs-on: ubuntu-latest 61 | needs: test 62 | steps: 63 | - uses: actions/checkout@v2 64 | - uses: actions/setup-python@v2 65 | with: 66 | python-version: 3.8 67 | - name: Install dependencies 68 | run: | 69 | python -m pip install --upgrade pip 70 | pip install ruff 71 | ruff . 72 | 73 | publish: 74 | runs-on: ubuntu-latest 75 | needs: [test, lint-black, lint-isort] 76 | steps: 77 | - uses: actions/checkout@v2 78 | - uses: actions/setup-python@v2 79 | with: 80 | python-version: 3.8 81 | - name: Install dependencies 82 | run: | 83 | python -m pip install --upgrade pip 84 | pip install build twine 85 | - name: Build 86 | run: | 87 | python -m build 88 | - name: Release to Test PyPi 89 | if: startsWith(github.ref, 'refs/tags/testv') 90 | env: 91 | TWINE_USERNAME: __token__ 92 | TWINE_PASSWORD: ${{ secrets.TEST_PYPI_PASSWORD }} 93 | TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ 94 | run: | 95 | twine upload -r testpypi dist/* 96 | - name: Release to PyPi 97 | if: startsWith(github.ref, 'refs/tags/v') 98 | env: 99 | TWINE_USERNAME: __token__ 100 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 101 | run: | 102 | twine upload dist/* 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | 22 | # Installer logs 23 | pip-log.txt 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Complexity 35 | output/*.html 36 | output/*/index.html 37 | 38 | # Sphinx 39 | docs/_build 40 | 41 | # Virtualenv 42 | venv* 43 | 44 | # Editor 45 | .idea 46 | 47 | # Example env 48 | example_django_react_templatetags/static* 49 | example_django_react_templatetags/django_react_templatetags 50 | 51 | # DB 52 | *.sqlite3 53 | 54 | # Pyenv 55 | .python-version 56 | -------------------------------------------------------------------------------- /.isort.cfg: -------------------------------------------------------------------------------- 1 | [settings] 2 | multi_line_output=3 3 | include_trailing_comma=True 4 | line_length=88 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | ### Added 9 | ### Changed 10 | ### Fixed 11 | ### Removed 12 | 13 | ## [8.0.0] - 2023-12-30 14 | 15 | ### Added 16 | - Add support for python 3.12 (@marteinn) 17 | - Add support for django 4.2 (@marteinn) 18 | - Add support for django 5.0 (@marteinn) 19 | 20 | ### Fixed 21 | - Add pyproject.toml 22 | - Add ruff linter 23 | - Upgrade python version to 3.12 in example 24 | - Fix install issue with netcat in example 25 | 26 | ### Removed 27 | - Drop support for django 4.0 28 | - Drop support for django 4.1 (@marteinn) 29 | - Drop support for python 3.7 (@marteinn) 30 | 31 | ## [7.0.1] - 2023-01-07 32 | 33 | ### Added 34 | - Add changelog 35 | 36 | ### Fixed 37 | - Add python 3.11 support (@marteinn) 38 | - Add django 4.1 support (@marteinn) 39 | - Fix invalid assert (code-review-doctor) 40 | 41 | ## [7.0.0] - 2022-01-04 42 | 43 | ### Fixed 44 | - Add Django 4.0 support 45 | - Add Python 3.10 support 46 | 47 | ### Removed 48 | - Drop Python 2 support 49 | - Drop Python 3.6 support 50 | - Drop support for EOL versions of Django (1 and 2) 51 | 52 | ## [6.0.2] - 2020-09-05 53 | 54 | ### Fixed 55 | - Make context_processor mandatory again 56 | 57 | ## [6.0.1] - 2020-08-30 58 | 59 | ### Fixed 60 | - Solved XSS issue (@anthonynsimon, @marteinn) 61 | - Add security policy 62 | 63 | ## [6.0.0] - 2020-04-28 64 | 65 | ### Added 66 | - Add official support for Hypernova SSR service 67 | - Add attribute “no_placeholder” when you want to skip the DRTT provided placeholder 68 | 69 | ### Fixed 70 | - Add official Django 3 support (@niespodd, @marteinn) 71 | - Remove context_processor required to run library (@niespodd) 72 | - Solve issue with requests import (Aria Moradi) 73 | - Use unit.patch instead of responses to mock requests (Umair) 74 | 75 | ### Removed 76 | - Drop Django 1 support 77 | - Drop pypy support 78 | 79 | 80 | ## [5.4.0] - 2019-04-28 81 | 82 | ### Added 83 | - Make it possible to use your own SSRService 84 | 85 | ### Fixed 86 | - Update docs 87 | - Add project logo 88 | 89 | ## [5.3.0] - 2019-04-24 90 | 91 | ### Added 92 | - Add SSRContext for passign values to SSR (@mikaelengstrom, @marteinn) 93 | - Add support for disabling SSR using header HTTP_X_DISABLE_SSR 94 | 95 | ### Fixed 96 | - Include example project 97 | - Drop python 3.4 support 98 | 99 | ## [5.2.1] - 2018-08-10 100 | 101 | ### Fixed 102 | - Add better SSR errors (thanks @mikaelengstrom) 103 | 104 | ## [5.2.0] - 2018-08-09 105 | 106 | ### Fixed 107 | - Use render() if SSR is not available 108 | - Fix bug when passing in standalone primitives in react_render 109 | - Update docs 110 | 111 | ## [5.1.0] - 2018-05-22 112 | 113 | ### Added 114 | - Add support for a replaceable tag manager (@aleehedl) 115 | - Add configurable headers for SSR (@aleehedl, @marteinn) 116 | 117 | ## [5.1.0] - 2018-04-05 118 | 119 | ### Fixed 120 | - Update react requirements in readme 121 | - Change NotImplemented error to to_react_representation 122 | 123 | ### Removed 124 | - Drop django support for django 1.10 and below 125 | - Drop custom JSON serializer 126 | 127 | ## [4.0.0] - 2017-12-06 128 | 129 | ### Fixed 130 | - Solved bug where to_react_representation triggered twice under SSR 131 | 132 | ### Removed 133 | - Dropped support for django 1.8 134 | - Removed templatetags for filters 135 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2024 Fröjd Interactive 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.md LICENSE runtests.py django_react_templatetags/templates/react_print.html 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Python tests](https://github.com/Frojd/django-react-templatetags/actions/workflows/main.yml/badge.svg?branch=develop)](https://github.com/Frojd/django-react-templatetags/actions/workflows/main.yml) [![PyPI version](https://badge.fury.io/py/django_react_templatetags.svg)](https://badge.fury.io/py/django_react_templatetags) 2 | 3 | ![Django-React-Templatetags](https://raw.githubusercontent.com/frojd/django-react-templatetags/develop/img/django-react-templatetags-logo.png) 4 | 5 | # Django-React-Templatetags 6 | 7 | This django library allows you to add React (16+) components into your django templates. 8 | 9 | 10 | ## Features 11 | 12 | - Include react components using django templatetags 13 | - Unlimited amount of components in one view 14 | - Support custom models (that is from the beginning not json-serializable) 15 | - Server side rendering with [Hypernova](https://github.com/airbnb/hypernova) or [Hastur](https://github.com/frojd/Hastur) 16 | 17 | 18 | ## Installation 19 | 20 | Install the library with pip: 21 | 22 | ``` 23 | $ pip install django_react_templatetags 24 | ``` 25 | 26 | 27 | ## Where to go from here? 28 | 29 | You should first read [Getting started](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/getting-started.md), then go through these topics: 30 | - [Settings](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/settings.md) 31 | - [How to use the templatetags included in this library](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/templatetags-params.md) 32 | - [Adding a single component](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/example-single-component.md) 33 | - [Adding multiple components](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/example-multiple-components.md) 34 | - [Examples](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/examples.md) 35 | - [Working with models](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/working-with-models.md) 36 | - [Server side rendering](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/server-side-rendering.md) 37 | - [FAQ](https://github.com/Frojd/django-react-templatetags/blob/develop/docs/faq.md) 38 | 39 | 40 | ## Tests 41 | 42 | This library include tests, just run `python runtests.py` 43 | 44 | You can also run separate test cases: `python runtests.py tests.test_filters.ReactIncludeComponentTest` 45 | 46 | 47 | ## Coverage 48 | 49 | Make sure you have Coverage.py installed, then run `coverage run runtests.py` to measure coverage. We are currently at 95%. 50 | 51 | 52 | ## Contributing 53 | 54 | Want to contribute? Awesome. Just send a pull request. 55 | 56 | 57 | ## Security 58 | 59 | If you believe you have found a security issue with any of our projects please email us at [security@frojd.se](security@frojd.se). 60 | 61 | 62 | ## License 63 | 64 | Django-React-Templatetags is released under the [MIT License](http://www.opensource.org/licenses/MIT). 65 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Issues 2 | 3 | If you believe you have found a security issue with any of our projects please email us at [security@frojd.se](security@frojd.se). 4 | 5 | -------------------------------------------------------------------------------- /django_react_templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | django_react_templatetags 5 | ---------- 6 | This extension allows you to add React components into your django templates. 7 | """ 8 | 9 | __title__ = "django_react_templatetags" 10 | __version__ = "8.0.0" 11 | __build__ = 702 12 | __author__ = "Martin Sandström" 13 | __license__ = "MIT" 14 | __copyright__ = "Copyright 2015-2024 Fröjd Interactive" 15 | -------------------------------------------------------------------------------- /django_react_templatetags/context_processors.py: -------------------------------------------------------------------------------- 1 | def react_context_processor(request): 2 | """Expose a global list of react components to be processed""" 3 | 4 | return { 5 | "REACT_COMPONENTS": [], 6 | } 7 | -------------------------------------------------------------------------------- /django_react_templatetags/encoders.py: -------------------------------------------------------------------------------- 1 | from django.core.serializers.json import DjangoJSONEncoder 2 | 3 | from django_react_templatetags.mixins import RepresentationMixin 4 | 5 | 6 | def json_encoder_cls_factory(context): 7 | class ReqReactRepresentationJSONEncoder(ReactRepresentationJSONEncoder): 8 | context = None 9 | 10 | ReqReactRepresentationJSONEncoder.context = context 11 | return ReqReactRepresentationJSONEncoder 12 | 13 | 14 | class ReactRepresentationJSONEncoder(DjangoJSONEncoder): 15 | """ 16 | Custom json encoder that adds support for RepresentationMixin 17 | """ 18 | 19 | def default(self, o): 20 | if isinstance(o, RepresentationMixin): 21 | args = [self.context if hasattr(self, "context") else None] 22 | args = [x for x in args if x is not None] 23 | 24 | return o.to_react_representation(*args) 25 | 26 | return super(ReactRepresentationJSONEncoder, self).default(o) 27 | -------------------------------------------------------------------------------- /django_react_templatetags/mixins.py: -------------------------------------------------------------------------------- 1 | class RepresentationMixin(object): 2 | def to_react_representation(self, context=None): 3 | raise NotImplementedError("Missing property to_react_representation in class") 4 | -------------------------------------------------------------------------------- /django_react_templatetags/ssr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frojd/django-react-templatetags/60cd4efa6b45d9b40adef2f1833a1ff01f6da859/django_react_templatetags/ssr/__init__.py -------------------------------------------------------------------------------- /django_react_templatetags/ssr/default.py: -------------------------------------------------------------------------------- 1 | """ 2 | This modules manages SSR rendering logic 3 | """ 4 | 5 | import json 6 | import logging 7 | 8 | import requests 9 | from django.conf import settings 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | class SSRService: 15 | def load_or_empty(self, component, headers={}, ssr_context=None): 16 | request_json = ( 17 | '{{"componentName": "{0}", "props": {1}, "context": {2}}}'.format( 18 | component["name"], 19 | component["json"], 20 | json.dumps(ssr_context) if ssr_context else {}, 21 | ) 22 | ) 23 | 24 | try: 25 | inner_html = self.load(request_json, headers) 26 | except requests.exceptions.RequestException as e: 27 | inner_html = "" 28 | 29 | msg = "SSR request to '{}' failed: {}".format( 30 | settings.REACT_RENDER_HOST, e.__class__.__name__ 31 | ) 32 | logger.exception(msg) 33 | 34 | return { 35 | "html": inner_html, 36 | "params": {}, 37 | } 38 | 39 | def load(self, request_json, headers): 40 | req = requests.post( 41 | settings.REACT_RENDER_HOST, 42 | timeout=get_request_timeout(), 43 | data=request_json, 44 | headers=headers, 45 | ) 46 | 47 | req.raise_for_status() 48 | return req.text 49 | 50 | 51 | def get_request_timeout(): 52 | if not hasattr(settings, "REACT_RENDER_TIMEOUT"): 53 | return 20 54 | 55 | return settings.REACT_RENDER_TIMEOUT 56 | -------------------------------------------------------------------------------- /django_react_templatetags/ssr/hypernova.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import hypernova 5 | from django.conf import settings 6 | 7 | logger = logging.getLogger(__name__) 8 | hypernova_id_re = re.compile(r"data-hypernova-id=\"([\w\-]*)\"") 9 | hypernova_key_re = re.compile(r"data-hypernova-key=\"([\w\-]*)\"") 10 | 11 | 12 | class HypernovaService: 13 | def load_or_empty(self, component, headers={}, ssr_context=None): 14 | # from hypernova.plugins.dev_mode import DevModePlugin 15 | 16 | renderer = hypernova.Renderer( 17 | settings.REACT_RENDER_HOST, 18 | # [DevModePlugin(logger)] if settings.DEBUG else [], 19 | [], 20 | timeout=get_request_timeout(), 21 | headers=headers, 22 | ) 23 | 24 | inner_html = "" 25 | try: 26 | props = component["json_obj"] 27 | if ssr_context: 28 | props["context"] = ssr_context 29 | inner_html = renderer.render({component["name"]: props}) 30 | except Exception as e: 31 | msg = "SSR request to '{}' failed: {}".format( 32 | settings.REACT_RENDER_HOST, e.__class__.__name__ 33 | ) 34 | logger.exception(msg) 35 | 36 | if not inner_html: 37 | return {"html": "", "params": {}} 38 | 39 | match = re.search(hypernova_id_re, inner_html) 40 | hypernova_id = match.group(1) if match else None 41 | 42 | match = re.search(hypernova_key_re, inner_html) 43 | hypernova_key = match.group(1) if match else None 44 | 45 | return { 46 | "html": inner_html, 47 | "params": { 48 | "hypernova_id": hypernova_id, 49 | "hypernova_key": hypernova_key, 50 | }, 51 | } 52 | 53 | 54 | def get_request_timeout(): 55 | if not hasattr(settings, "REACT_RENDER_TIMEOUT"): 56 | return 20 57 | 58 | return settings.REACT_RENDER_TIMEOUT 59 | -------------------------------------------------------------------------------- /django_react_templatetags/templates/react_print.html: -------------------------------------------------------------------------------- 1 | {% if components %} 2 | {% for component in components %} 3 | {{ component.json_obj|json_script:component.data_identifier }} 4 | 14 | {% endfor %} 15 | {% endif %} 16 | -------------------------------------------------------------------------------- /django_react_templatetags/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_react_templatetags/templatetags/react.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module contains tags for including react components into templates. 3 | """ 4 | import json 5 | import uuid 6 | 7 | from django import template 8 | from django.conf import settings 9 | from django.template import Node 10 | from django.utils.module_loading import import_string 11 | 12 | from django_react_templatetags.encoders import json_encoder_cls_factory 13 | 14 | register = template.Library() 15 | 16 | CONTEXT_KEY = "REACT_COMPONENTS" 17 | 18 | DEFAULT_SSR_HEADERS = { 19 | "Content-type": "application/json", 20 | "Accept": "text/plain", 21 | } 22 | 23 | 24 | def get_uuid(): 25 | return uuid.uuid4().hex 26 | 27 | 28 | def has_ssr(request): 29 | if request and request.META.get("HTTP_X_DISABLE_SSR"): 30 | return False 31 | 32 | return hasattr(settings, "REACT_RENDER_HOST") and settings.REACT_RENDER_HOST 33 | 34 | 35 | def get_ssr_headers(): 36 | if not hasattr(settings, "REACT_RENDER_HEADERS"): 37 | return DEFAULT_SSR_HEADERS 38 | return settings.REACT_RENDER_HEADERS 39 | 40 | 41 | def load_from_ssr(component, ssr_context=None): 42 | ssr_service = _get_ssr_service()() 43 | return ssr_service.load_or_empty( 44 | component, 45 | headers=get_ssr_headers(), 46 | ssr_context=ssr_context, 47 | ) 48 | 49 | 50 | def _get_ssr_service(): 51 | """ 52 | Loads a custom React Tag Manager if provided in Django Settings. 53 | """ 54 | 55 | class_path = getattr(settings, "REACT_SSR_SERVICE", "") 56 | if not class_path: 57 | from django_react_templatetags.ssr.default import SSRService 58 | 59 | return SSRService 60 | 61 | return import_string(class_path) 62 | 63 | 64 | class ReactTagManager(Node): 65 | """ 66 | Handles the printing of react placeholders and queueing, is invoked by 67 | react_render. 68 | """ 69 | 70 | def __init__( 71 | self, 72 | identifier, 73 | component, 74 | data=None, 75 | css_class=None, 76 | props=None, 77 | ssr_context=None, 78 | no_placeholder=None, 79 | ): 80 | component_prefix = "" 81 | if hasattr(settings, "REACT_COMPONENT_PREFIX"): 82 | component_prefix = settings.REACT_COMPONENT_PREFIX 83 | 84 | self.identifier = identifier 85 | self.component = component 86 | self.component_prefix = component_prefix 87 | self.data = data 88 | self.css_class = css_class 89 | self.props = props 90 | self.ssr_context = ssr_context 91 | self.no_placeholder = no_placeholder 92 | 93 | def render(self, context): 94 | qualified_component_name = self.get_qualified_name(context) 95 | identifier = self.get_identifier(context, qualified_component_name) 96 | component_props = self.get_component_props(context) 97 | json_str = self.props_to_json(component_props, context) 98 | 99 | component = { 100 | "identifier": identifier, 101 | "data_identifier": "{}_data".format(identifier), 102 | "name": qualified_component_name, 103 | "json": json_str, 104 | "json_obj": json.loads(json_str), 105 | } 106 | 107 | placeholder_attr = ( 108 | ("id", identifier), 109 | ("class", self.resolve_template_variable(self.css_class, context)), 110 | ) 111 | placeholder_attr = [x for x in placeholder_attr if x[1] is not None] 112 | 113 | component_html = "" 114 | if has_ssr(context.get("request", None)): 115 | ssr_resp = load_from_ssr( 116 | component, 117 | ssr_context=self.get_ssr_context(context), 118 | ) 119 | component_html = ssr_resp["html"] 120 | component["ssr_params"] = ssr_resp["params"] 121 | 122 | components = context.get(CONTEXT_KEY, []) 123 | components.append(component) 124 | context[CONTEXT_KEY] = components 125 | 126 | if self.no_placeholder: 127 | return component_html 128 | 129 | return self.render_placeholder(placeholder_attr, component_html) 130 | 131 | def get_qualified_name(self, context): 132 | component_name = self.resolve_template_variable(self.component, context) 133 | return "{}{}".format(self.component_prefix, component_name) 134 | 135 | def get_identifier(self, context, qualified_component_name): 136 | identifier = self.resolve_template_variable(self.identifier, context) 137 | 138 | if identifier: 139 | return identifier 140 | 141 | return "{}_{}".format(qualified_component_name, get_uuid()) 142 | 143 | def get_component_props(self, context): 144 | resolved_data = self.resolve_template_variable_else_none(self.data, context) 145 | resolved_data = resolved_data if resolved_data else {} 146 | 147 | for prop in self.props: 148 | data = self.resolve_template_variable_else_none( 149 | self.props[prop], 150 | context, 151 | ) 152 | resolved_data[prop] = data 153 | 154 | return resolved_data 155 | 156 | def get_ssr_context(self, context): 157 | if not self.ssr_context: 158 | return {} 159 | 160 | return self.resolve_template_variable(self.ssr_context, context) 161 | 162 | @staticmethod 163 | def resolve_template_variable(value, context): 164 | if isinstance(value, template.Variable): 165 | return value.resolve(context) 166 | 167 | return value 168 | 169 | @staticmethod 170 | def resolve_template_variable_else_none(value, context): 171 | try: 172 | data = value.resolve(context) 173 | except template.VariableDoesNotExist: 174 | data = None 175 | except AttributeError: 176 | data = None 177 | 178 | return data 179 | 180 | @staticmethod 181 | def props_to_json(resolved_data, context): 182 | cls = json_encoder_cls_factory(context) 183 | return json.dumps(resolved_data, cls=cls) 184 | 185 | @staticmethod 186 | def render_placeholder(attributes, component_html=""): 187 | attr_pairs = map(lambda x: '{}="{}"'.format(*x), attributes) 188 | return "
{}
".format( 189 | " ".join(attr_pairs), 190 | component_html, 191 | ) 192 | 193 | 194 | @register.tag 195 | def react_render(parser, token): 196 | """ 197 | Renders a react placeholder and adds it to the global render queue. 198 | 199 | Example: 200 | {% react_render component="ListRestaurants" data=restaurants %} 201 | """ 202 | 203 | values = _prepare_args(parser, token) 204 | tag_manager = _get_tag_manager() 205 | return tag_manager(**values) 206 | 207 | 208 | def _prepare_args(parses, token): 209 | """ 210 | Normalize token arguments that can be passed along to node renderer 211 | """ 212 | 213 | values = { 214 | "identifier": None, 215 | "css_class": None, 216 | "data": None, 217 | "props": {}, 218 | } 219 | 220 | key_mapping = { 221 | "id": "identifier", 222 | "class": "css_class", 223 | "props": "data", 224 | } 225 | 226 | args = token.split_contents() 227 | method = args[0] 228 | 229 | for arg in args[1:]: 230 | key, value = arg.split( 231 | r"=", 232 | ) 233 | 234 | key = key_mapping.get(key, key) 235 | is_standalone_prop = key.startswith("prop_") 236 | if is_standalone_prop: 237 | key = key[5:] 238 | 239 | value = template.Variable(value) 240 | if is_standalone_prop: 241 | values["props"][key] = value 242 | else: 243 | values[key] = value 244 | 245 | assert "component" in values, "{} is missing component value".format(method) # NOQA 246 | 247 | return values 248 | 249 | 250 | def _get_tag_manager(): 251 | """ 252 | Loads a custom React Tag Manager if provided in Django Settings. 253 | """ 254 | 255 | class_path = getattr(settings, "REACT_RENDER_TAG_MANAGER", "") 256 | if not class_path: 257 | return ReactTagManager 258 | 259 | return import_string(class_path) 260 | 261 | 262 | @register.inclusion_tag("react_print.html", takes_context=True) 263 | def react_print(context): 264 | """ 265 | Generates ReactDOM.hydate calls based on REACT_COMPONENT queue, 266 | this needs to be run after react has been loaded. 267 | 268 | The queue will be cleared after beeing called. 269 | 270 | Example: 271 | {% react_print %} 272 | """ 273 | components = context.get(CONTEXT_KEY, []) 274 | context[CONTEXT_KEY] = [] 275 | 276 | new_context = context.__copy__() 277 | new_context["ssr_available"] = has_ssr(context.get("request", None)) 278 | new_context["components"] = components 279 | 280 | return new_context 281 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frojd/django-react-templatetags/60cd4efa6b45d9b40adef2f1833a1ff01f6da859/django_react_templatetags/tests/demosite/__init__.py -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | 5 | from django_react_templatetags.mixins import RepresentationMixin 6 | 7 | 8 | class Person(RepresentationMixin, models.Model): 9 | first_name = models.CharField(max_length=255) 10 | last_name = models.CharField(max_length=255) 11 | 12 | def to_react_representation(self, context={}): 13 | return { 14 | "first_name": self.first_name, 15 | "last_name": self.last_name, 16 | } 17 | 18 | 19 | class Movie(RepresentationMixin, models.Model): 20 | title = models.CharField(max_length=255) 21 | year = models.IntegerField() 22 | 23 | def to_react_representation(self, context={}): 24 | return { 25 | "title": self.title, 26 | "year": self.year, 27 | "current_path": context["request"].path, 28 | } 29 | 30 | 31 | class MovieWithContext(RepresentationMixin, models.Model): 32 | title = models.CharField(max_length=255) 33 | year = models.IntegerField() 34 | 35 | def to_react_representation(self, context={}): 36 | return { 37 | "title": self.title, 38 | "year": self.year, 39 | "search_term": context["search_term"], 40 | } 41 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/settings.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | BASE_DIR = os.path.dirname(PROJECT_DIR) 7 | 8 | DEBUG = False 9 | 10 | TIME_ZONE = "Europe/Stockholm" 11 | 12 | DATABASES = { 13 | "default": { 14 | "ENGINE": "django.db.backends.sqlite3", 15 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 16 | } 17 | } 18 | 19 | SECRET_KEY = "not needed" 20 | 21 | USE_TZ = True 22 | 23 | LANGUAGE_CODE = "en" 24 | 25 | INSTALLED_APPS = [ 26 | "django.contrib.contenttypes", 27 | "django.contrib.auth", 28 | "django.contrib.sites", 29 | "django.contrib.admin", 30 | "django.contrib.messages", 31 | "django.contrib.sessions", 32 | "django_react_templatetags", 33 | "django_react_templatetags.tests.demosite", 34 | ] 35 | 36 | ROOT_URLCONF = "django_react_templatetags.tests.demosite.urls" 37 | 38 | MIDDLEWARE = ( 39 | "django.middleware.common.CommonMiddleware", 40 | "django.contrib.sessions.middleware.SessionMiddleware", 41 | "django.middleware.csrf.CsrfViewMiddleware", 42 | "django.middleware.locale.LocaleMiddleware", 43 | "django.contrib.auth.middleware.AuthenticationMiddleware", 44 | "django.contrib.messages.middleware.MessageMiddleware", 45 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 46 | ) 47 | 48 | TEMPLATES = [ 49 | { 50 | "BACKEND": "django.template.backends.django.DjangoTemplates", 51 | "DIRS": [], 52 | "APP_DIRS": True, 53 | "OPTIONS": { 54 | "context_processors": [ 55 | "django.template.context_processors.debug", 56 | "django.template.context_processors.request", 57 | "django.contrib.auth.context_processors.auth", 58 | "django.contrib.messages.context_processors.messages", 59 | "django_react_templatetags.context_processors.react_context_processor", 60 | ], 61 | }, 62 | }, 63 | ] 64 | 65 | DEFAULT_AUTO_FIELD = "django.db.models.AutoField" 66 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/templates/static-react.html: -------------------------------------------------------------------------------- 1 | {% load react %} 2 | 3 | {% react_render component="Component" props=props %} 4 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from django_react_templatetags.tests.demosite import views 4 | 5 | urlpatterns = [ 6 | path( 7 | "static-react-view", 8 | views.StaticReactView.as_view(), 9 | name="static_react_view", 10 | ), 11 | ] 12 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/demosite/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import TemplateView 2 | 3 | 4 | class StaticReactView(TemplateView): 5 | template_name = "static-react.html" 6 | 7 | def get_context_data(self, **kwargs): 8 | context = super(StaticReactView, self).get_context_data(**kwargs) 9 | context["props"] = { 10 | "artist": "Tom Waits", 11 | "recent_album": "Bad as me", 12 | } 13 | return context 14 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/mock_response.py: -------------------------------------------------------------------------------- 1 | class MockResponse: 2 | def __init__(self, data, status_code, ok=True): 3 | self.data = data 4 | self.text = data 5 | self.status_code = status_code 6 | self.ok = ok 7 | 8 | def raise_for_status(self): 9 | pass 10 | 11 | def json(self): 12 | return self.data 13 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/test_filters.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import django 4 | from django.template import Context, Template 5 | from django.test import SimpleTestCase, override_settings 6 | from django.test.client import RequestFactory 7 | 8 | from django_react_templatetags.tests.demosite.models import Movie, Person 9 | 10 | 11 | class ReactIncludeComponentTest(SimpleTestCase): 12 | def setUp(self): 13 | self.mocked_context = Context({"REACT_COMPONENTS": []}) 14 | 15 | def test_react_tag(self): 16 | "The react_render inserts one components into the template" 17 | 18 | out = Template( 19 | "{% load react %}" '{% react_render component="Component" %}' 20 | ).render(self.mocked_context) 21 | 22 | self.assertTrue('
Title", 200)] 40 | 41 | out = Template( 42 | "{% load react %}" '{% react_render component="Component" %}' 43 | ).render(self.mocked_context) 44 | 45 | self.assertFalse("{'html': " in out) 46 | 47 | @mock.patch("requests.post") 48 | def test_verify_rendition(self, mocked): 49 | "The SSR returns inner html" 50 | 51 | mocked.side_effect = [MockResponse("

Title

", 200)] 52 | 53 | out = Template( 54 | "{% load react %}" '{% react_render component="Component" %}' 55 | ).render(self.mocked_context) 56 | 57 | self.assertTrue("

Title

" in out) 58 | 59 | @mock.patch("requests.post") 60 | def test_request_body(self, mocked): 61 | "The SSR request sends the props in a expected way" 62 | 63 | mocked.side_effect = [MockResponse("

Title

", 200)] 64 | 65 | person = Person(first_name="Tom", last_name="Waits") 66 | 67 | self.mocked_context["person"] = person 68 | self.mocked_context["component_data"] = {"album": "Real gone"} 69 | 70 | Template( 71 | "{% load react %}" 72 | '{% react_render component="Component" prop_person=person data=component_data %}' # NOQA 73 | ).render(self.mocked_context) 74 | 75 | request_body = { 76 | "componentName": "Component", 77 | "props": { 78 | "album": "Real gone", 79 | "person": {"first_name": "Tom", "last_name": "Waits"}, 80 | }, 81 | "context": {}, 82 | } 83 | 84 | self.assertEqual(json.loads(mocked.call_args[1]["data"]), request_body) 85 | 86 | @mock.patch("requests.post") 87 | def test_request_body_context(self, mocked): 88 | "The SSR request sends the props in a expected way with context" 89 | 90 | mocked.side_effect = [MockResponse("

Title

", 200)] 91 | 92 | movie = MovieWithContext(title="Office space", year=1991) 93 | 94 | self.mocked_context["movie"] = movie 95 | self.mocked_context["search_term"] = "Stapler" 96 | 97 | Template( 98 | "{% load react %}" 99 | '{% react_render component="Component" prop_movie=movie %}' 100 | ).render(self.mocked_context) 101 | 102 | request_body = { 103 | "componentName": "Component", 104 | "props": { 105 | "movie": { 106 | "title": "Office space", 107 | "year": 1991, 108 | "search_term": "Stapler", 109 | } 110 | }, 111 | "context": {}, 112 | } 113 | 114 | self.assertEqual(json.loads(mocked.call_args[1]["data"]), request_body) 115 | 116 | @mock.patch("requests.post") 117 | def test_request_body_with_ssr_context(self, mocked): 118 | "The SSR request appends the 'ssr_context' in an expected way" 119 | 120 | mocked.side_effect = [MockResponse("

Title

", 200)] 121 | 122 | self.mocked_context["ssr_ctx"] = {"location": "http://localhost"} 123 | 124 | Template( 125 | "{% load react %}" 126 | '{% react_render component="Component" ssr_context=ssr_ctx %}' 127 | ).render(self.mocked_context) 128 | 129 | request_body = { 130 | "componentName": "Component", 131 | "props": {}, 132 | "context": {"location": "http://localhost"}, 133 | } 134 | 135 | self.assertEqual(json.loads(mocked.call_args[1]["data"]), request_body) 136 | 137 | @mock.patch("requests.post") 138 | def test_default_headers(self, mocked): 139 | "The SSR uses default headers with json as conten type" 140 | mocked.side_effect = [MockResponse("Foo Bar", 200)] 141 | 142 | Template("{% load react %}" '{% react_render component="Component" %}').render( 143 | self.mocked_context 144 | ) 145 | 146 | headers = { 147 | "Content-type": "application/json", 148 | "Accept": "text/plain", 149 | } 150 | 151 | self.assertEqual(mocked.call_count, 1) 152 | self.assertEqual(mocked.call_args[1]["headers"], headers) 153 | 154 | @override_settings(REACT_RENDER_HEADERS={"Authorization": "Basic 123"}) 155 | @mock.patch("requests.post") 156 | def test_custom_headers(self, mocked): 157 | "The SSR uses custom headers if present" 158 | mocked.side_effect = [MockResponse("Foo Bar", 200)] 159 | 160 | Template("{% load react %}" '{% react_render component="Component" %}').render( 161 | self.mocked_context 162 | ) 163 | 164 | self.assertTrue(mocked.call_count == 1) 165 | self.assertEqual(mocked.call_args[1]["headers"]["Authorization"], "Basic 123") 166 | 167 | @mock.patch("requests.post") 168 | def test_hydrate_if_ssr_present(self, mocked): 169 | "Makes sure ReactDOM.hydrate is used when SSR is active" 170 | mocked.side_effect = [MockResponse("Foo Bar", 200)] 171 | 172 | out = Template( 173 | "{% load react %}" 174 | '{% react_render component="Component" %}' 175 | "{% react_print %}" 176 | ).render(self.mocked_context) 177 | 178 | self.assertTrue("ReactDOM.hydrate(" in out) 179 | 180 | @mock.patch("requests.post") 181 | def test_ssr_params_are_stored_in_component_queue(self, mocked): 182 | mocked.side_effect = [MockResponse("Foo Bar", 200)] 183 | 184 | Template("{% load react %}" '{% react_render component="Component" %}').render( 185 | self.mocked_context 186 | ) 187 | 188 | queue = self.mocked_context["REACT_COMPONENTS"] 189 | self.assertTrue("ssr_params" in queue[0]) 190 | self.assertEqual(queue[0]["ssr_params"], {}) 191 | 192 | 193 | @override_settings( 194 | REACT_RENDER_HOST="http://react-service.dev/", 195 | ) 196 | class SSRViewTest(SimpleTestCase): 197 | @mock.patch("django_react_templatetags.ssr.default.SSRService.load_or_empty") 198 | def test_that_disable_ssr_header_disables_ssr(self, mocked_func): 199 | self.client.get( 200 | reverse("static_react_view"), 201 | HTTP_X_DISABLE_SSR="1", 202 | ) 203 | self.assertEqual(mocked_func.call_count, 0) 204 | 205 | 206 | @override_settings( 207 | REACT_RENDER_HOST="http://react-service.dev/batch", 208 | ) 209 | class DefaultServiceTest(SimpleTestCase): 210 | @mock.patch("requests.post") 211 | def test_load_or_empty_returns_ok_data(self, mocked): 212 | mocked.side_effect = [MockResponse("Foo Bar", 200)] 213 | 214 | service = SSRService() 215 | resp = service.load_or_empty( 216 | { 217 | "json": "{}", 218 | "name": "App", 219 | } 220 | ) 221 | self.assertTrue("html" in resp) 222 | self.assertTrue("Foo Bar" in resp["html"]) 223 | self.assertTrue("params" in resp) 224 | params = resp["params"] 225 | self.assertEqual(params, {}) 226 | -------------------------------------------------------------------------------- /django_react_templatetags/tests/test_ssr_hypernova_service.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from .mock_response import MockResponse 4 | 5 | try: 6 | from unittest import mock 7 | except ImportError: 8 | import mock 9 | 10 | from django.template import Context, Template 11 | from django.test import SimpleTestCase, override_settings 12 | 13 | from django_react_templatetags.ssr.hypernova import HypernovaService 14 | from django_react_templatetags.tests.demosite.models import MovieWithContext, Person 15 | 16 | 17 | @override_settings( 18 | REACT_RENDER_HOST="http://react-service.dev/batch", 19 | REACT_SSR_SERVICE="django_react_templatetags.ssr.hypernova.HypernovaService", 20 | ) 21 | class HypernovaTemplateTest(SimpleTestCase): 22 | def setUp(self): 23 | self.mocked_context = Context({"REACT_COMPONENTS": []}) 24 | 25 | @mock.patch("requests.post") 26 | def test_verify_404(self, mocked): 27 | "The SSR rendering falls back to client side rendering if 404" 28 | 29 | mocked.side_effect = [MockResponse({"error": "not found"}, 404, ok=False)] 30 | 31 | out = Template( 32 | "{% load react %}" '{% react_render component="Component" %}' 33 | ).render(self.mocked_context) 34 | 35 | self.assertTrue('
Title"), 42 | 200, 43 | ) 44 | ] 45 | 46 | out = Template( 47 | "{% load react %}" '{% react_render component="Component" %}' 48 | ).render(self.mocked_context) 49 | 50 | self.assertFalse("{'html': " in out) 51 | 52 | @mock.patch("requests.post") 53 | def test_verify_rendition(self, mocked): 54 | "The SSR returns inner html" 55 | 56 | mocked.side_effect = [ 57 | MockResponse( 58 | mock_hypernova_success_response("

Title

"), 59 | 200, 60 | ) 61 | ] 62 | 63 | out = Template( 64 | "{% load react %}" '{% react_render component="Component" %}' 65 | ).render(self.mocked_context) 66 | 67 | self.assertTrue("

Title

" in out) 68 | 69 | @mock.patch("requests.post") 70 | def test_request_body(self, mocked): 71 | "The SSR request sends the props in a expected way" 72 | 73 | mocked.side_effect = [ 74 | MockResponse( 75 | mock_hypernova_success_response("

Title

"), 76 | 200, 77 | ) 78 | ] 79 | 80 | person = Person(first_name="Tom", last_name="Waits") 81 | 82 | self.mocked_context["person"] = person 83 | self.mocked_context["component_data"] = {"album": "Real gone"} 84 | 85 | Template( 86 | "{% load react %}" 87 | '{% react_render component="Component" prop_person=person data=component_data %}' # NOQA 88 | ).render(self.mocked_context) 89 | 90 | request_body = { 91 | "album": "Real gone", 92 | "person": {"first_name": "Tom", "last_name": "Waits"}, 93 | } 94 | self.assertTrue("Component" in mocked.call_args[1]["json"]) 95 | self.assertEqual(mocked.call_args[1]["json"]["Component"]["data"], request_body) 96 | 97 | @mock.patch("requests.post") 98 | def test_request_body_context(self, mocked): 99 | "The SSR request sends the props in a expected way with context" 100 | 101 | mocked.side_effect = [ 102 | MockResponse( 103 | mock_hypernova_success_response("

Title

"), 104 | 200, 105 | ) 106 | ] 107 | 108 | movie = MovieWithContext(title="Office space", year=1991) 109 | 110 | self.mocked_context["movie"] = movie 111 | self.mocked_context["search_term"] = "Stapler" 112 | 113 | Template( 114 | "{% load react %}" 115 | '{% react_render component="Component" prop_movie=movie %}' 116 | ).render(self.mocked_context) 117 | 118 | request_body = { 119 | "movie": { 120 | "title": "Office space", 121 | "year": 1991, 122 | "search_term": "Stapler", 123 | } 124 | } 125 | 126 | self.assertTrue("Component" in mocked.call_args[1]["json"]) 127 | self.assertEqual(mocked.call_args[1]["json"]["Component"]["data"], request_body) 128 | 129 | @mock.patch("requests.post") 130 | def test_default_headers(self, mocked): 131 | "The SSR uses default headers with json as conten type" 132 | mocked.side_effect = [ 133 | MockResponse( 134 | mock_hypernova_success_response("Foo Bar"), 135 | 200, 136 | ) 137 | ] 138 | 139 | Template("{% load react %}" '{% react_render component="Component" %}').render( 140 | self.mocked_context 141 | ) 142 | 143 | headers = { 144 | "Content-type": "application/json", 145 | "Accept": "text/plain", 146 | } 147 | 148 | self.assertEqual(mocked.call_count, 1) 149 | self.assertEqual(mocked.call_args[1]["headers"], headers) 150 | 151 | @override_settings(REACT_RENDER_HEADERS={"Authorization": "Basic 123"}) 152 | @mock.patch("requests.post") 153 | def test_custom_headers(self, mocked): 154 | "The SSR uses custom headers if present" 155 | mocked.side_effect = [ 156 | MockResponse( 157 | mock_hypernova_success_response("Foo Bar"), 158 | 200, 159 | ) 160 | ] 161 | 162 | Template("{% load react %}" '{% react_render component="Component" %}').render( 163 | self.mocked_context 164 | ) 165 | 166 | self.assertTrue(mocked.call_count == 1) 167 | self.assertEqual(mocked.call_args[1]["headers"]["Authorization"], "Basic 123") 168 | 169 | @mock.patch("requests.post") 170 | def test_ssr_params_are_stored_in_component_queue(self, mocked): 171 | mocked.side_effect = [ 172 | MockResponse( 173 | mock_hypernova_success_response("Foo Bar"), 174 | 200, 175 | ) 176 | ] 177 | 178 | Template("{% load react %}" '{% react_render component="Component" %}').render( 179 | self.mocked_context 180 | ) 181 | 182 | queue = self.mocked_context["REACT_COMPONENTS"] 183 | self.assertTrue("ssr_params" in queue[0]) 184 | 185 | ssr_params = queue[0]["ssr_params"] 186 | self.assertIn("hypernova_id", ssr_params) 187 | self.assertIn("hypernova_key", ssr_params) 188 | 189 | @mock.patch("requests.post") 190 | def test_only_ssr_html_are_returned_on_no_placeholder(self, mocked): 191 | mocked.side_effect = [ 192 | MockResponse( 193 | mock_hypernova_success_response("Foo Bar"), 194 | 200, 195 | ) 196 | ] 197 | 198 | out = Template( 199 | "{% load react %}" 200 | '{% react_render component="Component" no_placeholder=1 %}' 201 | ).render(self.mocked_context) 202 | 203 | queue = self.mocked_context["REACT_COMPONENTS"] 204 | self.assertEqual(len(queue), 1) 205 | self.assertFalse(out.startswith('
' 287 | "{}".format(key, id, body) 288 | + "
\n" 289 | + '" 293 | ) # NOQA 294 | 295 | return { 296 | "success": True, 297 | "error": None, 298 | "results": { 299 | component_name: { 300 | "name": component_name, 301 | "html": html, 302 | "meta": {}, 303 | "duration": 0.279824, 304 | "statusCode": 200, 305 | "success": True, 306 | "error": None, 307 | } 308 | }, 309 | } 310 | -------------------------------------------------------------------------------- /docs/django-react-templatetags-logo.monopic: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frojd/django-react-templatetags/60cd4efa6b45d9b40adef2f1833a1ff01f6da859/docs/django-react-templatetags-logo.monopic -------------------------------------------------------------------------------- /docs/example-multiple-components.md: -------------------------------------------------------------------------------- 1 | # Adding multiple components 2 | 3 | You can also have multiple components in the same template 4 | 5 | This view... 6 | 7 | ```python 8 | from django.shortcuts import render 9 | 10 | def menu_view(request): 11 | return render(request, 'myapp/index.html', { 12 | 'menu_data': { 13 | 'example': 1, 14 | }, 15 | 'title_data': 'My title', 16 | 'footer_data': { 17 | 'credits': 'Copyright Company X' 18 | } 19 | }) 20 | ``` 21 | 22 | ... and this template: 23 | 24 | ```html 25 | {% load react %} 26 | 27 | ... 28 | 29 | 30 | 35 | 36 | 37 | {% react_print %} 38 | 39 | ``` 40 | 41 | Will transform into this: 42 | 43 | ```html 44 | 45 | ... 46 | 47 | 48 | 51 |
52 |
53 |
54 |
55 | 56 |
57 | 58 | 59 | 60 | 69 | 70 | 79 | 80 | 89 | 90 | ``` 91 | -------------------------------------------------------------------------------- /docs/example-single-component.md: -------------------------------------------------------------------------------- 1 | # Single Component Example 2 | 3 | This view... 4 | 5 | ```python 6 | from django.shortcuts import render 7 | 8 | def menu_view(request): 9 | return render(request, 'myapp/index.html', { 10 | 'menu_data': { 11 | 'example': 1, 12 | }, 13 | }) 14 | ``` 15 | 16 | ... and this template: 17 | 18 | ```html 19 | {% load react %} 20 | 21 | ... 22 | 23 | 24 | 27 | 28 | 29 | {% react_print %} 30 | 31 | ``` 32 | 33 | Will transform into this: 34 | 35 | ```html 36 | 37 | ... 38 | 39 | 40 | 43 | 44 | 45 | 46 | 55 | 56 | 57 | ``` 58 | 59 | -------------------------------------------------------------------------------- /docs/examples.md: -------------------------------------------------------------------------------- 1 | # Example Applications 2 | 3 | ### [Django-react-polls - A fully React-rendered Django application with react-sass-starterkit](https://github.com/mikaelengstrom/django-react-polls-example/) 4 | A example app for a Django-meetup talk that showcases how to use DRTT with webpack. you might find the [slides on Slideshare](https://www.slideshare.net/Frojd/integrating-react-in-django-while-staying-sane-and-happy) helpful. 5 | 6 | ### [Django React Polls with Create React App and Hypernova](https://github.com/marteinn/django-react-polls-with-hypernova-examples) 7 | A fork of Django-React-Polls that includes working examples on how to implement Create React App and Hypernova with Django React Templatetags 8 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | ### How do I override the markup generated by `react_print`? 4 | 5 | Simple! Just override the template `react_print.html` 6 | 7 | ### This library only contains templatetags, where is the react js library? 8 | 9 | This library only covers the template parts (that is: placeholder and js render). 10 | 11 | ### I dont like the autogenerated element id, can I supply my own? 12 | 13 | Sure! Just add the param `identifier="yourid"` in `react_render`. 14 | 15 | Example: 16 | ``` 17 | {% react_render component="Component" identifier="yourid" %} 18 | ``` 19 | 20 | ...will print 21 | ```html 22 |
23 | ``` 24 | 25 | ### How do I pass individual props? 26 | 27 | Add your props as arguments prefixed with `prop_*` to your `{% react_render ... %}`. 28 | 29 | Example: 30 | ```html 31 | {% react_render component="Component" prop_country="Sweden" prop_city="Stockholm" %} 32 | ``` 33 | 34 | ...will give the component this payload: 35 | ```javascript 36 | React.createElement(Component, {"country": "Sweden", "city": "Stockholm"}), 37 | ``` 38 | 39 | ### How do I apply my own css class to the autogenerated element? 40 | 41 | Add `class="yourclassname"` to your `{% react_render ... %}`. 42 | 43 | Example: 44 | ```html 45 | {% react_render component="Component" class="yourclassname" %} 46 | ``` 47 | 48 | ...will print 49 | ```html 50 |
51 | ``` 52 | 53 | 54 | ### Can I skip SSR on a certain request? 55 | 56 | Yup, just pass the header `HTTP_X_DISABLE_SSR` in your request and SSR will be skipped in that response. 57 | 58 | 59 | ### I want to pass the component name as a variable, is that possible? 60 | 61 | Yes! Just remove the string declaration and reference a variable in your `{% react_render ... %}`, the same way you do with `props`. 62 | 63 | Example: 64 | 65 | This view 66 | 67 | ```python 68 | render(request, 'myapp/index.html', { 69 | 'component_name': 'MegaMenu', 70 | }) 71 | ``` 72 | 73 | ...and this template 74 | 75 | ```html 76 | {% react_render component=component_name %} 77 | ``` 78 | 79 | ...will print: 80 | 81 | ```html 82 |
83 | React.createElement(MegaMenu), 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ### Requirements 4 | 5 | - Python 3.8+ 6 | - Django 3.2, 5.0 and 4.2 7 | 8 | 9 | ### Installation 10 | 11 | Install the library with pip: 12 | 13 | ``` 14 | $ pip install django_react_templatetags 15 | ``` 16 | 17 | 18 | ### Adding to django 19 | 20 | Make sure `django_react_templatetags` is added to your `INSTALLED_APPS`. 21 | 22 | ```python 23 | INSTALLED_APPS = ( 24 | # ... 25 | 'django_react_templatetags', 26 | ) 27 | ``` 28 | 29 | You also need to add the `react_context_processor` into the `context_middleware`: 30 | 31 | ```python 32 | TEMPLATES = [ 33 | { 34 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 35 | 'DIRS': [ 36 | 'templates...', 37 | ], 38 | 'APP_DIRS': True, 39 | 'OPTIONS': { 40 | 'debug': True, 41 | 'context_processors': [ 42 | ... 43 | 'django_react_templatetags.context_processors.react_context_processor', 44 | ], 45 | }, 46 | }, 47 | ] 48 | ``` 49 | 50 | 51 | ### Set up React/ReactDOM and expose your components 52 | 53 | There are various ways of compiling react and components, to many (and fast changing) to be included in these docs. The important thing is to make sure your javascript bundle exposes `ReactDOM` globally and of course your components. 54 | 55 | 56 | ### Connecting view and template 57 | 58 | This view... 59 | 60 | ```python 61 | from django.shortcuts import render 62 | 63 | def menu_view(request): 64 | return render(request, 'myapp/index.html', { 65 | 'menu_data': { 66 | 'example': 1, 67 | }, 68 | }) 69 | ``` 70 | 71 | ... and this template: 72 | 73 | ```html 74 | {% load react %} 75 | 76 | ... 77 | 78 | 79 | 82 | 83 | 84 | 88 | {% react_print %} 89 | 90 | ``` 91 | 92 | Will transform into this: 93 | 94 | ```html 95 | 96 | ... 97 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /docs/server-side-rendering.md: -------------------------------------------------------------------------------- 1 | # Server Side Rendering 2 | 3 | This library supports two SSR services, Hypernova and Hastur. 4 | 5 | ## Hypernova 6 | 7 | 8 | ### Installing 9 | 10 | (We assume you got a working Hypernova SSR service running). To use hypernova you need to do the following: 11 | 12 | 1. Install `django_react_templatetags` with the `hypernova` flag. 13 | 14 | ``` 15 | pip install django_react_templatetags[hypernova] 16 | ``` 17 | 18 | 2. Change SSR Service to hypernova (by adding this django setting) 19 | 20 | ``` 21 | REACT_SSR_SERVICE="django_react_templatetags.ssr.hypernova.HypernovaService" 22 | ``` 23 | 24 | 3. Make sure your `REACT_RENDER_HOST` points to the batch endpoint 25 | 26 | ``` 27 | REACT_RENDER_HOST='http://react-service.test/batch 28 | ``` 29 | 30 | ### Examples 31 | 32 | - [Django React Polls with Hypernova examples](https://github.com/marteinn/django-react-polls-with-hypernova-examples) covers two ways of implementing Hypernova in Django and DRTT. 33 | 34 | ## Hastur 35 | 36 | ### Installing 37 | 38 | To use django-react-templatetags with [Hastur](https://github.com/Frojd/Hastur) you need to do the following: 39 | 40 | 1. Install `django_react_templatetags` with the `ssr` flag. 41 | 42 | ``` 43 | pip install django_react_templatetags[ssr] 44 | ``` 45 | 46 | 2. Point the right endpoint: 47 | 48 | ``` 49 | REACT_RENDER_HOST='http://hastur-service.test/ 50 | ``` 51 | 52 | ### How it works 53 | It works by posting component name and props to endpoint, that returns the html rendition. Payload example: 54 | 55 | ```json 56 | { 57 | "componentName": "MyComponent", 58 | "props": { 59 | "title": "my props title", 60 | "anyProp": "another prop" 61 | }, 62 | "context": {"location": "http://localhost"}, 63 | "static": false 64 | } 65 | ``` 66 | 67 | You can set the context-parameter by using the `ssr_context` property on the template tag: 68 | ```html 69 | {% react_render component="Component" ssr_context=ctx %} 70 | ``` 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/settings.md: -------------------------------------------------------------------------------- 1 | # Settings 2 | 3 | ### General 4 | 5 | - `REACT_COMPONENT_PREFIX`: Adds a prefix to your React.createElement include, if you want to retrive your components from somewhere else then the global scope. (Default is `""`). 6 | - Example using (`REACT_COMPONENT_PREFIX="Cookie."`) will look for components in a global scope object named `Cookie`. 7 | - ...Becomes: `React.createElement(Cookie.MenuComponent, {})` 8 | - `REACT_RENDER_TAG_MANAGER`: This is a advanced setting that lets you replace our tag parsing rules (ReactTagManager) with your own. (Default is `""`) 9 | - Example: `"myapp.manager.MyReactTagManager"` 10 | 11 | ### SSR (Server Side Rendering) 12 | 13 | - `REACT_RENDER_HOST`: Which endpoint SSR requests should be posted at. (Default is `""`) 14 | - Example: `http://localhost:7000/render-component/` 15 | - The render host is a web service that accepts a post request and and renders the component to HTML. (This is what our [Hastur](https://github.com/Frojd/hastur) service does) 16 | - `REACT_RENDER_TIMEOUT`: Timeout for SSR requests, in seconds. (Default is `20`) 17 | - `REACT_RENDER_HEADERS`: Override the default request headers sent to the SSR service. Default: `{'Content-type': 'application/json', 'Accept': 'text/plain'}`. 18 | - Example: `REACT_RENDER_HEADERS = {'Authorization': 'Basic 123'}` 19 | - `REACT_SSR_SERVICE`: Replace the SSR Service with your own, can be useful if you have custom needs or our structure does not fit your use case. (Default is `django_react_templatetags.ssr.default.SSRService`). 20 | -------------------------------------------------------------------------------- /docs/templatetags-params.md: -------------------------------------------------------------------------------- 1 | # How to use the templatetags included in this library 2 | 3 | ### react_render 4 | 5 | This templatetag are used when you want to initialize a component 6 | 7 | Accepts the following params: 8 | - `component`: The name of the component you want to include 9 | - `identifier`: Lets you override the autogenerated id with your own (Optional) 10 | - `class`: Allows you to append classes to the placeholder div (Optional) 11 | - `props`: A dict with props you want to send to your react component (Optional) 12 | - `prop_*`: Allows you to pass individual props to a component (Optional) 13 | - `ssr_context`: A dictionary with values you want to send to the SSR (Optional) 14 | - `no_placeholder`: Does not print the autogenerated placeholder div, ssr content are still printed, for this reason it is a recommended param when you work with Hypernova. (Optional) 15 | -------------------------------------------------------------------------------- /docs/working-with-models.md: -------------------------------------------------------------------------------- 1 | # Working with models 2 | 3 | In this example, by adding `RepresentationMixin` as a mixin to the model, the templatetag will know how to generate the component data. You only need to pass the model instance to the `react_render` templatetag. 4 | 5 | This model... 6 | 7 | ```python 8 | from django.db import models 9 | from django_react_templatetags.mixins import RepresentationMixin 10 | 11 | class Person(RepresentationMixin, models.Model): 12 | first_name = models.CharField(max_length=255) 13 | last_name = models.CharField(max_length=255) 14 | 15 | def to_react_representation(self, context={}): 16 | return { 17 | 'first_name': self.first_name, 18 | 'last_name': self.last_name, 19 | } 20 | ``` 21 | 22 | ...and this view 23 | 24 | ```python 25 | import myapp.models import Person 26 | 27 | def person_view(request, pk): 28 | return render(request, 'myapp/index.html', { 29 | 'menu_data': { 30 | 'person': Person.objects.get(pk=pk), 31 | }, 32 | }) 33 | ``` 34 | 35 | ...and this template: 36 | 37 | ```html 38 | {% load react %} 39 | 40 | ... 41 | 42 | 43 | 46 | 47 | 48 | {% react_print %} 49 | 50 | ``` 51 | 52 | ...will transform into this: 53 | 54 | ```html 55 | ... 56 | 57 | 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /example_django_react_templatetags/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim 2 | MAINTAINER Frojd 3 | 4 | ENV PYTHONUNBUFFERED=1 \ 5 | REQUIREMENTS=requirements.txt 6 | 7 | RUN apt-get update \ 8 | && apt-get install -y netcat-traditional gcc libpq-dev \ 9 | && apt-get install -y binutils libproj-dev \ 10 | && apt-get install -y gettext \ 11 | && rm -rf /var/lib/apt/lists/* 12 | 13 | WORKDIR /app 14 | ADD . /app/ 15 | 16 | RUN pip install --upgrade pip \ 17 | && pip install -r $REQUIREMENTS --no-cache-dir 18 | 19 | EXPOSE 8080 20 | 21 | ENTRYPOINT ["./docker-entrypoint.sh"] 22 | CMD ["runserver"] 23 | -------------------------------------------------------------------------------- /example_django_react_templatetags/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | web: 4 | image: frojd/django-react-templatetags-web 5 | build: . 6 | volumes: 7 | - ./:/app 8 | ports: 9 | - "8086:8000" 10 | environment: 11 | - DJANGO_SETTINGS_MODULE=examplesite.settings 12 | -------------------------------------------------------------------------------- /example_django_react_templatetags/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # $0 is a script name, $1, $2, $3 etc are passed arguments 3 | # $1 is our command 4 | # Credits: https://rock-it.pl/how-to-write-excellent-dockerfiles/ 5 | CMD=$1 6 | 7 | setup_django () { 8 | echo Running migrations 9 | python manage.py migrate --noinput 10 | 11 | echo Collecting static-files 12 | python manage.py collectstatic --noinput 13 | 14 | } 15 | 16 | case "$CMD" in 17 | "runserver" ) 18 | setup_django 19 | 20 | echo Starting using manage.py runserver 21 | exec python manage.py runserver 0.0.0.0:8000 22 | ;; 23 | * ) 24 | # Run custom command. Thanks to this line we can still use 25 | # "docker run our_container /bin/bash" and it will work 26 | exec $CMD ${@:2} 27 | ;; 28 | esac 29 | -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frojd/django-react-templatetags/60cd4efa6b45d9b40adef2f1833a1ff01f6da859/example_django_react_templatetags/examplesite/__init__.py -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for examplesite project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.2. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.2/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = "EXAMPLE_SECRET_KEY" 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | "django.contrib.admin", 35 | "django.contrib.auth", 36 | "django.contrib.contenttypes", 37 | "django.contrib.sessions", 38 | "django.contrib.messages", 39 | "django.contrib.staticfiles", 40 | "django_react_templatetags", 41 | "examplesite", 42 | ] 43 | 44 | MIDDLEWARE = [ 45 | "django.middleware.security.SecurityMiddleware", 46 | "django.contrib.sessions.middleware.SessionMiddleware", 47 | "django.middleware.common.CommonMiddleware", 48 | "django.middleware.csrf.CsrfViewMiddleware", 49 | "django.contrib.auth.middleware.AuthenticationMiddleware", 50 | "django.contrib.messages.middleware.MessageMiddleware", 51 | "django.middleware.clickjacking.XFrameOptionsMiddleware", 52 | ] 53 | 54 | ROOT_URLCONF = "examplesite.urls" 55 | 56 | TEMPLATES = [ 57 | { 58 | "BACKEND": "django.template.backends.django.DjangoTemplates", 59 | "DIRS": [], 60 | "APP_DIRS": True, 61 | "OPTIONS": { 62 | "context_processors": [ 63 | "django.template.context_processors.debug", 64 | "django.template.context_processors.request", 65 | "django.contrib.auth.context_processors.auth", 66 | "django.contrib.messages.context_processors.messages", 67 | "django_react_templatetags.context_processors.react_context_processor", 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = "examplesite.wsgi.application" 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases 78 | 79 | DATABASES = { 80 | "default": { 81 | "ENGINE": "django.db.backends.sqlite3", 82 | "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 83 | } 84 | } 85 | 86 | 87 | # Password validation 88 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators 89 | 90 | AUTH_PASSWORD_VALIDATORS = [ 91 | { 92 | "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", 93 | }, 94 | { 95 | "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", 96 | }, 97 | { 98 | "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", 99 | }, 100 | { 101 | "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", 102 | }, 103 | ] 104 | 105 | 106 | # Internationalization 107 | # https://docs.djangoproject.com/en/2.2/topics/i18n/ 108 | 109 | LANGUAGE_CODE = "en-us" 110 | 111 | TIME_ZONE = "UTC" 112 | 113 | USE_I18N = True 114 | 115 | USE_L10N = True 116 | 117 | USE_TZ = True 118 | 119 | 120 | # Static files (CSS, JavaScript, Images) 121 | # https://docs.djangoproject.com/en/2.2/howto/static-files/ 122 | 123 | STATIC_ROOT = os.path.join(BASE_DIR, "static") 124 | STATIC_URL = "/static/" 125 | -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/templates/examplesite/index.html: -------------------------------------------------------------------------------- 1 | {% load react %} 2 | 3 | 4 | 7 | 8 | {% react_print %} 9 | 10 | 11 | -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/urls.py: -------------------------------------------------------------------------------- 1 | """examplesite URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.2/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path 18 | 19 | from .views import menu_view 20 | 21 | urlpatterns = [ 22 | path("admin/", admin.site.urls), 23 | path("menu_view", menu_view), 24 | ] 25 | -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def menu_view(request): 5 | return render( 6 | request, 7 | "examplesite/index.html", 8 | { 9 | "menu_data": { 10 | "example": 1, 11 | }, 12 | }, 13 | ) 14 | -------------------------------------------------------------------------------- /example_django_react_templatetags/examplesite/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for examplesite project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examplesite.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example_django_react_templatetags/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "examplesite.settings") 9 | try: 10 | from django.core.management import execute_from_command_line 11 | except ImportError as exc: 12 | raise ImportError( 13 | "Couldn't import Django. Are you sure it's installed and " 14 | "available on your PYTHONPATH environment variable? Did you " 15 | "forget to activate a virtual environment?" 16 | ) from exc 17 | execute_from_command_line(sys.argv) 18 | 19 | 20 | if __name__ == "__main__": 21 | main() 22 | -------------------------------------------------------------------------------- /example_django_react_templatetags/requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=4.0 2 | requests 3 | -------------------------------------------------------------------------------- /example_django_react_templatetags/runtests.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from django.core.management import execute_from_command_line 6 | 7 | os.environ[ 8 | "DJANGO_SETTINGS_MODULE" 9 | ] = "django_react_templatetags.tests.demosite.settings" 10 | 11 | 12 | def runtests(): 13 | args, rest = argparse.ArgumentParser().parse_known_args() 14 | 15 | argv = [sys.argv[0], "test"] + rest 16 | execute_from_command_line(argv) 17 | 18 | 19 | if __name__ == "__main__": 20 | runtests() 21 | -------------------------------------------------------------------------------- /img/django-react-templatetags-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Frojd/django-react-templatetags/60cd4efa6b45d9b40adef2f1833a1ff01f6da859/img/django-react-templatetags-logo.png -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.ruff] 2 | line-length = 88 3 | 4 | # Never enforce `E501` (line length violations). 5 | ignore = ["E501"] 6 | 7 | exclude = [ 8 | "venv", 9 | "*/migrations/*", 10 | ] 11 | 12 | [tool.black] 13 | exclude = ''' 14 | /( 15 | \.git 16 | | \.hg 17 | | \.mypy_cache 18 | | \.tox 19 | | \.venv 20 | | _build 21 | | buck-out 22 | | build 23 | | dist 24 | | migrations 25 | )/ 26 | ''' 27 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r tests.txt 2 | pypandoc 3 | -------------------------------------------------------------------------------- /requirements/tests.txt: -------------------------------------------------------------------------------- 1 | Django>=3.2 2 | requests # For Default SSR 3 | hypernova # For Hypernova SSR 4 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from django.core.management import execute_from_command_line 6 | 7 | os.environ[ 8 | "DJANGO_SETTINGS_MODULE" 9 | ] = "django_react_templatetags.tests.demosite.settings" 10 | 11 | 12 | def runtests(): 13 | args, rest = argparse.ArgumentParser().parse_known_args() 14 | 15 | argv = [sys.argv[0], "test"] + rest 16 | execute_from_command_line(argv) 17 | 18 | 19 | if __name__ == "__main__": 20 | runtests() 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import re 5 | from pathlib import Path 6 | 7 | from setuptools import find_packages, setup 8 | 9 | this_directory = Path(__file__).parent 10 | long_description = (this_directory / "README.md").read_text() 11 | 12 | version = "" 13 | with io.open("django_react_templatetags/__init__.py", "r", encoding="utf8") as fd: 14 | version = re.search( 15 | r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', fd.read(), re.MULTILINE 16 | ).group(1) 17 | 18 | setup( 19 | name="django_react_templatetags", 20 | version=version, 21 | description=( 22 | "This django library allows you to add React components into your django templates." 23 | ), # NOQA 24 | long_description=long_description, 25 | long_description_content_type="text/markdown", 26 | author="Fröjd", 27 | author_email="martin@marteinn.se", 28 | url="https://github.com/frojd/django-react-templatetags", 29 | packages=find_packages(exclude=("tests*", "example_django_react_templatetags")), 30 | include_package_data=True, 31 | install_requires=[ 32 | "Django>=3.2", 33 | ], 34 | extras_require={ 35 | "ssr": ["requests"], 36 | "hypernova": ["hypernova"], 37 | }, 38 | tests_require=[ 39 | "Django>=3.2", 40 | "requests", 41 | ], 42 | license="MIT", 43 | zip_safe=False, 44 | classifiers=[ 45 | "Development Status :: 5 - Production/Stable", 46 | "Environment :: Web Environment", 47 | "Intended Audience :: Developers", 48 | "Natural Language :: English", 49 | "Intended Audience :: Developers", 50 | "License :: OSI Approved :: MIT License", 51 | "Programming Language :: Python", 52 | "Programming Language :: Python :: 3", 53 | "Programming Language :: Python :: 3.8", 54 | "Programming Language :: Python :: 3.9", 55 | "Programming Language :: Python :: 3.10", 56 | "Programming Language :: Python :: 3.11", 57 | "Programming Language :: Python :: 3.12", 58 | "Framework :: Django", 59 | "Framework :: Django :: 3.2", 60 | "Framework :: Django :: 4.2", 61 | "Framework :: Django :: 5.0", 62 | "Topic :: Utilities", 63 | "Programming Language :: JavaScript", 64 | ], 65 | ) 66 | --------------------------------------------------------------------------------