Source code for django_sorcery.forms
"""Helper functions for creating Form classes from SQLAlchemy models."""
from collections import OrderedDict
from contextlib import suppress
from itertools import chain
from django.core.exceptions import (
NON_FIELD_ERRORS,
ImproperlyConfigured,
ValidationError,
)
from django.forms import ALL_FIELDS
from django.forms.forms import (
BaseForm as DjangoBaseForm,
DeclarativeFieldsMetaclass,
)
from django.forms.models import (
BaseModelForm as DjangoBaseModelForm,
ModelFormOptions,
)
from django.forms.utils import ErrorList
from .db import meta
def _get_default_kwargs(
info,
session,
fields=None,
exclude=None,
widgets=None,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
field_classes=None,
):
kwargs = {}
if widgets and info.name in widgets:
kwargs["widget"] = widgets[info.name]
if labels and info.name in labels:
kwargs["label"] = labels[info.name]
if help_texts and info.name in help_texts:
kwargs["help_text"] = help_texts[info.name]
if error_messages and info.name in error_messages:
kwargs["error_messages"] = error_messages[info.name]
if field_classes and info.name in field_classes:
kwargs["form_class"] = field_classes[info.name]
if localized_fields == ALL_FIELDS:
kwargs["localize"] = True
if localized_fields and info.name in localized_fields:
kwargs["localize"] = True
if session is None:
with suppress(AttributeError):
session = info.related_model.query.session
if isinstance(info, meta.relation_info):
kwargs["session"] = session
return kwargs
[docs]def fields_for_model(
model,
session,
fields=None,
exclude=None,
widgets=None,
formfield_callback=None,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
field_classes=None,
apply_limit_choices_to=True,
**kwargs,
):
"""Returns a dictionary containing form fields for a given model."""
field_list = []
info = meta.model_info(model)
for name, attr in chain(info.properties.items(), info.relationships.items()):
if name.startswith("_"):
continue
if fields and name not in fields:
continue
if exclude and name in exclude:
continue
kwargs = _get_default_kwargs(
attr,
session,
fields=fields,
exclude=exclude,
widgets=widgets,
localized_fields=localized_fields,
labels=labels,
help_texts=help_texts,
error_messages=error_messages,
field_classes=field_classes,
)
if formfield_callback is None:
formfield = attr.formfield(**kwargs)
elif not callable(formfield_callback):
raise TypeError("formfield_callback must be a function or callable")
else:
formfield = formfield_callback(attr, **kwargs)
if formfield is not None:
if apply_limit_choices_to:
apply_limit_choices_to_form_field(formfield)
field_list.append((name, formfield))
return OrderedDict(field_list)
[docs]def apply_limit_choices_to_form_field(formfield):
"""Apply limit_choices_to to the formfield's query if needed."""
if hasattr(formfield, "queryset") and hasattr(formfield, "get_limit_choices_to"):
limit_choices_to = formfield.get_limit_choices_to()
if limit_choices_to is not None:
formfield.queryset = formfield.queryset.filter(*limit_choices_to)
[docs]def model_to_dict(instance, fields=None, exclude=None):
"""Return a dict containing the data in ``instance`` suitable for passing
as a Form's ``initial`` keyword argument.
``fields`` is an optional list of field names. If provided, return only the
named.
``exclude`` is an optional list of field names. If provided, exclude the
named from the returned dict, even if they are listed in the ``fields``
argument.
"""
info = meta.model_info(type(instance))
fields = set(
fields or list(info.properties.keys()) + list(info.primary_keys.keys()) + list(info.relationships.keys())
)
exclude = set(exclude or [])
data = {}
for name in info.properties:
if name.startswith("_"):
continue
if name not in fields:
continue
if name in exclude:
continue
data[name] = getattr(instance, name)
for name, rel in info.relationships.items():
related_info = meta.model_info(rel.related_model)
if name.startswith("_"):
continue
if name not in fields:
continue
if name in exclude:
continue
if rel.uselist:
for obj in getattr(instance, name):
pks = related_info.primary_keys_from_instance(obj)
if pks:
data.setdefault(name, []).append(pks)
else:
obj = getattr(instance, name)
pks = related_info.primary_keys_from_instance(obj)
if pks:
data[name] = pks
return data
[docs]class SQLAModelFormOptions(ModelFormOptions):
"""Model form options for sqlalchemy."""
def __init__(self, options=None):
super().__init__(options=options)
self.session = getattr(options, "session", None)
[docs]class ModelFormMetaclass(DeclarativeFieldsMetaclass):
"""ModelForm metaclass for sqlalchemy models."""
def __new__(mcs, name, bases, attrs):
cls = super().__new__(mcs, name, bases, attrs)
base_formfield_callback = next(
(
base.Meta.formfield_callback
for base in bases
if hasattr(base, "Meta") and hasattr(base.Meta, "formfield_callback")
),
None,
)
formfield_callback = attrs.pop("formfield_callback", base_formfield_callback)
if bases == (BaseModelForm,):
return cls
opts = cls._meta = SQLAModelFormOptions(getattr(cls, "Meta", None))
for opt in ["fields", "exclude", "localized_fields"]:
value = getattr(opts, opt)
if isinstance(value, str) and value != ALL_FIELDS:
raise TypeError(
"%(model)s.Meta.%(opt)s cannot be a string. Did you mean to type: ('%(value)s',)?"
% {"model": cls.__name__, "opt": opt, "value": value}
)
if opts.model:
if opts.fields is None and opts.exclude is None:
raise ImproperlyConfigured(
"Creating a ModelForm without either the 'fields' attribute or the 'exclude' "
f"attribute is prohibited; form {name} needs updating."
)
if opts.fields == ALL_FIELDS:
opts.fields = None
cls.base_fields = fields_for_model(
opts.model,
opts.session,
error_messages=opts.error_messages,
exclude=opts.exclude,
field_classes=opts.field_classes,
fields=opts.fields,
help_texts=opts.help_texts,
labels=opts.labels,
localized_fields=opts.localized_fields,
widgets=opts.widgets,
formfield_callback=formfield_callback,
apply_limit_choices_to=False,
)
cls.base_fields.update(cls.declared_fields)
else:
cls.base_fields = cls.declared_fields
return cls
[docs]class BaseModelForm(DjangoBaseModelForm):
"""Base ModelForm for sqlalchemy models."""
def __init__(
self,
data=None,
files=None,
auto_id="id_%s",
prefix=None,
initial=None,
error_class=ErrorList,
label_suffix=None,
empty_permitted=False,
instance=None,
use_required_attribute=None,
renderer=None,
session=None,
):
opts = self._meta
opts.session = opts.session or session
if opts.model is None:
raise ValueError("ModelForm has no model class specified.")
if opts.session is None:
raise ValueError("ModelForm has no session specified.")
self.instance = opts.model() if instance is None else instance
object_data = self.model_to_dict()
object_data.update(initial or {})
self._validate_unique = False
DjangoBaseForm.__init__(
self,
data=data,
files=files,
auto_id=auto_id,
prefix=prefix,
initial=object_data,
error_class=error_class,
label_suffix=label_suffix,
empty_permitted=empty_permitted,
use_required_attribute=use_required_attribute,
renderer=renderer,
)
for field in self.fields.values():
apply_limit_choices_to_form_field(field)
[docs] def model_to_dict(self):
"""Returns a dict containing the data in ``instance`` suitable for
passing as forms ``initial`` keyword argument."""
opts = self._meta
return model_to_dict(self.instance, opts.fields, opts.exclude)
def _update_errors(self, errors):
custom_errors = self._meta.error_messages or {}
error_dict = getattr(errors, "error_dict", None) or {NON_FIELD_ERRORS: errors}
for field, messages in error_dict.items():
error_messages = {}
if field == NON_FIELD_ERRORS and NON_FIELD_ERRORS in custom_errors:
error_messages = custom_errors[NON_FIELD_ERRORS]
elif field in self.fields:
error_messages = self.fields[field].error_messages
for message in messages:
if isinstance(message, ValidationError) and message.code in error_messages:
message.message = error_messages[message.code]
self.add_error(None, errors)
[docs] def is_valid(self, rollback=True):
"""Return True if the form has no errors, or False otherwise.
Will also rollback the session transaction.
"""
is_valid = super().is_valid()
if not is_valid and rollback:
self._meta.session.rollback()
return is_valid
[docs] def save(self, flush=True, **kwargs):
"""Makes form's self.instance model persistent and flushes the
session."""
opts = self._meta
if self.errors:
raise ValueError(
f"The {self.instance.__class__.__name__} could not be saved because the data didn't validate."
)
if self.instance not in opts.session:
opts.session.add(self.instance)
if flush:
opts.session.flush()
return self.instance
def _post_clean(self):
"""Hook for performing additional cleaning after form cleaning is
complete.
Used for model validation in model forms.
"""
try:
self.instance = self.save_instance()
except ValidationError as e:
self._update_errors(e)
try:
getattr(self.instance, "full_clean", bool)()
except ValidationError as e:
self._update_errors(e)
[docs] def save_instance(self, instance=None, cleaned_data=None):
"""Updates form's instance with cleaned data."""
instance = instance or self.instance
cleaned_data = cleaned_data or self.cleaned_data
for name, field in self.fields.items():
if name in cleaned_data:
self.update_attribute(self.instance, name, field, cleaned_data[name])
return instance
[docs] def update_attribute(self, instance, name, field, value):
"""Provides hooks for updating form instance's attribute for a field
with value."""
field_setter = getattr(self, f"set_{name}", None)
if field_setter:
field_setter(instance, name, field, value)
else:
setattr(instance, name, value)
[docs]class ModelForm(BaseModelForm, metaclass=ModelFormMetaclass):
"""ModelForm base class for sqlalchemy models."""
[docs]def modelform_factory(model, form=ModelForm, formfield_callback=None, **kwargs):
"""Return a ModelForm class containing form fields for the given model."""
defaults = [
"fields",
"exclude",
"widgets",
"localized_fields",
"labels",
"help_texts",
"error_messages",
"field_classes",
"session",
]
attrs = {"model": model}
for key in defaults:
value = kwargs.get(key)
if value is not None:
attrs[key] = value
bases = (form.Meta,) if hasattr(form, "Meta") else ()
meta_ = type("Meta", bases, attrs)
if formfield_callback:
meta_.formfield_callback = staticmethod(formfield_callback)
class_name = f"{model.__name__}Form"
if getattr(meta_, "fields", None) is None and getattr(meta_, "exclude", None) is None:
raise ImproperlyConfigured(
"Calling modelform_factory without defining 'fields' or 'exclude' explicitly is prohibited."
)
return type(form)(str(class_name), (form,), {"Meta": meta_, "formfield_callback": formfield_callback})