django-rest-framework, multitable model inheritance, ModelSerializers and nested serializers - python

I can't find this info in the docs or on the interwebs.
latest django-rest-framework, django 1.6.5
How does one create a ModelSerializer that can handle a nested serializers where the nested model is implemented using multitable inheritance?
e.g.
######## MODELS
class OtherModel(models.Model):
stuff = models.CharField(max_length=255)
class MyBaseModel(models.Model):
whaddup = models.CharField(max_length=255)
other_model = models.ForeignKey(OtherModel)
class ModelA(MyBaseModel):
attr_a = models.CharField(max_length=255)
class ModelB(MyBaseModel):
attr_b = models.CharField(max_length=255)
####### SERIALIZERS
class MyBaseModelSerializer(serializers.ModelSerializer):
class Meta:
model=MyBaseModel
class OtherModelSerializer(serializer.ModelSerializer):
mybasemodel_set = MyBaseModelSerializer(many=True)
class Meta:
model = OtherModel
This obviously doesn't work but illustrates what i'm trying to do here.
In OtherModelSerializer, I'd like mybasemodel_set to serialize specific represenntations of either ModelA or ModelB depending on what we have.
If it matters, I'm also using django.model_utils and inheritencemanager so i can retrieve a queryset where each instance is already an instance of appropriate subclass.
Thanks

I've solved this issue a slightly different way.
Using:
DRF 3.5.x
django-model-utils 2.5.x
My models.py look like this:
class Person(models.Model):
first_name = models.CharField(max_length=40, blank=False, null=False)
middle_name = models.CharField(max_length=80, blank=True, null=True)
last_name = models.CharField(max_length=80, blank=False, null=False)
family = models.ForeignKey(Family, blank=True, null=True)
class Clergy(Person):
category = models.IntegerField(choices=CATEGORY, blank=True, null=True)
external = models.NullBooleanField(default=False, null=True)
clergy_status = models.ForeignKey(ClergyStatus, related_name="%(class)s_status", blank=True, null=True)
class Religious(Person):
religious_order = models.ForeignKey(ReligiousOrder, blank=True, null=True)
major_superior = models.ForeignKey(Person, blank=True, null=True, related_name="%(class)s_superior")
class ReligiousOrder(models.Model):
name = models.CharField(max_length=255, blank=False, null=False)
initials = models.CharField(max_length=20, blank=False, null=False)
class ClergyStatus(models.Model):
display_name = models.CharField(max_length=255, blank=True, null=True)
description = models.CharField(max_length=255, blank=True, null=True)
Basically - The base model is the "Person" model - and a person can either be Clergy, Religious, or neither and simply be a "Person". While the models that inherit Person have special relationships as well.
In my views.py I utilize a mixin to "inject" the subclasses into the queryset like so:
class PersonSubClassFieldsMixin(object):
def get_queryset(self):
return Person.objects.select_subclasses()
class RetrievePersonAPIView(PersonSubClassFieldsMixin, generics.RetrieveDestroyAPIView):
serializer_class = PersonListSerializer
...
And then real "unDRY" part comes in serializers.py where I declare the "base" PersonListSerializer, but override the to_representation method to return special serailzers based on the instance type like so:
class PersonListSerializer(serializers.ModelSerializer):
def to_representation(self, instance):
if isinstance(instance, Clergy):
return ClergySerializer(instance=instance).data
elif isinstance(instance, Religious):
return ReligiousSerializer(instance=instance).data
else:
return LaySerializer(instance=instance).data
class Meta:
model = Person
fields = '__all__'
class ReligiousSerializer(serializers.ModelSerializer):
class Meta:
model = Religious
fields = '__all__'
depth = 2
class LaySerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = '__all__'
class ClergySerializer(serializers.ModelSerializer):
class Meta:
model = Clergy
fields = '__all__'
depth = 2
The "switch" happens in the to_representation method of the main serializer (PersonListSerializer). It looks at the instance type, and then "injects" the needed serializer. Since Clergy, Religious are all inherited from Person getting back a Person that is also a Clergy member, returns all the Person fields and all the Clergy fields. Same goes for Religious. And if the Person is neither Clergy or Religious - the base model fields are only returned.
Not sure if this is the proper approach - but it seems very flexible, and fits my usecase. Note that I save/update/create Person thru different views/serializers - so I don't have to worry about that with this type of setup.

