├── tests ├── __init__.py ├── templates │ └── django │ │ └── forms │ │ └── widgets │ │ └── attrs.html ├── settings.py ├── test_jsoneditor_access.py ├── test_widget_security.py └── test_logic.py ├── django_json_widget ├── views.py ├── static │ ├── css │ │ └── django_json_widget.css │ └── dist │ │ ├── img │ │ └── jsoneditor-icons.svg │ │ └── jsoneditor.min.css ├── __init__.py ├── models.py ├── urls.py ├── apps.py ├── templates │ └── django_json_widget.html └── widgets.py ├── example ├── example │ ├── __init__.py │ ├── wsgi.py │ ├── urls.py │ └── settings.py ├── characters │ ├── __init__.py │ ├── migrations │ │ ├── __init__.py │ │ ├── 0002_alter_character_data_alter_character_other_data.py │ │ └── 0001_initial.py │ ├── tests.py │ ├── views.py │ ├── apps.py │ ├── models.py │ └── admin.py ├── requirements.txt ├── manage.py ├── README.md └── templates │ └── django_json_widget │ └── base.html ├── requirements.txt ├── requirements_test.txt ├── imgs ├── jsonfield_0.png └── jsonfield_1.png ├── .coveragerc ├── MANIFEST.in ├── requirements_dev.txt ├── setup.cfg ├── manage.py ├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── tests.yml ├── .editorconfig ├── tox.ini ├── AUTHORS.rst ├── .gitignore ├── LICENSE ├── .travis.yml ├── CHANGELOG.rst ├── Makefile ├── CONTRIBUTING.md ├── setup.py ├── pyproject.toml └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_json_widget/views.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/characters/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5 2 | -------------------------------------------------------------------------------- /example/characters/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_json_widget/static/css/django_json_widget.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /django_json_widget/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '2.1.1' 2 | -------------------------------------------------------------------------------- /django_json_widget/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /django_json_widget/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /example/characters/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /example/characters/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /requirements_test.txt: -------------------------------------------------------------------------------- 1 | Django 2 | coverage 3 | mock 4 | codecov 5 | pytest 6 | pytest-cov 7 | tox 8 | -------------------------------------------------------------------------------- /imgs/jsonfield_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmrivas86/django-json-widget/HEAD/imgs/jsonfield_0.png -------------------------------------------------------------------------------- /imgs/jsonfield_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jmrivas86/django-json-widget/HEAD/imgs/jsonfield_1.png -------------------------------------------------------------------------------- /example/requirements.txt: -------------------------------------------------------------------------------- 1 | # Your app requirements. 2 | -r ../requirements.txt 3 | 4 | # Your app in editable mode. 5 | -e ../ 6 | -------------------------------------------------------------------------------- /example/characters/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CharactersConfig(AppConfig): 5 | name = 'characters' 6 | -------------------------------------------------------------------------------- /django_json_widget/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | from django.apps import AppConfig 3 | 4 | 5 | class DjangoJsonWidgetConfig(AppConfig): 6 | name = 'django_json_widget' 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include CHANGELOG.md 4 | include LICENSE 5 | include README.rst 6 | recursive-include django_json_widget *.html *.png *.gif *js *.css *jpg *jpeg *svg *py 7 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | psycopg2-binary==2.9.9 3 | bumpversion==0.6.0 4 | twine==5.0.0 5 | wheel==0.43.0 6 | ruff==0.8.4 7 | ipdb==0.13.13 8 | django-extensions==3.2.3 9 | setuptools>=69.2.0 10 | -------------------------------------------------------------------------------- /tests/templates/django/forms/widgets/attrs.html: -------------------------------------------------------------------------------- 1 | {% for name, value in widget.attrs.items %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endfor %} 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 2.1.1 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | 8 | [bumpversion:file:django_json_widget/__init__.py] 9 | 10 | [wheel] 11 | universal = 1 12 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import sys 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /example/characters/models.py: -------------------------------------------------------------------------------- 1 | from django.db.models import JSONField 2 | from django.db import models 3 | 4 | 5 | class Character(models.Model): 6 | name = models.CharField(max_length=200) 7 | data = JSONField() 8 | other_data = JSONField() 9 | 10 | def __str__(self): # __unicode__ on Python 2 11 | return self.name 12 | -------------------------------------------------------------------------------- /example/characters/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.db.models import JSONField 3 | from .models import Character 4 | from django_json_widget.widgets import JSONEditorWidget 5 | 6 | 7 | @admin.register(Character) 8 | class CharacterAdmin(admin.ModelAdmin): 9 | formfield_overrides = { 10 | JSONField: {'widget': JSONEditorWidget}, 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | * django-json-widget 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /example/example/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for example 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', 'example.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /example/characters/migrations/0002_alter_character_data_alter_character_other_data.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 4.2.6 on 2023-10-06 23:55 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('characters', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='character', 15 | name='data', 16 | field=models.JSONField(), 17 | ), 18 | migrations.AlterField( 19 | model_name='character', 20 | name='other_data', 21 | field=models.JSONField(), 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | django32-py{39,310} 4 | django42-py{39,310,311,312} 5 | django51-py{310,311,312,313} 6 | django52-py{310,311,312,313} 7 | 8 | [gh-actions] 9 | python = 10 | 3.9: py39 11 | 3.10: py310 12 | 3.11: py311 13 | 3.12: py312 14 | 3.13: py313 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir}:{toxinidir}/django_json_widget 19 | DJANGO_SETTINGS_MODULE=tests.settings 20 | commands = coverage run --source django_json_widget manage.py test -v 1 21 | deps = 22 | -r{toxinidir}/requirements_test.txt 23 | django32: django~=3.2.0 24 | django42: django~=4.2.0 25 | django51: django~=5.1.0 26 | django52: django~=5.2.0 27 | -------------------------------------------------------------------------------- /example/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', 'example.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/README.md: -------------------------------------------------------------------------------- 1 | ##Example Project for django_json_widget 2 | 3 | This example is provided as a convenience feature to allow potential users to try the app straight from the app repo without having to create a django project. 4 | 5 | It can also be used to develop the app in place. 6 | 7 | To run this example, follow these instructions: 8 | 9 | 1. Navigate to the `example` directory 10 | 2. Install the requirements for the package: 11 | 12 | pip install -r requirements.txt 13 | 14 | 3. Make and apply migrations 15 | 16 | python manage.py makemigrations 17 | 18 | python manage.py migrate 19 | 20 | 4. Run the server 21 | 22 | python manage.py runserver 23 | 24 | 5. Access from the browser at `http://127.0.0.1:8000` 25 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Development Lead 6 | ---------------- 7 | 8 | * José Manuel Rivas (jmrivas86) 9 | 10 | Contributors 11 | ------------ 12 | 13 | * Knut Hühne (k-nut) 14 | * Qiying Wang (WqyJh) 15 | * Erkin Çakar (travijuu) 16 | * Vinay Pai (vinaypai) 17 | * Pedro Miguel Correia (pedroma) 18 | * Artur BarseghyanArtur Barseghyan (barseghyanartur) 19 | * Alexandre Voiney (avoiney) 20 | * Michał Bielawski (D3X) 21 | * Arcuri Davide (dadokkio) 22 | * Ling Li (lingster) 23 | * Steven Mapes (StevenMapes) 24 | * Stefan Wehrmeyer (stefanw) 25 | * Michał Bielawski (D3X) 26 | * Ashok Argent-Katwala (ashokdelphia) 27 | * Chris Culhane (cfculhane) 28 | * Amar Sahinovic (amarsahinovic) 29 | * Erfan Arefmehr (erfan-rfmhr) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | __pycache__ 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | .pypirc 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 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Pycharm/Intellij 40 | .idea 41 | 42 | # Complexity 43 | output/*.html 44 | output/*/index.html 45 | 46 | # Sphinx 47 | docs/_build 48 | /.tox/ 49 | /dist/ 50 | 51 | # venv 52 | /.pypirc 53 | /example/.python-version 54 | 55 | # OS 56 | .DS_Store 57 | example/.DS_Store 58 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | max-parallel: 5 15 | matrix: 16 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | python -m pip install tox tox-gh-actions 29 | 30 | - name: Run tests 31 | run: tox 32 | -------------------------------------------------------------------------------- /example/characters/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.2 on 2019-04-13 17:10 2 | 3 | import django.contrib.postgres.fields.jsonb 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | initial = True 10 | 11 | dependencies = [ 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Character', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('name', models.CharField(max_length=200)), 20 | ('data', django.contrib.postgres.fields.jsonb.JSONField()), 21 | ('other_data', django.contrib.postgres.fields.jsonb.JSONField()), 22 | ], 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /example/example/urls.py: -------------------------------------------------------------------------------- 1 | """example 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 | urlpatterns = [ 20 | path('admin/', admin.site.urls), 21 | ] 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | 4 | Copyright (c) 2017, José Manuel Rivas 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /django_json_widget/templates/django_json_widget.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | {% with script_id=widget.name|add:"_data" %} 6 | {{ widget.value|json_script:script_id }} 7 | {% endwith %} 8 | 9 | 30 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | DEBUG = True 2 | USE_TZ = True 3 | 4 | # SECURITY WARNING: keep the secret key used in production secret! 5 | SECRET_KEY = "uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuu" 6 | 7 | DATABASES = { 8 | "default": { 9 | "ENGINE": "django.db.backends.sqlite3", 10 | "NAME": ":memory:", 11 | } 12 | } 13 | 14 | INSTALLED_APPS = [ 15 | "django.contrib.auth", 16 | "django.contrib.contenttypes", 17 | "django.contrib.sites", 18 | "django_json_widget", 19 | ] 20 | 21 | SITE_ID = 1 22 | 23 | MIDDLEWARE = () 24 | 25 | # JSON Editor Widget Settings 26 | JSON_EDITOR_JS = 'dist/jsoneditor.min.js' 27 | JSON_EDITOR_CSS = 'dist/jsoneditor.min.css' 28 | 29 | # Template configuration 30 | TEMPLATES = [ 31 | { 32 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 33 | 'DIRS': ['tests/templates'], 34 | 'APP_DIRS': True, 35 | 'OPTIONS': { 36 | 'context_processors': [ 37 | 'django.template.context_processors.debug', 38 | 'django.template.context_processors.request', 39 | 'django.contrib.auth.context_processors.auth', 40 | 'django.contrib.messages.context_processors.messages', 41 | ], 42 | 'builtins': [ 43 | 'django.template.defaulttags', 44 | 'django.template.defaultfilters', 45 | 'django.template.loader_tags', 46 | ], 47 | }, 48 | }, 49 | ] -------------------------------------------------------------------------------- /django_json_widget/widgets.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django import forms 4 | from django.conf import settings 5 | 6 | 7 | class JSONEditorWidget(forms.Widget): 8 | class Media: 9 | js = ( 10 | getattr(settings, "JSON_EDITOR_JS", 'dist/jsoneditor.min.js'), 11 | ) 12 | css = { 13 | 'all': ( 14 | getattr(settings, "JSON_EDITOR_CSS", 'dist/jsoneditor.min.css'), 15 | ) 16 | } 17 | 18 | template_name = 'django_json_widget.html' 19 | 20 | def __init__(self, attrs=None, mode='code', options=None, width=None, height=None): 21 | default_options = { 22 | 'modes': ['text', 'code', 'tree', 'form', 'view'], 23 | 'mode': mode, 24 | 'search': True, 25 | } 26 | if options: 27 | default_options.update(options) 28 | 29 | self.options = default_options 30 | self.width = width 31 | self.height = height 32 | 33 | super().__init__(attrs=attrs) 34 | 35 | def get_context(self, name, value, attrs): 36 | context = super().get_context(name, value, attrs) 37 | context['widget']['options'] = json.dumps(self.options) 38 | context['widget']['width'] = self.width 39 | context['widget']['height'] = self.height 40 | 41 | return context 42 | 43 | def format_value(self, value): 44 | if not isinstance(value, (dict, list)): 45 | return json.loads(value) 46 | else: 47 | return value 48 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Config file for automatic testing at travis-ci.org 2 | 3 | language: python 4 | 5 | python: 6 | - "3.6" 7 | - "3.7" 8 | - "3.8" 9 | - "3.9" 10 | - "3.10" 11 | - "3.11" 12 | - "3.12" 13 | 14 | matrix: 15 | fast_finish: true 16 | include: 17 | # DJANGO 3.2.25 18 | - python: 3.6 19 | env: DJANGO_VERSION=3.2.25 20 | - python: 3.7 21 | env: DJANGO_VERSION=3.2.25 22 | - python: 3.8 23 | env: DJANGO_VERSION=3.2.25 24 | - python: 3.9 25 | env: DJANGO_VERSION=3.2.25 26 | - python: 3.10 27 | env: DJANGO_VERSION=3.2.25 28 | # DJANGO 4.2.11 29 | env: DJANGO_VERSION=3.2.25 30 | - python: 3.8 31 | env: DJANGO_VERSION=3.2.25 32 | - python: 3.9 33 | env: DJANGO_VERSION=3.2.25 34 | - python: 3.10 35 | env: DJANGO_VERSION=3.2.25 36 | - python: 3.11 37 | env: DJANGO_VERSION=3.2.25 38 | - python: 3.12 39 | env: DJANGO_VERSION=3.2.25 40 | # DJANGO 5.0.3 41 | - python: 3.10 42 | env: DJANGO_VERSION=3.2.25 43 | - python: 3.11 44 | env: DJANGO_VERSION=3.2.25 45 | - python: 3.12 46 | env: DJANGO_VERSION=3.2.25 47 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 48 | install: 49 | - echo "$DJANGO_VERSION" 50 | - pip install -r requirements_test.txt 51 | 52 | # command to run tests using coverage, e.g. python setup.py test 53 | script: coverage run --source django_json_widget runtests.py 54 | # script: tox -e $TOX_ENV 55 | 56 | # after_success: 57 | # - codecov -e TOX_ENV 58 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Changelog 3 | ========= 4 | 5 | 2.1.1 (2025-12-12) 6 | ------------------ 7 | 8 | * Issue #108: respect Django admin color scheme setting for JSON editor. 9 | 10 | 2.1.0 (2025-10-24) 11 | ------------------ 12 | 13 | * Issue #114: expose editor instance to window object and DOM element. 14 | 15 | 2.0.4 (2025-10-11) 16 | ------------------ 17 | 18 | * Fix dark mode CSS support #107 19 | 20 | 2.0.3 (2025-07-29) 21 | ------------------ 22 | 23 | 2.0.2 (2025-07-28) 24 | ------------------ 25 | 26 | * Fix Version 2.0.1 causes rendering HTML scripts issue #80 27 | 28 | 2.0.1 (2024-03-02) 29 | ------------------= 30 | 31 | * Fixed Version 2.0.0 causes issues with Django 4.x due to missing sourcemaps issue #83 32 | 33 | 2.0.0 (2024-03-24) 34 | ------------------ 35 | 36 | * Fix default paths for static files #53 37 | * Stop declaring support for Django versions before 3.2 #65 38 | * Avoid HTML injection via unsafe JSON injection #64 39 | * Remove source map reference from JS bundle #70 40 | * Add dark mode CSS support #82 41 | * Updated documentation 42 | 43 | 1.1.1 (2021-02-17) 44 | ------------------ 45 | 46 | * Fix for issue #51, updates the bundled libs to 9.1.9 and additional notes for Django 3.1 changes 47 | 48 | 1.1.0 (2021-02-05) 49 | ------------------ 50 | 51 | * Added functionality to override version of JSONEditor to use 52 | * update readme for django 3.1 53 | 54 | 1.0.1 (2020-04-17) 55 | ------------------ 56 | 57 | * Stop resolving paths to the static files 58 | 59 | 1.0.0 (2020-01-16) 60 | ------------------ 61 | 62 | * Update Makefile 63 | * Make Stable the project 64 | 65 | 66 | 0.3.0 (2020-01-16) 67 | ------------------ 68 | 69 | * Update requirements.txt 70 | * Fixed static to work with CDN tools like django-storages 71 | * Make widget more configurable 72 | * Fixed an incompatibility with Python 2.7 73 | * update jsoneditor to latest version 74 | 75 | 76 | -------------------------------------------------------------------------------- /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 | @perl -nle'print $& if m{^[a-zA-Z_-]+:.*?## .*$$}' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {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_json_widget tests 32 | 33 | test: ## run tests quickly with the default Python 34 | python runtests.py 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 --source django_json_widget runtests.py 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-json-widget.rst 47 | rm -f docs/modules.rst 48 | sphinx-apidoc -o docs/ django_json_widget 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 | python -m build 55 | twine upload dist/* 56 | 57 | test-release: clean ## package and upload a release to test PyPI 58 | python -m build 59 | twine upload --repository testpypi dist/* 60 | 61 | sdist: clean ## package 62 | python -m build --sdist 63 | ls -l dist 64 | 65 | twine-release: ## upload release to pypi 66 | twine upload dist/* 67 | -------------------------------------------------------------------------------- /tests/test_jsoneditor_access.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_jsoneditor_access 6 | ---------------------- 7 | 8 | Tests for JsonEditor instance accessibility from external JavaScript. 9 | """ 10 | 11 | from django.template import Context, Template 12 | from django.test import TestCase 13 | 14 | from django_json_widget.widgets import JSONEditorWidget 15 | 16 | 17 | class JSONEditorAccessTests(TestCase): 18 | """Test JsonEditor instance accessibility""" 19 | 20 | def test_template_exposes_editor_to_window(self): 21 | """Test that JsonEditor instance is exposed to window object""" 22 | widget = JSONEditorWidget() 23 | context = widget.get_context("test_field", '{"key": "value"}', {"id": "test_id"}) 24 | 25 | # Render the template 26 | template = Template('{% load static %}{% include "django_json_widget.html" %}') 27 | rendered = template.render(Context({"widget": context["widget"]})) 28 | 29 | # Check that the window assignment code is present 30 | self.assertIn("window['test_id_editor'] = editor", rendered) 31 | 32 | def test_template_exposes_editor_to_dom(self): 33 | """Test that JsonEditor instance is attached to DOM container""" 34 | widget = JSONEditorWidget() 35 | context = widget.get_context("test_field", '{"key": "value"}', {"id": "test_id"}) 36 | 37 | # Render the template 38 | template = Template('{% load static %}{% include "django_json_widget.html" %}') 39 | rendered = template.render(Context({"widget": context["widget"]})) 40 | 41 | # Check that the DOM assignment code is present 42 | self.assertIn("container.jsonEditor = editor", rendered) 43 | 44 | def test_multiple_widgets_different_ids(self): 45 | """Test that multiple widgets get different window object names""" 46 | widget1 = JSONEditorWidget() 47 | widget2 = JSONEditorWidget() 48 | 49 | context1 = widget1.get_context("field1", "{}", {"id": "id_field1"}) 50 | context2 = widget2.get_context("field2", "{}", {"id": "id_field2"}) 51 | 52 | template = Template('{% load static %}{% include "django_json_widget.html" %}') 53 | 54 | rendered1 = template.render(Context({"widget": context1["widget"]})) 55 | rendered2 = template.render(Context({"widget": context2["widget"]})) 56 | 57 | # Each widget should have its own unique window object name 58 | self.assertIn("window['id_field1_editor'] = editor", rendered1) 59 | self.assertIn("window['id_field2_editor'] = editor", rendered2) 60 | self.assertNotEqual(rendered1, rendered2) 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. 4 | 5 | You can contribute in many ways: 6 | 7 | ## Types of Contributions 8 | 9 | ### Report Bugs 10 | 11 | Report bugs at https://github.com/jmrivas86/django-json-widget/issues. 12 | 13 | If you are reporting a bug, please include: 14 | 15 | * Your operating system name and version. 16 | * Any details about your local setup that might be helpful in troubleshooting. 17 | * Detailed steps to reproduce the bug. 18 | 19 | ### Fix Bugs 20 | 21 | Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. 22 | 23 | ### Implement Features 24 | 25 | Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. 26 | 27 | ### Write Documentation 28 | 29 | django-json-widget could always use more documentation, whether as part of the official django-json-widget docs, in docstrings, or even on the web in blog posts, articles, and such. 30 | 31 | ### Submit Feedback 32 | 33 | The best way to send feedback is to file an issue at https://github.com/jmrivas86/django-json-widget/issues. 34 | 35 | If you are proposing a feature: 36 | 37 | * Explain in detail how it would work. 38 | * Keep the scope as narrow as possible, to make it easier to implement. 39 | * Remember that this is a volunteer-driven project, and that contributions are welcome :) 40 | 41 | ## Get Started! 42 | 43 | Ready to contribute? Here's how to set up `django-json-widget` for local development. 44 | 45 | 1. Fork the `django-json-widget` repo on GitHub. 46 | 47 | 2. Clone your fork locally: 48 | ```bash 49 | git clone git@github.com:your_name_here/django-json-widget.git 50 | ``` 51 | 52 | 3. Install development dependencies: 53 | ```bash 54 | cd django-json-widget/ 55 | python -m venv .venv 56 | source .venv/bin/activate 57 | pip install -r requirements_test.txt 58 | ``` 59 | 60 | 4. Create a branch for local development from the dev branch: 61 | ```bash 62 | git checkout dev 63 | git checkout -b name-of-your-branch 64 | ``` 65 | 66 | Now you can make your changes locally. 67 | 68 | 5. Commit your changes and push your branch: 69 | ```bash 70 | git add . 71 | git commit -m "Your detailed description of your changes." 72 | git push origin name-of-your-branch 73 | ``` 74 | 75 | 6. Submit a pull request through the GitHub website. 76 | 77 | ## Pull Request Guidelines 78 | 79 | Before you submit a pull request, check that it meets these guidelines: 80 | 81 | 1. **The pull request must be submitted against the `dev` branch.** 82 | 2. Please explain what you have done in the pull request. 83 | 3. Make sure you have had passed pre-commit hooks. 84 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | import os 4 | import re 5 | import sys 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | 13 | def get_version(*file_paths): 14 | """Retrieves the version from django_json_widget/__init__.py""" 15 | filename = os.path.join(os.path.dirname(__file__), *file_paths) 16 | version_file = open(filename).read() 17 | version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", 18 | version_file, re.M) 19 | if version_match: 20 | return version_match.group(1) 21 | raise RuntimeError('Unable to find version string.') 22 | 23 | 24 | # version = get_version("django_json_widget", "__init__.py") 25 | 26 | 27 | if sys.argv[-1] == 'publish': 28 | try: 29 | import wheel 30 | print("Wheel version: ", wheel.__version__) 31 | except ImportError: 32 | print('Wheel library missing. Please run "pip install wheel"') 33 | sys.exit() 34 | os.system('python setup.py sdist upload') 35 | os.system('python setup.py bdist_wheel upload') 36 | sys.exit() 37 | 38 | if sys.argv[-1] == 'tag': 39 | print("Tagging the version on git:") 40 | os.system("git tag -a %s -m 'version %s'" % (version, version)) 41 | os.system("git push --tags") 42 | sys.exit() 43 | 44 | readme = open('README.rst').read() 45 | history = open('CHANGELOG.rst').read().replace('.. :changelog:', '') 46 | 47 | setup( 48 | name='django-json-widget', 49 | version='2.1.1', 50 | description="""Django json widget is an alternative widget that makes it easy to edit the jsonfield field of django.""", 51 | long_description=readme + '\n\n' + history, 52 | author='José Manuel Rivas', 53 | author_email='jmrivas86@gmail.com', 54 | url='https://github.com/jmrivas86/django-json-widget', 55 | packages=[ 56 | 'django_json_widget', 57 | ], 58 | include_package_data=True, 59 | license="MIT", 60 | zip_safe=False, 61 | keywords='django-json-widget', 62 | classifiers=[ 63 | 'Development Status :: 5 - Production/Stable', 64 | 'Framework :: Django', 65 | 'Framework :: Django :: 3.2', 66 | 'Framework :: Django :: 4.2', 67 | 'Framework :: Django :: 5.0', 68 | 'Framework :: Django :: 5.2', 69 | 'Intended Audience :: Developers', 70 | 'License :: OSI Approved :: BSD License', 71 | 'Natural Language :: English', 72 | 'Programming Language :: Python :: 3', 73 | 'Programming Language :: Python :: 3.9', 74 | 'Programming Language :: Python :: 3.10', 75 | 'Programming Language :: Python :: 3.11', 76 | 'Programming Language :: Python :: 3.12', 77 | 'Programming Language :: Python :: 3.13', 78 | ], 79 | ) 80 | -------------------------------------------------------------------------------- /example/templates/django_json_widget/base.html: -------------------------------------------------------------------------------- 1 | {% load staticfiles i18n %} 2 | 3 | 4 | 5 | 6 | {% block title %}django-json-widget{% endblock title %} 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | {% block css %} 17 | 18 | 19 | 20 | 21 | 22 | {% endblock %} 23 | 24 | 25 | 26 | 27 |
28 | 46 |
47 | 48 |
49 | 50 | {% if messages %} 51 | {% for message in messages %} 52 |
{{ message }}
53 | {% endfor %} 54 | {% endif %} 55 | 56 | {% block content %} 57 |
58 |

