Source code for restless.models

import six

from django.core import serializers
from django.db import models

from django.utils.encoding import force_text

__all__ = ['serialize', 'flatten']


def serialize_deprecated(src, fields=None, related=None):
    """Serialize Model or QuerySet to JSON format.

    By default, all of the model fields (including 'id') are serialized, and
    foreign key fields are serialized as the id of the referenced object.

    If 'fields' tuple/list is specified, only fields listed in it are
    serialized. If 'related' dict is specified, fields listed in it
    will be fully (recursively) serialized.

    Format of 'related' is:
        field_name -> (related_object_fields, related_object_related,
            flatten)
    As a shortcut, field_name -> None is equivalent to
        field_name -> (None, None, False)

    The values in 'related' specify 'fields' and 'related' arguments
    to be passed to the related object serialization. If 'flatten' is True,
    the items from the sub-dict will be added to the current dict instead of
    adding a single subdict under the 'field_name' key (useful for OneToOne
    relations, where a model basically just extends the base one).
    """

    # for m2m fields we'll get a Manager instead of a Model; deal with it
    # by just getting all the items

    if (isinstance(src, models.Manager) or
            isinstance(src, models.query.QuerySet)):
        return [serialize_deprecated(item, fields, related)
            for item in src.all()]

    if isinstance(src, list):
        return [serialize_deprecated(item, fields, related) for item in src]

    # we use the Django python serializer to serialize the model
    # and optionally recurse into related fields
    elif isinstance(src, models.Model):
        # serialize fields
        data = serializers.serialize('python', [src], fields=fields)
        data = data[0]['fields']
        if fields is None or 'id' in fields:
            data['id'] = src.id

        # recursively serialize full fields, if any
        if related:
            for k, v in related.items():
                if v is None:
                    v = (None, None, False)
                (sub_fields, sub_related, flatten) = v
                sub = serialize_deprecated(getattr(src, k), sub_fields,
                    sub_related)
                if flatten and sub:
                    for subk, subv in sub.items():
                        data[subk] = subv
                    if k in data:
                        del data[k]
                else:
                    data[k] = sub

        return data

    # just in case ordinary Python data sneaked past us, just return it
    else:
        return src


def serialize_model(obj, fields=None, include=None, exclude=None,
        fixup=None):

    fieldmap = {}
    for f in obj._meta.concrete_model._meta.local_fields:
        fieldmap[f.name] = f.attname

    def getfield(f):
        return getattr(obj, fieldmap.get(f, f))

    if fields is None:
        fields = list(fieldmap.keys())
    else:
        fields = list(fields)

    if exclude is not None:
        fields = [f for f in fields if f not in exclude]

    if include is not None:
        for i in include:
            if isinstance(i, tuple) or (isinstance(i, six.string_types)):
                fields.append(i)

    data = {}
    for f in fields:
        if isinstance(f, six.string_types):
            data[f] = force_text(getfield(f), strings_only=True)
        elif isinstance(f, tuple):
            k, v = f
            if callable(v):
                data[k] = v(obj)
            elif isinstance(v, dict):
                data[k] = serialize(getattr(obj, k), **v)

    if fixup:
        data = fixup(obj, data)

    return data


[docs]def serialize(src, fields=None, related=None, include=None, exclude=None, fixup=None): """Serialize Model or a QuerySet instance to Python primitives. By default, all the model fields (and only the model fields) are serialized. If the field is a Python primitive, it is serialized as such, otherwise it is converted to string in utf-8 encoding. If `fields` is specified, it is a list of attribute descriptions to be serialized, replacing the default (all model fields). If `include` is specified, it is a list of attribute descriptions to add to the default list. If `exclude` is specified, it is a list of attribute descriptions to remove from the default list. Each attribute description can be either: * a string - includes a correspondingly named attribute of the object being serialized (eg. `name`, or `created_at`); this can be a model field, a property, class variable or anything else that's an attribute on the instance * a tuple, where the first element is a string key and the second is a function taking one argument - function will be run with the object being serialized as the argument, and the function result will be included in the result, with the key being the first tuple element * a tuple, where the first element is a related model attribute name and the second is a dictionary - related model instance(s) will be serialized recursively and added as sub-object(s) to the object being serialized; the dictionary may specify `fields`, `include`, `exclude` and `fixup` options for the related models following the same semantics as for the object being serialized. The `fixup` argument, if defined, is a function taking two arguments, the object being serialized, and the serialization result dict, and returning the modified serialization result. It's useful in cases where it's neccessary to modify the result of the automatic serialization, but its use is discouraged if the same result can be obtained through the attribute descriptions. The `related` argument (a different way of specifying related objects to be serialized) is deprecated and included only for backwards compatibility. Example:: serialize(obj, fields=[ 'name', # obj.name 'dob', # obj.dob ('age', lambda obj: date.today() - obj.dob), ('jobs', dict( # for job in obj.jobs.all() fields=[ 'title', # job.title 'from', # job.from 'to', # job.to, ('duration', lambda job: job.to - job.from), ] )) ]) Returns: a dict (if a single model instance was serialized) or a list od dicts (if a QuerySet was serialized) with the serialized data. The data returned is suitable for JSON serialization using Django's JSON serializator. """ if related is not None: import warnings warnings.warn("'related' is deprecated syntax", DeprecationWarning) return serialize_deprecated(src, fields=fields, related=related) def subs(subsrc): return serialize(subsrc, fields=fields, include=include, exclude=exclude, fixup=fixup) if isinstance(src, models.Manager): return [subs(i) for i in src.all()] elif (isinstance(src, list) or isinstance(src, models.query.QuerySet) or isinstance(src, set)): return [subs(i) for i in src] elif isinstance(src, dict): return dict((k, subs(v)) for k, v in src.items()) elif isinstance(src, models.Model): return serialize_model(src, fields=fields, include=include, exclude=exclude, fixup=fixup) else: return src
[docs]def flatten(attname): """Fixup helper for serialize. Given an attribute name, returns a fixup function suitable for serialize() that will pull all items from the sub-dict and into the main dict. If any of the keys from the sub-dict already exist in the main dict, they'll be overwritten. """ def fixup(obj, data): for k, v in data[attname].items(): data[k] = v del data[attname] return data return fixup