class JsonMixin:
"""A mixin for converting Model to a JSON-string and back to a Model."""
def to_dict(self, only_value: bool = False) -> dict[str, Any]:
"""Convert Model instance to a dictionary.
Agrs:
only_value (bool): True if need without field attributes.
"""
metadata = self.__class__.META
DESCRIPTOR_FIELDS = metadata["all_descriptor_fields"]
LANG_CODE = self._LANG_CODE
UTC_TIMEZONE = self._UTC_TIMEZONE
json_dict: dict[str, Any] = {"_lang_": LANG_CODE}
for f_name in DESCRIPTOR_FIELDS:
tmp__core = deepcopy(getattr(self, f"{f_name}__core"))
field_type = tmp__core.field_type
value = tmp__core.value
if value is not None:
if field_type == "IDField":
tmp__core.value = str(value)
elif field_type == "PasswordField":
tmp__core.value = None
elif field_type == "TextField":
tmp__core.value = value.get(LANG_CODE, "- -") if isinstance(value, dict) else value
elif "Date" in field_type:
if "Time" in field_type:
tmp__core.value = format_datetime(
datetime=value,
format="medium",
tzinfo=UTC_TIMEZONE,
locale=LANG_CODE,
)
else:
tmp__core.value = format_date(
date=value,
format="medium",
locale=LANG_CODE,
)
json_dict[f_name] = tmp__core.to_dict() if not only_value else tmp__core.value
return json_dict
def to_json(self, only_value: bool = False) -> str:
"""Convert Model instance to a JSON-string.
Agrs:
only_value (bool): True if need without field attributes.
"""
return orjson.dumps(self.to_dict(only_value)).decode("utf-8")
@classmethod
def from_dict(
cls,
json_dict: dict[str, Any],
) -> Any:
"""Convert JSON-dictionary to a Model instance."""
metadata = cls.META
DESCRIPTOR_FIELDS = metadata["all_descriptor_fields"]
lang = json_dict.get("_lang_")
# If there is no `_lang_` language marker in the JSON-dictionary
lang = json_dict.get("_lang_")
if lang is None:
err_msg = "The JSON-dictionary does not contain the `_lang_` marker."
logger.critical(err_msg)
raise ValueError(err_msg)
# If fields not contain attributes
if not isinstance(json_dict.get("created_at"), dict):
return cls.from_ajax_json(json_dict, lang)
# pyrefly: ignore [bad-argument-count]
instance_model: Any = cls(lang)
DATEPARSER_SETTINGS = instance_model.dateparser_settings
for f_name in DESCRIPTOR_FIELDS:
tmp__core_dict = deepcopy(json_dict[f_name])
field_type = tmp__core_dict["field_type"]
value = tmp__core_dict["value"]
if value is not None:
if field_type == "IDField":
tmp__core_dict["value"] = ObjectId(value)
elif "Date" in field_type:
if "Time" in field_type:
tmp__core_dict["value"] = parse(
value,
settings=DATEPARSER_SETTINGS,
).replace(microsecond=0)
else:
tmp__core_dict["value"] = parse(
value,
settings=DATEPARSER_SETTINGS,
).date()
else:
tmp__core_dict["value"] = value
setattr(instance_model, f_name, tmp__core_dict["value"])
f__core = getattr(instance_model, f"{f_name}__core")
for key, val in tmp__core_dict.items():
f__core.__dict__[key] = val
return instance_model
@classmethod
def from_json(
cls,
json_str: str,
) -> Any:
"""Convert JSON-string of Model to a Model instance."""
json_dict = orjson.loads(json_str)
lang = json_dict.get("_lang_")
# If there is no `_lang_` language marker in the JSON-dictionary
if lang is None:
return cls.from_ajax_json(json_dict, lang)
# If fields contain attributes
return cls.from_dict(json_dict)
@classmethod
def from_ajax_json(cls, json_str_or_dict: dict[str, Any] | str, lang_code: str) -> Any:
"""Convert JSON-string from web request to a Model instance.
If the JSON-string does not contain the field attributes and the `_lang_` language marker.
Hint:
- `lang_code` - This is necessary for multilingual (is_multilingual=True) text fields.
"""
metadata = cls.META
DESCRIPTOR_FIELDS = metadata["all_descriptor_fields"]
json_dict: dict[str, Any] = (
orjson.loads(json_str_or_dict) if isinstance(json_str_or_dict, str) else json_str_or_dict
)
# If fields contain attributes
if isinstance(json_dict.get("created_at"), dict):
err_msg = "Fields should not contain attributes, only values."
logger.critical(err_msg)
raise ValueError(err_msg)
# pyrefly: ignore [bad-argument-count]
instance_model: Any = cls(lang_code)
DATEPARSER_SETTINGS = instance_model.dateparser_settings
for f_name in DESCRIPTOR_FIELDS:
value = json_dict.get(f_name if f_name != "id" else "_id")
if value is None:
continue
f__core = getattr(instance_model, f"{f_name}__core")
field_type = f__core.field_type
if field_type == "IDField":
setattr(instance_model, f_name, ObjectId(value))
elif "Date" in field_type:
if "Time" in field_type:
setattr(
instance_model,
f_name,
parse(
value,
settings=DATEPARSER_SETTINGS,
).replace(microsecond=0),
)
else:
setattr(
instance_model,
f_name,
parse(
value,
settings=DATEPARSER_SETTINGS,
).date(),
)
else:
setattr(instance_model, f_name, value)
return instance_model