Use this document as a way to quick start any new project.

59 |

The current template is loaded from 60 | django-json-widget/example/templates/base.html.

61 |

Whenever you overwrite the contents of django-json-widget/django_json_widget/urls.py with your 62 | own content, you should see it here.

63 |
64 | {% endblock content %} 65 | 66 |
67 | 68 | {% block modal %}{% endblock modal %} 69 | 70 | 72 | 73 | {% block javascript %} 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {% endblock javascript %} 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /example/example/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 4 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 5 | 6 | 7 | # Quick-start development settings - unsuitable for production 8 | 9 | # SECURITY WARNING: keep the secret key used in production secret! 10 | SECRET_KEY = 't45v0vds1$#l6dn*%6$s#4-t!tm+t=lxr!fsb%$s4y&&%qx=n@' 11 | 12 | # SECURITY WARNING: don't run with debug turned on in production! 13 | DEBUG = True 14 | 15 | ALLOWED_HOSTS = [] 16 | 17 | 18 | # Application definition 19 | 20 | INSTALLED_APPS = [ 21 | 'django.contrib.admin', 22 | 'django.contrib.auth', 23 | 'django.contrib.contenttypes', 24 | 'django.contrib.sessions', 25 | 'django.contrib.messages', 26 | 'django.contrib.staticfiles', 27 | 'django_json_widget', 28 | 'django_extensions', 29 | # if your app has other dependencies that need to be added to the site 30 | # they should be added here 31 | 'characters' 32 | ] 33 | 34 | MIDDLEWARE = [ 35 | 'django.middleware.security.SecurityMiddleware', 36 | 'django.contrib.sessions.middleware.SessionMiddleware', 37 | 'django.middleware.common.CommonMiddleware', 38 | 'django.middleware.csrf.CsrfViewMiddleware', 39 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 40 | 'django.contrib.messages.middleware.MessageMiddleware', 41 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 42 | ] 43 | 44 | ROOT_URLCONF = 'example.urls' 45 | 46 | TEMPLATES = [ 47 | { 48 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 49 | 'DIRS': [], 50 | 'APP_DIRS': True, 51 | 'OPTIONS': { 52 | 'context_processors': [ 53 | 'django.template.context_processors.debug', 54 | 'django.template.context_processors.request', 55 | 'django.contrib.auth.context_processors.auth', 56 | 'django.contrib.messages.context_processors.messages', 57 | ], 58 | }, 59 | }, 60 | ] 61 | 62 | WSGI_APPLICATION = 'example.wsgi.application' 63 | 64 | # Database 65 | 66 | DATABASES = { 67 | 'default': { 68 | 'ENGINE': 'django.db.backends.postgresql', 69 | 'NAME': 'example_json_widget', 70 | 'USER': 'postgres', 71 | 'PASSWORD': 'docker', 72 | 'HOST': '0.0.0.0', 73 | 'PORT': '5432', 74 | } 75 | } 76 | 77 | 78 | # Password validation 79 | 80 | AUTH_PASSWORD_VALIDATORS = [ 81 | { 82 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 83 | }, 84 | { 85 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 86 | }, 87 | { 88 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 89 | }, 90 | { 91 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 92 | }, 93 | ] 94 | 95 | 96 | # Internationalization 97 | 98 | LANGUAGE_CODE = 'en-us' 99 | 100 | TIME_ZONE = 'UTC' 101 | 102 | USE_I18N = True 103 | 104 | USE_L10N = True 105 | 106 | USE_TZ = True 107 | 108 | 109 | # Static files (CSS, JavaScript, Images) 110 | 111 | STATIC_URL = '/static/' 112 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [tool.ruff] 6 | # Exclude a variety of commonly ignored directories. 7 | exclude = [ 8 | ".bzr", 9 | ".direnv", 10 | ".eggs", 11 | ".git", 12 | ".git-rewrite", 13 | ".hg", 14 | ".ipynb_checkpoints", 15 | ".mypy_cache", 16 | ".nox", 17 | ".pants.d", 18 | ".pyenv", 19 | ".pytest_cache", 20 | ".pytype", 21 | ".ruff_cache", 22 | ".svn", 23 | ".tox", 24 | ".venv", 25 | ".vscode", 26 | "__pypackages__", 27 | "_build", 28 | "buck-out", 29 | "build", 30 | "dist", 31 | "node_modules", 32 | "site-packages", 33 | "venv", 34 | "migrations", 35 | ] 36 | 37 | # Same as Black. 38 | line-length = 119 39 | indent-width = 4 40 | 41 | # Assume Python 3.9+. 42 | target-version = "py39" 43 | 44 | [tool.ruff.lint] 45 | # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. 46 | # Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or 47 | # McCabe complexity (`C901`) by default. 48 | select = [ 49 | # pycodestyle 50 | "E", 51 | # Pyflakes 52 | "F", 53 | # pyupgrade 54 | "UP", 55 | # flake8-bugbear 56 | "B", 57 | # flake8-simplify 58 | "SIM", 59 | # isort 60 | "I", 61 | # Django 62 | "DJ", 63 | # flake8-pie 64 | "PIE", 65 | # flake8-comprehensions 66 | "C4", 67 | # flake8-unused-arguments 68 | "ARG", 69 | # flake8-use-pathlib 70 | "PTH", 71 | # Ruff-specific rules 72 | "RUF", 73 | ] 74 | 75 | ignore = [ 76 | # Django model's __str__ method 77 | "DJ008", 78 | # Allow non-abstract empty models 79 | "DJ006", 80 | # Allow nullable CharField 81 | "DJ001", 82 | # Ignore complexity 83 | "C901", 84 | # Allow assert in tests 85 | "B011", 86 | # Allow print statements (can be useful for debugging) 87 | "T201", 88 | ] 89 | 90 | # Allow fix for all enabled rules (when `--fix`) is provided. 91 | fixable = ["ALL"] 92 | unfixable = [] 93 | 94 | # Allow unused variables when underscore-prefixed. 95 | dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" 96 | 97 | [tool.ruff.lint.per-file-ignores] 98 | # Tests can use magic values, assertions, and fixtures 99 | "tests/*" = ["PLR2004", "S101", "ARG", "FBT"] 100 | # Settings files can have unused imports 101 | "*/settings.py" = ["F401"] 102 | # __init__.py files can have unused imports 103 | "__init__.py" = ["F401"] 104 | # Example files can be more relaxed 105 | "example/*" = ["DJ", "ARG"] 106 | 107 | [tool.ruff.lint.isort] 108 | known-first-party = ["django_json_widget"] 109 | known-third-party = ["django"] 110 | section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] 111 | 112 | [tool.ruff.lint.mccabe] 113 | max-complexity = 10 114 | 115 | [tool.ruff.lint.pyupgrade] 116 | # Preserve types, even if a file imports `from __future__ import annotations`. 117 | keep-runtime-typing = true 118 | 119 | [tool.ruff.format] 120 | # Like Black, use double quotes for strings. 121 | quote-style = "double" 122 | 123 | # Like Black, indent with spaces, rather than tabs. 124 | indent-style = "space" 125 | 126 | # Like Black, respect magic trailing commas. 127 | skip-magic-trailing-comma = false 128 | 129 | # Like Black, automatically detect the appropriate line ending. 130 | line-ending = "auto" 131 | 132 | # Enable auto-formatting of code examples in docstrings. Markdown, 133 | # reStructuredText code/literal blocks and doctests are all supported. 134 | docstring-code-format = true 135 | 136 | # Set the line length limit used when formatting code snippets in 137 | # docstrings. 138 | docstring-code-line-length = "dynamic" 139 | 140 | [tool.pytest.ini_options] 141 | python_files = ["test_*.py", "*_test.py"] 142 | testpaths = ["tests"] 143 | addopts = [ 144 | "--strict-markers", 145 | "--strict-config", 146 | "--verbose", 147 | "--reuse-db", 148 | ] 149 | markers = [ 150 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 151 | "integration: marks tests as integration tests", 152 | ] 153 | DJANGO_SETTINGS_MODULE = "tests.settings" -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ============================= 2 | django-json-widget 3 | ============================= 4 | 5 | .. image:: https://badge.fury.io/py/django-json-widget.svg 6 | :target: https://badge.fury.io/py/django-json-widget 7 | 8 | 9 | An alternative widget that makes it easy to edit the new Django's field JSONField (PostgreSQL specific model fields) 10 | 11 | 12 | Quickstart 13 | ---------- 14 | 15 | Install django-json-widget:: 16 | 17 | pip install django-json-widget 18 | 19 | Add it to your `INSTALLED_APPS`: 20 | 21 | .. code-block:: python 22 | 23 | INSTALLED_APPS = ( 24 | ... 25 | 'django_json_widget', 26 | ... 27 | ) 28 | 29 | Add the widget in your admin.py: 30 | 31 | .. code-block:: python 32 | 33 | from django.contrib import admin 34 | from django.db.models import JSONField 35 | from django_json_widget.widgets import JSONEditorWidget 36 | from .models import YourModel 37 | 38 | 39 | @admin.register(YourModel) 40 | class YourModelAdmin(admin.ModelAdmin): 41 | formfield_overrides = { 42 | JSONField: {'widget': JSONEditorWidget}, 43 | } 44 | 45 | You can also add the widget in your forms.py: 46 | 47 | .. code-block:: python 48 | 49 | from django import forms 50 | from django_json_widget.widgets import JSONEditorWidget 51 | from .models import YourModel 52 | 53 | 54 | class YourForm(forms.ModelForm): 55 | class Meta: 56 | model = YourModel 57 | 58 | fields = ('jsonfield',) 59 | 60 | widgets = { 61 | 'jsonfield': JSONEditorWidget 62 | } 63 | 64 | Configuration 65 | ------------- 66 | 67 | You can customize the JSONEditorWidget with the following options: 68 | 69 | * **width**: Width of the editor as a string with CSS size units (px, em, % etc). Defaults to ``90%``. 70 | * **height**: Height of the editor as a string CSS size units. Defaults to ``550px``. 71 | * **options**: A dict of options accepted by the `JSON editor`_. Options that require functions (eg. onError) are not supported. 72 | * **mode (deprecated)**: The default editor mode. This argument is redundant because it can be specified as a part of ``options``. Preserved for backwards compatibility with version 0.2.0. 73 | * **attrs**: HTML attributes to be applied to the wrapper element. See the `Django Widget documentation`_. 74 | 75 | Accessing JsonEditor Instance 76 | ----------------------------- 77 | 78 | The JsonEditor instance is automatically exposed for external access through two methods: 79 | 80 | 1. **Window object**: Available as ``window['FIELD_ID_editor']`` where FIELD_ID is the field's HTML ID 81 | 2. **DOM element**: Available as ``container.jsonEditor`` property on the widget's container element 82 | 83 | Example usage for external JavaScript access: 84 | 85 | .. code-block:: javascript 86 | 87 | // Access via window object 88 | var editor = window['id_jsonfield_editor']; 89 | editor.set({'key': 'new value'}); 90 | 91 | // Access via DOM element 92 | var container = document.getElementById('id_jsonfield'); 93 | var editor = container.jsonEditor; 94 | editor.set({'key': 'new value'}); 95 | 96 | This allows you to programmatically call JsonEditor methods like ``set()``, ``get()``, ``update()``, etc. from custom JavaScript code running in your admin pages or forms. 97 | 98 | .. _json editor: https://github.com/josdejong/jsoneditor/blob/master/docs/api.md#configuration-options 99 | .. _Django Widget documentation: https://docs.djangoproject.com/en/2.1/ref/forms/widgets/#django.forms.Widget.attrs 100 | 101 | 102 | JSONEditorWidget widget 103 | ----------------------- 104 | 105 | Before: 106 | 107 | .. image:: https://raw.githubusercontent.com/jmrivas86/django-json-widget/master/imgs/jsonfield_0.png 108 | 109 | After: 110 | 111 | .. image:: https://raw.githubusercontent.com/jmrivas86/django-json-widget/master/imgs/jsonfield_1.png 112 | 113 | 114 | Development Guide 115 | ----------------- 116 | 117 | Read the contribution guide. 118 | 119 | 120 | Credits 121 | ------- 122 | 123 | Tools used in rendering this package: 124 | 125 | * Cookiecutter_ 126 | * `cookiecutter-djangopackage`_ 127 | 128 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter 129 | .. _`cookiecutter-djangopackage`: https://github.com/pydanny/cookiecutter-djangopackage 130 | -------------------------------------------------------------------------------- /tests/test_widget_security.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | Security tests for django-json-widget 6 | """ 7 | 8 | import json 9 | from django.test import TestCase 10 | from django.utils.safestring import SafeString 11 | from django_json_widget.widgets import JSONEditorWidget 12 | 13 | 14 | class JSONEditorWidgetSecurityTests(TestCase): 15 | """Test security aspects of the widget""" 16 | 17 | def test_xss_prevention_in_options(self): 18 | """Test that malicious JavaScript in options is properly escaped""" 19 | malicious_options = { 20 | 'mode': 'code', 21 | 'search': True, 22 | 'onError': '', 23 | 'onChange': 'maliciousFunction()', 24 | } 25 | 26 | widget = JSONEditorWidget(options=malicious_options) 27 | context = widget.get_context('test_field', '{}', {'id': 'test_id'}) 28 | 29 | options_json = context['widget']['options'] 30 | 31 | # Should be properly JSON encoded, not executable JavaScript 32 | self.assertIn('"onError": ""', options_json) 33 | self.assertIn('"onChange": "maliciousFunction()"', options_json) 34 | 35 | # Verify it's valid JSON and not executable code 36 | parsed = json.loads(options_json) 37 | self.assertEqual(parsed['onError'], '') 38 | self.assertEqual(parsed['onChange'], 'maliciousFunction()') 39 | 40 | def test_html_injection_in_widget_attrs(self): 41 | """Test that HTML injection in widget attributes is handled safely""" 42 | malicious_attrs = { 43 | 'id': 'test_id', 44 | 'class': 'safe-class" onload="alert(\'XSS\')" data-evil="', 45 | 'data-test': '', 46 | } 47 | 48 | widget = JSONEditorWidget() 49 | html = widget.render('test_field', '{}', malicious_attrs) 50 | 51 | # Should not contain executable JavaScript 52 | self.assertNotIn('safe-class" onload="alert(\'XSS\')" data-evil="', html) 53 | self.assertNotIn('', html) 54 | 55 | # But should contain the escaped attributes 56 | self.assertIn('class=', html) 57 | self.assertIn('data-test=', html) 58 | 59 | def test_json_value_sanitization(self): 60 | """Test that JSON values are properly sanitized""" 61 | # Test with potentially dangerous JSON content 62 | dangerous_json = '{"script": "", "html": ""}' 63 | 64 | widget = JSONEditorWidget() 65 | result = widget.format_value(dangerous_json) 66 | 67 | # Should parse correctly but not execute 68 | self.assertEqual(result['script'], '') 69 | self.assertEqual(result['html'], '') 70 | 71 | def test_safe_json_serialization_in_template(self): 72 | """Test that JSON serialization in template context is safe""" 73 | widget = JSONEditorWidget() 74 | 75 | # Create context with potentially dangerous data 76 | context = widget.get_context('test_field', '{"xss": ""}' 127 | 128 | html = widget.render('test_field', dangerous_json, {'id': 'test_id'}) 129 | 130 | # Should not contain unescaped script tags that could execute 131 | # The json_script template tag should handle proper escaping 132 | self.assertIn('test_field_data', html) # JSON script element should be present 133 | 134 | # Count script tags - should only be the ones we expect 135 | script_count = html.count('', 145 | height='javascript:alert(1)', 146 | options={'mode': ''} 147 | ) 148 | 149 | context = widget.get_context( 150 | 'test"}', 152 | {'id': 'test', context['widget']['width']) 158 | self.assertIn('javascript:alert(1)', context['widget']['height']) 159 | 160 | # Options should be JSON serialized (safe) 161 | options = json.loads(context['widget']['options']) 162 | self.assertEqual(options['mode'], '') 163 | -------------------------------------------------------------------------------- /tests/test_logic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """ 5 | test_django-json-widget 6 | ------------ 7 | 8 | Tests for `django-json-widget` widgets module. 9 | """ 10 | 11 | import json 12 | 13 | from django.forms import Form, CharField 14 | from django.test import TestCase 15 | 16 | from django_json_widget.widgets import JSONEditorWidget 17 | 18 | 19 | class JSONEditorWidgetInitializationTests(TestCase): 20 | """Test widget initialization and configuration""" 21 | 22 | def test_default_initialization(self): 23 | """Test widget with default parameters""" 24 | widget = JSONEditorWidget() 25 | 26 | expected_options = { 27 | "modes": ["text", "code", "tree", "form", "view"], 28 | "mode": "code", 29 | "search": True, 30 | } 31 | 32 | self.assertEqual(widget.options, expected_options) 33 | self.assertIsNone(widget.width) 34 | self.assertIsNone(widget.height) 35 | 36 | def test_custom_mode_initialization(self): 37 | """Test widget with custom mode""" 38 | widget = JSONEditorWidget(mode="tree") 39 | 40 | self.assertEqual(widget.options["mode"], "tree") 41 | 42 | def test_custom_options_initialization(self): 43 | """Test widget with custom options""" 44 | custom_options = { 45 | "mode": "form", 46 | "search": False, 47 | "statusBar": False, 48 | } 49 | widget = JSONEditorWidget(options=custom_options) 50 | 51 | # Should merge with defaults 52 | expected_options = { 53 | "modes": ["text", "code", "tree", "form", "view"], 54 | "mode": "form", # overridden 55 | "search": False, # overridden 56 | "statusBar": False, # added 57 | } 58 | 59 | self.assertEqual(widget.options, expected_options) 60 | 61 | def test_width_height_initialization(self): 62 | """Test widget with custom width and height""" 63 | widget = JSONEditorWidget(width="100%", height="600px") 64 | 65 | self.assertEqual(widget.width, "100%") 66 | self.assertEqual(widget.height, "600px") 67 | 68 | def test_attrs_initialization(self): 69 | """Test widget with custom attributes""" 70 | attrs = {"class": "custom-json-editor", "data-test": "value"} 71 | widget = JSONEditorWidget(attrs=attrs) 72 | 73 | self.assertEqual(widget.attrs, attrs) 74 | 75 | def test_options_override_precedence(self): 76 | """Test that custom options properly override defaults""" 77 | custom_options = { 78 | "modes": ["code", "text"], # Override default modes 79 | "mode": "text", # Override default mode 80 | "search": False, # Override default search 81 | } 82 | widget = JSONEditorWidget(options=custom_options) 83 | 84 | self.assertEqual(widget.options["modes"], ["code", "text"]) 85 | self.assertEqual(widget.options["mode"], "text") 86 | self.assertFalse(widget.options["search"]) 87 | 88 | 89 | class JSONEditorWidgetContextTests(TestCase): 90 | """Test widget context generation""" 91 | 92 | def test_get_context_basic(self): 93 | """Test basic context generation""" 94 | widget = JSONEditorWidget() 95 | context = widget.get_context( 96 | "test_field", '{"key": "value"}', {"id": "test_id"} 97 | ) 98 | self.assertIn("widget", context) 99 | self.assertEqual(context["widget"]["name"], "test_field") 100 | self.assertEqual(context["widget"]["value"], {"key": "value"}) 101 | self.assertIn("options", context["widget"]) 102 | self.assertIsNone(context["widget"]["width"]) 103 | self.assertIsNone(context["widget"]["height"]) 104 | 105 | def test_get_context_with_dimensions(self): 106 | """Test context generation with width and height""" 107 | widget = JSONEditorWidget(width="800px", height="400px") 108 | context = widget.get_context("test_field", "{}", {"id": "test_id"}) 109 | 110 | self.assertEqual(context["widget"]["width"], "800px") 111 | self.assertEqual(context["widget"]["height"], "400px") 112 | self.assertIn("options", context["widget"]) 113 | 114 | def test_get_context_options_serialization(self): 115 | """Test that options are properly JSON serialized in context""" 116 | custom_options = { 117 | "mode": "tree", 118 | "search": False, 119 | "navigationBar": True, 120 | } 121 | widget = JSONEditorWidget(options=custom_options) 122 | context = widget.get_context("test_field", "{}", {"id": "test_id"}) 123 | 124 | # Options should be JSON serialized 125 | options_json = context["widget"]["options"] 126 | self.assertIsInstance(options_json, str) 127 | 128 | # Should be valid JSON 129 | parsed_options = json.loads(options_json) 130 | self.assertEqual(parsed_options["mode"], "tree") 131 | self.assertFalse(parsed_options["search"]) 132 | self.assertTrue(parsed_options["navigationBar"]) 133 | 134 | 135 | class JSONEditorWidgetValueFormattingTests(TestCase): 136 | """Test widget value formatting""" 137 | 138 | def test_format_value_valid_json_string(self): 139 | """Test formatting valid JSON string""" 140 | widget = JSONEditorWidget() 141 | 142 | json_string = '{"name": "John", "age": 30}' 143 | result = widget.format_value(json_string) 144 | 145 | expected = {"name": "John", "age": 30} 146 | self.assertEqual(result, expected) 147 | 148 | def test_format_value_invalid_json(self): 149 | """Test formatting invalid JSON string raises error""" 150 | widget = JSONEditorWidget() 151 | 152 | invalid_json = '{"invalid": json}' 153 | 154 | with self.assertRaises(json.JSONDecodeError): 155 | widget.format_value(invalid_json) 156 | 157 | def test_format_value_none_or_empty(self): 158 | """Test formatting None value""" 159 | widget = JSONEditorWidget() 160 | 161 | with self.assertRaises((TypeError, json.JSONDecodeError)): 162 | widget.format_value(None) 163 | with self.assertRaises((TypeError, json.JSONDecodeError)): 164 | widget.format_value(None) 165 | 166 | def test_format_value_dict(self): 167 | widget = JSONEditorWidget() 168 | self.assertEqual(widget.format_value({}), {}) 169 | 170 | def test_format_value_list(self): 171 | widget = JSONEditorWidget() 172 | self.assertEqual(widget.format_value([]), []) 173 | 174 | 175 | class JSONEditorWidgetTemplateRenderingTests(TestCase): 176 | """Test widget template rendering""" 177 | 178 | def test_template_name(self): 179 | """Test correct template name is set""" 180 | widget = JSONEditorWidget() 181 | self.assertEqual(widget.template_name, "django_json_widget.html") 182 | 183 | def test_template_exists(self): 184 | """Test that template exists""" 185 | from os import getcwd, path 186 | widget = JSONEditorWidget() 187 | template_file = path.join(getcwd(), "django_json_widget", "templates", widget.template_name) 188 | self.assertTrue(str(template_file).endswith("django_json_widget.html")) 189 | self.assertTrue(path.exists(template_file)) 190 | 191 | def test_render_basic(self): 192 | """Test basic widget rendering""" 193 | widget = JSONEditorWidget() 194 | html = widget.render("test_field", '{"test": "value"}', {"id": "id_test_field"}) 195 | 196 | # Check for essential elements 197 | self.assertIn("id_test_field", html) 198 | self.assertIn("test_field", html) 199 | self.assertIn("JSONEditor", html) 200 | self.assertIn("textarea", html) 201 | 202 | def test_render_with_custom_dimensions(self): 203 | """Test rendering with custom width and height""" 204 | widget = JSONEditorWidget(width="100%", height="300px") 205 | html = widget.render("test_field", "{}", {"id": "id_test_field"}) 206 | 207 | self.assertIn("width:100%", html) 208 | self.assertIn("height:300px", html) 209 | 210 | def test_render_with_custom_attrs(self): 211 | """Test rendering with custom attributes""" 212 | widget = JSONEditorWidget() 213 | attrs = {"class": "custom-class", "data-test": "value"} 214 | html = widget.render("test_field", "{}", attrs) 215 | 216 | self.assertIn("custom-class", html) 217 | self.assertIn('data-test="value"', html) 218 | 219 | def test_render_javascript_options(self): 220 | """Test that JavaScript options are properly rendered""" 221 | custom_options = {"mode": "tree", "search": False} 222 | widget = JSONEditorWidget(options=custom_options) 223 | html = widget.render("test_field", "{}", {"id": "id_test_field"}) 224 | 225 | # Should contain the serialized options 226 | self.assertIn('"mode": "tree"', html) 227 | self.assertIn('"search": false', html) 228 | 229 | 230 | class JSONEditorWidgetFormIntegrationTests(TestCase): 231 | """Test widget integration with Django forms""" 232 | 233 | def test_form_field_integration(self): 234 | """Test widget works with form fields""" 235 | 236 | class TestForm(Form): 237 | json_data = CharField(widget=JSONEditorWidget()) 238 | 239 | form = TestForm() 240 | self.assertIsInstance(form.fields["json_data"].widget, JSONEditorWidget) 241 | 242 | def test_form_field_with_initial_data(self): 243 | """Test form field with initial JSON data""" 244 | 245 | class TestForm(Form): 246 | json_data = CharField( 247 | widget=JSONEditorWidget(mode="tree"), 248 | initial='{"name": "test", "value": 123}', 249 | ) 250 | 251 | form = TestForm() 252 | html = str(form["json_data"]) 253 | 254 | self.assertIn("json_data", html) 255 | self.assertIn('"mode": "tree"', html) 256 | 257 | def test_form_validation_with_widget(self): 258 | """Test form validation with widget""" 259 | 260 | class TestForm(Form): 261 | json_data = CharField(widget=JSONEditorWidget(), required=True) 262 | 263 | # Test valid data 264 | form = TestForm({"json_data": '{"valid": "json"}'}) 265 | self.assertTrue(form.is_valid()) 266 | 267 | # Test empty data 268 | form = TestForm({"json_data": ""}) 269 | self.assertFalse(form.is_valid()) 270 | 271 | def test_form_media_inclusion(self): 272 | """Test that form includes widget media""" 273 | 274 | class TestForm(Form): 275 | json_data = CharField(widget=JSONEditorWidget()) 276 | 277 | form = TestForm() 278 | media = form.media 279 | 280 | self.assertIn("dist/jsoneditor.min.js", str(media)) 281 | self.assertIn("dist/jsoneditor.min.css", str(media)) 282 | 283 | 284 | class JSONEditorWidgetEdgeCasesTests(TestCase): 285 | """Test edge cases and error conditions""" 286 | 287 | def test_unicode_json_handling(self): 288 | """Test widget handles Unicode JSON properly""" 289 | widget = JSONEditorWidget() 290 | unicode_json = '{"name": "José", "city": "São Paulo", "emoji": "🎉"}' 291 | 292 | result = widget.format_value(unicode_json) 293 | self.assertEqual(result["name"], "José") 294 | self.assertEqual(result["city"], "São Paulo") 295 | self.assertEqual(result["emoji"], "🎉") 296 | 297 | 298 | class JSONEditorWidgetAccessibilityTests(TestCase): 299 | """Test widget accessibility features""" 300 | 301 | def test_textarea_for_screen_readers(self): 302 | """Test that widget includes hidden textarea for accessibility""" 303 | widget = JSONEditorWidget() 304 | html = widget.render("test_field", "{}", {"id": "id_test_field"}) 305 | 306 | # Should have hidden textarea for form submission and screen readers 307 | self.assertIn("textarea", html) 308 | self.assertIn("id_test_field_textarea", html) 309 | self.assertIn('style="display: none"', html) 310 | 311 | def test_proper_field_naming(self): 312 | """Test that widget generates proper field names""" 313 | widget = JSONEditorWidget() 314 | html = widget.render("json_field", "{}", {"id": "id_json_field"}) 315 | 316 | self.assertIn('name="json_field"', html) 317 | self.assertIn('id="id_json_field"', html) 318 | -------------------------------------------------------------------------------- /django_json_widget/static/dist/img/jsoneditor-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | JSON Editor Icons 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | JSON Editor Icons 27 | 28 | 29 | 30 | 32 | 56 | 60 | 61 | 62 | 69 | 76 | 83 | 90 | 97 | 100 | 107 | 114 | 115 | 119 | 126 | 133 | 134 | 141 | 148 | 155 | 157 | 164 | 171 | 178 | 179 | 182 | 189 | 196 | 203 | 204 | 211 | 217 | 223 | 230 | 235 | 240 | 247 | 253 | 258 | 265 | 271 | 277 | 284 | 291 | 298 | 305 | 312 | 319 | 326 | 332 | 340 | 346 | 352 | 359 | 367 | 375 | 382 | 389 | 396 | 403 | 410 | 417 | 424 | 431 | 437 | 445 | 451 | 457 | 464 | 472 | 480 | 487 | 494 | 501 | 508 | 515 | 522 | 529 | 536 | 541 | 546 | 551 | 557 | 563 | 568 | 573 | 578 | 583 | 588 | 604 | 621 | 638 | 655 | 661 | 667 | 673 | 679 | 686 | 692 | 695 | 702 | 709 | 716 | 723 | 729 | 730 | 736 | 743 | 749 | 750 | -------------------------------------------------------------------------------- /django_json_widget/static/dist/jsoneditor.min.css: -------------------------------------------------------------------------------- 1 | .jsoneditor,.jsoneditor-modal{-webkit-text-size-adjust:none;text-size-adjust:none}.jsoneditor input,.jsoneditor input:not([type]),.jsoneditor input[type=search],.jsoneditor input[type=text],.jsoneditor-modal input,.jsoneditor-modal input:not([type]),.jsoneditor-modal input[type=search],.jsoneditor-modal input[type=text]{height:auto;border:inherit;box-shadow:none;font-size:inherit;box-sizing:inherit;padding:inherit;font-family:inherit;transition:none;line-height:inherit}.jsoneditor input:focus,.jsoneditor input:not([type]):focus,.jsoneditor input[type=search]:focus,.jsoneditor input[type=text]:focus,.jsoneditor-modal input:focus,.jsoneditor-modal input:not([type]):focus,.jsoneditor-modal input[type=search]:focus,.jsoneditor-modal input[type=text]:focus{border:inherit;box-shadow:inherit}.jsoneditor textarea,.jsoneditor-modal textarea{height:inherit}.jsoneditor select,.jsoneditor-modal select{display:inherit;height:inherit}.jsoneditor label,.jsoneditor-modal label{font-size:inherit;font-weight:inherit;color:inherit}.jsoneditor table,.jsoneditor-modal table{border-collapse:collapse;width:auto}.jsoneditor td,.jsoneditor th,.jsoneditor-modal td,.jsoneditor-modal th{padding:0;display:table-cell;text-align:left;vertical-align:inherit;border-radius:inherit}.jsoneditor .autocomplete.dropdown{position:absolute;background:#fff;box-shadow:2px 2px 12px rgba(128,128,128,.3);border:1px solid #d3d3d3;overflow-x:hidden;overflow-y:auto;cursor:default;margin:0;padding:5px;text-align:left;outline:0;font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px}.jsoneditor .autocomplete.dropdown .item{color:#1a1a1a}.jsoneditor .autocomplete.dropdown .item.hover{background-color:#ebebeb}.jsoneditor .autocomplete.hint{color:#a1a1a1;top:4px;left:4px}.jsoneditor-contextmenu-root{position:relative;width:0;height:0}.jsoneditor-contextmenu{position:absolute;box-sizing:content-box;z-index:2}.jsoneditor-contextmenu .jsoneditor-menu{position:relative;left:0;top:0;width:128px;height:auto;background:#fff;border:1px solid #d3d3d3;box-shadow:2px 2px 12px rgba(128,128,128,.3);list-style:none;margin:0;padding:0}.jsoneditor-contextmenu .jsoneditor-menu button{position:relative;padding:0 8px 0 0;margin:0;width:128px;height:auto;border:none;cursor:pointer;color:#4d4d4d;background:0 0;font-size:14px;font-family:arial,sans-serif;box-sizing:border-box;text-align:left}.jsoneditor-contextmenu .jsoneditor-menu button::-moz-focus-inner{padding:0;border:0}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-default{width:96px}.jsoneditor-contextmenu .jsoneditor-menu button.jsoneditor-expand{float:right;width:32px;height:24px;border-left:1px solid #e5e5e5}.jsoneditor-contextmenu .jsoneditor-menu li{overflow:hidden}.jsoneditor-contextmenu .jsoneditor-menu li ul{display:none;position:relative;left:-10px;top:0;border:none;box-shadow:inset 0 0 10px rgba(128,128,128,.5);padding:0 10px;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.jsoneditor-contextmenu .jsoneditor-menu li ul .jsoneditor-icon{margin-left:24px}.jsoneditor-contextmenu .jsoneditor-menu li ul li button{padding-left:24px;animation:all ease-in-out 1s}.jsoneditor-contextmenu .jsoneditor-menu li button .jsoneditor-expand{position:absolute;top:0;right:0;width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:0 -72px}.jsoneditor-contextmenu .jsoneditor-icon{position:absolute;top:0;left:0;width:24px;height:24px;border:none;padding:0;margin:0;background-image:url(./img/jsoneditor-icons.svg)}.jsoneditor-contextmenu .jsoneditor-text{padding:4px 0 4px 24px;word-wrap:break-word}.jsoneditor-contextmenu .jsoneditor-text.jsoneditor-right-margin{padding-right:24px}.jsoneditor-contextmenu .jsoneditor-separator{height:0;border-top:1px solid #e5e5e5;padding-top:5px;margin-top:5px}.jsoneditor-contextmenu button.jsoneditor-remove .jsoneditor-icon{background-position:-24px 0}.jsoneditor-contextmenu button.jsoneditor-append .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-insert .jsoneditor-icon{background-position:0 0}.jsoneditor-contextmenu button.jsoneditor-duplicate .jsoneditor-icon{background-position:-48px 0}.jsoneditor-contextmenu button.jsoneditor-sort-asc .jsoneditor-icon{background-position:-168px 0}.jsoneditor-contextmenu button.jsoneditor-sort-desc .jsoneditor-icon{background-position:-192px 0}.jsoneditor-contextmenu button.jsoneditor-transform .jsoneditor-icon{background-position:-216px 0}.jsoneditor-contextmenu button.jsoneditor-extract .jsoneditor-icon{background-position:0 -24px}.jsoneditor-contextmenu button.jsoneditor-type-string .jsoneditor-icon{background-position:-144px 0}.jsoneditor-contextmenu button.jsoneditor-type-auto .jsoneditor-icon{background-position:-120px 0}.jsoneditor-contextmenu button.jsoneditor-type-object .jsoneditor-icon{background-position:-72px 0}.jsoneditor-contextmenu button.jsoneditor-type-array .jsoneditor-icon{background-position:-96px 0}.jsoneditor-contextmenu button.jsoneditor-type-modes .jsoneditor-icon{background-image:none;width:6px}.jsoneditor-contextmenu li,.jsoneditor-contextmenu ul{box-sizing:content-box;position:relative}.jsoneditor-contextmenu .jsoneditor-menu button:focus,.jsoneditor-contextmenu .jsoneditor-menu button:hover{color:#1a1a1a;background-color:#f5f5f5;outline:0}.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus,.jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover{color:#fff;background-color:#ee422e}.jsoneditor-contextmenu .jsoneditor-menu li ul li button:focus,.jsoneditor-contextmenu .jsoneditor-menu li ul li button:hover{background-color:#f5f5f5}.jsoneditor-modal{max-width:95%;border-radius:2px!important;padding:45px 15px 15px 15px!important;box-shadow:2px 2px 12px rgba(128,128,128,.3);color:#4d4d4d;line-height:1.3em}.jsoneditor-modal.jsoneditor-modal-transform{width:600px!important}.jsoneditor-modal .pico-modal-header{position:absolute;box-sizing:border-box;top:0;left:0;width:100%;padding:0 10px;height:30px;line-height:30px;font-family:arial,sans-serif;font-size:11pt;background:#3883fa;color:#fff}.jsoneditor-modal table{width:100%}.jsoneditor-modal table td{padding:3px 0}.jsoneditor-modal table td.jsoneditor-modal-input{text-align:right;padding-right:0;white-space:nowrap}.jsoneditor-modal table td.jsoneditor-modal-actions{padding-top:15px}.jsoneditor-modal table th{vertical-align:middle}.jsoneditor-modal p:first-child{margin-top:0}.jsoneditor-modal a{color:#3883fa}.jsoneditor-modal .jsoneditor-jmespath-block{margin-bottom:10px}.jsoneditor-modal .pico-close{background:0 0!important;font-size:24px!important;top:7px!important;right:7px!important;color:#fff}.jsoneditor-modal input{padding:4px}.jsoneditor-modal input[type=text]{cursor:inherit}.jsoneditor-modal input[disabled]{background:#d3d3d3;color:grey}.jsoneditor-modal .jsoneditor-select-wrapper{position:relative;display:inline-block}.jsoneditor-modal .jsoneditor-select-wrapper:after{content:"";width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:6px solid #666;position:absolute;right:8px;top:14px;pointer-events:none}.jsoneditor-modal select{padding:3px 24px 3px 10px;min-width:180px;max-width:350px;-webkit-appearance:none;-moz-appearance:none;appearance:none;text-indent:0;text-overflow:"";font-size:14px;line-height:1.5em}.jsoneditor-modal select::-ms-expand{display:none}.jsoneditor-modal .jsoneditor-button-group input{padding:4px 10px;margin:0;border-radius:0;border-left-style:none}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-first{border-top-left-radius:3px;border-bottom-left-radius:3px;border-left-style:solid}.jsoneditor-modal .jsoneditor-button-group input.jsoneditor-button-last{border-top-right-radius:3px;border-bottom-right-radius:3px}.jsoneditor-modal .jsoneditor-transform-preview{background:#f5f5f5;height:200px}.jsoneditor-modal .jsoneditor-transform-preview.jsoneditor-error{color:#ee422e}.jsoneditor-modal .jsoneditor-jmespath-wizard{line-height:1.2em;width:100%;padding:0;border-radius:3px}.jsoneditor-modal .jsoneditor-jmespath-label{font-weight:700;color:#1e90ff;margin-top:20px;margin-bottom:5px}.jsoneditor-modal .jsoneditor-jmespath-wizard-table{width:100%;border-collapse:collapse}.jsoneditor-modal .jsoneditor-jmespath-wizard-label{font-style:italic;margin:4px 0 2px 0}.jsoneditor-modal .jsoneditor-inline{position:relative;display:inline-block;width:100%;padding-top:2px;padding-bottom:2px}.jsoneditor-modal .jsoneditor-inline:not(:last-child){padding-right:2px}.jsoneditor-modal .jsoneditor-jmespath-filter{display:flex;flex-wrap:wrap}.jsoneditor-modal .jsoneditor-jmespath-filter-field{width:180px}.jsoneditor-modal .jsoneditor-jmespath-filter-relation{width:100px}.jsoneditor-modal .jsoneditor-jmespath-filter-value{min-width:180px;flex:1}.jsoneditor-modal .jsoneditor-jmespath-sort-field{width:170px}.jsoneditor-modal .jsoneditor-jmespath-sort-order{width:150px}.jsoneditor-modal .jsoneditor-jmespath-select-fields{width:100%}.jsoneditor-modal .selectr-selected{border-color:#d3d3d3;padding:4px 28px 4px 8px}.jsoneditor-modal .selectr-selected .selectr-tag{background-color:#3883fa;border-radius:5px}.jsoneditor-modal table td,.jsoneditor-modal table th{text-align:left;vertical-align:middle;font-weight:400;color:#4d4d4d;border-spacing:0;border-collapse:collapse}.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal input[type=text]:focus,.jsoneditor-modal select,.jsoneditor-modal textarea{background:#fff;border:1px solid #d3d3d3;color:#4d4d4d;border-radius:3px;padding:4px}.jsoneditor-modal #query,.jsoneditor-modal textarea{border-radius:unset}.jsoneditor-modal,.jsoneditor-modal #query,.jsoneditor-modal input,.jsoneditor-modal input[type=text],.jsoneditor-modal option,.jsoneditor-modal select,.jsoneditor-modal table td,.jsoneditor-modal table th,.jsoneditor-modal textarea{font-size:10.5pt;font-family:arial,sans-serif}.jsoneditor-modal #query,.jsoneditor-modal .jsoneditor-transform-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;width:100%;box-sizing:border-box}.jsoneditor-modal input[type=button],.jsoneditor-modal input[type=submit]{background:#f5f5f5;padding:4px 20px}.jsoneditor-modal input,.jsoneditor-modal select{cursor:pointer}.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-asc input.jsoneditor-button-asc,.jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-desc input.jsoneditor-button-desc{background:#3883fa;border-color:#3883fa;color:#fff}.jsoneditor{color:#1a1a1a;border:thin solid #3883fa;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;height:100%;position:relative;padding:0;line-height:100%}a.jsoneditor-value,div.jsoneditor-default,div.jsoneditor-field,div.jsoneditor-readonly,div.jsoneditor-value{border:1px solid transparent;min-height:16px;min-width:32px;line-height:16px;padding:2px;margin:1px;word-wrap:break-word;word-break:break-word;overflow-wrap:break-word;float:left}div.jsoneditor-field p,div.jsoneditor-value p{margin:0}div.jsoneditor-value.jsoneditor-empty::after{content:"value"}div.jsoneditor-value.jsoneditor-string{color:#006000}div.jsoneditor-value.jsoneditor-number{color:#ee422e}div.jsoneditor-value.jsoneditor-boolean{color:#ff8c00}div.jsoneditor-value.jsoneditor-null{color:#004ed0}div.jsoneditor-value.jsoneditor-color-value{color:#1a1a1a}div.jsoneditor-value.jsoneditor-invalid{color:#1a1a1a}div.jsoneditor-readonly{min-width:16px;color:grey}div.jsoneditor-empty{border-color:#d3d3d3;border-style:dashed;border-radius:2px}div.jsoneditor-field.jsoneditor-empty::after{content:"field"}div.jsoneditor td{vertical-align:top}div.jsoneditor td.jsoneditor-separator{padding:3px 0;vertical-align:top;color:grey}div.jsoneditor td.jsoneditor-tree{vertical-align:top}div.jsoneditor.busy pre.jsoneditor-preview{background:#f5f5f5;color:grey}div.jsoneditor.busy div.jsoneditor-busy{display:inherit}div.jsoneditor code.jsoneditor-preview{background:0 0}div.jsoneditor.jsoneditor-mode-preview pre.jsoneditor-preview{width:100%;height:100%;box-sizing:border-box;overflow:auto;padding:2px;margin:0;white-space:pre-wrap;word-break:break-all}div.jsoneditor-default{color:grey;padding-left:10px}div.jsoneditor-tree{width:100%;height:100%;position:relative;overflow:auto;background:#fff}div.jsoneditor-tree button.jsoneditor-button{width:24px;height:24px;padding:0;margin:0;border:none;cursor:pointer;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg)}div.jsoneditor-tree button.jsoneditor-button:focus{background-color:#f5f5f5;outline:#e5e5e5 solid 1px}div.jsoneditor-tree button.jsoneditor-collapsed{background-position:0 -48px}div.jsoneditor-tree button.jsoneditor-expanded{background-position:0 -72px}div.jsoneditor-tree button.jsoneditor-contextmenu-button{background-position:-48px -72px}div.jsoneditor-tree button.jsoneditor-invisible{visibility:hidden;background:0 0}div.jsoneditor-tree button.jsoneditor-dragarea{background-image:url(./img/jsoneditor-icons.svg);background-position:-72px -72px;cursor:move}div.jsoneditor-tree :focus{outline:0}div.jsoneditor-tree div.jsoneditor-show-more{display:inline-block;padding:3px 4px;margin:2px 0;background-color:#e5e5e5;border-radius:3px;color:grey;font-family:arial,sans-serif;font-size:14px}div.jsoneditor-tree div.jsoneditor-show-more a{display:inline-block;color:grey}div.jsoneditor-tree div.jsoneditor-color{display:inline-block;width:12px;height:12px;margin:4px;border:1px solid grey;cursor:pointer}div.jsoneditor-tree div.jsoneditor-color.jsoneditor-color-readonly{cursor:inherit}div.jsoneditor-tree div.jsoneditor-date{background:#a1a1a1;color:#fff;font-family:arial,sans-serif;border-radius:3px;display:inline-block;padding:3px;margin:0 3px}div.jsoneditor-tree table.jsoneditor-tree{border-collapse:collapse;border-spacing:0;width:100%}div.jsoneditor-tree .jsoneditor-button{display:block}div.jsoneditor-tree .jsoneditor-button.jsoneditor-schema-error{width:24px;height:24px;padding:0;margin:0 4px 0 0;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}div.jsoneditor-outer{position:static;width:100%;height:100%;margin:0;padding:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}div.jsoneditor-outer.has-nav-bar{margin-top:-26px;padding-top:26px}div.jsoneditor-outer.has-nav-bar.has-main-menu-bar{margin-top:-61px;padding-top:61px}div.jsoneditor-outer.has-status-bar{margin-bottom:-26px;padding-bottom:26px}div.jsoneditor-outer.has-main-menu-bar{margin-top:-35px;padding-top:35px}div.jsoneditor-busy{position:absolute;top:15%;left:0;box-sizing:border-box;width:100%;text-align:center;display:none}div.jsoneditor-busy span{background-color:#ffffab;border:1px solid #fe0;border-radius:3px;padding:5px 15px;box-shadow:0 0 5px rgba(0,0,0,.4)}div.jsoneditor-field.jsoneditor-empty::after,div.jsoneditor-value.jsoneditor-empty::after{pointer-events:none;color:#d3d3d3;font-size:8pt}a.jsoneditor-value.jsoneditor-url,div.jsoneditor-value.jsoneditor-url{color:#006000;text-decoration:underline}a.jsoneditor-value.jsoneditor-url{display:inline-block;padding:2px;margin:2px}a.jsoneditor-value.jsoneditor-url:focus,a.jsoneditor-value.jsoneditor-url:hover{color:#ee422e}div.jsoneditor-field.jsoneditor-highlight,div.jsoneditor-field[contenteditable=true]:focus,div.jsoneditor-field[contenteditable=true]:hover,div.jsoneditor-value.jsoneditor-highlight,div.jsoneditor-value[contenteditable=true]:focus,div.jsoneditor-value[contenteditable=true]:hover{background-color:#ffffab;border:1px solid #fe0;border-radius:2px}div.jsoneditor-field.jsoneditor-highlight-active,div.jsoneditor-field.jsoneditor-highlight-active:focus,div.jsoneditor-field.jsoneditor-highlight-active:hover,div.jsoneditor-value.jsoneditor-highlight-active,div.jsoneditor-value.jsoneditor-highlight-active:focus,div.jsoneditor-value.jsoneditor-highlight-active:hover{background-color:#fe0;border:1px solid #ffc700;border-radius:2px}div.jsoneditor-value.jsoneditor-array,div.jsoneditor-value.jsoneditor-object{min-width:16px}div.jsoneditor-tree button.jsoneditor-contextmenu-button.jsoneditor-selected,div.jsoneditor-tree button.jsoneditor-contextmenu-button:focus,div.jsoneditor-tree button.jsoneditor-contextmenu-button:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button{background-position:-48px -48px}div.jsoneditor-tree div.jsoneditor-show-more a:focus,div.jsoneditor-tree div.jsoneditor-show-more a:hover{color:#ee422e}.ace-jsoneditor,textarea.jsoneditor-text{min-height:150px}.ace-jsoneditor.ace_editor,textarea.jsoneditor-text.ace_editor{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace}textarea.jsoneditor-text{width:100%;height:100%;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;outline-width:0;border:none;background-color:#fff;resize:none}tr.jsoneditor-highlight,tr.jsoneditor-selected{background-color:#d3d3d3}tr.jsoneditor-selected button.jsoneditor-contextmenu-button,tr.jsoneditor-selected button.jsoneditor-dragarea{visibility:hidden}tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{visibility:visible}div.jsoneditor-tree button.jsoneditor-dragarea:focus,div.jsoneditor-tree button.jsoneditor-dragarea:hover,tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea{background-position:-72px -48px}div.jsoneditor td,div.jsoneditor th,div.jsoneditor tr{padding:0;margin:0}.jsoneditor-popover,.jsoneditor-schema-error,div.jsoneditor td,div.jsoneditor textarea,div.jsoneditor th,div.jsoneditor-field,div.jsoneditor-value,pre.jsoneditor-preview{font-family:consolas,menlo,monaco,"Ubuntu Mono",source-code-pro,monospace;font-size:14px;color:#1a1a1a}.jsoneditor-schema-error{cursor:default;display:inline-block;height:24px;line-height:24px;position:relative;text-align:center;width:24px}.jsoneditor-popover{background-color:#4c4c4c;border-radius:3px;box-shadow:0 0 5px rgba(0,0,0,.4);color:#fff;padding:7px 10px;position:absolute;cursor:auto;width:200px}.jsoneditor-popover.jsoneditor-above{bottom:32px;left:-98px}.jsoneditor-popover.jsoneditor-above:before{border-top:7px solid #4c4c4c;bottom:-7px}.jsoneditor-popover.jsoneditor-below{top:32px;left:-98px}.jsoneditor-popover.jsoneditor-below:before{border-bottom:7px solid #4c4c4c;top:-7px}.jsoneditor-popover.jsoneditor-left{top:-7px;right:32px}.jsoneditor-popover.jsoneditor-left:before{border-left:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;right:-14px;left:inherit;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover.jsoneditor-right{top:-7px;left:32px}.jsoneditor-popover.jsoneditor-right:before{border-right:7px solid #4c4c4c;border-top:7px solid transparent;border-bottom:7px solid transparent;content:"";top:19px;left:-14px;margin-left:inherit;margin-top:-7px;position:absolute}.jsoneditor-popover:before{border-right:7px solid transparent;border-left:7px solid transparent;content:"";display:block;left:50%;margin-left:-7px;position:absolute}.jsoneditor-text-errors tr.jump-to-line:hover{text-decoration:underline;cursor:pointer}.jsoneditor-schema-error:focus .jsoneditor-popover,.jsoneditor-schema-error:hover .jsoneditor-popover{display:block;animation:fade-in .3s linear 1,move-up .3s linear 1}@keyframes fade-in{from{opacity:0}to{opacity:1}}.jsoneditor .jsoneditor-validation-errors-container{max-height:130px;overflow-y:auto}.jsoneditor .jsoneditor-validation-errors{width:100%;overflow:hidden}.jsoneditor .jsoneditor-additional-errors{position:absolute;margin:auto;bottom:31px;left:calc(50% - 92px);color:grey;background-color:#ebebeb;padding:7px 15px;border-radius:8px}.jsoneditor .jsoneditor-additional-errors.visible{visibility:visible;opacity:1;transition:opacity 2s linear}.jsoneditor .jsoneditor-additional-errors.hidden{visibility:hidden;opacity:0;transition:visibility 0s 2s,opacity 2s linear}.jsoneditor .jsoneditor-text-errors{width:100%;border-collapse:collapse;border-top:1px solid #ffc700}.jsoneditor .jsoneditor-text-errors td{padding:3px 6px;vertical-align:middle}.jsoneditor .jsoneditor-text-errors td pre{margin:0;white-space:pre-wrap}.jsoneditor .jsoneditor-text-errors tr{background-color:#ffffab}.jsoneditor .jsoneditor-text-errors tr.parse-error{background-color:rgba(238,46,46,.4392156863)}.jsoneditor-text-errors .jsoneditor-schema-error{border:none;width:24px;height:24px;padding:0;margin:0 4px 0 0;cursor:pointer}.jsoneditor-text-errors tr .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;background-color:transparent}.jsoneditor-text-errors tr.parse-error .jsoneditor-schema-error{background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0;background-color:transparent}.jsoneditor-anchor{cursor:pointer}.jsoneditor-anchor .picker_wrapper.popup.popup_bottom{top:28px;left:-10px}.fadein{-webkit-animation:fadein .3s;animation:fadein .3s;-moz-animation:fadein .3s;-o-animation:fadein .3s}@keyframes fadein{0%{opacity:0}100%{opacity:1}}.jsoneditor-modal input[type=search].selectr-input{border:1px solid #d3d3d3;width:calc(100% - 4px);margin:2px;padding:4px;box-sizing:border-box}.jsoneditor-modal button.selectr-input-clear{right:8px}.jsoneditor-menu{width:100%;height:35px;padding:2px;margin:0;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:#fff;background-color:#3883fa;border-bottom:1px solid #3883fa}.jsoneditor-menu>.jsoneditor-modes>button,.jsoneditor-menu>button{width:26px;height:26px;margin:2px;padding:0;border-radius:2px;border:1px solid transparent;background-color:transparent;background-image:url(./img/jsoneditor-icons.svg);color:#fff;opacity:.8;font-family:arial,sans-serif;font-size:14px;float:left}.jsoneditor-menu>.jsoneditor-modes>button:hover,.jsoneditor-menu>button:hover{background-color:rgba(255,255,255,.2);border:1px solid rgba(255,255,255,.4)}.jsoneditor-menu>.jsoneditor-modes>button:active,.jsoneditor-menu>.jsoneditor-modes>button:focus,.jsoneditor-menu>button:active,.jsoneditor-menu>button:focus{background-color:rgba(255,255,255,.3)}.jsoneditor-menu>.jsoneditor-modes>button:disabled,.jsoneditor-menu>button:disabled{opacity:.5;background-color:transparent;border:none}.jsoneditor-menu>button.jsoneditor-collapse-all{background-position:0 -96px}.jsoneditor-menu>button.jsoneditor-expand-all{background-position:0 -120px}.jsoneditor-menu>button.jsoneditor-sort{background-position:-120px -96px}.jsoneditor-menu>button.jsoneditor-transform{background-position:-144px -96px}.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-form>.jsoneditor-menu>button.jsoneditor-transform,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-sort,.jsoneditor.jsoneditor-mode-view>.jsoneditor-menu>button.jsoneditor-transform{display:none}.jsoneditor-menu>button.jsoneditor-undo{background-position:-24px -96px}.jsoneditor-menu>button.jsoneditor-undo:disabled{background-position:-24px -120px}.jsoneditor-menu>button.jsoneditor-redo{background-position:-48px -96px}.jsoneditor-menu>button.jsoneditor-redo:disabled{background-position:-48px -120px}.jsoneditor-menu>button.jsoneditor-compact{background-position:-72px -96px}.jsoneditor-menu>button.jsoneditor-format{background-position:-72px -120px}.jsoneditor-menu>button.jsoneditor-repair{background-position:-96px -96px}.jsoneditor-menu>.jsoneditor-modes{display:inline-block;float:left}.jsoneditor-menu>.jsoneditor-modes>button{background-image:none;width:auto;padding-left:6px;padding-right:6px}.jsoneditor-menu>.jsoneditor-modes>button.jsoneditor-separator,.jsoneditor-menu>button.jsoneditor-separator{margin-left:10px}.jsoneditor-menu a{font-family:arial,sans-serif;font-size:14px;color:#fff;opacity:.8;vertical-align:middle}.jsoneditor-menu a:hover{opacity:1}.jsoneditor-menu a.jsoneditor-poweredBy{font-size:8pt;position:absolute;right:0;top:0;padding:10px}.jsoneditor-navigation-bar{width:100%;height:26px;line-height:26px;padding:0;margin:0;border-bottom:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;color:grey;background-color:#ebebeb;overflow:hidden;font-family:arial,sans-serif;font-size:14px}.jsoneditor-search{font-family:arial,sans-serif;position:absolute;right:4px;top:4px;border-collapse:collapse;border-spacing:0;display:flex}.jsoneditor-search input{color:#1a1a1a;width:120px;border:none;outline:0;margin:1px;line-height:20px;font-family:arial,sans-serif}.jsoneditor-search button{width:16px;height:24px;padding:0;margin:0;border:none;background:url(./img/jsoneditor-icons.svg);vertical-align:top}.jsoneditor-search button:hover{background-color:transparent}.jsoneditor-search button.jsoneditor-refresh{width:18px;background-position:-99px -73px}.jsoneditor-search button.jsoneditor-next{cursor:pointer;background-position:-124px -73px}.jsoneditor-search button.jsoneditor-next:hover{background-position:-124px -49px}.jsoneditor-search button.jsoneditor-previous{cursor:pointer;background-position:-148px -73px;margin-right:2px}.jsoneditor-search button.jsoneditor-previous:hover{background-position:-148px -49px}.jsoneditor-results{font-family:arial,sans-serif;color:#fff;padding-right:5px;line-height:26px}.jsoneditor-frame{border:1px solid transparent;background-color:#fff;padding:0 2px;margin:0}.jsoneditor-statusbar{line-height:26px;height:26px;color:grey;background-color:#ebebeb;border-top:1px solid #d3d3d3;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px}.jsoneditor-statusbar>.jsoneditor-curserinfo-val{margin-right:12px}.jsoneditor-statusbar>.jsoneditor-curserinfo-count{margin-left:4px}.jsoneditor-statusbar>.jsoneditor-validation-error-icon{float:right;width:24px;height:24px;padding:0;margin-top:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-168px -48px;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-validation-error-count{float:right;margin:0 4px 0 0;cursor:pointer}.jsoneditor-statusbar>.jsoneditor-parse-error-icon{float:right;width:24px;height:24px;padding:0;margin:1px;background-image:url(./img/jsoneditor-icons.svg);background-position:-25px 0}.jsoneditor-statusbar .jsoneditor-array-info a{color:inherit}div.jsoneditor-statusbar>.jsoneditor-curserinfo-label,div.jsoneditor-statusbar>.jsoneditor-size-info{margin:0 4px}.jsoneditor-treepath{padding:0 5px;overflow:hidden;white-space:nowrap;outline:0}.jsoneditor-treepath.show-all{word-wrap:break-word;white-space:normal;position:absolute;background-color:#ebebeb;z-index:1;box-shadow:2px 2px 12px rgba(128,128,128,.3)}.jsoneditor-treepath.show-all span.jsoneditor-treepath-show-all-btn{display:none}.jsoneditor-treepath div.jsoneditor-contextmenu-root{position:absolute;left:0}.jsoneditor-treepath .jsoneditor-treepath-show-all-btn{position:absolute;background-color:#ebebeb;left:0;height:20px;padding:0 3px;cursor:pointer}.jsoneditor-treepath .jsoneditor-treepath-element{margin:1px;font-family:arial,sans-serif;font-size:14px}.jsoneditor-treepath .jsoneditor-treepath-seperator{margin:2px;font-size:9pt;font-family:arial,sans-serif}.jsoneditor-treepath span.jsoneditor-treepath-element:hover,.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover{cursor:pointer;text-decoration:underline}/*! 2 | * Selectr 2.4.13 3 | * http://mobius.ovh/docs/selectr 4 | * 5 | * Released under the MIT license 6 | */.selectr-container{position:relative}.selectr-container li{list-style:none}.selectr-hidden{position:absolute;overflow:hidden;clip:rect(0,0,0,0);width:1px;height:1px;margin:-1px;padding:0;border:0 none}.selectr-visible{position:absolute;left:0;top:0;width:100%;height:100%;opacity:0;z-index:11}.selectr-desktop.multiple .selectr-visible{display:none}.selectr-desktop.multiple.native-open .selectr-visible{top:100%;min-height:200px!important;height:auto;opacity:1;display:block}.selectr-container.multiple.selectr-mobile .selectr-selected{z-index:0}.selectr-selected{position:relative;z-index:1;box-sizing:border-box;width:100%;padding:7px 28px 7px 14px;cursor:pointer;border:1px solid #999;border-radius:3px;background-color:#fff}.selectr-selected::before{position:absolute;top:50%;right:10px;width:0;height:0;content:"";-o-transform:rotate(0) translate3d(0,-50%,0);-ms-transform:rotate(0) translate3d(0,-50%,0);-moz-transform:rotate(0) translate3d(0,-50%,0);-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0);border-width:4px 4px 0 4px;border-style:solid;border-color:#6c7a86 transparent transparent}.selectr-container.native-open .selectr-selected::before,.selectr-container.open .selectr-selected::before{border-width:0 4px 4px 4px;border-style:solid;border-color:transparent transparent #6c7a86}.selectr-label{display:none;overflow:hidden;width:100%;white-space:nowrap;text-overflow:ellipsis}.selectr-placeholder{color:#6c7a86}.selectr-tags{margin:0;padding:0;white-space:normal}.has-selected .selectr-tags{margin:0 0 -2px}.selectr-tag{list-style:none;position:relative;float:left;padding:2px 25px 2px 8px;margin:0 2px 2px 0;cursor:default;color:#fff;border:medium none;border-radius:10px;background:#acb7bf none repeat scroll 0 0}.selectr-container.multiple.has-selected .selectr-selected{padding:5px 28px 5px 5px}.selectr-options-container{position:absolute;z-index:10000;top:calc(100% - 1px);left:0;display:none;box-sizing:border-box;width:100%;border-width:0 1px 1px;border-style:solid;border-color:transparent #999 #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.open .selectr-options-container{display:block}.selectr-input-container{position:relative;display:none}.selectr-clear,.selectr-input-clear,.selectr-tag-remove{position:absolute;top:50%;right:22px;width:20px;height:20px;padding:0;cursor:pointer;-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);border:medium none;background-color:transparent;z-index:11}.selectr-clear,.selectr-input-clear{display:none}.selectr-container.has-selected .selectr-clear,.selectr-input-container.active .selectr-input-clear{display:block}.selectr-selected .selectr-tag-remove{right:2px}.selectr-clear::after,.selectr-clear::before,.selectr-input-clear::after,.selectr-input-clear::before,.selectr-tag-remove::after,.selectr-tag-remove::before{position:absolute;top:5px;left:9px;width:2px;height:10px;content:" ";background-color:#6c7a86}.selectr-tag-remove::after,.selectr-tag-remove::before{top:4px;width:3px;height:12px;background-color:#fff}.selectr-clear:before,.selectr-input-clear::before,.selectr-tag-remove::before{-o-transform:rotate(45deg);-ms-transform:rotate(45deg);-moz-transform:rotate(45deg);-webkit-transform:rotate(45deg);transform:rotate(45deg)}.selectr-clear:after,.selectr-input-clear::after,.selectr-tag-remove::after{-o-transform:rotate(-45deg);-ms-transform:rotate(-45deg);-moz-transform:rotate(-45deg);-webkit-transform:rotate(-45deg);transform:rotate(-45deg)}.selectr-input-container.active,.selectr-input-container.active .selectr-clear{display:block}.selectr-input{top:5px;left:5px;box-sizing:border-box;width:calc(100% - 30px);margin:10px 15px;padding:7px 30px 7px 9px;border:1px solid #999;border-radius:3px}.selectr-notice{display:none;box-sizing:border-box;width:100%;padding:8px 16px;border-top:1px solid #999;border-radius:0 0 3px 3px;background-color:#fff}.selectr-container.notice .selectr-notice{display:block}.selectr-container.notice .selectr-selected{border-radius:3px 3px 0 0}.selectr-options{position:relative;top:calc(100% + 2px);display:none;overflow-x:auto;overflow-y:scroll;max-height:200px;margin:0;padding:0}.selectr-container.notice .selectr-options-container,.selectr-container.open .selectr-input-container,.selectr-container.open .selectr-options{display:block}.selectr-option{position:relative;display:block;padding:5px 20px;list-style:outside none none;cursor:pointer;font-weight:400}.selectr-options.optgroups>.selectr-option{padding-left:25px}.selectr-optgroup{font-weight:700;padding:0}.selectr-optgroup--label{font-weight:700;margin-top:10px;padding:5px 15px}.selectr-match{text-decoration:underline}.selectr-option.selected{background-color:#ddd}.selectr-option.active{color:#fff;background-color:#5897fb}.selectr-option.disabled{opacity:.4}.selectr-option.excluded{display:none}.selectr-container.open .selectr-selected{border-color:#999 #999 transparent #999;border-radius:3px 3px 0 0}.selectr-container.open .selectr-selected::after{-o-transform:rotate(180deg) translate3d(0,50%,0);-ms-transform:rotate(180deg) translate3d(0,50%,0);-moz-transform:rotate(180deg) translate3d(0,50%,0);-webkit-transform:rotate(180deg) translate3d(0,50%,0);transform:rotate(180deg) translate3d(0,50%,0)}.selectr-disabled{opacity:.6}.has-selected .selectr-placeholder,.selectr-empty{display:none}.has-selected .selectr-label{display:block}.taggable .selectr-selected{padding:4px 28px 4px 4px}.taggable .selectr-selected::after{display:table;content:" ";clear:both}.taggable .selectr-label{width:auto}.taggable .selectr-tags{float:left;display:block}.taggable .selectr-placeholder{display:none}.input-tag{float:left;min-width:90px;width:auto}.selectr-tag-input{border:medium none;padding:3px 10px;width:100%;font-family:inherit;font-weight:inherit;font-size:inherit}.selectr-input-container.loading::after{position:absolute;top:50%;right:20px;width:20px;height:20px;content:"";-o-transform:translate3d(0,-50%,0);-ms-transform:translate3d(0,-50%,0);-moz-transform:translate3d(0,-50%,0);-webkit-transform:translate3d(0,-50%,0);transform:translate3d(0,-50%,0);-o-transform-origin:50% 0 0;-ms-transform-origin:50% 0 0;-moz-transform-origin:50% 0 0;-webkit-transform-origin:50% 0 0;transform-origin:50% 0 0;-moz-animation:.5s linear 0s normal forwards infinite running selectr-spin;-webkit-animation:.5s linear 0s normal forwards infinite running selectr-spin;animation:.5s linear 0s normal forwards infinite running selectr-spin;border-width:3px;border-style:solid;border-color:#aaa #ddd #ddd;border-radius:50%}@-webkit-keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}@keyframes selectr-spin{0%{-webkit-transform:rotate(0) translate3d(0,-50%,0);transform:rotate(0) translate3d(0,-50%,0)}100%{-webkit-transform:rotate(360deg) translate3d(0,-50%,0);transform:rotate(360deg) translate3d(0,-50%,0)}}.selectr-container.open.inverted .selectr-selected{border-color:transparent #999 #999;border-radius:0 0 3px 3px}.selectr-container.inverted .selectr-options-container{border-width:1px 1px 0;border-color:#999 #999 transparent;border-radius:3px 3px 0 0;background-color:#fff}.selectr-container.inverted .selectr-options-container{top:auto;bottom:calc(100% - 1px)}.selectr-container ::-webkit-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::-moz-placeholder{color:#6c7a86;opacity:1}.selectr-container :-ms-input-placeholder{color:#6c7a86;opacity:1}.selectr-container ::placeholder{color:#6c7a86;opacity:1} 7 | 8 | /*Dark mode - Respects Django admin data-theme attribute */ 9 | @media (prefers-color-scheme:dark){html:not([data-theme="light"]) div.jsoneditor,html:not([data-theme="light"]) div.jsoneditor-menu{border-color:#4b4b4b}html:not([data-theme="light"]) div.jsoneditor-menu{background-color:#4b4b4b}html:not([data-theme="light"]) div.jsoneditor textarea.jsoneditor-text,html:not([data-theme="light"]) div.jsoneditor-tree{background-color:#666;color:#fff}html:not([data-theme="light"]) div.jsoneditor-field,html:not([data-theme="light"]) div.jsoneditor-value,html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-invalid{color:#fff}html:not([data-theme="light"]) table.jsoneditor-search div.jsoneditor-frame{background:grey}html:not([data-theme="light"]) tr.jsoneditor-highlight,html:not([data-theme="light"]) tr.jsoneditor-selected{background-color:grey}html:not([data-theme="light"]) div.jsoneditor-field.jsoneditor-highlight,html:not([data-theme="light"]) div.jsoneditor-field[contenteditable=true]:focus,html:not([data-theme="light"]) div.jsoneditor-field[contenteditable=true]:hover,html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-highlight,html:not([data-theme="light"]) div.jsoneditor-value[contenteditable=true]:focus,html:not([data-theme="light"]) div.jsoneditor-value[contenteditable=true]:hover{background-color:grey;border-color:grey}html:not([data-theme="light"]) div.jsoneditor-field.highlight-active,html:not([data-theme="light"]) div.jsoneditor-field.highlight-active:focus,html:not([data-theme="light"]) div.jsoneditor-field.highlight-active:hover,html:not([data-theme="light"]) div.jsoneditor-value.highlight-active,html:not([data-theme="light"]) div.jsoneditor-value.highlight-active:focus,html:not([data-theme="light"]) div.jsoneditor-value.highlight-active:hover{background-color:#b1b1b1;border-color:#b1b1b1}html:not([data-theme="light"]) div.jsoneditor-tree button:focus{background-color:#868686}html:not([data-theme="light"]) div.jsoneditor td.jsoneditor-separator,html:not([data-theme="light"]) div.jsoneditor-readonly{color:#acacac}html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-string{color:#0f8}html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-array,html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-object{color:#bababa}html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-number{color:#ff4040}html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-boolean{color:#ff8048}html:not([data-theme="light"]) div.jsoneditor-value.jsoneditor-null{color:#49a7fc}html:not([data-theme="light"]) div.jsoneditor input[type=text]{background-color:#121212;color:#fff}}html[data-theme="dark"] div.jsoneditor,html[data-theme="dark"] div.jsoneditor-menu{border-color:#4b4b4b}html[data-theme="dark"] div.jsoneditor-menu{background-color:#4b4b4b}html[data-theme="dark"] div.jsoneditor textarea.jsoneditor-text,html[data-theme="dark"] div.jsoneditor-tree{background-color:#666;color:#fff}html[data-theme="dark"] div.jsoneditor-field,html[data-theme="dark"] div.jsoneditor-value,html[data-theme="dark"] div.jsoneditor-value.jsoneditor-invalid{color:#fff}html[data-theme="dark"] table.jsoneditor-search div.jsoneditor-frame{background:grey}html[data-theme="dark"] tr.jsoneditor-highlight,html[data-theme="dark"] tr.jsoneditor-selected{background-color:grey}html[data-theme="dark"] div.jsoneditor-field.jsoneditor-highlight,html[data-theme="dark"] div.jsoneditor-field[contenteditable=true]:focus,html[data-theme="dark"] div.jsoneditor-field[contenteditable=true]:hover,html[data-theme="dark"] div.jsoneditor-value.jsoneditor-highlight,html[data-theme="dark"] div.jsoneditor-value[contenteditable=true]:focus,html[data-theme="dark"] div.jsoneditor-value[contenteditable=true]:hover{background-color:grey;border-color:grey}html[data-theme="dark"] div.jsoneditor-field.highlight-active,html[data-theme="dark"] div.jsoneditor-field.highlight-active:focus,html[data-theme="dark"] div.jsoneditor-field.highlight-active:hover,html[data-theme="dark"] div.jsoneditor-value.highlight-active,html[data-theme="dark"] div.jsoneditor-value.highlight-active:focus,html[data-theme="dark"] div.jsoneditor-value.highlight-active:hover{background-color:#b1b1b1;border-color:#b1b1b1}html[data-theme="dark"] div.jsoneditor-tree button:focus{background-color:#868686}html[data-theme="dark"] div.jsoneditor td.jsoneditor-separator,html[data-theme="dark"] div.jsoneditor-readonly{color:#acacac}html[data-theme="dark"] div.jsoneditor-value.jsoneditor-string{color:#0f8}html[data-theme="dark"] div.jsoneditor-value.jsoneditor-array,html[data-theme="dark"] div.jsoneditor-value.jsoneditor-object{color:#bababa}html[data-theme="dark"] div.jsoneditor-value.jsoneditor-number{color:#ff4040}html[data-theme="dark"] div.jsoneditor-value.jsoneditor-boolean{color:#ff8048}html[data-theme="dark"] div.jsoneditor-value.jsoneditor-null{color:#49a7fc}html[data-theme="dark"] div.jsoneditor input[type=text]{background-color:#121212;color:#fff} 10 | --------------------------------------------------------------------------------