Django serialization of inherited model - python

I have a problem with serialization of Django inherited models. For example
class Animal(models.Model):
color = models.CharField(max_length=50)
class Dog(Animal):
name = models.CharField(max_length=50)
...
# now I want to serialize Dog model with Animal inherited fields obviously included
print serializers.serialize('xml', Dog.objects.all())
and only Dog model has been serialized.
I can do smth like
all_objects = list(Animal.objects.all()) + list(Dog.objects.all())
print serializers.serialize('xml', all_objects)
But it looks ugly and because my models are very big so I have to use SAX parser and with such output it's difficult to parse.
Any idea how to serialize django models with parent class?
**EDIT: ** It use to work ok before this patch has been applied. And the explanation why the patch exist "Model saving was too aggressive about creating new parent class instances during deserialization. Raw save on a model now skips saving of the parent class. " I think there should be an option to be able to serialize "local fields only" by default and second option - "all" - to serialize all inherited fields.

You found your answer in the documentation of the patch.
all_objects = list(Animal.objects.all()) + list(Dog.objects.all())
print serializers.serialize('xml', all_objects)
However, if you change Animal to be an abstract base class it will work:
class Animal(models.Model):
color = models.CharField(max_length=50)
class Meta:
abstract = True
class Dog(Animal):
name = models.CharField(max_length=50)
This works as of Django 1.0. See http://docs.djangoproject.com/en/dev/topics/db/models/.

You'll need a custom serializer to support inherited fields, as Django's serializer will only serialize local fields.
I ended up writing my own when dealing with this issue, feel free to copy it: https://github.com/zmathew/django-backbone/blob/master/backbone/serializers.py
In order to use it on its own, you need to do:
serializer = AllFieldsSerializer()
serializer.serialize(queryset, fields=fields)
print serializer.getvalue()

I had the same problem, and i wrote a 'small' queryset serializer which navigates up the inheritance tree and returns all the fields serialized.
It's far from perfect... but works for me :)
a = QuerySetSerializer(MyModel, myqueryset)
a.serialize()
And the snippet:
from __future__ import unicode_literals
import json
import inspect
from django.core import serializers
from django.db.models.base import Model as DjangoBaseModel
class QuerySetSerializer(object):
def __init__(self, model, initial_queryset):
"""
#param model: The model of your queryset
#param initial_queryset: The queryset to serialize
"""
self.model = model
self.initial_queryset = initial_queryset
self.inheritance_tree = self._discover_inheritance_tree()
def serialize(self):
list_of_querysets = self._join_inheritance_tree_objects()
merged_querysets = self._zip_queryset_list(list_of_querysets)
result = []
for related_objects in merged_querysets:
result.append(self._serialize_related_objects(related_objects))
return json.dumps(result)
def _serialize_related_objects(self, related_objects):
"""
In this method, we serialize each instance using the django's serializer function as shown in :
See https://docs.djangoproject.com/en/1.10/topics/serialization/#inherited-models
However, it returns a list with mixed objects... Here we join those related objects into one single dict
"""
serialized_objects = []
for related_object in related_objects:
serialized_object = self._serialize_object(related_object)
fields = serialized_object['fields']
fields['pk'] = serialized_object['pk']
serialized_objects.append(fields)
merged_related_objects = {k: v for d in serialized_objects for k, v in d.items()}
return merged_related_objects
def _serialize_object(self, obj):
data = serializers.serialize('json', [obj, ])
struct = json.loads(data)
return struct[0]
def _discover_inheritance_tree(self):
# We need to find the inheritance tree which excludes abstract classes,
# so we can then join them when serializing the instance
return [x for x in inspect.getmro(self.model) if x is not object and x is not DjangoBaseModel and not x._meta.abstract]
def _join_inheritance_tree_objects(self):
"""
Here we join the required querysets from the non abstract inherited models, which we need so we are able to
serialize them.
Lets say that MyUser inherits from Customer and customer inherits from django's User model
This will return [list(MyUser.objects.filter(...), list(Customer.objects.filter(...), list(User.objects.filter(...)
"""
initial_ids = self._get_initial_ids()
inheritance__querysets = [list(x.objects.filter(id__in=initial_ids).order_by("id")) for x in self.inheritance_tree]
return inheritance__querysets
def _zip_queryset_list(self, list_of_querysets):
"""
At this stage, we have something like:
(
[MyUser1, MyUser2, MyUser3],
[Customer1, Customer2, Customer3],
[User1, User2, User3]
)
And to make it easier to work with, we 'zip' the list of lists so it looks like:
(
[MyUser1, Customer1, User1],
[MyUser2, Customer2, User2],
[MyUser3, Customer3, User3],
)
"""
return zip(*list_of_querysets)
def _get_initial_ids(self):
"""
Returns a list of ids of the initial queryset
"""
return self.initial_queryset.order_by("id").values_list("id", flat=True)

