# Copyright (c) 2025 Ferrosoft GmbH. All rights reserved.
from collections.abc import Callable
from dataclasses import dataclass
from typing import Iterable, Any, Protocol
from django import forms
from django.core.exceptions import ImproperlyConfigured
from django.forms.models import ModelFormMetaclass, ModelForm
PersistRelatedFunc = Callable[[], None]
"""Callable saving objects related to the instance being copied."""
class PreparatorFunc(Protocol):
"""Callable preparing a copied instance with custom logic.
It can return an iterable of persisters, to save related objects after
the copied instance is saved, to satisfy foreign key constraints."""
def __call__(
self, source: Any, target: Any, **kwargs
) -> Iterable[PersistRelatedFunc]: ...
[docs]
def copy_fields(source, target, *, fields: Iterable[str], **kwargs):
"""Copy all named fields from source to target.
Any keyword arguments passed to this function override values in target.
Target is returned for convenience, allowing this function to be used in
list comprehension."""
for field in fields:
setattr(target, field, getattr(source, field))
for field, value in kwargs.items():
setattr(target, field, value)
return target
@dataclass
class ModelCopyingFormOptions:
"""Configuration for a ``ModelCopyingForm``.
Attributes:
fields_to_copy: Field names that are copied verbatim from the source
instance to the new instance without user interaction.
preparator: Optional callable for custom copy logic called after the
standard field copy. May return an iterable of
``PersistRelatedFunc`` callables that are invoked after the new
instance is saved to satisfy foreign-key constraints.
"""
fields_to_copy: list[str]
preparator: PreparatorFunc | None = None
def _modelform_factory(
model,
form=ModelForm,
fields=None,
exclude=None,
formfield_callback=None,
widgets=None,
localized_fields=None,
labels=None,
help_texts=None,
error_messages=None,
field_classes=None,
additional_fields=None,
):
"""
This is mostly a copy of Django modelform_factory, adding additional_fields parameter.
If additional fields is a dict, it is added to the resulting form class. This can be
used to add custom form fields.
"""
# Build up a list of attributes that the Meta object will have.
attrs = {"model": model}
if fields is not None:
attrs["fields"] = fields
if exclude is not None:
attrs["exclude"] = exclude
if widgets is not None:
attrs["widgets"] = widgets
if localized_fields is not None:
attrs["localized_fields"] = localized_fields
if labels is not None:
attrs["labels"] = labels
if help_texts is not None:
attrs["help_texts"] = help_texts
if error_messages is not None:
attrs["error_messages"] = error_messages
if field_classes is not None:
attrs["field_classes"] = field_classes
# If parent form class already has an inner Meta, the Meta we're
# creating needs to inherit from the parent's inner meta.
bases = (form.Meta,) if hasattr(form, "Meta") else ()
Meta = type("Meta", bases, attrs)
if formfield_callback:
Meta.formfield_callback = staticmethod(formfield_callback)
# Give this new form class a reasonable name.
class_name = model.__name__ + "Form"
# Class attributes for the new form class.
form_class_attrs = {"Meta": Meta}
if isinstance(additional_fields, dict):
form_class_attrs.update(additional_fields)
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."
)
# Instantiate type(form) in order to use the same metaclass as form.
return type(form)(class_name, (form,), form_class_attrs)
__all__ = ["copy_fields", "copy_form_factory", "ModelCopyingForm"]