From b2865ae374dbea50cd09a03bda0a02f72d99ed63 Mon Sep 17 00:00:00 2001 From: Savva Surenkov Date: Mon, 1 Apr 2024 14:39:24 +0400 Subject: [PATCH] Instead of conditional jsonform widget injection, make it pluggable on the form/admin level definition --- django_pydantic_field/v2/fields.py | 11 +---- django_pydantic_field/v2/forms.py | 60 ++++++++++++++++++-------- tests/settings/django_test_settings.py | 9 +++- tests/test_app/admin.py | 15 ++++++- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/django_pydantic_field/v2/fields.py b/django_pydantic_field/v2/fields.py index c1c70b7..5543ee8 100644 --- a/django_pydantic_field/v2/fields.py +++ b/django_pydantic_field/v2/fields.py @@ -4,7 +4,6 @@ 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 @@ -179,16 +178,8 @@ 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=form_cls, + form_class=forms.SchemaField, # 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 e161351..c0049e1 100644 --- a/django_pydantic_field/v2/forms.py +++ b/django_pydantic_field/v2/forms.py @@ -1,9 +1,9 @@ from __future__ import annotations import typing as ty +import warnings 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 _ @@ -12,7 +12,12 @@ from . import types -__all__ = ("SchemaField",) +if ty.TYPE_CHECKING: + import typing_extensions as te + from django.forms.widgets import Widget + + +__all__ = ("SchemaField", "JSONFormSchemaWidget") class SchemaField(JSONField, ty.Generic[types.ST]): @@ -35,6 +40,11 @@ def __init__( self.config = config self.export_kwargs = types.SchemaAdapter.extract_export_kwargs(kwargs) self.adapter = types.SchemaAdapter(schema, config, None, None, allow_null, **self.export_kwargs) + + widget = kwargs.get("widget") + if widget is not None: + kwargs["widget"] = _prepare_jsonform_widget(widget, self.adapter) + super().__init__(*args, **kwargs) def get_bound_field(self, form: ty.Any, field_name: str): @@ -96,13 +106,40 @@ def _try_coerce(self, 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 + from django.forms.widgets import Textarea + + def _prepare_jsonform_widget(widget, adapter: types.SchemaAdapter[types.ST]) -> Widget | type[Widget]: + return widget + + class JSONFormSchemaWidget(Textarea): + def __init__(self, *args, **kwargs): + warnings.warn( + "The 'django_jsonform' package is not installed. Please install it to use the widget.", + ImportWarning, + ) + super().__init__(*args, **kwargs) + else: - class JSONFormSchemaWidget(_JSONFormWidget, ty.Generic[types.ST]): + def _prepare_jsonform_widget(widget, adapter: types.SchemaAdapter[types.ST]) -> Widget | type[Widget]: # type: ignore[no-redef] + if not isinstance(widget, type): + return widget + + if issubclass(widget, JSONFormSchemaWidget): + widget = widget( + schema=adapter.prepared_schema, + config=adapter.config, + export_kwargs=adapter.export_kwargs, + allow_null=adapter.allow_null, + ) + elif issubclass(widget, _JSONFormWidget): + widget = widget(schema=adapter.json_schema()) # type: ignore[call-arg] + + return widget + + class JSONFormSchemaWidget(_JSONFormWidget, ty.Generic[types.ST]): # type: ignore[no-redef] def __init__( self, schema: type[types.ST] | ty.ForwardRef | te.Annotated[type[types.ST], ...] | str, @@ -115,16 +152,3 @@ def __init__( 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/tests/settings/django_test_settings.py b/tests/settings/django_test_settings.py index 17d55e6..483d83b 100644 --- a/tests/settings/django_test_settings.py +++ b/tests/settings/django_test_settings.py @@ -15,11 +15,18 @@ "django.contrib.messages", "django.contrib.staticfiles", "django.contrib.admin", - "django_jsonform", "tests.sample_app", "tests.test_app", ] +try: + import django_jsonform # type: ignore[import-untyped] +except ImportError: + pass +else: + INSTALLED_APPS.append("django_jsonform") + + MIDDLEWARE = [ "django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", diff --git a/tests/test_app/admin.py b/tests/test_app/admin.py index 07d70af..3c7912e 100644 --- a/tests/test_app/admin.py +++ b/tests/test_app/admin.py @@ -1,5 +1,16 @@ from django.contrib import admin +try: + from django_jsonform.widgets import JSONFormWidget + from django_pydantic_field.v2.fields import PydanticSchemaField + from django_pydantic_field.v2.forms import JSONFormSchemaWidget + + json_formfield_overrides = {PydanticSchemaField: {"widget": JSONFormWidget}} + json_schema_formfield_overrides = {PydanticSchemaField: {"widget": JSONFormSchemaWidget}} +except ImportError: + json_formfield_overrides = {} + json_schema_formfield_overrides = {} + from . import models @@ -10,12 +21,12 @@ class SampleModelAdmin(admin.ModelAdmin): @admin.register(models.SampleForwardRefModel) class SampleForwardRefModelAdmin(admin.ModelAdmin): - pass + formfield_overrides = json_formfield_overrides # type: ignore @admin.register(models.SampleModelWithRoot) class SampleModelWithRootAdmin(admin.ModelAdmin): - pass + formfield_overrides = json_schema_formfield_overrides # type: ignore @admin.register(models.ExampleModel)