How to add new serializer field along with the all model fields? - python

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

Related

DRF SerializerMethodField how to pass parameters

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

DRF: Conditially change serializer

I'm using Django 3.0 and I have a serializer using django-rest-framework. Let's say that for example I have a Forum object. Each forum has an owner that is a user.
In my GET /forums/ endpoint, I'd like to just have the owner_id. However, in my GET /forums/<forum_id>/ endpoint I'd like to return the entire embedded object.
Is there any way to have one serializer support both of these scenarios? If not, I would hate to have to make two serializers just to support this.
class ForumSerializer(serializers.ModelSerializer, compact=True):
if self.compact is False:
owner = UserSerializer(source='owner', read_only=True)
else:
owner_id = serializers.UUIDField(source='owner_id')
...
How can I achieve this compact thing?
class Meta:
fields = [...]
read_only_fields = ['owner', 'owner_id']
You can add a SerializerMethodField like this:
class ForumSerializer(serializers.ModelSerializer):
owner = serializer.SerializerMethodField()
def get_owner(self, obj):
if self.context['is_compact'] == True:
return obj.owner.pk
else:
return UserSerializer(obj.owner).data
class Meta:
model = YourModel
fields = '__all__'
# Usage in view
serializer = ForumSerializer(context={'is_compact':True})
I am passing is_compact value through serializer's extra context.
create two serializer classes
class ForumSerializerId(ModelSerializer):
class Meta:
model = Forum
fields = ['forum_id']
class ForumSerializerDetail(ModelSerializer):
class Meta:
model = Forum
on your view.py
forums(request):
forum_list = Forum.objects.all()
forum_serializer = ForumSerializerId(forum_list,many=True)
return Response({"form":forum_serializer.data})
forum_detail(request,pk):
forum = get_object_or_404(Forum,pk)
forum_serializer = ForumSerializerDetail(forum)
return Response({"form":forum_serializer.data})

Get Image Field Absolute Path in Django Rest Framework - Non Request Flows