I was able to do this by creating a custom relatedfield
class MyBaseModelField(serializers.RelatedField):
def to_native(self, value):
if isinstance(value, ModelA):
a_s = ModelASerializer(instance=value)
return a_s.data
if isinstance(value, ModelB):
b_s = ModelBSerializer(instance=value)
return b_s.data
raise NotImplementedError
class OtherModelSerializer(serializer.ModelSerializer):
mybasemodel_set = MyBaseModelField(many=True)
class Meta:
model = OtherModel
fields = # make sure we manually include the reverse relation (mybasemodel_set, )
I do have concerns that instanting a Serializer for each object is the reverse relation queryset is expensive so I'm wondering if there is a better way to do this.
Another approach i tried was dynamically changing the model field on MyBaseModelSerializer inside of __init__ but I ran into the issue described here:
django rest framework nested modelserializer

Using Django 3.1, I found that it is possible to override get_serializer instead of get_serializer_class, in which case you can access the instance as well as self.action and more.
By default get_serializer will call get_serializer_class, but this behavior can be adjusted to your needs.
This is cleaner and easier than the solutions proposed above, so I'm adding it to the thread.
Example:
class MySubclassViewSet(viewsets.ModelViewSet):
# add your normal fields and methods ...
def get_serializer(self, *args, **kwargs):
if self.action in ('list', 'destroy'):
return MyListSerializer(args[0], **kwargs)
if self.action in ('retrieve', ):
instance = args[0]
if instance.name.contains("really?"): # or check if instance of a certain Model...
return MyReallyCoolSerializer(instance)
else return MyNotCoolSerializer(instance)
# ...
return MyListSerializer(*args, **kwargs) # default

I'm attempting to use a solution that involves different serializer subclasses for the different model subclasses:
class MyBaseModelSerializer(serializers.ModelSerializer):
#staticmethod
def _get_alt_class(cls, args, kwargs):
if (cls != MyBaseModel):
# we're instantiating a subclass already, use that class
return cls
# < logic to choose an alternative class to use >
# in my case, I'm inspecting kwargs["data"] to make a decision
# alt_cls = SomeSubClass
return alt_cls
def __new__(cls, *args, **kwargs):
alt_cls = MyBaseModel.get_alt_class(cls, args, kwargs)
return super(MyBaseModel, alt_cls).__new__(alt_cls, *args, **kwargs)
class Meta:
model=MyBaseModel
class ModelASerializer(MyBaseModelSerializer):
class Meta:
model=ModelA
class ModelBSerializer(MyBaseModelSerializer):
class Meta:
model=ModelB
That is, when you try and instantiate an object of type MyBaseModelSerializer, you actually end up with an object of one of the subclasses, which serialize (and crucially for me, deserialize) correctly.
I've just started using this, so it's possible that there are problems I've not run into yet.

