├── .bumpversion.cfg ├── .coveragerc ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── semgrep.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── HISTORY.rst ├── LICENSE ├── Makefile ├── README.rst ├── bin ├── test ├── upgrade_js_libs.sh └── wait_for_it.py ├── django_reactive ├── __init__.py ├── apps.py ├── fields.py ├── forms.py ├── schema_validator.py ├── static │ ├── css │ │ └── django_reactive.css │ ├── dist │ │ ├── react-dom.js │ │ ├── react-jsonschema-form.js │ │ ├── react-jsonschema-form.js.map │ │ └── react.js │ └── js │ │ └── django_reactive.js ├── templates │ └── django_reactive.html └── widget │ ├── __init__.py │ ├── fields.py │ └── widgets.py ├── docker-compose.yaml ├── example ├── __init__.py ├── example │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── manage.py ├── static │ ├── css │ │ └── extra.css │ └── js │ │ └── extra.js ├── templates │ ├── detail.html │ └── form.html ├── tester │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── fixtures │ │ └── admin.json │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ └── views.py └── todos │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── constants.py │ ├── migrations │ ├── 0001_initial.py │ └── __init__.py │ └── models.py ├── images └── simple.png ├── pyproject.toml ├── pytest.ini ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── test_fields.py ├── test_schema_validator.py └── testapp ├── __init__.py ├── apps.py ├── models.py └── settings.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.0.12 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:pyproject.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = true 3 | 4 | [report] 5 | omit = 6 | *site-packages* 7 | *tests* 8 | *.tox* 9 | show_missing = True 10 | exclude_lines = 11 | raise NotImplementedError 12 | -------------------------------------------------------------------------------- /.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,rst,ini}] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{html,css,scss,json,yml}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | 22 | [Makefile] 23 | indent_style = tab 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * django-reactive version: 2 | * Django version: 3 | * Python version: 4 | * Operating System: 5 | 6 | ### Description 7 | 8 | Describe what you were trying to get done. 9 | Tell us what happened, what went wrong, and what you expected to happen. 10 | 11 | ### What I Did 12 | 13 | ``` 14 | Paste the command(s) you ran and the output. 15 | If there was a crash, please include the traceback here. 16 | ``` 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "19:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v** # Run on all version tags 9 | pull_request: 10 | 11 | jobs: 12 | pytest: 13 | name: pytest 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | python: ["3.8", "3.9", "3.10", "3.11"] 19 | django: ["3.2.16", "4.0.8", "4.1.4" ] 20 | exclude: 21 | # Excludes Python 3.11 for Django < 4.1 22 | - python: "3.11" 23 | django: "3.2.16" 24 | - python: "3.11" 25 | django: "4.0.8" 26 | 27 | services: 28 | postgres: 29 | image: postgres 30 | env: 31 | POSTGRES_PASSWORD: postgres 32 | options: >- 33 | --health-cmd pg_isready 34 | --health-interval 5s 35 | --health-timeout 1s 36 | --health-retries 5 37 | ports: 38 | - 5432:5432 39 | 40 | steps: 41 | - uses: actions/checkout@master 42 | - uses: actions/setup-python@v2 43 | with: 44 | python-version: ${{ matrix.python }} 45 | architecture: x64 46 | - run: pip install --upgrade pip 47 | - run: pip install poetry 48 | - run: poetry config --local virtualenvs.in-project true 49 | - run: poetry install 50 | - run: pip install -U django==${{ matrix.django }} 51 | - run: PGPASSWORD=postgres psql -c 'create database tests;' -U postgres -h localhost -p 5432 52 | - run: poetry run ./bin/test 53 | 54 | pre-commit: 55 | runs-on: ubuntu-latest 56 | steps: 57 | - uses: actions/checkout@v3 58 | - uses: actions/setup-python@v3 59 | - uses: pre-commit/action@v3.0.0 60 | 61 | publish: 62 | if: startsWith(github.event.ref, 'refs/tags') 63 | name: publish 64 | needs: pytest 65 | runs-on: ubuntu-latest 66 | steps: 67 | - uses: actions/checkout@master 68 | - uses: actions/setup-python@v2 69 | with: 70 | python-version: "3.10" 71 | architecture: x64 72 | - run: pip install --upgrade pip 73 | - run: pip install poetry 74 | - run: poetry build 75 | - uses: pypa/gh-action-pypi-publish@master 76 | with: 77 | password: ${{ secrets.pypi_password_test }} 78 | repository_url: https://test.pypi.org/legacy/ 79 | - uses: pypa/gh-action-pypi-publish@master 80 | with: 81 | password: ${{ secrets.pypi_password }} 82 | - uses: actions/create-release@v1 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | with: 86 | tag_name: ${{ github.ref }} 87 | release_name: ${{ github.ref }} 88 | body: | 89 | Changes: 90 | - ... 91 | - ... 92 | draft: true 93 | prerelease: false 94 | -------------------------------------------------------------------------------- /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: {} 3 | push: 4 | branches: 5 | - main 6 | - master 7 | paths: 8 | - .github/workflows/semgrep.yml 9 | schedule: 10 | - cron: '0 0 * * 0' 11 | name: Semgrep 12 | jobs: 13 | semgrep: 14 | name: Scan 15 | runs-on: ubuntu-20.04 16 | env: 17 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 18 | container: 19 | image: returntocorp/semgrep 20 | steps: 21 | - uses: actions/checkout@v3 22 | - run: semgrep ci 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | *.egg 5 | *.egg-info 6 | 7 | # Installer logs 8 | pip-log.txt 9 | 10 | # Unit test / coverage reports 11 | .coverage 12 | .tox 13 | coverage.xml 14 | htmlcov 15 | 16 | # Mr Developer 17 | .mr.developer.cfg 18 | .project 19 | .pydevproject 20 | 21 | # Complexity 22 | output/*.html 23 | output/*/index.html 24 | 25 | # Sphinx 26 | docs/_build 27 | 28 | # Poetry 29 | poetry.lock 30 | /dist 31 | poetry.toml 32 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v4.4.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: debug-statements 9 | - id: double-quote-string-fixer 10 | 11 | - repo: https://github.com/asottile/pyupgrade 12 | rev: v3.3.1 13 | hooks: 14 | - id: pyupgrade 15 | args: [--py38-plus] 16 | 17 | - repo: https://github.com/pycqa/flake8 18 | rev: '6.0.0' 19 | hooks: 20 | - id: flake8 21 | 22 | - repo: https://github.com/psf/black 23 | rev: 22.12.0 24 | hooks: 25 | - id: black 26 | args: [--skip-string-normalization] 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10 2 | 3 | ENV PYTHONUNBUFFERED 1 4 | 5 | RUN mkdir /code 6 | WORKDIR /code 7 | 8 | RUN pip install poetry 9 | RUN poetry config virtualenvs.create false 10 | RUN poetry install 11 | -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | .. :changelog: 2 | 3 | History 4 | ------- 5 | 6 | 0.0.2 (2018-12-17) 7 | ++++++++++++++++++ 8 | 9 | * First release on PyPI. 10 | 11 | 0.0.4 (2020-02-02) 12 | ++++++++++++++++++ 13 | 14 | * Switched to poetry 15 | * Updated React and React JSON Schema Form 16 | * Updated some other dependencies 17 | * Added support for Python 3.8 18 | 19 | 0.0.8 (2020-11-11) 20 | ++++++++++++++++++ 21 | 22 | * Added *extra_css* and *extra_js* config options to ReactJSONSchemaField class 23 | that allows injecting extra assets required for the filed 24 | * Added *on_render* config option to ReactJSONSchemaField class that adds an ability to 25 | configure dynamic *schema* and *ui_schema* based on object fields 26 | * Updated some *react* amd *react-dom* dependencies 27 | * Added support for Python 3.9 and Django 2.2, 3.0 and 3.1 in testing environment 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020, Artem Kolesnikov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: clean-pyc clean-build docs help 2 | .DEFAULT_GOAL := help 3 | define BROWSER_PYSCRIPT 4 | import os, webbrowser, sys 5 | try: 6 | from urllib import pathname2url 7 | except: 8 | from urllib.request import pathname2url 9 | 10 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) 11 | endef 12 | export BROWSER_PYSCRIPT 13 | BROWSER := python -c "$$BROWSER_PYSCRIPT" 14 | 15 | help: 16 | @grep '^[a-zA-Z]' $(MAKEFILE_LIST) | sort | awk -F ':.*?## ' 'NF==2 {printf "\033[36m %-25s\033[0m %s\n", $$1, $$2}' 17 | 18 | clean: clean-build clean-pyc 19 | 20 | clean-build: ## remove build artifacts 21 | rm -fr build/ 22 | rm -fr dist/ 23 | rm -fr *.egg-info 24 | 25 | clean-pyc: ## remove Python file artifacts 26 | find . -name '*.pyc' -exec rm -f {} + 27 | find . -name '*.pyo' -exec rm -f {} + 28 | find . -name '*~' -exec rm -f {} + 29 | 30 | lint: ## check style with flake8 31 | flake8 django_reactive tests example 32 | 33 | test: ## run tests quickly with the default Python 34 | pytest tests 35 | 36 | test-all: ## run tests on every Python version with tox 37 | tox 38 | 39 | coverage: ## check code coverage quickly with the default Python 40 | coverage run -m pytest tests 41 | coverage report -m 42 | coverage html 43 | open htmlcov/index.html 44 | 45 | docs: ## generate Sphinx HTML documentation, including API docs 46 | rm -f docs/django-reactive.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ django_reactive 49 | $(MAKE) -C docs clean 50 | $(MAKE) -C docs html 51 | $(BROWSER) docs/_build/html/index.html 52 | 53 | release: clean ## package and upload a release 54 | poetry publish 55 | 56 | sdist: clean ## package 57 | poetry build 58 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | django-reactive 3 | ============================= 4 | 5 | .. image:: https://badge.fury.io/py/django-reactive.svg 6 | :target: https://badge.fury.io/py/django-reactive 7 | 8 | .. image:: https://github.com/tyomo4ka/django-reactive/workflows/CI/badge.svg?branch=master 9 | :target: https://github.com/tyomo4ka/django-reactive/actions 10 | 11 | .. image:: https://codecov.io/gh/tyomo4ka/django-reactive/branch/master/graph/badge.svg 12 | :target: https://codecov.io/gh/tyomo4ka/django-reactive 13 | 14 | django-reactive integrates `react-jsonschema-form `_ (RJSF) 15 | in Django projects. 16 | 17 | Motivation 18 | ---------- 19 | 20 | `JSON types `_ in Postgres allow combining both relational 21 | and non-relational approaches to storing data. That can lead to a simpler database design in the most cases. 22 | 23 | Django provides ORM support for JSON types in Postgres and other databases via the 24 | `JSONField model field `_. Also the 25 | `JSONField form field `_ allows basic support of JSON in forms. 26 | Django ORM even `allows querying `_ against the data stored 27 | inside the JSON structures. Moreover, it is possible to improve performance of these queries using 28 | `GIN indexes `_ with **jsonb** types 29 | `in Django `_, which 30 | makes opens up a wide range of possibilities for simplifying application design, such as polymorphic collections, storing complex hierarchies in JSON, lists of related entities, etc. 31 | 32 | However, the main limitation of JSONField in Django is the lack of good support of UI for JSON structures as defining JSON objects 33 | inside the textarea inputs is not practical for most use cases. django-reactive tries to address this problem by offering an 34 | integration between JSONField and the awesome `react-jsonschema-form `_ 35 | (RJSF) JavaScript library. 36 | 37 | django-reactive also uses Python `jsonschema ` library for backend validation. Such integration 38 | can significantly reduce the amount of work needed for building custom forms for JSONField types. 39 | 40 | In most cases it only requires a JSON schema configuration for such field and optionally a UI schema 41 | to modify some representation parameters. 42 | 43 | A basic example of this is demonstrated below: 44 | 45 | .. code-block:: python 46 | 47 | from django.db import models 48 | 49 | from django_reactive.fields import ReactJSONSchemaField 50 | 51 | 52 | class Registration(models.Model): 53 | basic = ReactJSONSchemaField( 54 | help_text="Registration form", 55 | schema={ 56 | "title": "Register now!", 57 | "description": "Fill out the form to register.", 58 | "type": "object", 59 | "required": [ 60 | "firstName", 61 | "lastName" 62 | ], 63 | "properties": { 64 | "firstName": { 65 | "type": "string", 66 | "title": "First name" 67 | }, 68 | "lastName": { 69 | "type": "string", 70 | "title": "Last name" 71 | }, 72 | "age": { 73 | "type": "integer", 74 | "title": "Age" 75 | }, 76 | "bio": { 77 | "type": "string", 78 | "title": "Bio" 79 | }, 80 | "password": { 81 | "type": "string", 82 | "title": "Password", 83 | "minLength": 3 84 | }, 85 | "telephone": { 86 | "type": "string", 87 | "title": "Telephone", 88 | "minLength": 10 89 | } 90 | } 91 | }, 92 | ui_schema={ 93 | "firstName": { 94 | "ui:autofocus": True, 95 | "ui:emptyValue": "" 96 | }, 97 | "age": { 98 | "ui:widget": "updown", 99 | "ui:title": "Age of person", 100 | "ui:description": "(earthian year)" 101 | }, 102 | "bio": { 103 | "ui:widget": "textarea" 104 | }, 105 | "password": { 106 | "ui:widget": "password", 107 | "ui:help": "Hint: Make it strong!" 108 | }, 109 | "date": { 110 | "ui:widget": "alt-datetime" 111 | }, 112 | "telephone": { 113 | "ui:options": { 114 | "inputType": "tel" 115 | } 116 | } 117 | }, 118 | ) 119 | 120 | It will generate a form like this: 121 | 122 | .. image:: images/simple.png 123 | 124 | Quick start 125 | ----------- 126 | 127 | Install django-reactive:: 128 | 129 | pip install django-reactive 130 | 131 | Add it to your `INSTALLED_APPS`: 132 | 133 | .. code-block:: python 134 | 135 | INSTALLED_APPS = ( 136 | ... 137 | 'django_reactive', 138 | ... 139 | ) 140 | 141 | Running the example 142 | ------------------- 143 | 144 | Build the docker image for the Django application in `example/`: 145 | 146 | * Run `docker compose up -d` 147 | 148 | This will automatically create the database, run migrations, import the default superuser, and run the Django development server on `http://127.0.0.1:8000`. 149 | 150 | Django admin example 151 | ==================== 152 | 153 | * Open http://127.0.0.1:8000/admin/ and login with username `admin` and password `test`. 154 | * Go to the "Test models" admin section to see the example forms. 155 | 156 | Normal Django view example 157 | ========================== 158 | 159 | * Open http://127.0.0.1:8000/create/ to create a basic form example. 160 | 161 | You will be redirected to the detail view of the created object after the form saves. 162 | 163 | Usage outside of Django admin 164 | ----------------------------- 165 | 166 | To use outside of the Django admin, the following are required in the template: 167 | 168 | * A call to the form media property using {{ form.media }} 169 | 170 | * An HTML submit input with `name="_save"`. 171 | 172 | .. code-block:: html 173 | 174 | 175 | 176 | 177 | Homepage 178 | 179 | 180 | {{ form.media }} 181 |
182 | {% csrf_token %} 183 | {{ form }} 184 | 185 |
186 | 187 | 188 | 189 | Optional configuration 190 | ---------------------- 191 | 192 | Schema fields accept the following parameters for additional configuration: 193 | 194 | * ``extra_css``: Include additional static CSS files available in the widget. 195 | * ``extra_js``: Include additional static JavaScript files available in the widget. 196 | * ``on_render``: A python method to make dynamic schema modifications at render-time. 197 | 198 | Extra CSS and JSS files should be accessible using Django's staticfiles configurations and passed as a list of strings. 199 | 200 | Render methods require both ``schema`` and ``ui_schema`` as arguments to allow dynamic schema modification when rendering the widget. An optional ``instance`` keyword argument may also be used for referencing an object instance (must be set on the widget in the form). This method does not return anything. 201 | 202 | Example usage 203 | ============= 204 | 205 | The example below demonstrates a use-case in which the options available for a particular field may be dynamic and unavailable in the initial schema definition. These would be populated at render-time and made available in the form UI. 206 | 207 | .. code-block:: python 208 | 209 | def set_task_types(schema, ui_schema): 210 | from todos.models import TaskType 211 | 212 | task_types = list(TaskType.objects.all().values_list("name", flat=True)) 213 | schema["definitions"]["Task"]["properties"]["task_type"]["enum"] = task_types 214 | ui_schema["task_lists"]["items"]["tasks"]["items"]["task_type"][ 215 | "ui:help" 216 | ] = f"Select 1 of {len(task_types)} task types" 217 | 218 | class Todo(models.Model): 219 | """ 220 | A collection of task lists for a todo. 221 | """ 222 | 223 | name = models.CharField(max_length=255) 224 | task_lists = ReactJSONSchemaField( 225 | help_text="Task lists", 226 | schema=TODO_SCHEMA, 227 | ui_schema=TODO_UI_SCHEMA, 228 | on_render=set_task_types, 229 | extra_css=["css/extra.css"], 230 | extra_js=["js/extra.js"], 231 | ) 232 | 233 | Schema model form class 234 | ======================= 235 | 236 | The form class ``ReactJSONSchemaModelForm`` (subclassed from Django's ``ModelForm``) can be used to provide the model form's instance object to the schema field widgets: 237 | 238 | .. code-block:: python 239 | 240 | from django_reactive.forms import ReactJSONSchemaModelForm 241 | class MyModelForm(ReactJSONSchemaModelForm): 242 | ... 243 | 244 | This allows the ``on_render`` method set for a schema field to reference the instance like this: 245 | 246 | .. code-block:: python 247 | 248 | def update_the_schema(schema, ui_schema, instance=None): 249 | if instance and instance.some_condition: 250 | ui_schema["my_schema_prop"]["ui:help"] = "Some extra help text" 251 | 252 | Features 253 | -------- 254 | 255 | * React, RJSF and other JS assets are bundled with the package. 256 | * Integration with default Django admin theme. 257 | * Backend and frontend validation. 258 | * Configurable static media assets. 259 | * Dynamic schema mutation in widget renders. 260 | 261 | Limitations 262 | ----------- 263 | 264 | * `Additional properties `_ ( a feature of RJSF) is not supported. 265 | 266 | To implement this behavior you can define an array schema with one property serving as a key of the object and do 267 | transformation in the Django form. 268 | 269 | * An outdated version (1.8) of RJSF is used in this project. Not all features of RJSF 1.8 are compatible with JSON Schema 4.0. Please, refer to the documentation if any issues. 270 | 271 | Future development 272 | ------------------ 273 | 274 | * At the moment there is no plans to add new features or support a newer version of RJSF. 275 | * Probably, it is a good idea to replace RJSF with a more Django-friendly solution. It would require significant development effort though, that's why the idea is put on back burner at the moment. 276 | -------------------------------------------------------------------------------- /bin/test: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | export PREFIX="" 4 | if [ -d 'venv' ] ; then 5 | export PREFIX=".venv/bin/" 6 | export PATH=".venv/bin:$PATH" 7 | fi 8 | 9 | set -x 10 | 11 | PYTHONPATH=. ${PREFIX}pytest --ignore venv --cov=django_reactive --cov-report=term-missing "${@}" 12 | -------------------------------------------------------------------------------- /bin/upgrade_js_libs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) 4 | ROOT=$DIR/.. 5 | DIST_DIR=${ROOT}/django_reactive/static/dist 6 | 7 | curl --request GET -sL \ 8 | --url 'https://unpkg.com/react@16/umd/react.production.min.js' \ 9 | --output "${DIST_DIR}/react.js" 10 | 11 | curl --request GET -sL \ 12 | --url 'https://unpkg.com/react-dom@16/umd/react-dom.production.min.js' \ 13 | --output "${DIST_DIR}/react-dom.js" 14 | 15 | curl --request GET -sL \ 16 | --url 'https://unpkg.com/react-jsonschema-form@1.8/dist/react-jsonschema-form.js' \ 17 | --output "${DIST_DIR}/react-jsonschema-form.js" 18 | 19 | curl --request GET -sL \ 20 | --url 'https://unpkg.com/react-jsonschema-form@1.8/dist/react-jsonschema-form.js.map' \ 21 | --output "${DIST_DIR}/react-jsonschema-form.js.map" 22 | -------------------------------------------------------------------------------- /bin/wait_for_it.py: -------------------------------------------------------------------------------- 1 | #!/bin/usr/env python 2 | 3 | from optparse import OptionParser 4 | import socket 5 | import time 6 | import sys 7 | 8 | 9 | class OptionException(Exception): 10 | def __init__(self, value): 11 | self.value = value 12 | 13 | 14 | class wait_for_app: 15 | def log(self, loginfo): 16 | if self.options.quiet is not False: 17 | print(loginfo) 18 | 19 | def build_log(self, type, app, time=0): 20 | # 1=enable_timeout,2=disable_timeout,3=success_msg,4=unavailable,5=timeout_msg 21 | loginfo = { 22 | 1: '%s: waiting %d seconds for %s' % (sys.argv[0], time, app), 23 | 2: f'{sys.argv[0]}: waiting for {app} without a timeout', 24 | 3: '%s: %s is available after %d seconds' % (sys.argv[0], app, time), 25 | 4: f'{sys.argv[0]}: {app} is unavailable', 26 | 5: '%s: timeout occurred after waiting %d seconds for %s' % (sys.argv[0], time, app), 27 | }.get(type) 28 | return loginfo 29 | 30 | def wait_for(self, host, port, timeout): 31 | self.app = ('%s:%d') % (host, port) 32 | sk = socket.socket() 33 | logmsg = self.build_log(2, self.app, timeout) 34 | if timeout != 0: 35 | logmsg = self.build_log(1, self.app, timeout) 36 | sk.settimeout(timeout) 37 | self.log(logmsg) 38 | start_ts = int(time.time()) 39 | sk.connect((host, port)) 40 | end_ts = int(time.time()) 41 | diff_ts = end_ts - start_ts 42 | logmsg = self.build_log(3, self.app, diff_ts) 43 | self.log(logmsg) 44 | 45 | def get_parser(self): 46 | parser = OptionParser() 47 | parser.add_option('-a', '--address', dest='address', help='Host or IP under test') 48 | parser.add_option('-p', '--port', dest='port', help='TCP port under test') 49 | parser.add_option( 50 | '-t', '--timeout', dest='timeout', default='15', help='Timeout in seconds, zero for no timeout' 51 | ) 52 | parser.add_option('-q', '--quiet', dest='quiet', action='store_false', help="Don't output any status messages") 53 | return parser 54 | 55 | def verify_options(self): 56 | if self.options.address is None: 57 | raise OptionException('The address must be set!') 58 | elif self.options.port is None: 59 | raise OptionException('The port must be set!') 60 | elif str(self.options.port).isnumeric() is False: 61 | raise OptionException('The value of port must be number!') 62 | 63 | def start_up(self): 64 | try: 65 | parser = self.get_parser() 66 | self.options, self.args = parser.parse_args() 67 | self.verify_options() 68 | self.wait_for(self.options.address, int(self.options.port), int(self.options.timeout)) 69 | except OptionException as err: 70 | print(err) 71 | parser.print_help() 72 | except socket.timeout: 73 | logmsg = self.build_log(5, self.app, int(self.options.timeout)) 74 | self.log(logmsg) 75 | except ConnectionRefusedError: 76 | logmsg = self.build_log(4, self.app) 77 | self.log(logmsg) 78 | 79 | 80 | if __name__ == '__main__': 81 | w = wait_for_app() 82 | w.start_up() 83 | -------------------------------------------------------------------------------- /django_reactive/__init__.py: -------------------------------------------------------------------------------- 1 | default_app_config = 'django_reactive.apps.DjangoReactJsonSchemaFormConfig' 2 | -------------------------------------------------------------------------------- /django_reactive/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class DjangoReactJsonSchemaFormConfig(AppConfig): 5 | name = 'django_reactive' 6 | -------------------------------------------------------------------------------- /django_reactive/fields.py: -------------------------------------------------------------------------------- 1 | from django.core.exceptions import ValidationError 2 | from django.core import checks 3 | from jsonschema import validate, ValidationError as JSONSchemaValidationError 4 | 5 | from .widget.fields import ReactJSONSchemaFormField 6 | from .widget.widgets import ReactJSONSchemaFormWidget 7 | from .schema_validator import validate_json_schema 8 | 9 | try: 10 | # DJANGO 3.1 11 | from django.db.models import JSONField as BaseJSONField 12 | except ImportError: 13 | from django.contrib.postgres.fields import JSONField as BaseJSONField 14 | 15 | 16 | class ReactJSONSchemaField(BaseJSONField): 17 | def __init__(self, schema=None, ui_schema=None, on_render=None, extra_css=None, extra_js=None, **kwargs): 18 | kwargs.setdefault('default', dict) 19 | super().__init__(**kwargs) 20 | self.schema = schema 21 | self.ui_schema = ui_schema 22 | self.on_render = on_render 23 | self.extra_css = extra_css 24 | self.extra_js = extra_js 25 | 26 | def formfield(self, **kwargs): 27 | defaults = { 28 | 'required': not self.blank, 29 | } 30 | defaults.update(**kwargs) 31 | return ReactJSONSchemaFormField( 32 | widget=ReactJSONSchemaFormWidget( 33 | schema=self.schema, 34 | ui_schema=self.ui_schema, 35 | on_render=self.on_render, 36 | extra_css=self.extra_css, 37 | extra_js=self.extra_js, 38 | ), 39 | **defaults, 40 | ) 41 | 42 | def validate(self, value, model_instance): 43 | super().validate(value, model_instance) 44 | try: 45 | validate(value, self.schema) 46 | except JSONSchemaValidationError: 47 | raise ValidationError('This field has errors.') 48 | 49 | def check(self, **kwargs): 50 | errors = super().check(**kwargs) 51 | res, schema_errors = validate_json_schema(self.schema) 52 | if not res: 53 | msg = ','.join(schema_errors) 54 | errors = [ 55 | checks.Error( 56 | f'JSON schema is not valid: {msg}', 57 | obj=self.model, 58 | id='fields.JSON_SCHEMA_ERROR', 59 | ) 60 | ] 61 | 62 | return errors 63 | -------------------------------------------------------------------------------- /django_reactive/forms.py: -------------------------------------------------------------------------------- 1 | from django.forms import ModelForm 2 | 3 | from .widget.fields import ReactJSONSchemaFormField 4 | 5 | 6 | class ReactJSONSchemaModelForm(ModelForm): 7 | """ 8 | Provides the instance object of a ModelForm to all of the schema field widgets. 9 | """ 10 | 11 | def __init__(self, *args, **kwargs): 12 | on_render_object = kwargs.get('instance', None) 13 | super().__init__(*args, **kwargs) 14 | for field_name, field_value in self.fields.items(): 15 | if isinstance(field_value, ReactJSONSchemaFormField): 16 | self.fields[field_name].widget.on_render_object = on_render_object 17 | -------------------------------------------------------------------------------- /django_reactive/schema_validator.py: -------------------------------------------------------------------------------- 1 | from jsonschema import Draft7Validator 2 | from typing import List, Tuple 3 | 4 | 5 | def validate_json_schema(schema: dict) -> Tuple[bool, List[str]]: 6 | """ 7 | Validate a JSON schema using the Draft 7 validator. 8 | """ 9 | validator = Draft7Validator( 10 | schema=Draft7Validator.META_SCHEMA, 11 | format_checker=Draft7Validator.FORMAT_CHECKER, 12 | ) 13 | errors = [f"{'.'.join(e.path)}: {e.message}" for e in validator.iter_errors(schema)] 14 | 15 | return not bool(errors), errors 16 | -------------------------------------------------------------------------------- /django_reactive/static/css/django_reactive.css: -------------------------------------------------------------------------------- 1 | /* Hide description for now. TODO: find a way do display it in the form. */ 2 | .form-row .rjsf .field-description { 3 | display: none; 4 | } 5 | 6 | /* Add bare minimum of styles to look like a normal Django field */ 7 | .form-row .rjsf .form-group { 8 | padding: 10px 0 0 20px; 9 | } 10 | 11 | .form-row .rjsf .text-danger { 12 | color: #ba2121; 13 | } 14 | 15 | .form-row .rjsf .error-detail li { 16 | list-style-type: square; 17 | } 18 | 19 | .form-row .rjsf input[type='checkbox'], .form-row .rjsf input[type='radio'] { 20 | margin-right: 5px; 21 | } 22 | 23 | /* Arrays */ 24 | .form-row .rjsf .array-item { 25 | padding: 10px; 26 | } 27 | 28 | .form-row .rjsf .array-item-toolbox { 29 | position: relative; 30 | float: right; 31 | bottom: 20px; 32 | } 33 | 34 | .form-row .rjsf button { 35 | background: dimgray; 36 | height: 20px; 37 | line-height: 20px; 38 | text-align: center; 39 | padding: 0 6px; 40 | border: none; 41 | border-radius: 4px; 42 | color: #fff; 43 | cursor: pointer; 44 | margin: 0 0 0 5px; 45 | } 46 | 47 | .form-row .rjsf button:disabled { 48 | background: lightgray; 49 | } 50 | 51 | .form-row .rjsf i.glyphicon { 52 | display: none; 53 | } 54 | 55 | .form-row .rjsf .btn-add::after { 56 | content: 'Add item'; 57 | } 58 | 59 | .form-row .rjsf .array-item-move-up::after { 60 | content: '⬆'; 61 | } 62 | 63 | .form-row .rjsf .array-item-move-down::after { 64 | content: '⬇'; 65 | } 66 | 67 | .form-row .rjsf .array-item-remove::after { 68 | content: '✘'; 69 | } 70 | 71 | /* Fields */ 72 | .form-row .rjsf .field-array .checkboxes { 73 | display: inline-block; 74 | } 75 | 76 | .form-row .rjsf .field-radio-group label { 77 | display: block; 78 | width: auto; 79 | height: 38px; 80 | line-height: 38px; 81 | } 82 | 83 | .form-row .rjsf .field-radio-group span { 84 | height: 38px; 85 | line-height: 38px; 86 | } 87 | 88 | .form-row .rjsf .radio-inline > span { 89 | display: inline-block; 90 | vertical-align: middle; 91 | } 92 | 93 | .form-row .rjsf .field-radio-group { 94 | display: inline-block; 95 | line-height: 38px; 96 | vertical-align: top; 97 | } 98 | 99 | .form-row .rjsf .field-integer .field-integer { 100 | height: 38px; 101 | line-height: 38px; 102 | } 103 | 104 | .form-row .rjsf .field-integer .form-input .field-range-wrapper .range-view { 105 | margin-left: 5px; 106 | } 107 | 108 | .form-row .rjsf .field-integer .form-input .field-range-wrapper .form-control { 109 | height: 32px; 110 | width: calc(100% - 200px); 111 | } 112 | 113 | .form-row .rjsf .field-boolean .form-input .checkbox { 114 | height: 32px; 115 | } 116 | 117 | .form-row .rjsf .field-array .form-input select { 118 | min-width: 200px; 119 | } 120 | 121 | .form-row .rjsf .list-inline .btn-clear, 122 | .form-row .rjsf .list-inline .btn-now { 123 | margin: 4px; 124 | } 125 | 126 | .form-row .rjsf .list-inline li { 127 | display: inline; 128 | } 129 | -------------------------------------------------------------------------------- /django_reactive/static/dist/react.js: -------------------------------------------------------------------------------- 1 | /** @license React v16.14.0 2 | * react.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | 'use strict';(function(d,r){"object"===typeof exports&&"undefined"!==typeof module?r(exports):"function"===typeof define&&define.amd?define(["exports"],r):(d=d||self,r(d.React={}))})(this,function(d){function r(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,c=1;cC.length&&C.push(a)}function O(a,b,c,g){var e=typeof a;if("undefined"===e||"boolean"===e)a=null;var d=!1;if(null===a)d=!0;else switch(e){case "string":case "number":d=!0;break;case "object":switch(a.$$typeof){case x:case xa:d=!0}}if(d)return c(g,a,""===b?"."+P(a,0):b),1;d=0;b=""===b?".":b+":";if(Array.isArray(a))for(var f=0;f>>1,e=a[g];if(void 0!== 15 | e&&0D(f,c))void 0!==k&&0>D(k,f)?(a[g]=k,a[h]=c,g=h):(a[g]=f,a[d]=c,g=d);else if(void 0!==k&&0>D(k,c))a[g]=k,a[h]=c,g=h;else break a}}return b}return null}function D(a,b){var c=a.sortIndex-b.sortIndex;return 0!==c?c:a.id-b.id}function F(a){for(var b=n(u);null!== 16 | b;){if(null===b.callback)E(u);else if(b.startTime<=a)E(u),b.sortIndex=b.expirationTime,S(p,b);else break;b=n(u)}}function T(a){y=!1;F(a);if(!v)if(null!==n(p))v=!0,z(U);else{var b=n(u);null!==b&&G(T,b.startTime-a)}}function U(a,b){v=!1;y&&(y=!1,V());H=!0;var c=m;try{F(b);for(l=n(p);null!==l&&(!(l.expirationTime>b)||a&&!W());){var g=l.callback;if(null!==g){l.callback=null;m=l.priorityLevel;var e=g(l.expirationTime<=b);b=q();"function"===typeof e?l.callback=e:l===n(p)&&E(p);F(b)}else E(p);l=n(p)}if(null!== 17 | l)var d=!0;else{var f=n(u);null!==f&&G(T,f.startTime-b);d=!1}return d}finally{l=null,m=c,H=!1}}function oa(a){switch(a){case 1:return-1;case 2:return 250;case 5:return 1073741823;case 4:return 1E4;default:return 5E3}}var f="function"===typeof Symbol&&Symbol.for,x=f?Symbol.for("react.element"):60103,xa=f?Symbol.for("react.portal"):60106,Aa=f?Symbol.for("react.fragment"):60107,Ba=f?Symbol.for("react.strict_mode"):60108,Ca=f?Symbol.for("react.profiler"):60114,Da=f?Symbol.for("react.provider"):60109, 18 | Ea=f?Symbol.for("react.context"):60110,Fa=f?Symbol.for("react.forward_ref"):60112,Ga=f?Symbol.for("react.suspense"):60113,Ha=f?Symbol.for("react.memo"):60115,Ia=f?Symbol.for("react.lazy"):60116,la="function"===typeof Symbol&&Symbol.iterator,pa=Object.getOwnPropertySymbols,Ja=Object.prototype.hasOwnProperty,Ka=Object.prototype.propertyIsEnumerable,I=function(){try{if(!Object.assign)return!1;var a=new String("abc");a[5]="de";if("5"===Object.getOwnPropertyNames(a)[0])return!1;var b={};for(a=0;10>a;a++)b["_"+ 19 | String.fromCharCode(a)]=a;if("0123456789"!==Object.getOwnPropertyNames(b).map(function(a){return b[a]}).join(""))return!1;var c={};"abcdefghijklmnopqrst".split("").forEach(function(a){c[a]=a});return"abcdefghijklmnopqrst"!==Object.keys(Object.assign({},c)).join("")?!1:!0}catch(g){return!1}}()?Object.assign:function(a,b){if(null===a||void 0===a)throw new TypeError("Object.assign cannot be called with null or undefined");var c=Object(a);for(var g,e=1;e=ua};f=function(){};X=function(a){0>a||125d?(a.sortIndex=e,S(u,a),null===n(p)&&a===n(u)&&(y?V():y=!0,G(T,e-d))):(a.sortIndex=c,S(p,a),v||H||(v=!0,z(U)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=m;return function(){var c=m;m=b;try{return a.apply(this,arguments)}finally{m=c}}},unstable_getCurrentPriorityLevel:function(){return m}, 27 | unstable_shouldYield:function(){var a=q();F(a);var b=n(p);return b!==l&&null!==l&&null!==b&&null!==b.callback&&b.startTime<=a&&b.expirationTime