How add custom fields to django-serializer dynamically? - django

I am trying to write a simple CRUD application. I have several pretty similar model classes and one serializer:
class TestModelSerializer(serializers.ModelSerializer):
queryset = None
#classmethod
def initialize(cls, queryset, context_class, **kwargs):
_srlz = cls()
_srlz.Meta.model = context_class
_srlz.Meta.fields = '__all__'
_srlz.queryset = queryset
_srlz = cls(_srlz.queryset, **kwargs)
return _srlz
class Meta:
model = None
fields = ''
so I can serialize my classes, calling initialize function like this:
_q = Foo.objects.all()
_srlz = TestModelSerializer.initialize(_q, Foo, many = True)
_q2 = Bar.objects.all()
_srlz2 = TestModelSerializer.initialize(_q2, Bar, many = True)
and so on.
But I have faced to one problem. Sometimes my classes are in hard One-to-Many relation (composition):
class Foo(models.Model):
pass
class Bar(models.Model):
foo = models.ForeignKey(Foo, on_delete = models.CASCADE)
When I serialize Foo class I want to serialize a list of related Bar classes as well and nest result data to Foo's serializer data.
I am not intended to write custom serializer for each of these classes but instead try to implement some generic approach.
I decided to make some experiments and finally create an interface that provides me several methods for my collection items:
class Foo(models.Model):
bars = iCollection
pass
Now when instantiate a serializer I can get all fields of my model class that have iCollection type.
The question is how can I add them to my serializer?
I tried to write an abstract fabric and create a serializer class using type(), but this approach is too ugly, I suppose there should be much more easier solution.

Related

Django Rest Framework: Shared Fields on Serializers

I have a few Serializers that share a few fields like meta_id, category_id and so on.
Obviously I could just declare these on the serializer as a SerializerMethodField individually but I would like to find a way to reuse the logic, either with a Mixin, Decorator, or inheritance.
How can I declare a base serializer and inherit it— while still inheriting from serializers.ModelSerializer? So that I can reuse the get_meta_id and make sure it shows up in the fields?
class Foo(serializers.ModelSerializer, somethingHere?):
meta_id = Serializers.SerializerMethodField()
class Meta:
model = Foo
fields = [...]
def get_meta_id(self, obj):
...
Is it possible to just pass two parameters into the class
You can crete a Base serializer and use inheritence for other serializers.Like that:
class BaseSerializer(serializers.Serializer):
#your codes and extra fields
test_field = serializer.SerializerMethodField()
def get_test_field(self.obj):
return 'test' # or you can use obj instance here
class ExtendedSerializer(BaseSerializer,serializers.ModelSerializer):
#your extra fields
class Meta:
fields = BaseSerializer.Meta.fields + () # you can add your additional fields here
class BaseSerializer(serializers.ModelSerializer): # The BaseSerializer class inherit everthing from ModelSerializer
class Meta(serializers.ModelSerializer.Meta): # The BaseSerializer.Meta inherits everything from ModelSerializer.Meta
def get_meta_id(self, obj):
return self.meta_id
class Foo(BaseSerializer):
meta_id = Serializers.SerializerMethodField()
class Meta(BaseSerializer.Meta): # Here you should get the ModelSerializer.Meta + your custom get_meta_id
model = Foo
fields = [...]

How to chain model managers?