I found this post via Google trying to figure out how to handle multiple table inheritance without having to check the model instance type. I implemented my own solution.
I created a class factory and a mixin to generate the serializers for the child classes with the help of InheritanceManger from django-model-utils.
models.py
from django.db import models
from model_utils import InheritanceManager
class Place(models.Model):
name = models.CharField(max_length=50)
address = models.CharField(max_length=80)
# Use the InheritanceManager for select_subclasses()
objects = InheritanceManager()
class Restaurant(Place):
serves_hot_dogs = models.BooleanField(default=False)
serves_pizza = models.BooleanField(default=False)
serializers.py
from rest_framework import serializers
from .models import Location
def modelserializer_factory(model, class_name='ModelFactorySerializer',
meta_cls=None, **kwargs):
"""Generate a ModelSerializer based on Model"""
if meta_cls is None:
# Create a Meta class with the model passed
meta_cls = type('Meta', (object,), dict(model=model))
elif not hasattr(meta_cls, 'model'):
# If a meta_cls is provided but did not include a model,
# set it to the model passed into this function
meta_cls.model = model
# Create the ModelSerializer class with the Meta subclass
# we created above; also pass in any additional keyword
# arguments via kwargs
ModelFactorySerializer = type(class_name, (serializers.ModelSerializer,),
dict(Meta=meta_cls, **kwargs))
ModelFactorySerializer.__class__.__name__ = class_name
return ModelFactorySerializer
class InheritedModelSerializerMixin:
def to_representation(self, instance):
# Get the model of the instance
model = instance._meta.model
# Override the model with the inherited model
self.Meta.model = model
# Create the serializer via the modelserializer_factory
# This will use the name of the class this is mixed with.
serializer = modelserializer_factory(model, self.__class__.__name__,
meta_cls=self.Meta)
# Instantiate the Serializer class with the instance
# and return the data
return serializer(instance=instance).data
# Mix in the InheritedModelSerializerMixin
class LocationSerializer(InheritedModelSerializerMixin, serializers.ModelSerializer):
class Meta:
model = Location # 'model' is optional since it will use
# the instance's model
exclude = ('serves_pizza',) # everything else works as well
depth = 2 # including depth
views.py
from .models import Location
from .serializers import LocationSerializer
# Any view should work.
# This is an example using viewsets.ReadOnlyModelViewSet
# Everything else works as usual. You will need to chain
# ".select_subclasses()" to the queryset to select the
# child classes.
class LocationViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Location.objects.all().select_subclasses()
serializer_class = LocationSerializer

Related

django-countries how to add serializer field

I'm trying to add the CountryField to a serializer for the Register process (using dj-rest-auth) and can't find the correct way to implement it.
All the answers I found just say to use what the documentation says, but that doesn't help for me, maybe Im just not doing it right.
This is what the documentation of django-countries says:
from django_countries.serializers import CountryFieldMixin
class CountrySerializer(CountryFieldMixin, serializers.ModelSerializer):
class Meta:
model = models.Person
fields = ('name', 'email', 'country')
I need to add the field here:
class CustomRegisterSerializer(RegisterSerializer, CountryFieldMixin):
birth_date = serializers.DateField()
country = CountryField()
gender = serializers.ChoiceField(choices=GENDER)
# class Meta:
# model = User
# fields = ('country')
# Define transaction.atomic to rollback the save operation in case of error
#transaction.atomic
def save(self, request):
user = super().save(request)
user.birth_date = self.data.get('birth_date')
user.country = self.data.get('country')
user.gender = self.data.get('gender')
user.save()
return user
User Model
class User(AbstractUser):
"""
Default custom user model
"""
name = models.CharField(max_length=30)
birth_date = models.DateField(null=True, blank=True)
country = CountryField(null=True, blank=True, blank_label='Select country')
gender = models.CharField(choices=GENDER, max_length=6, null=True, blank=True)
...
I tried different things besides this and nothing worked.
For the serializer, you import the CountryField of the django_countries.serializer_fields module, so:
from django_countries.serializer_fields import CountryField
class CustomRegisterSerializer(RegisterSerializer):
# …
country = CountryField()
# …
If you instead want to work with the Mixin (which will use such CountryField serializer field), you should specify the CountryFieldMixin before the RegisterSerializer, otherwise it will not override the .build_standard_field(…) method.
You thus inherit with:
class CustomRegisterSerializer(CountryFieldMixin, RegisterSerializer):
# …
In that case you should not specify the country serializer field manually, since that will render the mixin ineffective.

Django Include ManyToManyField on "other" model in ModelForm

