├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ └── pypi.yml ├── .gitignore ├── .prettierignore ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs └── django-flat-json-widget-demo.gif ├── flat_json_widget ├── __init__.py ├── static │ └── flat-json-widget │ │ ├── css │ │ └── flat-json-widget.css │ │ └── js │ │ ├── flat-json-widget.js │ │ └── lib │ │ └── underscore-umd-min.js ├── templates │ └── flat_json_widget │ │ └── flat_json_widget.html ├── tests.py └── widgets.py ├── pyproject.toml ├── run-qa-checks ├── runtests.py ├── setup.cfg ├── setup.py └── tests ├── manage.py ├── settings.py ├── test_app ├── __init__.py ├── admin.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_alter_jsondocument_id.py │ └── __init__.py └── models.py └── urls.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "monthly" 12 | commit-message: 13 | prefix: "[deps] " 14 | - package-ecosystem: "github-actions" # Check for GitHub Actions updates 15 | directory: "/" 16 | schedule: 17 | interval: "monthly" # Check for updates weekly 18 | commit-message: 19 | prefix: "[ci] " 20 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | name: Django Flat JSON Widget CI Build 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | 15 | build: 16 | name: Python==${{ matrix.python-version }} | ${{ matrix.django-version }} 17 | runs-on: ubuntu-24.04 18 | 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | python-version: 23 | - "3.9" 24 | - "3.10" 25 | - "3.11" 26 | - "3.12" 27 | - "3.13" 28 | django-version: 29 | - django~=4.2.0 30 | - django~=5.1.0 31 | - django~=5.2.0 32 | exclude: 33 | # Django 5.1+ requires Python >=3.10 34 | - python-version: "3.9" 35 | django-version: django~=5.1.0 36 | - python-version: "3.9" 37 | django-version: django~=5.2.0 38 | # Python 3.13 supported only in Django >=5.1.3 39 | - python-version: "3.13" 40 | django-version: django~=4.2.0 41 | 42 | steps: 43 | - uses: actions/checkout@v4 44 | with: 45 | ref: ${{ github.event.pull_request.head.sha }} 46 | 47 | - name: Cache APT packages 48 | uses: actions/cache@v4 49 | with: 50 | path: /var/cache/apt/archives 51 | key: apt-${{ runner.os }}-${{ hashFiles('.github/workflows/ci.yml') }} 52 | restore-keys: | 53 | apt-${{ runner.os }}- 54 | 55 | - name: Disable man page auto-update 56 | run: | 57 | echo 'set man-db/auto-update false' | sudo debconf-communicate >/dev/null 58 | sudo dpkg-reconfigure man-db 59 | 60 | - name: Set up Python ${{ matrix.python-version }} 61 | uses: actions/setup-python@v5 62 | with: 63 | python-version: ${{ matrix.python-version }} 64 | cache: 'pip' 65 | cache-dependency-path: | 66 | setup.py 67 | 68 | - name: Install Dependencies 69 | id: deps 70 | run: | 71 | pip install -U pip wheel setuptools 72 | pip install -U -e .[test] 73 | pip install ${{ matrix.django-version }} 74 | sudo npm install -g prettier 75 | 76 | - name: QA checks 77 | run: | 78 | ./run-qa-checks 79 | 80 | - name: Tests 81 | if: ${{ !cancelled() && steps.deps.conclusion == 'success' }} 82 | run: | 83 | coverage run runtests.py 84 | coverage xml 85 | 86 | - name: Upload Coverage 87 | if: ${{ success() }} 88 | uses: coverallsapp/github-action@v2 89 | with: 90 | parallel: true 91 | format: cobertura 92 | flag-name: python-${{ matrix.env.env }} 93 | github-token: ${{ secrets.GITHUB_TOKEN }} 94 | 95 | coveralls: 96 | needs: build 97 | runs-on: ubuntu-latest 98 | steps: 99 | - name: Coveralls Finished 100 | uses: coverallsapp/github-action@v2 101 | with: 102 | parallel-finished: true 103 | -------------------------------------------------------------------------------- /.github/workflows/pypi.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package to Pypi.org 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | id-token: write 9 | 10 | jobs: 11 | pypi-publish: 12 | name: Release Python Package on Pypi.org 13 | runs-on: ubuntu-latest 14 | environment: 15 | name: pypi 16 | url: https://pypi.org/p/django-flat-json-widget 17 | permissions: 18 | id-token: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: '3.10' 25 | - name: Install dependencies 26 | run: | 27 | pip install -U pip 28 | pip install build 29 | - name: Build package 30 | run: python -m build 31 | - name: Publish package distributions to PyPI 32 | uses: pypa/gh-action-pypi-publish@v1.12.4 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | /lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | tests/openwisp2/media/ 50 | tests/openwisp2/firmware/ 51 | !tests/openwisp2/firmware/fake-img*.bin 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # editors 60 | *.komodoproject 61 | .vscode 62 | 63 | # other 64 | *.DS_Store* 65 | *~ 66 | ._* 67 | local_settings.py 68 | *.db 69 | *.tar.gz 70 | Pipfile 71 | 72 | # celery 73 | tests/celerybeat* 74 | 75 | # virtual env 76 | /venv/ 77 | 78 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | flat_json_widget/static/flat-json-widget/js/lib/*.js 2 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 0.4.0 [unreleased] 5 | -------------------------- 6 | 7 | WIP. 8 | 9 | Version 0.3.1 [2024-11-19] 10 | -------------------------- 11 | 12 | - [fix:security] Upgraded underscore.js to v1.13.7 to fix CVE-2021-23358 13 | 14 | Version 0.3.0 [2024-08-23] 15 | -------------------------- 16 | 17 | Changes 18 | ~~~~~~~ 19 | 20 | **Dependencies:** 21 | 22 | - Added support for Python ``3.10``. 23 | - Dropped support for Python ``3.7``. 24 | - Added support for Django ``4.2``. 25 | - Dropped support for Django ``3.1.x`` and ``4.0.x``. 26 | 27 | Version 0.2.0 [2022-03-19] 28 | -------------------------- 29 | 30 | Changes 31 | ~~~~~~~ 32 | 33 | - Added support for Django 3.2 and Django 4.0 34 | - Added support for python 3.9 35 | 36 | Version 0.1.3 [2021-07-20] 37 | -------------------------- 38 | 39 | - [fix] Added jquery.init.js in the widget media to fix `django.jQuery` JS 40 | error 41 | 42 | Version 0.1.2 [2020-12-05] 43 | -------------------------- 44 | 45 | - [change] Rebuild UI based on CSS display property instead of visibility 46 | This change allows to manipulate the contents of the text area and 47 | trigger the recompilation of the UI also if the field is not visible. 48 | 49 | Version 0.1.1 [2020-09-14] 50 | -------------------------- 51 | 52 | - Made clear raw editing mode is JSON 53 | 54 | Version 0.1.0 [2020-08-17] 55 | -------------------------- 56 | 57 | - first release 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020, Federico Capoano 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of django-loci, openwisp nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | include CHANGES.rst 4 | include requirements.txt 5 | recursive-include flat_json_widget * 6 | recursive-exclude * *.pyc 7 | recursive-exclude * *.swp 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.db 10 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-flat-json-widget 2 | ======================= 3 | 4 | .. image:: https://github.com/openwisp/django-flat-json-widget/workflows/Django%20Flat%20JSON%20Widget%20CI%20Build/badge.svg?branch=master 5 | :target: https://github.com/openwisp/openwisp-controller/actions?query=workflow%3A%22Django%20Flat%20JSON%20Widget%20CI%20Build%22 6 | :alt: CI build status 7 | 8 | .. image:: https://coveralls.io/repos/openwisp/django-flat-json-widget/badge.svg 9 | :target: https://coveralls.io/r/openwisp/django-flat-json-widget 10 | :alt: Test Coverage 11 | 12 | .. image:: https://img.shields.io/librariesio/release/github/openwisp/django-flat-json-widget 13 | :target: https://libraries.io/github/openwisp/django-flat-json-widget#repository_dependencies 14 | :alt: Dependency monitoring 15 | 16 | .. image:: https://img.shields.io/gitter/room/nwjs/nw.js.svg 17 | :target: https://gitter.im/openwisp/general 18 | :alt: Chat 19 | 20 | .. image:: https://badge.fury.io/py/django-flat-json-widget.svg 21 | :target: http://badge.fury.io/py/django-flat-json-widget 22 | :alt: Pypi Version 23 | 24 | .. image:: https://pepy.tech/badge/django-flat-json-widget 25 | :target: https://pepy.tech/project/django-flat-json-widget 26 | :alt: Downloads 27 | 28 | .. image:: https://img.shields.io/badge/code%20style-black-000000.svg 29 | :target: https://pypi.org/project/black/ 30 | :alt: code style: black 31 | 32 | .. image:: https://raw.githubusercontent.com/openwisp/django-flat-json-widget/master/docs/django-flat-json-widget-demo.gif 33 | :target: https://github.com/openwisp/django-flat-json-widget/tree/master/docs/django-flat-json-widget-demo.gif 34 | :alt: Django Flat JSON (key/value) Widget 35 | 36 | ---- 37 | 38 | If you ever needed to store a flexible dictionary of keys and values in 39 | your django models, you may have felt the need of giving your users a 40 | widget to easily manipulate the data by adding or removing rows, instead 41 | of having to edit the raw JSON. 42 | 43 | This package solves exactly that problem: **it offers a widget to 44 | manipulate a flat JSON object made of simple keys and values**. 45 | 46 | Compatibility 47 | ------------- 48 | 49 | Tested on python >= 3.9 and Django 4.2, 5.1 and 5.2. 50 | 51 | It should work also on previous versions of Django. 52 | 53 | Install stable version from pypi 54 | -------------------------------- 55 | 56 | Install from pypi: 57 | 58 | .. code-block:: shell 59 | 60 | pip install django-flat-json-widget 61 | 62 | Usage 63 | ----- 64 | 65 | Add ``flat_json_widget`` to ``INSTALLED_APPS``: 66 | 67 | .. code-block:: python 68 | 69 | INSTALLED_APPS = [ 70 | # other apps... 71 | "flat_json_widget", 72 | ] 73 | 74 | Then load the widget where you need it, for example, here's how to use it 75 | in the django admin site: 76 | 77 | .. code-block:: python 78 | 79 | from django.contrib import admin 80 | from django import forms 81 | from .models import JsonDocument 82 | 83 | from flat_json_widget.widgets import FlatJsonWidget 84 | 85 | 86 | class JsonDocumentForm(forms.ModelForm): 87 | class Meta: 88 | widgets = {"content": FlatJsonWidget} 89 | 90 | 91 | @admin.register(JsonDocument) 92 | class JsonDocumentAdmin(admin.ModelAdmin): 93 | list_display = ["name"] 94 | form = JsonDocumentForm 95 | 96 | Installing for development 97 | -------------------------- 98 | 99 | Install your forked repo: 100 | 101 | .. code-block:: shell 102 | 103 | git clone git://github.com//django-flat-json-widget 104 | cd django-flat-json-widget/ 105 | python setup.py develop 106 | 107 | Install development dependencies: 108 | 109 | .. code-block:: shell 110 | 111 | pip install -e .[test] 112 | npm install -g jslint jshint stylelint 113 | 114 | Create database: 115 | 116 | .. code-block:: shell 117 | 118 | cd tests/ 119 | ./manage.py migrate 120 | ./manage.py createsuperuser 121 | 122 | Launch development server: 123 | 124 | .. code-block:: shell 125 | 126 | ./manage.py runserver 0.0.0.0:8000 127 | 128 | You can access the admin interface at http://127.0.0.1:8000/admin/. 129 | 130 | Run tests with: 131 | 132 | .. code-block:: shell 133 | 134 | ./runtests.py 135 | 136 | Run quality assurance tests with: 137 | 138 | .. code-block:: shell 139 | 140 | ./run-qa-checks 141 | 142 | Contributing 143 | ------------ 144 | 145 | Please refer to the `OpenWISP contributing guidelines 146 | `_. 147 | 148 | Changelog 149 | --------- 150 | 151 | See `CHANGES 152 | `_. 153 | 154 | License 155 | ------- 156 | 157 | See `LICENSE 158 | `_. 159 | 160 | Support 161 | ------- 162 | 163 | See `OpenWISP Support Channels `_. 164 | -------------------------------------------------------------------------------- /docs/django-flat-json-widget-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-flat-json-widget/2c929ddd5fc96148f139c3d804dd3e088664b5ef/docs/django-flat-json-widget-demo.gif -------------------------------------------------------------------------------- /flat_json_widget/__init__.py: -------------------------------------------------------------------------------- 1 | VERSION = (0, 4, 0, 'alpha') 2 | __version__ = VERSION # alias 3 | 4 | 5 | def get_version(): # pragma: no cover 6 | version = '%s.%s' % (VERSION[0], VERSION[1]) 7 | if VERSION[2]: 8 | version = '%s.%s' % (version, VERSION[2]) 9 | if VERSION[3:] == ('alpha', 0): 10 | version = '%s pre-alpha' % version 11 | else: 12 | if VERSION[3] != 'final': 13 | try: 14 | rev = VERSION[4] 15 | except IndexError: 16 | rev = 0 17 | version = '%s%s%s' % (version, VERSION[3][0:1], rev) 18 | return version 19 | -------------------------------------------------------------------------------- /flat_json_widget/static/flat-json-widget/css/flat-json-widget.css: -------------------------------------------------------------------------------- 1 | .flat-json-textarea textarea { 2 | font-family: monospace; 3 | } 4 | .flat-json-add-row, 5 | .flat-json-toggle-textarea { 6 | text-decoration: none !important; 7 | } 8 | .flat-json-key { 9 | width: 140px !important; 10 | margin-right: 2px; 11 | } 12 | .flat-json-value { 13 | margin-left: 6px; 14 | width: 320px; 15 | } 16 | .flat-json .help { 17 | margin: 0 !important; 18 | padding: 0 !important; 19 | } 20 | 21 | @media screen and (max-width: 767px) { 22 | .flat-json-add-row { 23 | margin-right: 10px; 24 | } 25 | .flat-json-key { 26 | width: 25% !important; 27 | margin-right: 4px !important; 28 | flex: none !important; 29 | } 30 | .flat-json-value { 31 | width: 55% !important; 32 | margin-left: 6px !important; 33 | flex: none !important; 34 | } 35 | .flat-json-rows .field-data { 36 | line-height: 33px; 37 | } 38 | .flat-json-remove-row { 39 | margin-left: 5px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /flat_json_widget/static/flat-json-widget/js/flat-json-widget.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var initJsonKeyValueWidget = function (fieldName, inlinePrefix) { 3 | // ignore inline templates 4 | // if fieldName contains "__prefix__" 5 | if (fieldName.indexOf("__prefix__") > -1) { 6 | return; 7 | } 8 | 9 | var $ = django.jQuery; 10 | 11 | // processing inlines 12 | if (fieldName.indexOf("inline") > -1) { 13 | var inlineClass = $("#id_" + fieldName) 14 | .parents(".inline-related") 15 | .attr("class"); 16 | // if using TabularInlines stop here 17 | // TabularInlines not supported 18 | if (inlineClass.indexOf("tabular") > -1) { 19 | return; 20 | } 21 | } 22 | 23 | // reusable function that retrieves a template even if ID is not correct 24 | // (written to support inlines) 25 | var retrieveTemplate = function (templateName, fieldName) { 26 | var template = $("#" + templateName + "-" + fieldName); 27 | // if found specific template return that 28 | if (template.length) { 29 | return template.html(); 30 | } else { 31 | // get fallback template 32 | var html = $("." + templateName + "-inline").html(); 33 | // replace all occurrences of __prefix__ with fieldName 34 | // and return 35 | html = html.replace(/__prefix__/g, inlinePrefix); 36 | return html; 37 | } 38 | }; 39 | 40 | // reusable function that compiles the UI 41 | var compileUI = function (params) { 42 | var fieldId = "id_" + fieldName, 43 | originalTextarea = $("#" + fieldId), 44 | originalValue = originalTextarea.val(), 45 | originalContainer = originalTextarea.parents(".form-row").eq(0), 46 | errorHtml = originalContainer.find(".errorlist").html(), 47 | jsonData = { "": "" }; 48 | 49 | if (originalValue !== "" && originalValue !== "{}") { 50 | // manage case in which textarea is blank 51 | try { 52 | jsonData = JSON.parse(originalValue); 53 | } catch (e) { 54 | alert("invalid JSON:\n" + e); 55 | return false; 56 | } 57 | } 58 | 59 | var fieldData = { 60 | id: fieldId, 61 | label: originalContainer.find("label").text(), 62 | name: fieldName, 63 | value: originalTextarea.val(), 64 | help: originalContainer.find(".help").text(), 65 | errors: errorHtml, 66 | data: jsonData, 67 | }, 68 | // compile template 69 | uiHtml = retrieveTemplate("flat-json-ui-template", fieldName), 70 | compiledUiHtml = _.template(uiHtml)(fieldData); 71 | 72 | // this is just to DRY up a bit 73 | if (params && params.replaceOriginal === true) { 74 | // remove original textarea to avoid having two textareas with same ID 75 | originalTextarea.remove(); 76 | // inject compiled template and hide original 77 | originalContainer.after(compiledUiHtml).hide(); 78 | } 79 | 80 | return compiledUiHtml; 81 | }; 82 | 83 | // generate UI 84 | compileUI({ replaceOriginal: true }); 85 | 86 | // cache other objects that we'll reuse 87 | var row_html = retrieveTemplate("flat-json-row-template", fieldName), 88 | emptyRow = _.template(row_html)({ key: "", value: "" }), 89 | $json = $("#id_" + fieldName).parents(".flat-json"); 90 | 91 | // reusable function that updates the textarea value 92 | var updateTextarea = function (container) { 93 | // init empty json object 94 | var newValue = {}, 95 | rawTextarea = container.find("textarea"), 96 | rows = container.find(".form-row"); 97 | 98 | // loop over each object and populate json 99 | rows.each(function () { 100 | var inputs = $(this).find("input"), 101 | key = inputs.eq(0).val(), 102 | value = inputs.eq(1).val(); 103 | newValue[key] = value; 104 | }); 105 | 106 | // update textarea value 107 | $(rawTextarea).val(JSON.stringify(newValue, null, 4)); 108 | }; 109 | 110 | // remove row link 111 | $json.delegate("a.flat-json-remove-row", "click", function (e) { 112 | e.preventDefault(); 113 | // cache container jquery object before $(this) gets removed 114 | $(this).parents(".form-row").eq(0).remove(); 115 | updateTextarea($json); 116 | }); 117 | 118 | // add row link 119 | $json.delegate("a.flat-json-add-row", "click", function (e) { 120 | e.preventDefault(); 121 | $json.find(".flat-json-rows").append(emptyRow); 122 | }); 123 | 124 | // toggle textarea link 125 | $json.delegate(".flat-json-toggle-textarea", "click", function (e) { 126 | e.preventDefault(); 127 | 128 | var rawTextarea = $json.find(".flat-json-textarea"), 129 | jsonRows = $json.find(".flat-json-rows"), 130 | addRow = $json.find(".flat-json-add-row"); 131 | 132 | if (rawTextarea.css("display") !== "none") { 133 | var compiledUi = compileUI(); 134 | // in case of JSON error 135 | if (compiledUi === false) { 136 | return; 137 | } 138 | 139 | var $ui = $($.parseHTML(compiledUi)); 140 | 141 | // update rows with only relevant content 142 | jsonRows.html($ui.find(".flat-json-rows").html()); 143 | rawTextarea.hide(); 144 | jsonRows.show(); 145 | addRow.show(); 146 | } else { 147 | rawTextarea.show(); 148 | jsonRows.hide(); 149 | addRow.hide(); 150 | } 151 | }); 152 | 153 | // update textarea whenever a field changes 154 | $json.delegate("input[type=text]", "input propertychange", function () { 155 | updateTextarea($json); 156 | }); 157 | }; 158 | 159 | django.jQuery(function ($) { 160 | // support inlines 161 | // bind only once 162 | if (typeof django.jsonWidgetBoundInlines === "undefined") { 163 | $("form").delegate(".inline-group .add-row a", "click", function () { 164 | var jsonOriginalTextareas = $(this) 165 | .parents(".inline-group") 166 | .eq(0) 167 | .find(".flat-json-original-textarea"); 168 | // if module contains .flat-json-original-textarea 169 | if (jsonOriginalTextareas.length > 0) { 170 | // loop over each inline 171 | $(this) 172 | .parents(".inline-group") 173 | .find(".inline-related") 174 | .each(function (e, i) { 175 | var prefix = i; 176 | // loop each textarea 177 | $(this) 178 | .find(".flat-json-original-textarea") 179 | .each(function () { 180 | // cache field name 181 | var fieldName = $(this).attr("name"); 182 | // ignore templates 183 | // if name attribute contains __prefix__ 184 | if (fieldName.indexOf("prefix") > -1) { 185 | // skip to next 186 | return; 187 | } 188 | initJsonKeyValueWidget(fieldName, prefix); 189 | }); 190 | }); 191 | } 192 | }); 193 | django.jsonWidgetBoundInlines = true; 194 | } 195 | }); 196 | -------------------------------------------------------------------------------- /flat_json_widget/static/flat-json-widget/js/lib/underscore-umd-min.js: -------------------------------------------------------------------------------- 1 | !function(n,r){"object"==typeof exports&&"undefined"!=typeof module?module.exports=r():"function"==typeof define&&define.amd?define("underscore",r):(n="undefined"!=typeof globalThis?globalThis:n||self,function(){var t=n._,e=n._=r();e.noConflict=function(){return n._=t,e}}())}(this,(function(){ 2 | // Underscore.js 1.13.7 3 | // https://underscorejs.org 4 | // (c) 2009-2024 Jeremy Ashkenas, Julian Gonggrijp, and DocumentCloud and Investigative Reporters & Editors 5 | // Underscore may be freely distributed under the MIT license. 6 | var n="1.13.7",r="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||Function("return this")()||{},t=Array.prototype,e=Object.prototype,u="undefined"!=typeof Symbol?Symbol.prototype:null,i=t.push,o=t.slice,a=e.toString,f=e.hasOwnProperty,c="undefined"!=typeof ArrayBuffer,l="undefined"!=typeof DataView,s=Array.isArray,p=Object.keys,v=Object.create,h=c&&ArrayBuffer.isView,y=isNaN,d=isFinite,g=!{toString:null}.propertyIsEnumerable("toString"),b=["valueOf","isPrototypeOf","toString","propertyIsEnumerable","hasOwnProperty","toLocaleString"],m=Math.pow(2,53)-1;function j(n,r){return r=null==r?n.length-1:+r,function(){for(var t=Math.max(arguments.length-r,0),e=Array(t),u=0;u=0&&t<=m}}function J(n){return function(r){return null==r?void 0:r[n]}}var G=J("byteLength"),H=K(G),Q=/\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;var X=c?function(n){return h?h(n)&&!q(n):H(n)&&Q.test(a.call(n))}:C(!1),Y=J("length");function Z(n,r){r=function(n){for(var r={},t=n.length,e=0;e":">",'"':""","'":"'","`":"`"},$n=zn(Ln),Cn=zn(wn(Ln)),Kn=tn.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g},Jn=/(.)^/,Gn={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},Hn=/\\|'|\r|\n|\u2028|\u2029/g;function Qn(n){return"\\"+Gn[n]}var Xn=/^\s*(\w|\$)+\s*$/;var Yn=0;function Zn(n,r,t,e,u){if(!(e instanceof r))return n.apply(t,u);var i=Mn(n.prototype),o=n.apply(i,u);return w(o)?o:i}var nr=j((function(n,r){var t=nr.placeholder,e=function(){for(var u=0,i=r.length,o=Array(i),a=0;a1)er(a,r-1,t,e),u=e.length;else for(var f=0,c=a.length;f0&&(t=r.apply(this,arguments)),n<=1&&(r=null),t}}var cr=nr(fr,2);function lr(n,r,t){r=Pn(r,t);for(var e,u=nn(n),i=0,o=u.length;i0?0:u-1;i>=0&&i0?a=i>=0?i:Math.max(i+f,a):f=i>=0?Math.min(i+1,f):i+f+1;else if(t&&i&&f)return e[i=t(e,u)]===u?i:-1;if(u!=u)return(i=r(o.call(e,a,f),$))>=0?i+a:-1;for(i=n>0?a:f-1;i>=0&&i0?0:o-1;for(u||(e=r[i?i[a]:a],a+=n);a>=0&&a=3;return r(n,Rn(t,u,4),e,i)}}var _r=wr(1),Ar=wr(-1);function xr(n,r,t){var e=[];return r=Pn(r,t),mr(n,(function(n,t,u){r(n,t,u)&&e.push(n)})),e}function Sr(n,r,t){r=Pn(r,t);for(var e=!tr(n)&&nn(n),u=(e||n).length,i=0;i=0}var Er=j((function(n,r,t){var e,u;return D(r)?u=r:(r=Bn(r),e=r.slice(0,-1),r=r[r.length-1]),jr(n,(function(n){var i=u;if(!i){if(e&&e.length&&(n=Nn(n,e)),null==n)return;i=n[r]}return null==i?i:i.apply(n,t)}))}));function Br(n,r){return jr(n,Dn(r))}function Nr(n,r,t){var e,u,i=-1/0,o=-1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ai&&(i=e);else r=Pn(r,t),mr(n,(function(n,t,e){((u=r(n,t,e))>o||u===-1/0&&i===-1/0)&&(i=n,o=u)}));return i}var Ir=/[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;function Tr(n){return n?U(n)?o.call(n):S(n)?n.match(Ir):tr(n)?jr(n,Tn):jn(n):[]}function kr(n,r,t){if(null==r||t)return tr(n)||(n=jn(n)),n[Un(n.length-1)];var e=Tr(n),u=Y(e);r=Math.max(Math.min(r,u),0);for(var i=u-1,o=0;o1&&(e=Rn(e,r[1])),r=an(n)):(e=qr,r=er(r,!1,!1),n=Object(n));for(var u=0,i=r.length;u1&&(t=r[1])):(r=jr(er(r,!1,!1),String),e=function(n,t){return!Mr(r,t)}),Ur(n,e,t)}));function zr(n,r,t){return o.call(n,0,Math.max(0,n.length-(null==r||t?1:r)))}function Lr(n,r,t){return null==n||n.length<1?null==r||t?void 0:[]:null==r||t?n[0]:zr(n,n.length-r)}function $r(n,r,t){return o.call(n,null==r||t?1:r)}var Cr=j((function(n,r){return r=er(r,!0,!0),xr(n,(function(n){return!Mr(r,n)}))})),Kr=j((function(n,r){return Cr(n,r)}));function Jr(n,r,t,e){A(r)||(e=t,t=r,r=!1),null!=t&&(t=Pn(t,e));for(var u=[],i=[],o=0,a=Y(n);or?(e&&(clearTimeout(e),e=null),a=c,o=n.apply(u,i),e||(u=i=null)):e||!1===t.trailing||(e=setTimeout(f,l)),o};return c.cancel=function(){clearTimeout(e),a=0,e=u=i=null},c},debounce:function(n,r,t){var e,u,i,o,a,f=function(){var c=Wn()-u;r>c?e=setTimeout(f,r-c):(e=null,t||(o=n.apply(a,i)),e||(i=a=null))},c=j((function(c){return a=this,i=c,u=Wn(),e||(e=setTimeout(f,r),t&&(o=n.apply(a,i))),o}));return c.cancel=function(){clearTimeout(e),e=i=a=null},c},wrap:function(n,r){return nr(r,n)},negate:ar,compose:function(){var n=arguments,r=n.length-1;return function(){for(var t=r,e=n[r].apply(this,arguments);t--;)e=n[t].call(this,e);return e}},after:function(n,r){return function(){if(--n<1)return r.apply(this,arguments)}},before:fr,once:cr,findKey:lr,findIndex:pr,findLastIndex:vr,sortedIndex:hr,indexOf:dr,lastIndexOf:gr,find:br,detect:br,findWhere:function(n,r){return br(n,kn(r))},each:mr,forEach:mr,map:jr,collect:jr,reduce:_r,foldl:_r,inject:_r,reduceRight:Ar,foldr:Ar,filter:xr,select:xr,reject:function(n,r,t){return xr(n,ar(Pn(r)),t)},every:Sr,all:Sr,some:Or,any:Or,contains:Mr,includes:Mr,include:Mr,invoke:Er,pluck:Br,where:function(n,r){return xr(n,kn(r))},max:Nr,min:function(n,r,t){var e,u,i=1/0,o=1/0;if(null==r||"number"==typeof r&&"object"!=typeof n[0]&&null!=n)for(var a=0,f=(n=tr(n)?n:jn(n)).length;ae||void 0===t)return 1;if(t 3 |
4 |
5 | 6 | : 7 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | 18 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /flat_json_widget/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from flat_json_widget.widgets import FlatJsonWidget 4 | 5 | 6 | class TestFlatJsonWidget(TestCase): 7 | def test_render(self): 8 | widget = FlatJsonWidget() 9 | html = widget.render(name='content', value=None) 10 | self.assertIn('flat-json-original-textarea', html) 11 | self.assertIn('flat-json-textarea', html) 12 | self.assertIn('icon-addlink.svg', html) 13 | self.assertIn('icon-changelink.svg', html) 14 | 15 | def test_media(self): 16 | widget = FlatJsonWidget() 17 | html = widget.media.render() 18 | expected_list = [ 19 | '/static/flat-json-widget/css/flat-json-widget.css', 20 | '/static/flat-json-widget/js/lib/underscore-umd-min.js', 21 | '/static/flat-json-widget/js/flat-json-widget.js', 22 | ] 23 | for expected in expected_list: 24 | self.assertIn(expected, html) 25 | -------------------------------------------------------------------------------- /flat_json_widget/widgets.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.admin.widgets import AdminTextareaWidget 3 | from django.template.loader import get_template 4 | from django.utils.safestring import mark_safe 5 | 6 | 7 | class FlatJsonWidget(AdminTextareaWidget): 8 | """Flat JSON Key/Value widget""" 9 | 10 | @property 11 | def media(self): 12 | internal_js = ['lib/underscore-umd-min.js', 'flat-json-widget.js'] 13 | js = ['admin/js/jquery.init.js'] + [ 14 | f'flat-json-widget/js/{path}' for path in internal_js 15 | ] 16 | css = {'all': ('flat-json-widget/css/flat-json-widget.css',)} 17 | return forms.Media(js=js, css=css) 18 | 19 | def render(self, name, value, attrs=None, renderer=None): 20 | attrs = attrs or {} 21 | # it's called "original" because it will be replaced by a copy 22 | attrs['class'] = 'flat-json-original-textarea' 23 | html = super().render(name, value, attrs) 24 | template = get_template('flat_json_widget/flat_json_widget.html') 25 | html += template.render({'field_name': name}) 26 | return mark_safe(html) 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.coverage.run] 2 | source = ["flat_json_widget"] 3 | omit = [ 4 | "flat_json_widget/__init__.py", 5 | "*/tests/*", 6 | "*/migrations/*", 7 | ] 8 | 9 | [tool.isort] 10 | known_third_party = ["django"] 11 | default_section = "THIRDPARTY" 12 | line_length = 88 13 | multi_line_output = 3 14 | use_parentheses = true 15 | include_trailing_comma = true 16 | force_grid_wrap = 0 17 | -------------------------------------------------------------------------------- /run-qa-checks: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | openwisp-qa-check --skip-checkmigrations \ 5 | --jslinter \ 6 | --csslinter 7 | -------------------------------------------------------------------------------- /runtests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert(0, 'tests') 7 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') 8 | 9 | if __name__ == '__main__': 10 | from django.core.management import execute_from_command_line 11 | 12 | args = sys.argv 13 | args.insert(1, 'test') 14 | args.insert(2, 'flat_json_widget') 15 | execute_from_command_line(args) 16 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [isort] 5 | known_third_party = django 6 | default_section = THIRDPARTY 7 | skip = migrations 8 | line_length=88 9 | multi_line_output=3 10 | use_parentheses=True 11 | include_trailing_comma=True 12 | force_grid_wrap=0 13 | 14 | [flake8] 15 | ignore = W605, W503, W504 16 | max-line-length = 88 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import find_packages, setup 3 | 4 | from flat_json_widget import get_version 5 | 6 | setup( 7 | name='django-flat-json-widget', 8 | version=get_version(), 9 | license='BSD-3-Clause', 10 | author='OpenWISP', 11 | author_email='support@openwisp.io', 12 | description='Django Flat JSON Key/Value Widget', 13 | long_description=open('README.rst').read(), 14 | url='https://github.com/openwisp/django-flat-json-widget', 15 | download_url='https://github.com/openwisp/django-flat-json-widget/releases', 16 | platforms=['Platform Independent'], 17 | keywords=['django', 'json', 'key-value', 'widget'], 18 | packages=find_packages(exclude=['tests*', 'docs*']), 19 | include_package_data=True, 20 | zip_safe=False, 21 | python_requires='>=3.9', 22 | install_requires=[ 23 | 'django>=4.2,<5.3', 24 | ], 25 | extras_require={ 26 | 'test': [ 27 | ( 28 | 'openwisp-utils[qa]' 29 | ' @ https://github.com/openwisp/openwisp-utils/tarball/1.2' 30 | ), 31 | 'django-extensions>=3.2,<4.2', 32 | ] 33 | }, 34 | classifiers=[ 35 | 'Development Status :: 5 - Production/Stable ', 36 | 'Environment :: Web Environment', 37 | 'Topic :: Internet :: WWW/HTTP', 38 | 'Intended Audience :: Developers', 39 | 'License :: OSI Approved :: BSD License', 40 | 'Operating System :: OS Independent', 41 | 'Framework :: Django', 42 | 'Framework :: Django :: 4.2', 43 | 'Framework :: Django :: 5.1', 44 | 'Framework :: Django :: 5.2', 45 | 'Programming Language :: Python :: 3', 46 | 'Programming Language :: Python :: 3.9', 47 | 'Programming Language :: Python :: 3.10', 48 | 'Programming Language :: Python :: 3.11', 49 | 'Programming Language :: Python :: 3.12', 50 | 'Programming Language :: Python :: 3.13', 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 5 | DEBUG = True 6 | TESTING = sys.argv[1:2] == ['test'] 7 | SHELL = 'shell' in sys.argv or 'shell_plus' in sys.argv 8 | 9 | ALLOWED_HOSTS = ['*'] 10 | 11 | DATABASES = { 12 | 'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'flat-json-widget.db'} 13 | } 14 | 15 | SECRET_KEY = 'fn)t*+$)ugeyip6-#txyy$5wf2ervc0d2n#h)qb)y5@ly$t*@w' 16 | 17 | INSTALLED_APPS = [ 18 | 'django.contrib.auth', 19 | 'django.contrib.contenttypes', 20 | 'django.contrib.sessions', 21 | 'django.contrib.messages', 22 | 'django.contrib.staticfiles', 23 | 'django_extensions', 24 | 'django.contrib.admin', 25 | 'flat_json_widget', 26 | 'test_app' 27 | # 'debug_toolbar', 28 | ] 29 | 30 | STATICFILES_FINDERS = [ 31 | 'django.contrib.staticfiles.finders.FileSystemFinder', 32 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 33 | ] 34 | 35 | MIDDLEWARE = [ 36 | 'django.middleware.security.SecurityMiddleware', 37 | 'django.contrib.sessions.middleware.SessionMiddleware', 38 | 'django.middleware.common.CommonMiddleware', 39 | 'django.middleware.csrf.CsrfViewMiddleware', 40 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 41 | 'django.contrib.messages.middleware.MessageMiddleware', 42 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 43 | ] 44 | 45 | if 'debug_toolbar' in INSTALLED_APPS: 46 | MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware') 47 | INTERNAL_IPS = ['127.0.0.1'] 48 | 49 | ROOT_URLCONF = 'urls' 50 | 51 | TIME_ZONE = 'Europe/Rome' 52 | LANGUAGE_CODE = 'en-gb' 53 | USE_TZ = True 54 | USE_I18N = False 55 | USE_L10N = False 56 | STATIC_URL = '/static/' 57 | MEDIA_URL = '/media/' 58 | MEDIA_ROOT = f'{os.path.dirname(BASE_DIR)}/media/' 59 | 60 | TEMPLATES = [ 61 | { 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 63 | 'OPTIONS': { 64 | 'loaders': [ 65 | 'django.template.loaders.filesystem.Loader', 66 | 'django.template.loaders.app_directories.Loader', 67 | 'openwisp_utils.loaders.DependencyLoader', 68 | ], 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | } 77 | ] 78 | 79 | LOGIN_REDIRECT_URL = 'admin:index' 80 | 81 | # during development only 82 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 83 | 84 | LOGGING = { 85 | 'version': 1, 86 | 'filters': {'require_debug_true': {'()': 'django.utils.log.RequireDebugTrue'}}, 87 | 'handlers': { 88 | 'console': { 89 | 'level': 'DEBUG', 90 | 'filters': ['require_debug_true'], 91 | 'class': 'logging.StreamHandler', 92 | } 93 | }, 94 | } 95 | 96 | if not TESTING and SHELL: 97 | LOGGING.update( 98 | { 99 | 'loggers': { 100 | '': { 101 | # this sets root level logger to log debug and higher level 102 | # logs to console. All other loggers inherit settings from 103 | # root level logger. 104 | 'handlers': ['console'], 105 | 'level': 'DEBUG', 106 | 'propagate': False, 107 | }, 108 | 'django.db': { 109 | 'level': 'DEBUG', 110 | 'handlers': ['console'], 111 | 'propagate': False, 112 | }, 113 | } 114 | } 115 | ) 116 | 117 | TEST_RUNNER = 'openwisp_utils.tests.TimeLoggingTestRunner' 118 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 119 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-flat-json-widget/2c929ddd5fc96148f139c3d804dd3e088664b5ef/tests/test_app/__init__.py -------------------------------------------------------------------------------- /tests/test_app/admin.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib import admin 3 | 4 | from flat_json_widget.widgets import FlatJsonWidget 5 | 6 | from .models import JsonDocument 7 | 8 | 9 | class JsonDocumentForm(forms.ModelForm): 10 | class Meta: 11 | widgets = {'content': FlatJsonWidget} 12 | 13 | 14 | @admin.register(JsonDocument) 15 | class JsonDocumentAdmin(admin.ModelAdmin): 16 | list_display = ['name'] 17 | form = JsonDocumentForm 18 | -------------------------------------------------------------------------------- /tests/test_app/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 3.1 on 2020-08-17 18:09 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | initial = True 8 | 9 | dependencies = [] 10 | 11 | operations = [ 12 | migrations.CreateModel( 13 | name='JsonDocument', 14 | fields=[ 15 | ( 16 | 'id', 17 | models.AutoField( 18 | auto_created=True, 19 | primary_key=True, 20 | serialize=False, 21 | verbose_name='ID', 22 | ), 23 | ), 24 | ('name', models.CharField(max_length=64, unique=True)), 25 | ('content', models.JSONField()), 26 | ], 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /tests/test_app/migrations/0002_alter_jsondocument_id.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.7 on 2025-04-01 17:18 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | dependencies = [ 8 | ("test_app", "0001_initial"), 9 | ] 10 | 11 | operations = [ 12 | migrations.AlterField( 13 | model_name="jsondocument", 14 | name="id", 15 | field=models.BigAutoField( 16 | auto_created=True, primary_key=True, serialize=False, verbose_name="ID" 17 | ), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /tests/test_app/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openwisp/django-flat-json-widget/2c929ddd5fc96148f139c3d804dd3e088664b5ef/tests/test_app/migrations/__init__.py -------------------------------------------------------------------------------- /tests/test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class JsonDocument(models.Model): 5 | name = models.CharField(max_length=64, unique=True) 6 | content = models.JSONField() 7 | -------------------------------------------------------------------------------- /tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls.static import static 3 | from django.contrib import admin 4 | from django.contrib.staticfiles.urls import staticfiles_urlpatterns 5 | from django.urls import include, path 6 | 7 | urlpatterns = [ 8 | path('admin/', admin.site.urls), 9 | ] 10 | 11 | urlpatterns += staticfiles_urlpatterns() 12 | urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 13 | 14 | if settings.DEBUG and 'debug_toolbar' in settings.INSTALLED_APPS: 15 | import debug_toolbar 16 | 17 | urlpatterns += [path('^__debug__/', include(debug_toolbar.urls))] 18 | --------------------------------------------------------------------------------