So I have a Django project with Django REST Framework with large number of models. For frontend to be user friendly I should display not only related object's id but also name. My idea for the solution was to replace all the PrimaryKeyRelated fields with StringRelatedFields in serializers on response. As the number of models is large I decided to make a single abstract serializer/mixin and intercept field creation replacing the field if is of correct type. This is how far I got up to now:
class AbstractSerializer(serializers.ModelSerializer):
class Meta:
model: AbstractModel = AbstractModel
read_only_fields: list = [
'created_at',
'created_by',
'modified_at',
'modified_by',
'is_deleted',
'deleted_at',
'deleted_by'
] + ['is_active'] if 'is_active' in [field.attname for field in model._meta.fields] else []
abstract: bool = True
def to_representation(self, instance):
serializer = AbstractRequestResponseSerializer(instance)
return serializer.data
class AbstractRequestResponseSerializer(AbstractSerializer):
class Meta(AbstractSerializer.Meta):
pass
#classmethod
def _get_declared_fields(cls, bases, attrs):
fields = [(field_name, attrs.pop(field_name))
for field_name, obj in list(attrs.items())
if isinstance(obj, Field)]
fields.sort(key=lambda x: x[1]._creation_counter)
new_fields = []
for field in fields:
if isinstance(field, PrimaryKeyRelatedField):
field = StringRelatedField(source=field.source, required=False)
new_fields.append(field)
fields = new_fields
known = set(attrs)
def visit(name):
known.add(name)
return name
base_fields = [
(visit(name), f)
for base in bases if hasattr(base, '_declared_fields')
for name, f in base._declared_fields.items() if name not in known
]
return OrderedDict(base_fields + fields)
This gives an infinite loop error because of __new__ method and I started to wonder if I am overriding the right function. I also tried to replace to_representation function but I guess that function occurs too late in the flow when all the field instances are created already. Which function should I override?
class ParentModelSerializer(serializers.ModelSerializer):
class Meta:
model = ParentModel
fields = '__all__'
class ChildModelSerializer(serializers.ModelSerializer):
parent = ParentModelSerializer(read_only=True)
class Meta:
model = ChildModel
fields = '__all__'
Or if you want to display the children in your parent:
class ParentModelSerializer(serializers.ModelSerializer):
children = ChildModelSerializer(read_only=True, many=True)
# children is the "related_name"
class Meta:
model = ParentModel
fields = '__all__'
class ChildModelSerializer(serializers.ModelSerializer):
class Meta:
model = ChildModel
fields = '__all__'
Maybe I phrased the question incorrectly (you could rephrase that to help future generations :) ), but the solution I made looks like this:
class AbstractSerializer(serializers.ModelSerializer):
class Meta:
model: AbstractModel = AbstractModel
read_only_fields: list = [
'created_at',
'created_by',
'modified_at',
'modified_by',
'is_deleted',
'deleted_at',
'deleted_by'
] + ['is_active'] if 'is_active' in [field.attname for field in model._meta.fields] else []
abstract: bool = True
def to_representation(self, instance):
ret = OrderedDict()
fields = self._readable_fields
for field in fields:
if isinstance(field, PrimaryKeyRelatedField):
parent = field.parent
field_name = field.field_name
source = field.source
if source != field_name:
field = StringRelatedField(source=field.source, required=False)
else:
field = StringRelatedField(required=False)
field.bind(field_name, parent)
try:
attribute = field.get_attribute(instance)
except SkipField:
continue
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
if check_for_none is None:
ret[field.field_name] = None
else:
ret[field.field_name] = field.to_representation(attribute)
return ret
Related
I am using DRF to get and create data from and to the API. I was struggling with a model Question and a attribute category which is a model too. So in order to create and read data I had to implement this question's answer method. Therefore, whenever I create a question, its category is an integer but when I read it, is an object.
Whenever I use the default API route I can create and read the data, but I am getting the following error whenever I write a different route:
AttributeError: Got AttributeError when attempting to get a value for field category on serializer QuestionSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the QuerySet instance.
Original exception text was: 'QuerySet' object has no attribute 'category'.
This is my custom code, where something is wrong...:
class UserQuestions(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
questions = Question.objects.filter(created_by=request.user.id).all()
data = QuestionSerializer(questions).data
return Response({
'questions': data
})
Just in case, this is my answer's implementation:
class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.serializer = kwargs.pop('serializer', None)
if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
raise TypeError('"serializer" no es una clase serializer vĂ¡lida')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False if self.serializer else True
def to_representation(self, instance):
if self.serializer:
return self.serializer(instance, context=self.context).data
return super().to_representation(instance)
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
class QuestionSerializer(serializers.ModelSerializer):
category = RelatedFieldAlternative(queryset=Category.objects.all(), serializer=CategorySerializer)
answers = AnswerSerializer(many=True, source='answer_set', allow_null=True, required=False)
created_by = UserSerializer(required=False)
def to_representation(self, instance):
response = super().to_representation(instance)
response['category'] = CategorySerializer(instance.category).data
return response
class Meta:
model = Question
fields = '__all__'
And this is the Question model:
class Question(models.Model):
id = models.AutoField(primary_key=True)
category = models.ForeignKey(Category, on_delete=models.CASCADE, null=True)
question = models.CharField(max_length=1000)
...
So, what am I doing wrong?
First of all, you are passing multiple items to serializer so you should use many=True. So in your case, it will be
data = QuestionSerializer(questions, many=True).data
Second, you don't need relatedFieldAlternative. You can just use a nested serailizer.
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = '__all__'
class QuestionSerializer(serializers.ModelSerializer):
category = CategorySerializer()
class Meta:
model = Question
fields = '__all__'
Here I have a model which has so many fields. So I want to use __all__ to return all the fields. But now I needed to add new field image_url so I customize a serializer like this but now with this I need to put all the model fields in the Meta class like this fields=['name','..', 'image_url'] in order to return the image_url.
Is there any way to return image_url without specifying it in the Meta.fields ?
I mean I don't want to write all the model fields in the Meta.fields (since the fields are too many) and want to return the image_url also.
serializers.py
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField('get_image_url')
class Meta:
model = MyModel
fields = '__all__'
def get_image_url(self, obj):
return obj.image.url
You can try to subclass te serializer:
class MySerializer(ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
class MyChildSerializer(MySerializer):
image_url = serializers.SerializerMethodField()
class Meta:
fields = MySerializer.Meta.fields + ['image_url']
def get_image_url(self, obj):
return obj.image.url
Never tried something like this, but since Meta.fields is a list you can perform basic python operations on it.
ps. If you're using pattern get_<field_name> for getter, you do not need to specify it in SerializerMethodField arguments.
Try this:
class MySerializer(ModelSerializer):
image_url = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = [f.name for f in MyModel._meta.fields] + ['image_url']
def get_image_url(self, obj):
return obj.image.url
Is there a way to pass paremeters to a Django Rest Framework's SerializerMethodField?
Assume I have the models:
class Owner(models.Model):
name = models.CharField(max_length=10)
class Item(models.Model):
name = models.CharField(max_length=10)
owner = models.ForeignKey('Owner', related_name='items')
itemType = models.CharField(max_length=5) # either "type1" or "type2"
What I need is to return an Owner JSON object with the fields: name, type1items, type2items.
My current solution is this:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = models.Item
fields = ('name', 'itemType')
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type1")
return ItemSerializer(queryset, many=True).data
def getType2Items(self, ownerObj):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType="type2")
return ItemSerializer(queryset, many=True).data
This works. But it would be much cleaner if I could pass a parameter to the method instead of using two methods with almost the exact code. Ideally it would look like this:
...
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getItems', "type1")
type2items = serializers.SerializerMethodField(method_name='getItems', "type2")
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
In the docs SerializerMethodField accepts only one parameter which is method_name.
Is there any way to achieve this behaviour using SerializerMethodField? (The example code here is overly simplified so there might be mistakes.)
There is no way to do this with the base field.
You need to write a custom serializer field to support it. Here is an example one, which you'll probably want to modify depending on how you use it.
This version uses the kwargs from the field to pass as args to the function. I'd recommend doing this rather than using *args since you'll get more sensible errors, and flexibility in how you write your function/field definitions.
class MethodField(SerializerMethodField):
def __init__(self, method_name=None, **kwargs):
# use kwargs for our function instead, not the base class
super().__init__(method_name)
self.func_kwargs = kwargs
def to_representation(self, value):
method = getattr(self.parent, self.method_name)
return method(value, **self.func_kwargs)
Using the field in a serializer:
class Simple(Serializer):
field = MethodField("get_val", name="sam")
def get_val(self, obj, name=""):
return "my name is " + name
>>> print(Simple(instance=object()).data)
{'field': 'my name is sam'}
You could just refactor what you have:
class OwnerSerializer(serializers.ModelSerializer):
type1items = serializers.SerializerMethodField(method_name='getType1Items')
type2items = serializers.SerializerMethodField(method_name='getType2Items')
class Meta:
model = models.Owner
fields = ('name', 'type1items', 'type2items')
def getType1Items(self, ownerObj):
return getItems(ownerObj,"type1")
def getType2Items(self, ownerObj):
return getItems(ownerObj,"type2")
def getItems(self, ownerObj, itemType):
queryset = models.Item.objects.filter(owner__id=ownerObj.id).filter(itemType=itemType)
return ItemSerializer(queryset, many=True).data
Roughly said, I have the following schema in ORM:
class Page(models.Model):
title = models.CharField(max_length=255, null=False, blank=False)
#property
def content(self):
return [Video.objects.all()[0], Text.objects.all()[0], Video.objects.all()[1]]
and I have the following set of classes to support serialization for detailed view:
class ContentSerializer(serializers.ListSerializer):
class Meta:
model = ???
fields = '???'
class PageDetailSerializer(serializers.ModelSerializer):
content = ContentSerializer(many=True)
class Meta:
model = Page
fields = ('title', 'content', )
So I'm looking for a way to serialize that Page.content property - which is:
a list;
will contain heterogeneous data (combination of, let's say Video, Audio, Text and other models.
So I need somehow patch one of builtin serializers to iterate thru the list and check type of each object. And then decide how to serialize each one. E.g. I could prepare kind of dynamically created ModelSerializer with:
obj_type = type(obj)
class ContentModelSerializer(serializers.ModelSerializer):
class Meta:
model = obj_type
fields = '__all__'
serialized_obj = ContentModelSerializer(obj)
How could I implement that?
You can simply achieve this by overriding the to_representation method of Page serializer. like this:
class PageDetailSerializer(serializers.ModelSerializer):
class Meta:
model = Page
fields = ('title', 'content', )
def to_representation(self, instance):
ctx = super(PageDetailSerializer, self).to_representation(instance)
content = instance.content # property field of page, will return list of items
serialized_content = []
for c in content:
if type(c) == Video:
serialized_content.append({... serialized data of video type ..})
elif type(c) == ...
# other conditions here..
I had googled a lot before found the solution. This article has a reference to SerializerMethodField, which let you add custom handler for a field. And the final solution, which worked for me is:
class PageDetailSerializer(serializers.ModelSerializer):
_cache_serializers = {}
content = serializers.SerializerMethodField()
class Meta:
model = Page
fields = ('title', 'content', )
def _get_content_item_serializer(self, content_item_type):
if content_item_type not in self._cache_serializers:
class ContentItemSerializer(serializers.ModelSerializer):
class Meta:
model = content_item_type
exclude = ('id', 'page', )
self._cache_serializers[content_item_type] = ContentItemSerializer
return self._cache_serializers[content_item_type]
def get_content(self, page):
return [
self._get_content_item_serializer(type(content_item))(content_item).data for content_item in page.content
]
I have UpdateView from CBV:
class AuthorUpdate(UpdateView):
model = Author
fields = ['name']
How can I set different fields list for specific user type?
For example:
if request.user.pk == 1:
fields = ['name', 'job', ]
else:
fields = ['name', 'title', ]
Where can I override this?
One option to do this would be to define two different form classes, then override get_form_class. You can access the user with self.request.user, then return the appropriate form class.
class JobForm(forms.ModelForm):
class Meta:
fields = ['name', 'job']
class TitleForm(forms.ModelForm):
class Meta:
fields = ['name', 'title']
class AuthorUpdate(UpdateView):
model = Author
def get_form_class(self):
if self.request.user.pk == 1:
return JobForm
else:
return TitleForm
Another option is to set the fields based on your condition like so;
def get_form_class(self):
if condition1:
self.fields = ['field1', 'field2]
else:
self.fields = ['field3', 'field4']
return super().get_form_class()