I've a periodic celery task which needs to store representation of a object in a specific json field.
Here is the simplified model structure.
Parent <-- ChildWrapper <-- Child Image
So basically I've a 'ChildImage' model referring to 'ChildWrapper' which in turn refers to 'Parent'.
class Parent(TimeStampedModel):
label = models.CharField(max_length=30, unique=True)
live_content = JSONField(blank=True, null=True)
is_template = models.BooleanField(default=False)
reference_image = models.ImageField(upload_to=get_web_row_reference_image_path, blank=True, null=True)
# Around 8 Other Fields
def __str__(self):
return '%s' % self.label
class ChildWrapper(TimeStampedModel):
name = models.CharField(max_length=25, blank=True, null=True)
row = models.ForeignKey(Parent, on_delete=models.CASCADE, related_name='web_column')
order = models.PositiveIntegerField(default=0)
# Around 20 Other Fields
def __str__(self):
return '%s' % self.name
class ChildImage(TimeStampedModel):
image = models.ImageField(upload_to=get_web_image_path)
column = models.ForeignKey(ChildWrapper, on_delete=models.CASCADE, related_name='web_image')
# Around 10 Other Fields
def __str__(self):
return '%s' % self.column
This is the serializers defined for the models.
class ChildImageSerializer(serializers.ModelSerializer):
class Meta:
model = ChildImage
fields = '__all__'
class ChildWrapperSerializer(serializers.ModelSerializer):
web_image = ChildImageSerializer(read_only=True, many=True)
class Meta:
model = ChildWrapper
fields = '__all__'
class ParentSerializer(serializers.ModelSerializer):
web_column = ChildWrapperSerializer(many=True, read_only=True)
class Meta:
model = Parent
fields = '__all__'
Here is the periodic celery task which does the required
#app.task(bind=True)
def update_data(self):
# Get Parent By a condition.
parent = Parent.objects.filter(to_update=True).first()
parent.live_content = None
parent.live_content = ParentSerializer(parent).data
print(parent.live_content)
parent.save()
The above task gets output of child image something like this with imagefield being relative path instead of absolute path.
{
"id": 1
"image": '/api/col/info.jpg'
}
Is there any way to get the absolute path for the image field?
{
"id": 1
"image": "http://localhost:8000/admin/media/api/col/info.jpg"
}
PS:
I cannot pass Request context to serializer as ParentSerializer(parent, context={'request': request}) as there is no request object involved here.
I think you have two ways to resolve this.
First one, is to pass request. You can take this approach:
class ChildImageSerializer(serializers.ModelSerializer):
img_url = serializers.SerializerMethodField()
class Meta:
model = ChildImage
fields = '__all__'
def get_img_url(self, obj):
return self.context['request'].build_absolute_uri(obj.image.url)
class ChildWrapperSerializer(serializers.ModelSerializer):
web_image = serializers.SerializerMethodField()
class Meta:
model = ChildWrapper
fields = '__all__'
def get_web_image(self, obj):
serializer_context = {'request': self.context.get('request') }
children = ChildImage.objects.filter(row=obj)
serializer = ChildImageSerializer(children, many=True, context=serializer_context)
return serializer.data
class ParentSerializer(serializers.ModelSerializer):
web_column = serializers.SerializerMethodField()
class Meta:
model = Parent
fields = '__all__'
def get_web_column(self, obj):
serializer_context = {'request': self.context.get('request') }
children = ChildWrapper.objects.filter(row=obj)
serializer = ChildWrapperSerializer(children, many=True, context=serializer_context)
return serializer.data
Here I am using SerializerMethodField to pass the request on to the next serializer.
Second approach is to use Django Sites Framework(mentioned by #dirkgroten). You can do the following:
class ChildImageSerializer(serializers.ModelSerializer):
img_url = serializers.SerializerMethodField()
class Meta:
model = ChildImage
fields = '__all__'
def get_img_url(self, obj):
return 'http://%s%s%s' % (Site.objects.get_current().domain, settings.MEDIA_URL, obj.img.url)
Update: I totally missed the celery part. For production, I don't think you need to worry as they are in S3, the absolute path should be coming from obj.image.url. And in dev and stage, you can get the absolute path using the given example. So, try like this:
class ChildImageSerializer(serializers.ModelSerializer):
img_url = serializers.SerializerMethodField()
class Meta:
model = ChildImage
fields = '__all__'
def get_img_url(self, obj):
if settings.DEBUG: # debug enabled for dev and stage
return 'http://%s%s%s' % (Site.objects.get_current().domain, settings.MEDIA_URL, obj.img.url)
return obj.img.url
Alternatively, there is a way to get request using django-crequest in celery, but I am not sure if its convenient to you.
Got it working,
Added MEDIA_URL to my settings file as mentioned here.
It seems DRF uses MEDIA_URL as a default prefix for urls(FileField & ImageField), even for non request/response flows.
Since I had a different settings file for staging, development and production it was easier for me to set different URLs for each environment.
Even though I'm not using 'django-versatileimagefield' library, the suggestion there still worked.
I solved the problem by adding , context={'request': request} in view.
serializer = Business_plansSerializer(business_plans[start:end], many=True, context={'request': request})
Another solution is hard code the host:
from django.conf import settings
IMG_HOST = {
'/home/me/path/to/project': 'http://localhost:8000',
'/home/user/path/to/project': 'https://{AWS_HOST}',
}[str(settings.BASE_DIR)]
class ChildImageSerializer(serializers.ModelSerializer):
image = serializers.SerializerMethodField()
def get_image(self, obj):
if obj.image:
return IMG_HOST + obj.image.url
class Meta:
model = ChildImage
fields = '__all__'

DRF: Serializer for heterogneous list of related models

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
]

The proper Django way to make a form field required on CreateView, but optional on UpdateView?

Let's say I create an model MyModel instance that has a FileField using CreateView and associated MyModelCreateForm, and now I want to update it without uploading the same avatar image:
class MyModel(models.Model):
name = models.CharField()
avatar = models.ImageField()
class MyModelCreateForm(forms.ModeForm):
class Meta:
model = MyModel
exclude = None
class MyModelCreate(CreateView):
model = MyModel
form_class = MyModelCreateForm
class MyModelCreate(UpdateView):
model = MyModel
form_class = ?
On the CreateView's form the image is compulsory. If I want to make it optional on the update field, what's the most Django-ish way of doing this?
Do I need to make a new form for the UpdateView that inherits from MyModelCreateForm but overrides the ImageField required attribute? Or is there a more "batteries-included" way?
Here a solution to make a field required :
class Foo(CreateView):
model = Fiz
fields = ('bar',)
def get_form(self, form_class=None):
form = super(Foo, self).get_form(form_class)
form.fields['bar'].required = True
return form
i think no but you can implement in it !
class ModelXForm(ModelForm):
class Meta:
model = x
fields = ['image','name']
def __init__(self,update, survey, **kwargs):
super(FBRequestForm, self).__init__(**kwargs)
if not update:
self.fields['image'].widget.attrs['readonly'] = True
self.fields['image'].widget = forms.HiddenInput()

Categories

Resources