I have some models that look like the following, where I would like to serialize Parent with a boolean field representing whether an associated Child reverse lookup exists.
class Parent(models.Model):
... some fields
class Child(models.Model):
parent_fk = models.OneToOneField(
"Parent",
on_delete=models.CASCADE,
related_name="child",
)
... some other fields
class ParentSerializer(serializers.ModelSerializer):
has_child = serializers.BooleanField()
class Meta:
model = Parent
fields = ["has_child", ... some fields]
These are the solutions I've tried and what I understand to be the problems with them
just add "child" to fields - this inserts the value of the foreign key?
use SerializerMethodField - triggers a DB lookup for each row?
Is there a clean way to implement this feature?
You can annotate the Parent with an extra attribute:
from django.db.models import Exists, OuterRef
Parent.objects.annotate(
has_child=Exists(Child.objects.filter(parent_fk_id=OuterRef('pk')))
)
You can add this as queryset to the ViewSet, ListAPIView, etc.
The Parent objects that arise from this queryset will have an extra attribute .has_child.
Related
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)
Say I have a serializer A
class SerializerA(ModelSerializer):
some_field = CharField()
some_other_field = CharField()
field_require_other_model = SerializerMethodField()
class Meta:
model = ModelA
fields = ('some_field', 'some_other_field', 'field_require_other_model')
def get_field_require_other_model(self, instance):
other_model_qs = ModelB.objects.filter(email=instance.email)
# say I want to get whatever that comes first
return other_model_qs.first().useful_info
As seen above, SerializerA uses ModelA for getting all the fields except that one in ModelB. I can get the info from ModelB doing what I did, but I don't know if this is the best way getting the data. I'm not sure if I need to hit database so many times or if there's a way to lazily evaluate it.
Also, what if I have another SerializerMethodField() that utilizes ModelB but for different info. Is this way still the best way to get the data?
How about using .annotate, annotating the other field onto modelA from modelB and then defining it as a charfield(or whatever the type is) on the serializer?
Something like
queryset = ModelA.objects.all().annotate(other_field_on_model_b=F('ModelB__other_field_on_model_b'))
then in the seralizer
class SerializerA(ModelSerializer):
some_field = CharField()
some_other_field = CharField()
other_field_on_model_b = CharField(required=False) #or whatever the field type is.
class Meta:
model = ModelA
fields = ('some_field', 'some_other_field', 'other_field_on_model_b')
Could do the annotation in get_queryset() or in the end point itself.
I have it almost working.
Models:
class Child(models.Model):
parent = models.ForeignKey('project.Parent')
name = models.CharField(max_length=100)
class Parent(models.Model):
text = models.CharField(max_length=100)
Resource:
class ParentResource(resources.ModelResource):
children = fields.Field(widget=widgets.ForeignKeyWidget(Parent))
class Meta:
model = Parent
use_transactions = True
fields = ('text', 'children__child__name')
Then the view calls the resource and downloads it. The issue is, name is blank. So, everything else works just fine, but I can't get child.name to show up. What am I missing?
First of all, widgets.ForeignKeyWidget is used by modelA to look up a related modelB that is a ForeignKey of modelA.
I.e. a ChildResource can use widgets.ForeignKeyWidget to look up a Parent, but not vice versa.
Doing it in reverse, i.e. to loop up and/or display some fields of a set of Childs (who has a ForeignKey Parent) from a ParentResource, you need to do something like this:
from import_export import fields, resources
from .models import Parent
class ParentResource(resources.ModelResource):
children = fields.Field()
class Meta:
model = Parent
use_transactions = True
fields = ('text', 'children')
def dehydrate_children(self, parent):
children = list(parent.child_set.all())
return ','.join([child.name for child in children])
Using the dehydrate() method. Then when you export ParentResource, you'll get a dataset object with a "children" key whose value is a comma-separated list of the names of its children.
Suppose ModelA and (ModelB1, ModelB2 but they don't have common ancestor) has manytomany relationships.
Now I have a through model ModelAtoB.
class ModelAToB(Model):
model_a = models.ForeignKey(ModelA)
content_type=models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
model_b = generic.GenericForeignKey('content_type', 'object_id')
I see ManyToManyField being used in the django doc, how can I use it in my case as well?
class ModelA(Model):
pass
class ModelB(Model): # hum.. there's no ModelB which is a common ancestor of ModelB1, ModelB2
model_a_list = models.ManyToManyField(ModelA, through='ModelAtoB')
You cannot use the ManyToManyField in your case however your through model (ModelAToB) is enough to store the relation.
If you want to query all related ModelBs models to a given ModelAs you would do it somehow like this:
related_b = ModelAToB.objects.filter(model_a=a).prefetch_related('model_b')
b_instances = [rel.model_b for rel in related_b]
If you want all related ModelAs to a given ModelB this would be done this way:
from django.contrib.contenttypes.models import ContentType
model_b_ct = ContentType.objects.get_for_model(b)
related_a = ModelAToB.objects.filter(object_id=b.id, content_type=model_b_ct)
a_instance = [rel.model_a for rel in related_a]
Making a model method might be a handy tweak.
class ModelB(Model)::
def model_a_list(self):
return ModelA.objects.filter(
modelatob__content_type__app_label=self._meta.app_label,
modelatob__content_type__model=self._meta.module_name,
modelatob__object_id=self.id
)
This returns a queryset so you still can do things like .count(), .filter() on it.
I want to perform a filter on a model and return all objects that have a specific attribute.
model.objects.filter(hasattr(model, 'attrname'))
This obviously doesn't work, but not sure how to efficiently implement something siilar.
Thanks
EDIT
An example of where I would use this is when a model is inherited from another
class model1(models.Model):
...
class model2(model1):
...
if I do a model1.objects.all() each of the returned objects that are in model2 will have an extra attribute
If the models are related, you can the isnull in the filter.
model1.objects.filter('related_name__field_name__isnull=False)
where related name is in for the foreign key in model2
For Example:
class Owner(models.Model):
user = models.CharField(max_length=10)
class Car(models.Model):
car_type = models.CharField(max_length=10)
owner = models.ForeignKey(Owner, related_name='cars',
on_delete=models.CASCADE)
For owners with cars:
owners = Owner.objects.filter(cars__id__isnull=False)
I just put it in a:
try:
....
except AttributeError:
....
The way I did it was to suppress the FieldError exception:
from django.core.exceptions import FieldError
from contextlib import suppress
with suppress(FieldError):
model.objects.filter(field_in_other_class=value)
hope that helps