From a19a71907a4b87c1036e39db986e36257191875b Mon Sep 17 00:00:00 2001 From: Savva Surenkov Date: Mon, 1 Apr 2024 00:27:44 +0400 Subject: [PATCH] Add `django-jsonform` widget injection for v2 implementation --- .github/workflows/python-test.yml | 2 +- Makefile | 10 +++++-- django_pydantic_field/v2/fields.py | 13 +++++++-- django_pydantic_field/v2/forms.py | 38 +++++++++++++++++++++++++- pyproject.toml | 4 +-- tests/settings/django_test_settings.py | 5 +++- tests/settings/urls.py | 6 ++++ 7 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 tests/settings/urls.py diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index e47a414..86cecba 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -32,7 +32,7 @@ jobs: run: | python -m pip install -e .[dev,test] - name: Lint package - run: mypy . + run: python -m mypy . pre-commit: runs-on: ubuntu-latest diff --git a/Makefile b/Makefile index 88d7574..739be3a 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,5 @@ + DJANGO_SETTINGS_MODULE ?= "tests.settings.django_test_settings" + .PHONY: install build test lint upload upload-test clean install: @@ -8,7 +10,11 @@ build: python3 -m build migrations: - DJANGO_SETTINGS_MODULE="tests.settings.django_test_settings" python3 -m django makemigrations --noinput + python3 -m django makemigrations --noinput + +runserver: + python3 -m django migrate && \ + python3 -m django runserver test: A= test: @@ -16,7 +22,7 @@ test: lint: A=. lint: - mypy $(A) + python3 -m mypy $(A) upload: python3 -m twine upload dist/* diff --git a/django_pydantic_field/v2/fields.py b/django_pydantic_field/v2/fields.py index 9ca1f0e..c1c70b7 100644 --- a/django_pydantic_field/v2/fields.py +++ b/django_pydantic_field/v2/fields.py @@ -4,6 +4,7 @@ import pydantic import typing_extensions as te +from django.apps import apps from django.core import checks, exceptions from django.core.serializers.json import DjangoJSONEncoder from django.db.models.expressions import BaseExpression, Col, Value @@ -70,7 +71,7 @@ class PydanticSchemaField(JSONField, ty.Generic[types.ST]): def __init__( self, *args, - schema: type[types.ST] | BaseContainer | ty.ForwardRef | str | None = None, + schema: type[types.ST] | te.Annotated[type[types.ST], ...] | BaseContainer | ty.ForwardRef | str | None = None, config: pydantic.ConfigDict | None = None, **kwargs, ): @@ -178,8 +179,16 @@ def get_default(self) -> ty.Any: return default_value def formfield(self, **kwargs): + try: + if apps.is_installed("django_jsonform"): + form_cls = forms.JSONFormSchemaField + else: + form_cls = forms.SchemaField + except AttributeError: + form_cls = forms.SchemaField + field_kwargs = dict( - form_class=forms.SchemaField, + form_class=form_cls, # Trying to resolve the schema before passing it to the formfield, since in Django < 4.0, # formfield is unbound during form validation and is not able to resolve forward refs defined in the model. schema=self.adapter.prepared_schema, diff --git a/django_pydantic_field/v2/forms.py b/django_pydantic_field/v2/forms.py index ec0114c..e161351 100644 --- a/django_pydantic_field/v2/forms.py +++ b/django_pydantic_field/v2/forms.py @@ -3,6 +3,7 @@ import typing as ty import pydantic +import typing_extensions as te from django.core.exceptions import ValidationError from django.forms.fields import InvalidJSONInput, JSONField, JSONString from django.utils.translation import gettext_lazy as _ @@ -15,7 +16,7 @@ class SchemaField(JSONField, ty.Generic[types.ST]): - adapter: types.SchemaAdapter + adapter: types.SchemaAdapter[types.ST] default_error_messages = { "schema_error": _("Schema didn't match for %(title)s."), } @@ -92,3 +93,38 @@ def _try_coerce(self, value): value = self.adapter.validate_json(value) return value + + +try: + # Add the support of django-jsonform widgets, if installed + from django_jsonform.widgets import JSONFormWidget as _JSONFormWidget # type: ignore[import-untyped] +except ImportError: + pass +else: + + class JSONFormSchemaWidget(_JSONFormWidget, ty.Generic[types.ST]): + def __init__( + self, + schema: type[types.ST] | ty.ForwardRef | te.Annotated[type[types.ST], ...] | str, + config: pydantic.ConfigDict | None = None, + allow_null: bool | None = None, + export_kwargs: types.ExportKwargs | None = None, + **kwargs, + ): + if export_kwargs is None: + export_kwargs = {} + adapter = types.SchemaAdapter[types.ST](schema, config, None, None, allow_null, **export_kwargs) + super().__init__(adapter.json_schema(), **kwargs) + + class JSONFormSchemaField(SchemaField[types.ST]): + def __init__( + self, + schema: type[types.ST] | ty.ForwardRef | te.Annotated[type[types.ST], ...] | str, + config: pydantic.ConfigDict | None = None, + allow_null: bool | None = None, + *args, + **kwargs, + ): + export_kwargs = types.SchemaAdapter.extract_export_kwargs(kwargs) + kwargs.setdefault("widget", JSONFormSchemaWidget(schema, config, allow_null, export_kwargs)) + super().__init__(schema, config, allow_null, *args, **kwargs) diff --git a/pyproject.toml b/pyproject.toml index fa7d865..5aede49 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dependencies = [ [project.optional-dependencies] openapi = ["uritemplate", "inflection"] coreapi = ["coreapi"] +jsonform = ["django_jsonform>=2.0,<3"] dev = [ "build", "black", @@ -63,7 +64,7 @@ dev = [ "pytest-django>=4.5,<5", ] test = [ - "django_pydantic_field[openapi,coreapi]", + "django_pydantic_field[openapi,coreapi,jsonform]", "dj-database-url~=2.0", "djangorestframework>=3,<4", "pyyaml", @@ -108,7 +109,6 @@ plugins = [ "mypy_drf_plugin.main" ] exclude = [".env", ".venv", "tests"] -enable_incomplete_feature = ["Unpack"] [tool.django-stubs] django_settings_module = "tests.settings.django_test_settings" diff --git a/tests/settings/django_test_settings.py b/tests/settings/django_test_settings.py index 162999e..17d55e6 100644 --- a/tests/settings/django_test_settings.py +++ b/tests/settings/django_test_settings.py @@ -5,6 +5,7 @@ SITE_ID = 1 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) STATIC_URL = "/static/" +DEBUG = True INSTALLED_APPS = [ "django.contrib.contenttypes", @@ -14,14 +15,15 @@ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.admin", + "django_jsonform", "tests.sample_app", "tests.test_app", ] MIDDLEWARE = [ + "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", ] TEMPLATES = [ { @@ -56,3 +58,4 @@ CURRENT_TEST_DB = "default" REST_FRAMEWORK = {"COMPACT_JSON": True} +ROOT_URLCONF = "tests.settings.urls" diff --git a/tests/settings/urls.py b/tests/settings/urls.py new file mode 100644 index 0000000..d469a3e --- /dev/null +++ b/tests/settings/urls.py @@ -0,0 +1,6 @@ +from django.urls import path +from django.contrib import admin + +urlpatterns = [ + path("admin/", admin.site.urls), +]