├── .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 | [](https://github.com/Frojd/django-react-templatetags/actions/workflows/main.yml) [](https://badge.fury.io/py/django_react_templatetags) 2 | 3 |  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 "