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'
Related
Given the following models...
class Player(models.Model):
user = models.ForeignKey(User)
class Activity(models.Model):
player = models.ForeignKey(Player)
and these serializers...
class PlayerSerializer(serializers.ModelSerializer):
class Meta:
model = Player
fields = ['user']
class ActivitySerializer(serializers.ModelSerializer):
player = PlayerSerializer()
class Meta:
model = Activity
fields = ['player']
if I want to use django-rest-framework to list all the activities, I need to do something like this...
class ActivityViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Activity.objects.select_related("player__user") <--- this is needed because the activity serializer is going to serialize the player which has a user
serializer_class = ActivitySerializer
That's all fine. But then, every time I write a new view which, at some level, uses PlayerSerializer, I'm going to have to remember to do the proper select_related clause in order to keep the number of DB lookups low.
I'm going to end up writing a lot of views which have the same select_related clauses (mine are actually a LOT more complicated than this example) and, if the PlayerSerializer ever changes, I'm going to need to remember to change all the view lookups.
This doesn't seem very DRY to me and I feel like there must be a better way to do it. Have I missed something obvious?
Maybe having a base parent class like:
class BaseViewset(viewsets.ReadOnlyModelViewSet):
SELECT_RELATED_FIELD = None # for providing select related field in your queryset or maybe have a default value for this one (based on your use case)
def get_queryset(self):
if not self.SELECT_RELATED_FIELD or not isinstance(self.SELECT_RELATED_FIELD, str):
raise NotImplementedError("Ensure 'SELECT_RELATED_FIELD' is not empty/ Is instance of string")
return super().get_queryset().select_related(self.SELECT_RELATED_FIELD)
And then use this parent class in all of your views instead of ReadOnlyModelViewSet:
class ActivityViewSet(BaseViewset):
SELECT_RELATED_FIELD = "player__user"
queryset = Activity.objects.all()
serializer_class = ActivitySerializer
would be a good start (in all views that you have select_related part).
For a ForeignKey relationship, how can I change the serializer used based off some criteria?
# models.py
class Relationship(models.Model):
...
class Item(models.Model):
relationships = ForeignKey(Relationship)
class OddPKSerializer(serializers.Serializer):
...
class EvenPKSerializer(serializers.Serializer):
...
class ItemSerializer(serializer.Serializer):
# relationships = OddPKSerializer and/or EvenPKSerializer(many=True)
# depending on some criteria
class Meta:
model = Item
fields = ["relationships"]
# Where for example, OddPKSerializer is used if
# the specific relationship object has an odd pk
# and EvenPKSerializer is used for even pks
In my actual use case, I'm looking to serialize differently based on the ForeignKey object's class. (I'm using proxy models so a single ForeignKey field can point to different classes.)
I've tried using SerializerMethodField but that only seems to act on the "Item" object and not the "Relationship" objects that I'm looking to serialize.
This works:
class OddPKSerializer(serializers.Serializer):
...
class EvenPKSerializer(serializers.Serializer):
...
class SwitchSerializer():
item = serializers.SerializerMethodField()
def get_item(self, obj):
if obj.pk/2 == 0:
return EvenPKSerializer(obj).data
return OddPKSerializer(obj).data
class ItemSerializer():
item = SwitchSerializer(many=True)
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)
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.
I want to be able to make complex queries combining different custom manager functions from different Abstract classes.
My models are like these:
class GenderManager(models.Manager):
def male(self):
return self.filter(gender="M")
def female(self):
return self.filter(gender="F")
class SpeciesManager(models.Manager):
def lion(self):
return self.filter(species="L")
def tiger(self):
return self.filter(species="T")
class GenderModel(models.Model):
gender = models.CharField(max_length=1)
objects = GenderManager()
class Meta:
abstract = True
class SpeciesModel(models.Model):
species = models.CharField(max_length=1)
objects = SpeciesManager()
class Meta:
abstract = True
class Animal(GenderModel,SpeciesModel):
name = models.CharField(max_length=30)
age = models.DecimalField(max_digits=4, decimal_places=2)
The reason I want to split Gender and Species is that in my models sometimes I will need to inherit only from GenderModel and sometimes only from SpeciesModel.
In the cases where I want to inherit form both (like in Animal class), I would like to be able to make queries like this:
Animal.objects.male().tiger().filter(age__gte = 10.00)
But it doesn't work.
However, if I don't use custom manager functions it works:
Animal.objects.filter(gender="M").filter(species="T").filter(age__gte = 10.00)
How can I make it work with custom manager functions do make it DRY?
Thank you!
This can't be done due to how Managers actually work but there can be different ways to refactor your model and the logic, depending on the complexity and how independent the models actually need to be.
Since your manager is actually needed for the concrete class and not the abstract class, concanate both managers into a single manager, afterall you are filtering on the concrete class and not the Abstract.
class GenderModel(models.Model):
gender = models.CharField(max_length=1)
class Meta:
abstract = True
class SpeciesModel(models.Model):
species = models.CharField(max_length=1)
class Meta:
abstract = True
class AnimalManager(models.Manager):
def gender(self):
return self.filter(gender="M")
def female(self):
return self.filter(gender="F")
def lion(self):
return self.filter(species="L")
def tiger(self):
return self.filter(species="T")
def get_male_tigers(self):
return self.filter(species="T").filter(gender="M").all()
class Animal(GenderModel,SpeciesModel):
name = models.CharField(max_length=30)
age = models.DecimalField(max_digits=4, decimal_places=2)
objects = AnimalManager()
Then:
animals = Animal.objects.get_male_tigers()
Of course you can further refactor to your needs
objects, of course, can't be a reference to both GenderManager and SpeciesManager. Given how MRO works, in your example it is a SpeciesManager instance.
You can create a third manager:
class AnimalManager(GenderManager, SpeciesManager):
def male_and_tiger(self):
return self.male & self.tiger()
[all other combinations here]
class Animal(GenderModel,SpeciesModel):
[...]
objects = AnimalManager()
But really I think you are being possessed by the OOP inheritance demon :) A plain call to the orm is much better and more future proof (unless you want to add a new method to your SpeciesManager each time you add a new species)