I would like to have a form with the preselected checkboxes of a ManyToManyField.
models.py
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
forms.py
class StoreForm(ModelForm):
class Meta:
model = Store
fields = ('brands',)
I get this exception:
django.core.exceptions.FieldError: Unknown field(s) (brands) specified for Store
I know that I can add the field manually to the class:
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
If I do this the checkboxes are not preselected.
How is it possible to include the ManyToMany field from "the other side" of the model (from Store)?
#hedgie To change the field in the other model is not a good option for me because I use it already.
But the __init__() was a good hint. I come up with this solution and it seems to work.
class StoreForm(ModelForm):
def __init__(self, *args, **kwargs):
if kwargs.get('instance'):
brand_ids = [t.pk for t in kwargs['instance'].brands.all()]
kwargs['initial'] = {
'brands': brand_ids,
}
super().__init__(*args, **kwargs)
# https://stackoverflow.com/questions/49932426/save-many-to-many-field-django-forms
def save(self, commit=True):
# Get the unsaved Pizza instance
instance = forms.ModelForm.save(self, False)
# Prepare a 'save_m2m' method for the form,
old_save_m2m = self.save_m2m
def save_m2m():
old_save_m2m()
# This is where we actually link the pizza with toppings
instance.brands.clear()
for brand in self.cleaned_data['brands']:
instance.brands.add(brand)
self.save_m2m = save_m2m
# Do we need to save all changes now?
# Just like this
# if commit:
instance.save()
self.save_m2m()
return instance
brands = forms.ModelMultipleChoiceField(
queryset=Brand.objects.all(),
widget=forms.CheckboxSelectMultiple,
)
Though it seems to be not very elegant. I wonder why django does not support a better way.
One possibility is to define the field on the "other" model. So instead of writing this:
class Store(models.Model):
...
class Brand(models.Model):
stores = models.ManyToManyField(Store, blank=True, related_name="brands")
You can write this:
class Brand(models.Model):
...
class Store(models.Model):
brands = models.ManyToManyField(Brand, blank=True, related_name="stores")
Or, if you have manually added the field to the form, you could populate its initial value in the form's __init__() method.

Django Access to Foreign Key data to set a field default value

I have two models with their respective forms. One has a Foreign Key link to the other and from, here I would like to set some fields default data.
class Lexicon(models.Model):
[...]
case_sensitive = models.BooleanField(default=True)
invariant = models.NullBooleanField(default=False)
diacritics = models.BooleanField(default=True)
[...]
class Meta:
verbose_name = "lexicon"
ordering = ["filename"]
def __str__(self):
return self.filename
class Lexeme(models.Model):
lexicon = models.ForeignKey(Lexicon, on_delete=models.CASCADE)
case_sensitive = models.BooleanField(default=True)
diacritics = models.BooleanField(default=True)
[...]
class Meta:
verbose_name = "lexeme"
I would like the Lexeme model fields "case_sensitive" and "diacritics" to default from Lexicon. I suppose the forms may be a better place to do this.
Any idea ?
As I understand, you only need to populate data from Lexicon to Lexeme model fields. You can override get_form_kwargs in your FormView as follows
def get_form_kwargs(self):
lex_obj = Lexeme.objects.get(pk=self.kwargs['pk'])
kwargs = super().get_form_kwargs()
kwargs['initial']['case_sensitive'] = lex_obj.lexicon.case_sensitive
kwargs['initial']['diacritics'] = lex_obj.lexicon.diacritics
return kwargs
Is that what you want? I have not tested but, I have used similar thing on my project. Let me know if works or not.
I finally found the way to go. It was just basic initial setting of field, no need to touch to forms.py, models.py nor the html template.
I passed data to my form like this:
lexeme_form = LexemeForm(initial={'case_sensitive': lexicon.case_sensitive, 'diacritics': lexicon.diacritics})
use Ajax at template to change the initial value of "case_sensitive" and "diacritics" when Lexicon changed, and abstract model can be used to reduce repeat lines :
class BaseLex(models.Model):
case_sensitive = models.BooleanField(default=True)
diacritics = models.BooleanField(default=True)
class Meta:
abstract = True
class Lexicon(BaseLex):
# without `case_sensitive` and `diacritics' fields
...
class Lexeme(BaseLex):
# without `case_sensitive` and `diacritics' fields
lexicon = models.ForeignKey(Lexicon, on_delete=models.CASCADE)
...

How to Serialize generic foreign key In DRF