You can define a custom Serializer:
class DogSerializer(serializers.ModelSerializer):
class Meta:
model = Dog
fields = ('color','name')
Use it like:
serializer = DogSerializer(Dog.objects.all(), many=True)
print serializer.data enter code here

Did you look at select_related() ?
as in
serializers.serialize('xml', Dog.objects.select_related().all())

Related

Django get all base class objects that their derived class has a mixin

I have models that look something like this:
class SomeModel(model.Model):
pass
class Base(models.Model):
field1 = ...
class Mixin1(models.Model):
some_model = models.ForeignKey('SomeModel', on_delete=models.CASCADE)
class Meta:
abstract = True
class Derive1(Base, Mixin1):
field2 = ...
class Derive2(Base, Mixin1):
field3 = ...
class Derive3(Base):
field4 = ...
Now I want to be able to get all objects that derive from Base and Mixin1, without explicitly listing all classes that use Mixin1.
Something like:
Base.objects.filter(some_model=X)
Why this doesn't work I understand, but how to actually make it work I do not know.
Solution: Thanks to valentjedi's answer I got to a working solution. I am posting it here in case it will help someone one day.
On top of wanting all the results together, I also wanted them sorted by a date field in Base and the ability to filter them freely. So I created in Mixin1 the function:
#staticmethod
def get_mixin1_objects(some_model, q_filter=None):
mixin_models = set(Mixin1.__subclasses__()) & set(Base.__subclasses__())
results = []
for mdl in mixin_models:
apply_filter = Q(some_model=some_model)
if q_filter is not None:
apply_filter &= q_filter
results.append(list(mdl.objects.filter(apply_filter).order_by('date').all()))
# Merge sort all the events
return list(heapq.merge(*results))
While adding to Base the method:
def __gt__(self, other):
return self.date > other.date
Thus I can apply filters however I like, and can get order_by a constant field results (which suffices my current needs).
Example of usage:
lst = Mixinq.get_mixin1_objects(some_model_instance, q_filter=~Q(some_field_in_base=some_value))
I might not understand your question clearly, but you can use __subclasses__ magic method for something like this:
>>> set(Mixin1.__subclasses__()) & set(Base.__subclasses__())
{Derive2, Derive1}
Then you can filter those models and do whatever you want to them.

Django Integrated Test Passing When it Should Fail

