From bd08ca6ca8bc23003274e02e17bc9eeaa50e54be Mon Sep 17 00:00:00 2001 From: Floris272 Date: Tue, 7 Jan 2025 13:51:39 +0100 Subject: [PATCH 1/6] Add jsonschema model --- README.rst | 22 +++++++-- .../locale/nl/LC_MESSAGES/django.po | 43 ++++++++++++++++++ django_json_schema/migrations/0001_initial.py | 45 +++++++++++++++++++ django_json_schema/migrations/__init__.py | 0 django_json_schema/models.py | 26 +++++++++++ docs/index.rst | 8 ++-- docs/quickstart.rst | 14 +++++- manage.py | 10 +++++ pyproject.toml | 3 +- tests/test_json_schema.py | 33 ++++++++++++++ 10 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 django_json_schema/locale/nl/LC_MESSAGES/django.po create mode 100644 django_json_schema/migrations/0001_initial.py create mode 100644 django_json_schema/migrations/__init__.py create mode 100644 django_json_schema/models.py create mode 100755 manage.py create mode 100644 tests/test_json_schema.py diff --git a/README.rst b/README.rst index aa27220..a2a94ec 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Welcome to django-json-schema's documentation! |python-versions| |django-versions| |pypi-version| - +A reusable Django app to store json schemas. .. contents:: @@ -21,8 +21,10 @@ Welcome to django-json-schema's documentation! Features ======== -* ... -* ... +* JsonSchemaModel consisting of + - name CharField + - schema JsonField + - validate(json) method to validate JSON against the schema. Installation ============ @@ -32,6 +34,7 @@ Requirements * Python 3.10 or above * Django 4.2 or newer +* A database supporting django.db.models.JSONField Install @@ -45,7 +48,18 @@ Install Usage ===== - +.. code-block:: python + from django-json-schema.models import JsonSchema + + class ProductType(models.Model): + schema = models.ForeignKey(JsonSchema, on_delete=models.PROTECT) + + class Product(models.Model): + json = models.JsonField() + type = models.ForeignKey(ProductType, on_delete=models.CASCADE) + + def clean(self): + self.type.schema.validate(self.json) Local development ================= diff --git a/django_json_schema/locale/nl/LC_MESSAGES/django.po b/django_json_schema/locale/nl/LC_MESSAGES/django.po new file mode 100644 index 0000000..ab1a20d --- /dev/null +++ b/django_json_schema/locale/nl/LC_MESSAGES/django.po @@ -0,0 +1,43 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-01-07 05:36-0600\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: models.py:10 +msgid "name" +msgstr "naam" + +#: models.py:10 +msgid "Name of the json schema." +msgstr "Naam van het json schema." + +#: models.py:14 +msgid "schema" +msgstr "" + +#: models.py:14 +msgid "The schema that can be validated against." +msgstr "Het schema waartegen gevalideerd kan worden." + +#: models.py:18 +msgid "Json schema" +msgstr "" + +#: models.py:19 +msgid "Json Schemas" +msgstr "Json Schema's" diff --git a/django_json_schema/migrations/0001_initial.py b/django_json_schema/migrations/0001_initial.py new file mode 100644 index 0000000..9859ce5 --- /dev/null +++ b/django_json_schema/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 4.2.17 on 2025-01-03 16:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="JsonSchema", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="Name of the json schema.", + max_length=200, + verbose_name="name", + ), + ), + ( + "schema", + models.JSONField( + help_text="The schema that can be validated against.", + verbose_name="schema", + ), + ), + ], + options={ + "verbose_name": "Json schema", + "verbose_name_plural": "Json Schemas", + }, + ), + ] diff --git a/django_json_schema/migrations/__init__.py b/django_json_schema/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_json_schema/models.py b/django_json_schema/models.py new file mode 100644 index 0000000..880d384 --- /dev/null +++ b/django_json_schema/models.py @@ -0,0 +1,26 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from jsonschema import validate +from jsonschema.exceptions import ValidationError as JsonSchemaValidationError + + +class JsonSchema(models.Model): + name = models.CharField( + _("name"), help_text=_("Name of the json schema."), max_length=200 + ) + + schema = models.JSONField( + _("schema"), help_text=_("The schema that can be validated against.") + ) + + class Meta: + verbose_name = _("Json schema") + verbose_name_plural = _("Json Schemas") + + def validate(self, json: dict) -> None: + try: + validate(json, self.schema) + except JsonSchemaValidationError as e: + raise ValidationError(e.message) diff --git a/docs/index.rst b/docs/index.rst index 4c29831..d622231 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,13 +10,15 @@ Welcome to django-json-schema's documentation! .. |docs| |python-versions| |django-versions| |pypi-version| - +A reusable Django app to store json schemas. Features ======== -* ... -* ... +* JsonSchemaModel consisting of + - name CharField + - schema JsonField + - validate(json) method to validate JSON against the schema. .. toctree:: :maxdepth: 2 diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 0ac8fdd..4807b48 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -15,4 +15,16 @@ Install from PyPI with pip: Usage ===== - +.. code-block:: python + from django.db import models + from django-json-schema.models import JsonSchema + + class ProductType(models.Model): + schema = models.ForeignKey(JsonSchema, on_delete=models.PROTECT) + + class Product(models.Model): + json = models.JsonField() + type = models.ForeignKey(ProductType, on_delete=models.CASCADE) + + def clean(self): + self.type.schema.validate(self.json) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..08f5eb1 --- /dev/null +++ b/manage.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ["DJANGO_SETTINGS_MODULE"] = "testapp.settings" + + from django.core.management import execute_from_command_line + + execute_from_command_line(sys.argv) diff --git a/pyproject.toml b/pyproject.toml index f85ba1e..abd0600 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ classifiers = [ ] requires-python = ">=3.10" dependencies = [ - "django>=4.2" + "django>=4.2", + "jsonschema>=4.23", ] [project.urls] diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py new file mode 100644 index 0000000..1b9893d --- /dev/null +++ b/tests/test_json_schema.py @@ -0,0 +1,33 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase + +from django_json_schema.models import JsonSchema + + +class TestJsonSchema(TestCase): + def setUp(self): + self.schema = JsonSchema.objects.create( + name="schema", + schema={ + "type": "object", + "properties": { + "price": {"type": "number"}, + "name": {"type": "string"}, + }, + "required": ["price", "name"], + }, + ) + + def test_valid_json(self): + self.schema.validate( + { + "price": 10, + "name": "test", + } + ) + + def test_invalid_json(self): + with self.assertRaisesMessage( + ValidationError, "'price' is a required property" + ): + self.schema.validate({"name": "Eggs"}) From 1d656127c0edc5d67ac4a27885b50a9893e1a5e3 Mon Sep 17 00:00:00 2001 From: Floris272 Date: Tue, 7 Jan 2025 14:45:29 +0100 Subject: [PATCH 2/6] Add JsonSchemaAdmin --- django_json_schema/admin.py | 8 ++++++++ django_json_schema/models.py | 3 +++ 2 files changed, 11 insertions(+) create mode 100644 django_json_schema/admin.py diff --git a/django_json_schema/admin.py b/django_json_schema/admin.py new file mode 100644 index 0000000..c14676d --- /dev/null +++ b/django_json_schema/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import JsonSchema + + +@admin.register(JsonSchema) +class JsonSchemaAdmin(admin.ModelAdmin): + search_fields = ["name"] diff --git a/django_json_schema/models.py b/django_json_schema/models.py index 880d384..dce2ae2 100644 --- a/django_json_schema/models.py +++ b/django_json_schema/models.py @@ -19,6 +19,9 @@ class Meta: verbose_name = _("Json schema") verbose_name_plural = _("Json Schemas") + def __str__(self): + return self.name + def validate(self, json: dict) -> None: try: validate(json, self.schema) From 03fb1cf3323cf82068abacaf3d8b9c1c2f9288f8 Mon Sep 17 00:00:00 2001 From: Floris272 Date: Tue, 7 Jan 2025 15:17:27 +0100 Subject: [PATCH 3/6] fix docs --- README.rst | 1 + docs/quickstart.rst | 1 + 2 files changed, 2 insertions(+) diff --git a/README.rst b/README.rst index a2a94ec..9b9cd12 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,7 @@ Usage ===== .. code-block:: python + from django-json-schema.models import JsonSchema class ProductType(models.Model): diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 4807b48..eadf8f0 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -16,6 +16,7 @@ Usage ===== .. code-block:: python + from django.db import models from django-json-schema.models import JsonSchema From 4b5fb2f5fb0a42cc4f595c58829549c16033907b Mon Sep 17 00:00:00 2001 From: Floris272 Date: Wed, 8 Jan 2025 14:07:51 +0100 Subject: [PATCH 4/6] Add feedback --- README.rst | 4 ++-- docs/index.rst | 2 +- docs/quickstart.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 9b9cd12..6920976 100644 --- a/README.rst +++ b/README.rst @@ -12,7 +12,7 @@ Welcome to django-json-schema's documentation! |python-versions| |django-versions| |pypi-version| -A reusable Django app to store json schemas. +A reusable Django app to store JSON schemas. .. contents:: @@ -50,7 +50,7 @@ Usage .. code-block:: python - from django-json-schema.models import JsonSchema + from django_json_schema.models import JsonSchema class ProductType(models.Model): schema = models.ForeignKey(JsonSchema, on_delete=models.PROTECT) diff --git a/docs/index.rst b/docs/index.rst index d622231..99ec9ae 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -10,7 +10,7 @@ Welcome to django-json-schema's documentation! .. |docs| |python-versions| |django-versions| |pypi-version| -A reusable Django app to store json schemas. +A reusable Django app to store JSON schemas. Features ======== diff --git a/docs/quickstart.rst b/docs/quickstart.rst index eadf8f0..e3190fe 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -18,7 +18,7 @@ Usage .. code-block:: python from django.db import models - from django-json-schema.models import JsonSchema + from django_json_schema.models import JsonSchema class ProductType(models.Model): schema = models.ForeignKey(JsonSchema, on_delete=models.PROTECT) From 68c576694123c0156b7fc7618d6ad8b5a162644b Mon Sep 17 00:00:00 2001 From: Floris272 Date: Thu, 9 Jan 2025 14:58:29 +0100 Subject: [PATCH 5/6] Add IndentedJSONEncoder --- django_json_schema/admin.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/django_json_schema/admin.py b/django_json_schema/admin.py index c14676d..70e8522 100644 --- a/django_json_schema/admin.py +++ b/django_json_schema/admin.py @@ -1,8 +1,24 @@ +import json + from django.contrib import admin +from django import forms from .models import JsonSchema +class IndentedJSONEncoder(json.JSONEncoder): + def __init__(self, *args, indent, sort_keys, **kwargs): + super().__init__(*args, indent=2, **kwargs) + + +class JsonSchemaAdminForm(forms.ModelForm): + schema = forms.JSONField(encoder=IndentedJSONEncoder) + + class Meta: + model = JsonSchema + fields = '__all__' + @admin.register(JsonSchema) class JsonSchemaAdmin(admin.ModelAdmin): + form = JsonSchemaAdminForm search_fields = ["name"] From 34a9eb4abf97ced66b87c7a6d451d5d3997b43fb Mon Sep 17 00:00:00 2001 From: Floris272 Date: Fri, 10 Jan 2025 14:21:36 +0100 Subject: [PATCH 6/6] Add clean method --- django_json_schema/admin.py | 5 +++-- django_json_schema/models.py | 15 ++++++++++++--- tests/test_dummy.py | 2 -- tests/test_json_schema.py | 10 ++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) delete mode 100644 tests/test_dummy.py diff --git a/django_json_schema/admin.py b/django_json_schema/admin.py index 70e8522..b964433 100644 --- a/django_json_schema/admin.py +++ b/django_json_schema/admin.py @@ -1,10 +1,11 @@ import json -from django.contrib import admin from django import forms +from django.contrib import admin from .models import JsonSchema + class IndentedJSONEncoder(json.JSONEncoder): def __init__(self, *args, indent, sort_keys, **kwargs): super().__init__(*args, indent=2, **kwargs) @@ -15,7 +16,7 @@ class JsonSchemaAdminForm(forms.ModelForm): class Meta: model = JsonSchema - fields = '__all__' + fields = "__all__" @admin.register(JsonSchema) diff --git a/django_json_schema/models.py b/django_json_schema/models.py index dce2ae2..29dcb28 100644 --- a/django_json_schema/models.py +++ b/django_json_schema/models.py @@ -2,8 +2,11 @@ from django.db import models from django.utils.translation import gettext_lazy as _ -from jsonschema import validate -from jsonschema.exceptions import ValidationError as JsonSchemaValidationError +from jsonschema import Draft202012Validator, validate +from jsonschema.exceptions import ( + SchemaError, + ValidationError as JsonSchemaValidationError, +) class JsonSchema(models.Model): @@ -22,8 +25,14 @@ class Meta: def __str__(self): return self.name + def clean(self): + try: + Draft202012Validator.check_schema(self.schema) + except SchemaError as e: + raise ValidationError(e.message) + def validate(self, json: dict) -> None: try: - validate(json, self.schema) + validate(json, self.schema, cls=Draft202012Validator) except JsonSchemaValidationError as e: raise ValidationError(e.message) diff --git a/tests/test_dummy.py b/tests/test_dummy.py deleted file mode 100644 index f4f5361..0000000 --- a/tests/test_dummy.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_dummy(): - assert True diff --git a/tests/test_json_schema.py b/tests/test_json_schema.py index 1b9893d..c7d7ce2 100644 --- a/tests/test_json_schema.py +++ b/tests/test_json_schema.py @@ -31,3 +31,13 @@ def test_invalid_json(self): ValidationError, "'price' is a required property" ): self.schema.validate({"name": "Eggs"}) + + def test_clean_with_invalid_schema(self): + self.schema.schema = {"type": []} + with self.assertRaisesMessage( + ValidationError, "[] is not valid under any of the given schemas" + ): + self.schema.clean() + + def test_clean_with_valid_schema(self): + self.schema.clean()