I have two abstract models:
class SoftDeleteModel(models.Model):
objects = SoftDeletableManager()
class Meta:
abstract = True
class BookAwareModel(models.Model):
book = models.ForeignKey(Book)
class Meta:
abstract = True
I use often use these models together for DRY purposes, e.g.:
class MyNewModel(SoftDeleteModel, BookAwareModel):
The SoftDeleteModel has a custom manager SoftDeletableManager():
class SoftDeletableManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_removed=False)
If I want to extend the BookAware abstract model to add a queryset filter on Books whilst still preserving the SoftDeletableManager() how would I go about this?
E.g. I can't add objects = BookManager() to BookAwareModel because it will overwrite the SoftDeletableManager.
Having played with your code a bit I came up with three possible solutions which seem to work (according to my tests):
Option 1:
Create a combined manager which is used when defining your concrete MyNewModel and use it for that model:
class CombiManager(SoftDeletableManager, BookAwareManager):
def get_queryset(self):
qs1 = SoftDeletableManager.get_queryset(self)
qs2 = BookAwareManager.get_queryset(self)
return qs1.intersection(qs2)
and then
class MyNewModel(SoftDeleteModel, BookAwareModel):
objects = CombiManager()
Option 2:
Create a Manager for the BookAware model as a subclass of the SoftDeleteableManager
class BookAwareManager(SoftDeletableManager):
def get_queryset(self):
return super().get_queryset().filter(your_filter)
and then add it to your BookAware model with a different name than 'objects':
class BookAwareModel(models.Model):
book = models.ForeignKey(Book)
book_objects = BookAwareManager()
class Meta:
abstract = True
allowing you to get the filtered queryset like
MyNewModel.book_objects.all()
Option 3
Put the BookAwareManager as in Option two as manager into your concrete MyNewModel. Then you can leave the managers name as the default 'objects'

Django Rest Framework: serializer for two models that inherit from a common base abstract model

There is an abstract model that defines an interface for two child models.
I've been asked to create an API endpoint that will return instances from those child models (including only the common fields defined thanks to the interface father class).
The problem raises when defining the Serializer.Meta.model attribute.
Anyway, code is always clearer:
models.py
class Children(Model):
class Meta:
abstract = True
def get_foo(self):
raise NotImplementedError
class Daughter(Children):
def get_foo(self):
return self.xx
class Son(Children):
def get_foo(self):
return self.yy
api/views.py
class ChildrenApiView(ListAPIView):
serializer_class = ChildrenSerializer
def get_queryset(self):
daughters = Daughter.objects.all()
sons = Son.objects.all()
return list(daughters) + list(sons)
serializers.py
class ChildrenSerializer(ModelSerializer):
foo = CharField(source="get_foo", read_only=True)
class Meta:
model = Children # <========= HERE IS THE PROBLEM
fields = ('foo',)
Some thoughts;
I know I'm not able to point out to the abstract model Children (wrote it for showing the inntention)
I tried to leave ChildrenSerializer.Meta.model empty
Seems that I can choose whichever Daughter or Son but not sure if that solution has any side-effect or is the way to go.
Tried to create DaughterSerializer & SonSerializer and use the method get_serializer_class(self) at the view, but wasn't able to make it run
I would probabaly not have a model serializer, and instead have a standard Serializer, with all the fields that you want to return in the view.
This will make it applicable for both Son and Daughter.
So the serializer would be something like:
class ChildrenSerializer(serializers.Serializer):
foo = CharField(source="get_foo", read_only=True)

Avoid nested objects when using nested serializers

I have two models, one contains the other in a foreignKey relationship, I wanted to make an API that would return the combine of these two models, so I attempted to use nested Serializers to add the related model as well, but the data are not all on the same level, the related models is a object inside the first.
Here are the Models
class ModelOne(models.Model):
last_counter = models.IntegerField()
class ModelTwo(models.Model):
model_one = models.ForeignKey(ModelOne, on_delete=models.CASCADE)
category = models.CharField(max_length=64)
counter_type = models.CharField(max_length=32)
Here are the serializers
class ModelOneSerializer(serializers.ModelSerializer):
class Meta:
model = ModelOne
fields = "__all__"
class ModelTwoSerializer(serializers.ModelSerializer):
model_one= ModelOneSerializer(read_only=True)
class Meta:
model = ModelTwo
fields = "__all__"
This would return from the API in the form of
{
"category" : ...,
"counter_type" : ...,
"model_one" : {
"last_counter" : ...
}
}
But I don't want the response to be like that, I want it more like this
{
"category" : ...,
"counter_type" : ...,
"last_counter" : ...,
}
Is there a way to achieve this through serializers?
Use SerializerMethodField
from rest_framework.fields import SerializerMethodField
class ModelTwoSerializer(serializers.ModelSerializer):
last_counter = SerializerMethodField()
class Meta:
model = ModelTwo
fields = "__all__"
def get_last_counter(self, obj):
return ModelOneSerializer(obj.model_one).data['last_counter']
When creating custom fields(field_one for example) with SerializerMethodField, you have to create a method called get_field_one, for this method to be automatically detected by the serializer.
You can achieve what you want to do using SerializerMethodField from drf fields:
SerializerMethodField is a read-only field that computes its value at request processing time, by calling a method on the serializer class it is attached to. For example for your case it will look like this. Notice that the computed last_counter is added on the serialized model fields.
from rest_framework.fields import SerializerMethodField
class ModelTwoSerializer(serializers.ModelSerializer):
last_counter = serializers.SerializerMethodField()
class Meta:
model = ModelTwo
fields = ["category", "counter_type", "last_counter"]
def get_last_counter(self, obj):
return int(obj.model_one.last_counter)
SerializerMethodField accepts method_name, but it’s usually more convenient to use the default pattern for naming those methods, which is get_. Just make sure you‘re not overburdening your method fields with any heavy-lifting operations.
You can read more on the official documentation:enter link description here