I have model with generic foreign key and I want to serialize that model.
model.py:
class AddressType(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type','object_id')
def __unicode__(self):
return u'%s' % str(self.content_type)
class AddressBook(TimeStampedModel):
class Meta:
db_table = 'address_book'
uuid = UUIDField(auto=True)
address_tag = models.CharField(null=True, blank=True, max_length=20)
# should be a generic foreign key
address_object_type = GenericRelation(AddressType)
address1 = models.CharField(
verbose_name='Address1',
max_length=200,
)
address2 = models.CharField(
verbose_name='Address2',
max_length=200,
)
serializer.py:
class AddressBookSerializer(serializers.ModelSerializer):
class Meta:
model = AddressBook
fields = ('id','uuid','address_tag','address_object_type','address1','address2')
How can I serialize JSON on above model?
This case is perfectly described in the documentation.
So if you want to serialize AddressType you will need to implement something like this:
class ContentObjectRelatedField(serializers.RelatedField):
"""
A custom field to use for the `content_object` generic relationship.
"""
def to_representation(self, value):
"""
Serialize tagged objects to a simple textual representation.
"""
if isinstance(value, Bookmark):
return 'Bookmark: ' + value.url
elif isinstance(value, Note):
return 'Note: ' + value.text
raise Exception('Unexpected type of tagged object')
Where Bookmark and Note are objects which may be have associated contents.
If you want to serialize AddressBook you can try doing something like this:
class AddressBookSerializer(serializers.ModelSerializer):
address_object_type = ContentObjectRelatedField()
class Meta:
model = AddressBook
fields = ('id','uuid','address_tag','address_object_type','address1','address2')
I'd prefer use this third party (rest-framework-generic-relations) as also described in documentation.
from generic_relations.relations import GenericRelatedField
class TagSerializer(serializers.ModelSerializer):
"""
A `TaggedItem` serializer with a `GenericRelatedField` mapping all possible
models to their respective serializers.
"""
tagged_object = GenericRelatedField({
Bookmark: BookmarkSerializer(),
Note: NoteSerializer()
})
class Meta:
model = TaggedItem
fields = ('tag_name', 'tagged_object')
If your code is more structured, i.e. each app has its serializers.py, and serializer naming follows a convention (i.e. [ModelName]Serializer) you could use dynamic importing to avoid if ... elif ... else ... logic with a benefit of loose coupling:
from django.utils.module_loading import import_string
class ContentObjectRelatedField(serializers.RelatedField):
"""
A custom field to serialize generic relations
"""
def to_representation(self, object):
object_app = object._meta.app_label
object_name = object._meta.object_name
serializer_module_path = f'{object_app}.serializers.{object_name}Serializer'
serializer_class = import_string(serializer_module_path)
return serializer_class(object).data

Django rest framework represent flatten nested object

I have a parent and a one-to-one related child model and I would like to render the fields from the child flat in the parent representation (read only). Currently, I have achieved that with a custom to_representation implementation but that seems very involved and I wonder if there is no easier way to achieve this.
It is made more complicated by the fact that my related model is connected via a property.
So here is the concrete example:
By default a related object would be rendered like:
{
parent_name:'Bob',
child:{
name:'Alice'
}
}
This is what I want and currently get with my to_representation:
{
parent_name:'Bob',
child_name:'Alice'
}
My models look like this:
class ChildModel(models.Model):
name = models.CharField(max_length=100, null=True)
class ParentModel(models.Model):
name = models.CharField(max_length=100, null=True)
_child = models.ForeignKey('ChildModel', null=True)
#property
def child(self):
return self._most_recent_status
#name.setter
def child(self, value):
self._child = value
Here are my serializers:
class FlatChildField(serializers.RelatedField):
def to_representation(self, value):
return value.name
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = FlatChildField(source='_child', read_only=True)
class Meta:
model = Parent
fields = ('name', 'child_name')
For a simpler solution to get a flat representation of related models I would be grateful.
For completeness, I would be interested to hear if there is a simpler solution for "normal" related models (i.e. not property model fields as well). I was looking for the equivalent of the django model query syntax of related_model__field, but I cannot find that. Does that exist for django rest framework?
Many thanks
The simplest means would be to use source:
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = serializers.CharField(source='_child.name', read_only=True)
class Meta:
model = Parent
fields = ('name', 'child_name')
You can use SerializerMethodField, it saves you really a lot of work and it's so clean and trivial:
class FlatParentSerializer(serializers.ModelSerializer):
parent_name = serializers.CharField(source='name', read_only=True)
child_name = serializers.SerializerMethodField('get_child_name')
class Meta:
model = Parent
fields = ('name', 'child_name')
def get_child_name(self, obj):
return obj._child.name

Categories

Resources