I'm in the process of creating an assessment system using Django; however, I have an integrated test that passes and I'm not sure as to why (it should be failing). In the test, I set the grade field of the bobenrollment object to "Excellent". As you can see from the models below, the Enrollment model doesn't have a grade field (none of the models do). I was under the impression that dot notation of model objects would access the model fields (I'm probably incorrect about this). I don't want to write ineffective tests, so I would like to know what makes this test pass and what I should do to make it break. Thanks!
class ClassAndSemesterModelTest(TestCase):
def add_two_classes_to_semester_add_two_students_to_class(self):
first_semester = Semester.objects.create(text='201530')
edClass = EdClasses.objects.create(name='EG 5000')
edClass2 = EdClasses.objects.create(name='EG 6000')
first_semester.classes.add(edClass)
first_semester.classes.add(edClass2)
bob = Student.objects.create(name="Bob DaBuilder")
jane = Student.objects.create(name="Jane Doe")
bobenrollment = Enrollment.objects.create(student=bob, edclass=edClass)
janeenrollment = Enrollment.objects.create(student=jane,edclass=edClass)
bobenrollment2 = Enrollment.objects.create(student=bob,edclass=edClass2)
janeenrollment2 = Enrollment.objects.create(student=jane,edclass=edClass2)
def test_students_link_to_enrollments(self):
self.add_two_classes_to_semester_add_two_students_to_class()
edclass1 = EdClasses.objects.get(name="EG 5000")
bob = Student.objects.get(name="Bob DaBuilder")
#The three lines below are the subject of my question
bobenrollment = Enrollment.objects.get(edclass=edclass1, student=bob)
bobenrollment.grade = "Excellent"
self.assertEqual(bobenrollment.grade, "Excellent")
And the models below:
from django.db import models
class Student(models.Model):
name = models.TextField(default="")
def __str__(self):
return self.name
#TODO add models
class EdClasses(models.Model):
name = models.TextField(default='')
students = models.ManyToManyField(Student, through="Enrollment")
def __str__(self):
return self.name
class Semester(models.Model):
text = models.TextField(default='201530')
classes = models.ManyToManyField(EdClasses)
def __str__(self):
return self.text
class Enrollment(models.Model):
student = models.ForeignKey(Student)
edclass = models.ForeignKey(EdClasses)
Requirements.txt
beautifulsoup4==4.4.1
Django==1.5.4
ipython==3.1.0
LiveWires==2.0
nose==1.3.3
Pillow==2.7.0
projectname==0.1
pyperclip==1.5.11
pytz==2015.2
requests==2.10.0
selenium==2.53.6
six==1.9.0
South==1.0.2
swampy==2.1.7
virtualenv==1.11.5
I was under the impression that dot notation of model objects would access the model fields (I'm probably incorrect about this)
You're correct about this. What you're not taking into account is the fact that you can dynamically add properties to python objects. For instance:
In [1]: class MyClass():
...: pass
...:
In [2]: a = MyClass()
In [3]: a.im_a_property = 'hello'
In [4]: print a.im_a_property
hello
As you can see, the a instance will have the im_a_propery property even though it's not defined by the class. The same applies for the following line in your code:
bobenrollment.grade = "Excellent"
Django models override this behavior so you can seamlessly get DB values as properties of your model instance, but the instance is just a regular python object.
If you want to test the grade property gets saved correctly, you should modify your test to add the value of grade when creating the record and making sure the instance you assert against is the one you read from your DB (i.e. not modifying it beforehand).

Django-REST Serializer: Queryset does not filter PrimaryKeyRelatedField results

So I have a serializer that looks like this
class BuildingsSerializer(serializers.ModelSerializer):
masterlisting_set = serializers.PrimaryKeyRelatedField(many=True,
queryset=Masterlistings.objects.all())
and it works great
serializer = BuildingsSerializer(Buildings.objects.get(pk=1))
serializer.data
produces
OrderedDict([
("masterlistings_set", [
"0a06e3d7-87b7-4526-a877-c10f54fa5bc9",
"343643ac-681f-4597-b8f5-ff7e5be65eef",
"449a3ad2-c76c-4cb8-bb86-1be72fafcf64",
])
])
but if I change the queryset in the serializer to
class BuildingsSerializer(serializers.ModelSerializer):
masterlistings_set = serializers.PrimaryKeyRelatedField(many=True, queryset=[])
I still get the same exact result back.
OrderedDict([
("masterlistings_set", [
"0a06e3d7-87b7-4526-a877-c10f54fa5bc9",
"343643ac-681f-4597-b8f5-ff7e5be65eef",
"449a3ad2-c76c-4cb8-bb86-1be72fafcf64",
])
])
Is this supposed to be happening? Am I using querysets incorrectly?
I used [] as an easy example to show that no matter what I put in nothing changes.
Please any insight would be invaluable
It should be noted that masterlistings has a primary key relationship that points to buildings. So a masterlisting belong to a building.
As pointed out by #zymud, queryset argument in PrimaryKeyRelatedField is used for validating field input for creating new entries.
Another solution for filtering out masterlistings_set is to use serializers.SerializerMethodField() as follows:
class BuildingsSerializer(serializers.ModelSerializer):
masterlisting_set = serializers.SerializerMethodField()
def get_masterlisting_set(self, obj):
return MasterListing.objects.filter(building=obj).values_list('pk',flat=True)
queryset in related field limits only acceptable values. So with queryset=[] you will not be able to add new values to masterlisting_set or create new Buildings.
UPDATE. How to use queryset for filtering
This is a little bi tricky - you need to rewrite ManyRelatedField and many_init method in your RelatedField.
# re-define ManyRelatedField `to_representation` method to filter values
# based on queryset
class FilteredManyRelatedField(serializers.ManyRelatedField):
def to_representation(self, iterable):
iterable = self.child_relation.queryset.filter(
pk__in=[value.pk for value in iterable])
return super(FilteredManyRelatedField, self).to_representation(iterable)
# use overridden FilteredManyRelatedField in `many_init`
class FilteredPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
#classmethod
def many_init(cls, *args, **kwargs):
kwargs['child_relation'] = cls(queryset=kwargs.pop('queryset'))
return FilteredManyRelatedField(*args, **kwargs)

Sorting for custom fields in models in django admin

I want to have sorting functionality for custom model field in django admin.
The code is similar to
class MyModel(models.Model):
first_name = models.CharField()
last_name = models.CharField()
def most_recent_mailing_date(self):
""" Return the most recent mailing date """
mailingHistories = self.mailinghistory_set.all()
if len(mailingHistories) != 0:
today = datetime.date.today()
mostRecentHistory = None
diff = -1
for mailingHistory in mailingHistories:
if mailingHistory.mailing_date < today and (diff == -1 or (today - mailingHistory.mailing_date) < diff):
mostRecentHistory = mailingHistory
diff = today - mostRecentHistory.mailing_date
if mostRecentHistory is None:
return "No Mailing History"
else:
return mostRecentHistory.mailing_date
else:
return "No Mailing History"
most_recent_mailing_date.admin_order_field = 'self.most_recent_mailing_date'
The field I want to order is most_recent_mailing_date.
It is a custom field.
Is it possible?
Thanks in advance!
I don't think that's possible. From the docs:
You have four possible values that can be used in list_display:
....
A string representing an attribute on the model. This behaves almost
the same as the callable, but self in this context is the model
instance. Here’s a full model example:
from django.db import models from django.contrib import admin
class Person(models.Model):
name = models.CharField(max_length=50)
birthday = models.DateField()
def decade_born_in(self):
return self.birthday.strftime('%Y')[:3] + "0's"
decade_born_in.short_description = 'Birth decade'
class PersonAdmin(admin.ModelAdmin):
list_display = ('name', 'decade_born_in')
Thus, your field is the fourth option. However:
A few special cases to note about list_display:
...
Usually, elements of list_display that aren’t actual database fields
can’t be used in sorting (because Django does all the sorting at the
database level).
...(goes on to describe exception that doesn't apply here).
Thus, you can only sort on actual database fields.
You can't use Django's order_by since it is applied at the database level. The database does not know anything about your python methods and properties.
However, You can do the ordering in Python
objects = MyModel.objects.all()
sorted(objects, key=lambda k: k.most_recent_mailing_date())
If you want reverse ordering,
objects = MyModel.objects.all()
sorted(objects, key=lambda k: k.most_recent_mailing_date(), reverse=True)
Advice
I think you should be consistent on your return type. If there are no mailing history, you can return some old date instead of returning a string.
I think you should consider using the #property decorator on your most_recent_mailing_date() so you can simply refer to it as instance.most_recent_mailing_date. This will make it somehow consistent on how you refer to your actual model fields.

Determine if a property is a backref in sqlalchemy

I have the following relationship set up in a model:
role_profiles = Table('roleprofile', Base.metadata,
Column('role_id', Integer, ForeignKey('role.id')),
Column('profile_id', Integer, ForeignKey('profile.id'))
)
class profile(Base):
__tablename__ = 'profile'
# Columns...
roles = relationship('role', secondary=role_profiles, backref='profiles')
class role(Base):
__tablename__ = 'role'
# Columns...
So as I now understand that it works is that the roles property on the profile object will contain a list of role classes (which it does).
What I want to do is to serialize for each property of the model class generically. It works fine for the top class profile and I determine that there is a list of roles that I should recurse into:
# I need a statement here to check if the field.value is a backref
#if field.value is backref:
# continue
if isinstance(field.value, list):
# Get the json for the list
value = serialize.serialize_to_json(field.value)
else:
# Get the json for the value
value = cls._serialize(field.value)
The problem is that the backref of the relationship adds a pointer back to the profile. The same profile is then serialized and it recurse the roles over and over again until stack overflow.
Is there a way to determine that the property is a backref added by the relationship?
Update
Maybe I should add that it works fine in this case if I remove the backref since I don't need it but I would like to keep it in.
Update
As a temporary fix I added a class property to my base class:
class BaseModelMixin(object):
"""Base mixin for models using stamped data"""
__backref__ = None
and add it like this:
class role(Base):
__tablename__ = 'role'
__backref__ = ('profiles', )
# Columns...
and use it like this in my recursion:
if self.__backref__ and property_name in self.__backref__:
continue
If there is a better way please let me know because this doesn't look optimal.
Not sure if this is the best practice, but this code works for me. It returns True if the attribute is a reference, False if a regular column type.
def is_relation(orm_object, attr_name):
return hasattr(getattr(orm_object.__class__, attr_name).property, 'mapper')
You can create a __relationships__ in your class BaseModelMixin as a #property, which has a list of all relationships name which are not as a backref name in a model.
class BaseModelMixin(object):
"""Base mixin for models using stamped data"""
#property
def __relationships__(self):
"""
Return a list of relationships name which are not as a backref
name in model
"""
back_ref_relationships = list()
items = self.__mapper__.relationships.items()
for (key, value) in items:
if isinstance(value.backref, tuple):
back_ref_relationships.append(key)
return back_ref_relationships
As you have two class profile and role, so
>>> p = profile()
>>> p.__relationships__
# ['roles']
>>> r = role()
>>> r.__relationships__
# []
have a look at inspect
e.g.
from sqlalchemy import inspect
mapper = inspect(MyModelClass)
# dir(mapper)
# mapper.relationships.keys()

Categories

Resources