Django: Reuse form fields without inheriting?

If I have two forms, based on different base classes (say, Form and ModelForm), but I want to use a few fields in both, can I reuse them in a DRY way?
Consider the following scenario:
class AfricanSwallowForm(forms.ModelForm):
airspeed_velocity = forms.IntegerField(some_important_details_here)
is_migratory = forms.BooleanField(more_important_details)
class Meta:
model = AfricanBird
class EuropeanSwallowForm(forms.Form):
airspeed_velocity = forms.IntegerField(some_important_details_here)
is_migratory = forms.BooleanField(more_important_details)
....is there a way I can just reuse the fields airspeed_velocity and is_migratory? Imagine I have a couple dozen of these type of forms. The code will be soaking if I write these over and over again.
(Assume, for the purposes of this question, that I can't or won't turn airspeed_velocity and is_migratory into fields of the model AfricanBird.)
You could use multiple inheritance aka mixins, to factor out the fields that are used in both Form and ModelForm.
class SwallowFormFields:
airspeed_velocity = forms.IntegerField( ... )
is_migratory = forms.BooleanField( ... )
class AfricanSwallowForm(forms.ModelForm, SwallowFormFields):
class Meta:
model = AfricanBird
class EuropeanSwallowForm(forms.Form, SwallowFormFields):
pass
UPDATE:
Since this does not work with Django metaprogramming, you either need to create a custom __init__ constructor that adds the inherited fields to the object's fields list or you can add the references explicitly inside the class definition:
class SwallowFormFields:
airspeed_velocity = forms.IntegerField()
is_migratory = forms.BooleanField()
class AfricanSwallowForm(forms.ModelForm):
airspeed_velocity = SwallowFormFields.airspeed_velocity
is_migratory = SwallowFormFields.is_migratory
class Meta:
model = AfricanSwallow
class EuropeanSwallowForm(forms.Form):
airspeed_velocity = SwallowFormFields.airspeed_velocity
is_migratory = SwallowFormFields.is_migratory
UPDATE:
Of course you don't have to nest your shared fields into a class -- you could also simply define them as globals ...
airspeed_velocity = forms.IntegerField()
is_migratory = forms.BooleanField()
class AfricanSwallowForm(forms.ModelForm):
airspeed_velocity = airspeed_velocity
is_migratory = is_migratory
class Meta:
model = AfricanSwallow
class EuropeanSwallowForm(forms.Form):
airspeed_velocity = airspeed_velocity
is_migratory = is_migratory
UPDATE:
Okay, if you really want to DRY to the max, you have to go with the metaclasses.
So here is how you may do it:
from django.forms.models import ModelForm, ModelFormMetaclass
from django.forms.forms import get_declared_fields, DeclarativeFieldsMetaclass
from django.utils.copycompat import deepcopy
class MixinFormMetaclass(ModelFormMetaclass, DeclarativeFieldsMetaclass):
def __new__(cls, name, bases, attrs):
# default __init__ that calls all base classes
def init_all(self, *args, **kwargs):
for base in bases:
super(base, self).__init__(*args, **kwargs)
attrs.setdefault('__init__', init_all)
# collect declared fields
attrs['declared_fields'] = get_declared_fields(bases, attrs, False)
# create the class
new_cls = super(MixinFormMetaclass, cls).__new__(cls, name, bases, attrs)
return new_cls
class MixinForm(object):
__metaclass__ = MixinFormMetaclass
def __init__(self, *args, **kwargs):
self.fields = deepcopy(self.declared_fields)
You can now derive your collections of formfields from MixinForm like this:
class SwallowFormFields(MixinForm):
airspeed_velocity = forms.IntegerField()
is_migratory = forms.BooleanField()
class MoreFormFields(MixinForm):
is_endangered = forms.BooleanField()
Then add them to the list of base classes like this:
class EuropeanSwallowForm(forms.Form, SwallowFormFields, MoreFormFields):
pass
class AfricanSwallowForm(forms.ModelForm, SwallowFormFields):
class Meta:
model = AfricanSwallow
So what does it do?
The metaclass collects all the fields declared in your MixinForm
It then adds custom __init__ constructors, to make sure that the __init__ method of the MixinForm gets magically called. (Otherwise you would have to call it explicitly.)
MixinForm.__init__ copies the declared fields int the field attribute
Please note that I am neither a Python guru nor a django developer, and that metaclasses are dangerous. So if you encounter weird behaviour better stick with the more verbose approach above :)
Good Luck!
How about a factory-style approach?
def form_factory(class_name, base, field_dict):
always_has = {
'airspeed_velocity': forms.IntegerField(some_important_details_here),
'is_migratory': forms.BooleanField(more_important_details)
}
always_has.update(field_dict)
return type(class_name, (base,), always_has)
def meta_factory(form_model):
class Meta:
model = form_model
return Meta
AfricanSwallowForm = form_factory('AfricanSwallowForm', forms.ModelForm, {
'other' = forms.IntegerField(some_important_details_here),
'Meta': meta_factory(AfricanBird),
})
EuropeanSwallowForm = form_factory('EuropeanSwallowForm', forms.Form, {
'and_a_different' = forms.IntegerField(some_important_details_here),
})
For that matter, you could modify the factory function here to look into an existing form class and pick out the attributes you want, so that you don't lose the declarative syntax...
class SwallowForm(forms.Form):
airspeed_velocity = forms.IntegerField()
is_migratory = forms.BooleanField()
class AfricanSwallowForm(forms.ModelForm, SwallowForm):
class Meta:
model = AfricanSwallow
class EuropeanSwallowForm(forms.Form, SwallowForm):
...
Should work.
I have some long running code that works and has the fields attr that looks like this.
languages_field = forms.ModelMultipleChoiceField(
queryset=Language.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False
)
class FooLanguagesForm(forms.ModelForm):
languages = languages_field
class Meta:
model = Foo
fields = ('languages', )
Notice that I still use the fields tuple in Meta. The field shows up in the fields dict on the form instance whether or not it is in the Meta.fields.
I have a regular form that uses this pattern as well:
genres_field = forms.ModelMultipleChoiceField(
queryset=blah,
widget=forms.CheckboxSelectMultiple,
#required=False,
)
class FooGenresForm(forms.Form):
genres = genres_field
I see that my fields dict is working.
In [6]: f = FooLanguagesForm()
In [7]: f.fields
Out[7]: {'languages': <django.forms.models.ModelMultipleChoiceField object at 0x1024be450>}
In [8]: f2 = FooGenresForm()
In [9]: f2.fields
Out[9]: {'genres': <django.forms.models.ModelMultipleChoiceField object at 0x1024be3d0>}
Create a subclass of the IntegerField
class AirspeedField(forms.IntegerField):
def __init__():
super(AirspeedField, self).__init__(some_important_details_here)
I have just made a snippet that resolves this issue in a DRY way:
https://djangosnippets.org/snippets/10523/
It uses crispy-form, but the same idea can be used without crispy-forms. The idea is to use multiple forms under the same form tag.