Source

djutils / djutils / models / __init__.py

__all__ = (
    'set_field_order',
    'copy_model_instance',
    'has_related_objects',
)


from django.db.models.base import ModelBase
from django.db.models.options import Options
def set_field_order(*requests):
    """Return a model metaclass that ensures a certain field order, as
    specified by ``requests``. This is useful in inheritance scenarios,
    where the order cannot be controlled declaratively.

    Example:
        class MyModel(AbstractBase):
            __metaclass__ = set_field_order(('name', 'aliases',))

    ``requests`` is expected to be tuple of field names, or an iterable
    of such tuples.

    Ultimately, the model field order should be irrelevant and the form
    system might be a better place to do this. Generic admin appliations
    should approach this as newforms-admin does, registering this sort
    of information with the application by way of a custom class, or
    something similar. Once newforms-admin lands, applications uses this
    should probably consider switching to using the same config data as
    newforms-admin.

    This used to just change the Options (_meta) field_cache order, but
    since https://github.com/django/django/commit/7d11c30994c5f90603264c6e2fcf1d9c79ab8924
    ModelForms started to resort the fields on it's own.
    """

    if not requests:
        return None

    # support simplified syntax for single sort requests
    if not isinstance(requests[0], (list,tuple)):
        requests = (requests,)

    class OrderMetaclass(ModelBase):
        def __new__(cls, name, bases, attrs):
            model = super(OrderMetaclass, cls).__new__(cls, name, bases, attrs)

            # Start with all fields in default order. Create a copy, because
            # we don't care to actually change it's order. Make sure it is
            # sorted (will be according to Field.creation_counter).
            fields = sorted(list(model._meta.fields))

            # Change the order of the fields in this list according to
            # user's request.
            for request in requests:
                anchor_idx = None
                for to_be_ordered in request:
                    # Find the current index of this field.
                    for i in range(0, len(fields)):
                        if fields[i].name == to_be_ordered:
                            field_idx = i
                            break
                    else:
                        raise ValueError('Invalid field name "%s"' % to_be_ordered)

                    # First field in the request is the anchor, all the
                    # other listed fields will be inserted after it.
                    if anchor_idx is None:
                        anchor_idx = field_idx
                    else:
                        field = fields[field_idx]
                        fields.insert(anchor_idx+1, field)
                        del fields[field_idx]
                        if field_idx > anchor_idx:
                            anchor_idx += 1

            # Rewrite the whole model's creation_counters to match the new
            # field order.
            for idx, field in enumerate(fields):
                field.creation_counter = idx + 1  # start with 1

            return model

    return OrderMetaclass


from django.db.models import AutoField
def copy_model_instance(obj):
    """Create a copy of a model instance.

    Works in model inheritance case where ``instance.pk = None`` is
    not good enough, since the subclass instance refers to the
    parent_link's primary key during save.

    M2M relationships are currently not handled, i.e. they are not
    copied.

    See also Django #4027.
    """
    initial = dict([(f.name, getattr(obj, f.name))
                    for f in obj._meta.fields
                    if not isinstance(f, AutoField) and\
                       not f in obj._meta.parents.values()])
    return obj.__class__(**initial)


from django.db.models.deletion import Collector
from django.db import router
def has_related_objects(obj, ignore=[]):
    """Returns ``True`` if the model instance ``obj`` has any related
    objects depending on it.

    This allows you to check, for example, whether a delete would be
    cascading, i.e. cause other objects to be deleted, too.

    You may pass a list of model classes in ``ignore`` - dependencies
    from those models will then not be considered.

    With respect to model inheritance, parent model records will be
    ignored, while subclass model will count. For example, say ``B``
    inherits ``A``. Now, when deleting ``b``, ``a`` would not be
    considered a related object by this function. However, when
    deleting ``a``, ``b`` would.

    # TODO: untested with multi-level inheritance, multiple parents
    # TODO: add tests
    """
    model = type(obj)

    collector = Collector(using=router.db_for_write(model))
    collector.collect([obj])

    for so_klass, items in collector.data.items():
        # Make sure there are instances for this relation.
        if not len(items):
            continue
        # Auto-created m2m models we don't care about. The admin ignores
        # those in it's ``django.contrib.admin.util.NestedObjects`` class.
        if so_klass._meta.auto_created:
            continue
        # Also ignore any subclasses (in case of inheritance).
        if issubclass(so_klass, model):
            continue
        # Does the user explicitely ask us to ignore this model?
        if so_klass in ignore:
            continue
        # -> So there is at least one related object that counts
        return True
    return False