-
-
Notifications
You must be signed in to change notification settings - Fork 36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DataclassJSONField
implementation
#77
base: master
Are you sure you want to change the base?
Changes from all commits
74986c4
d8471d7
58159b5
ac574a2
ecad317
f7f66a0
cbf4141
f36a11d
35472be
b0c7f51
b3434ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# contrib | ||
|
||
Community contributed modules which extend the core `django-jsonform` project. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# `django_jsonform.contrib.dataclasses` | ||
|
||
**Status:** experimental | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
from __future__ import annotations | ||
|
||
import json | ||
from typing import Optional | ||
|
||
from django_jsonform.contrib.dataclasses.typedefs import SerializableValue | ||
from django_jsonform.forms.fields import JSONFormField | ||
|
||
|
||
class DataclassJSONFormField(JSONFormField): | ||
def prepare_value(self, value: SerializableValue) -> Optional[str]: | ||
if value is None: | ||
return None | ||
if isinstance(value, list): | ||
return json.dumps( | ||
[x if isinstance(x, dict) else x.to_dict(validate=False) for x in value] | ||
) | ||
return json.dumps( | ||
value if isinstance(value, dict) else value.to_dict(validate=False) | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
from __future__ import annotations | ||
|
||
import json | ||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type | ||
|
||
from django_jsonform.contrib.dataclasses.forms.fields import DataclassJSONFormField | ||
from django_jsonform.contrib.dataclasses.typedefs import DataclassJsonSchema, SerializableValue | ||
from django_jsonform.contrib.dataclasses.utils import json_schema_array | ||
from django_jsonform.exceptions import JSONSchemaValidationError | ||
from django_jsonform.models.fields import JSONField | ||
|
||
if TYPE_CHECKING: | ||
from django.db import models | ||
from django.db.backends.base.base import BaseDatabaseWrapper | ||
from django.db.models.expressions import Expression | ||
|
||
|
||
class DataclassJSONField(JSONField): | ||
form_class = DataclassJSONFormField | ||
|
||
def __init__( | ||
self, | ||
dataclass_cls: Type[DataclassJsonSchema], | ||
*, | ||
many: bool = False, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This But we can consider making this a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to leave this decision up to you. I'm not familiar with the |
||
**kwargs: Any, | ||
) -> None: | ||
self._dataclass_cls = dataclass_cls | ||
self._many = many | ||
self._json_schema = ( | ||
json_schema_array(dataclass_cls) if many else dataclass_cls.json_schema() | ||
) | ||
|
||
super().__init__(schema=self._json_schema, **kwargs) # type: ignore | ||
|
||
def deconstruct(self) -> Tuple[str, str, List[Any], Dict[str, Any]]: | ||
name, path, args, kwargs = super().deconstruct() | ||
kwargs["dataclass_cls"] = self._dataclass_cls | ||
kwargs["many"] = self._many | ||
return name, path, args, kwargs | ||
|
||
def from_db_value( | ||
self, | ||
value: Optional[str], | ||
expression: Expression, | ||
connection: BaseDatabaseWrapper, | ||
) -> SerializableValue: | ||
if value is None: | ||
return None | ||
data = json.loads(value) | ||
if isinstance(data, list): | ||
return [self._dataclass_cls.from_dict(x, validate=False) for x in data] | ||
return self._dataclass_cls.from_dict(data, validate=False) | ||
|
||
def get_prep_value(self, value: SerializableValue) -> Optional[str]: | ||
if value is None: | ||
return None | ||
if isinstance(value, list): | ||
return json.dumps( | ||
[x if isinstance(x, dict) else x.to_dict(validate=False) for x in value] | ||
) | ||
return json.dumps( | ||
value if isinstance(value, dict) else value.to_dict(validate=False) | ||
) | ||
|
||
def validate(self, value: SerializableValue, model_instance: models.Model) -> None: | ||
if value is None: | ||
if not self.null: | ||
raise JSONSchemaValidationError("Null value in non-nullable field.") # type: ignore | ||
return | ||
values_to_validate = value if isinstance(value, list) else [value] | ||
if len(values_to_validate) == 0: | ||
if not self.blank: | ||
raise JSONSchemaValidationError("Blank value in non-blank field.") # type: ignore | ||
return | ||
errs: List[str] = [] | ||
for i, val in enumerate(values_to_validate): | ||
try: | ||
if isinstance(val, dict): | ||
self._dataclass_cls.from_dict( | ||
val, validate=True, validate_enums=True | ||
) | ||
else: | ||
val.to_dict(validate=True, validate_enums=True) | ||
except Exception as e: | ||
errs.append(f"[{i}] {e}") | ||
if errs: | ||
raise JSONSchemaValidationError(errs) # type: ignore |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
from __future__ import annotations | ||
|
||
from typing import List, Optional, Union | ||
|
||
from dataclasses_jsonschema import JsonSchemaMixin | ||
from dataclasses_jsonschema.type_defs import JsonDict | ||
|
||
DataclassJsonSchema = JsonSchemaMixin | ||
"""TODO: Make this a Protocol or something. Not sure.""" | ||
|
||
|
||
SerializableValue = Optional[ | ||
Union[DataclassJsonSchema, JsonDict, List[Union[DataclassJsonSchema, JsonDict]]] | ||
] |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,24 @@ | ||||||
from __future__ import annotations | ||||||
|
||||||
import dataclasses | ||||||
from typing import Type | ||||||
|
||||||
from dataclasses_jsonschema import JsonSchemaMixin | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we should add this as a dependency to this project. Need to research how to make this Maybe removing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is okay. To make it an optional dependency, please modify the +
+ [options.extras_require]
+ contrib-dataclasses = dataclasses-jsonschema Users who wish to use this feature can install the dependencies by:
|
||||||
from dataclasses_jsonschema.type_defs import JsonDict | ||||||
|
||||||
|
||||||
def json_schema_array(dataclass_cls: Type[JsonSchemaMixin]) -> JsonDict: | ||||||
"""Get JSON schema representing an array of `dataclass_cls`.""" | ||||||
WrapperDataClass = type("WrapperDataClass", (JsonSchemaMixin,), {}) | ||||||
WrapperDataClass.__annotations__["item"] = dataclass_cls | ||||||
WrapperDataClass = dataclasses.dataclass(WrapperDataClass) | ||||||
|
||||||
schema: JsonDict = WrapperDataClass.json_schema() # type: ignore | ||||||
|
||||||
schema["type"] = "array" | ||||||
schema["description"] = f"An array of {dataclass_cls.__name__} objects." | ||||||
del schema["required"] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code example in your comment above gives me
Suggested change
|
||||||
schema["items"] = schema["properties"]["item"] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Line 21 also results in a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm.. This shouldn't be happening. What Python and dataclasses-jsonschema versions are you using? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Python: 3.8 dataclasses-jsonschema: 2.16.0 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm using Will have to try 3.8 later (or make a separate package and drop support for older versions) I noticed you're supporting a lot of older Django versions too. I only tested with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just tested with Python 3.10 and Django 4.1.3. Everything works fine this time. |
||||||
del schema["properties"] | ||||||
|
||||||
return schema |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,8 @@ class DjangoArrayField: | |
|
||
|
||
class JSONField(DjangoJSONField): | ||
form_class = JSONFormField | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so it's overridable |
||
|
||
def __init__(self, *args, **kwargs): | ||
self.schema = kwargs.pop('schema', {}) | ||
self.pre_save_hook = kwargs.pop('pre_save_hook', None) | ||
|
@@ -32,7 +34,7 @@ def __init__(self, *args, **kwargs): | |
|
||
def formfield(self, **kwargs): | ||
return super().formfield(**{ | ||
'form_class': JSONFormField, | ||
'form_class': self.form_class, | ||
'schema': self.schema, | ||
'model_name': self.model.__name__, | ||
'file_handler': self.file_handler, | ||
|
@@ -49,6 +51,8 @@ def pre_save(self, model_instance, add): | |
|
||
|
||
class ArrayField(DjangoArrayField): | ||
form_class = ArrayFormField | ||
|
||
def __init__(self, *args, **kwargs): | ||
if hasattr(DjangoArrayField, 'mock_field'): | ||
raise ImproperlyConfigured('ArrayField requires psycopg2 to be installed.') | ||
|
@@ -57,4 +61,4 @@ def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | ||
|
||
def formfield(self, **kwargs): | ||
return super().formfield(**{'form_class': ArrayFormField, 'nested': self.nested, **kwargs}) | ||
return super().formfield(**{'form_class': self.form_class, 'nested': self.nested, **kwargs}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO