Django Rest Framework SerializerMethodField only on GET Request - django

Running into a little snag here with my DRF backend.
I am populating fields with choices on certain models.
I have a foreign key requirement on one model. When I create the model I want to save it under the foreign id.
When I request the models, I want the model with whatever the choice field maps to.
I was able to do this with SerializerMethodField, however when I try to create a model, I get a 400 error because the block is not valid. If I remove the SerializerMethodField, I can save, but get the number stored in the db from the request.
Any help would be appreciated.
class BlockViewSet(ModelViewSet):
model = apps.get_model('backend', 'Block')
queryset = model.objects.all()
serializer_class = serializers.BlockSerializer
permissions = ('All',)
def create(self, request, format=None):
data = request.data
data['user'] = request.user.id
data['goal'] = WorkoutGoal.objects.get(goal=data['goal']).id
block = serializers.BlockSerializer(data=data, context={'request': request})
if block.is_valid():
new_block = block.save()
return Response({'block': {'name': new_block.name, 'id': new_block.id}}, status=status.HTTP_201_CREATED)
else:
return Response(block.errors, status=status.HTTP_400_BAD_REQUEST)
class WorkoutGoalSerializer(serializers.ModelSerializer):
class Meta:
model = apps.get_model('backend', 'WorkoutGoal')
fields = ('goal',)
goal = serializers.SerializerMethodField(read_only=True, source='get_goal')
def get_goal(self, obj):
return dict(WorkoutGoal.GOALS).get(obj.goal)
class BlockSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
goal = WorkoutGoalSerializer()
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
The above code returns the correct choice, but I can't save under it. Remove the goal = WorkoutGoalSerializer() and it saves but doesn't return the mapped choice.

I think this will work like a charm,
class WorkoutGoalSerializer(serializers.ModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'request' in self.context and self.context['request'].method == 'GET':
self.fields['goal'] = serializers.SerializerMethodField(read_only=True, source='get_goal')
class Meta:
model = apps.get_model('backend', 'WorkoutGoal')
fields = ('goal',)
goal = serializers.SerializerMethodField(read_only=True, source='get_goal') # remove this line
def get_goal(self, obj):
return dict(WorkoutGoal.GOALS).get(obj.goal)
How this Work?
It will re-initiate the goal field with SerializerMethodField, if the reuested method is GET.
Remember one thing, you should remove the line,
goal = serializers.SerializerMethodField(read_only=True, source='get_goal')

serializers.py
class BlockCreateSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
class BlockSerializer(serializers.ModelSerializer):
workout_count = serializers.IntegerField(required=False)
completed_workouts = serializers.IntegerField(required=False)
goal = WorkoutGoalSerializer()
class Meta:
model = apps.get_model('backend', 'Block')
read_only_fields = ('workout_count', 'completed_workouts')
fields = read_only_fields + ('id', 'name', 'user', 'created', 'goal')
views.py
class BlockViewSet(ModelViewSet):
model = apps.get_model('backend', 'Block')
queryset = model.objects.all()
serializer_class = serializers.BlockSerializer
permissions = ('All',)
def get_serializer_class(self):
if self.action == 'create':
return serializers.BlockCreateSerializer
else:
return self.serializer_class
def create(self, request, format=None):
data = request.data
data['user'] = request.user.id
data['goal'] = WorkoutGoal.objects.get(goal=data['goal']).id
block = self.get_serializer(data=data)
if block.is_valid():
new_block = block.save()
return Response({'block': {'name': new_block.name, 'id': new_block.id}}, status=status.HTTP_201_CREATED)
else:
return Response(block.errors, status=status.HTTP_400_BAD_REQUEST)
override get_serializer_class to return different serializer_class for create and other action(list\retrieve\update\partial_update)

Related

Update field without rest API

I have a model.py
class UserPaymentInformation(models.Model):
...
awaiting_confirmation = models.BooleanField(default=False)
I want to make awaiting_confirmation = True in code. But forbid awaiting_confirmation update via RestAPI call.
views.py
class UserPaymentInformationUpdateAPIView(generics.UpdateAPIView):
permission_classes = (IsAuthenticatedDriver,)
serializer_class = UserPaymentInformationUpdateSerializer
queryset = serializer_class.Meta.model.objects.all()
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.serializer_class.Meta.model.objects.get(
user=self.request.user
)
self.mark_user_as_new()
# awaiting_confirmation = True # I WANT SOMETHING LIKE THIS
serializer = self.get_serializer(
instance, data=request.data, partial=partial
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response({"result": serializer.data})
serializers.py
class UserPaymentInformationUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = UserPaymentInformation
fields = ("id", "full_name", "card_number", "account_number", "bik", "awaiting_confirmation")
How can I fix update method?
In your UserPaymentInformationUpdateSerializer you could set the read_only_fields = ('awaiting_confirmation',), so your serializer would become:
class UserPaymentInformationUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = UserPaymentInformation
fields = ("id", "full_name", "card_number", "account_number", "bik", "awaiting_confirmation")
read_only_fields = ("awaiting_confirmation",)
This would mean it'd still be returned in the serializer data but it would not be possible to update it through an API request.

How to properly serialize an object whose model has a foreign key which also takes an ImageField?

# models.py
class Post(models.Model):
content = models.TextField(blank=True, default='')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class PostImage(models.Model):
image = models.ImageField(upload_to=unique_upload)
post = models.ForeignKey(
Post, related_name='images', on_delete=models.CASCADE)
This is my model set up for a basic scenario where a user can enter content or upload images as their posts.
I want to bundle my logic to handle creating a post with either content or images or both.
I first started playing around with GenericViewSet and CreateViewSet but was images was never being passed to my serializer.
# views.py
class CreatePostViewSet(generics.CreateAPIView /* viewsets.GenericViewSet */):
permission_classes = (IsAuthenticated,)
queryset = Post.objects.order_by('id')
serializer_class = CreatePostSerializer
def create(self, request, *args, **kwargs):
data = {}
print(request.data)
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save(created_by=request.user)
# post = serializer.instance
# print(post)
# for im in post.images.all():
# im.save(post=post)
# print(post.images.all())
return Response(data,
status=status.HTTP_201_CREATED,
headers=self.get_success_headers(serializer.data))
# serializers.py
class PostImageSerializer(serializers.ModelSerializer):
class Meta:
model = PostImage
fields = ('id', 'url', 'image', 'post',)
read_only_fields = ('post',)
depth = 1
class CreatePostSerializer(serializers.ModelSerializer):
images = PostImageSerializer(many=True, required=False)
class Meta:
model = Post
fields = ('id', 'url', 'content', 'images',)
read_only_fields = ('created_by',)
depth = 1
def create(self, validated_data):
# validated_data['images'] is always []
print(validated_data)
raise
images is always [] when I pass it to a serializer, but it does exist in request.data['images'] as [<TemporaryUploadedFile: 1 - 5H5hHgY.png (image/png)>, ...
I was hoping to use ModelSerializer to help auto-resolve the ImageField.
# CreatePostSerializer serializers breaks down to
CreatePostSerializer():
id = UUIDField(read_only=True)
url = HyperlinkedIdentityField(view_name='post-detail')
content = CharField(allow_blank=True, required=False, style={'base_template': 'textarea.html'})
images = PostImageSerializer(many=True, required=False):
id = UUIDField(read_only=True)
url = HyperlinkedIdentityField(view_name='postimage-detail')
image = ImageField(max_length=100)
post = NestedSerializer(read_only=True):
id = UUIDField(read_only=True)
content = CharField(allow_blank=True, required=False, style={'base_template': 'textarea.html'})
created_by = PrimaryKeyRelatedField(queryset=User.objects.all())
It think request.data['images'] will need to be changed slightly because your PostImageSerializer will be expecting an object containing the "image" key, whereas you are passing the list of TemporaryUploadedFile.
Given request.data['images'] you could do something like the following in your view before you pass the data to the serializer:
images_list: List[TemporaryUploadedFile] = request.data.pop("images")
images = []
for image in images_list:
images.append({
"image": image,
})
request.data["images"] = images
So we are transforming your list of TemporaryUploadedFiles into a list of objects with the image key.
:edit: So you don't want to transform your data at the view to be compatible with the serializer? Then you can change the serializer to be compatible with the data, this involves customizing the create and update methods, I'm just going to show you how to override the create method for now.
class CreatePostSerializer(serializers.ModelSerializer):
images = serializers.ImageField(many=True)
class Meta:
model = Post
fields = ('id', 'url', 'content', 'images',)
read_only_fields = ('created_by',)
depth = 1
def create(self, validated_data):
images = validated_data.pop("images")
post = super().create(validated_data)
for image in images:
serializer = PostImageSerializer(data={"image": image, "post": post.pk}, context=self.context)
serializer.is_valid()
serializer.save()
return post
So you don't want to override the data in the request and you don't want to customize the serializers create method? Change how the serializer converts your initial data into validated data with the validate method (I think this works for nested serializers but its untested):
class CreatePostSerializer(serializers.ModelSerializer):
images = PostImageSerializer(many=True, required=False)
class Meta:
model = Post
fields = ('id', 'url', 'content', 'images',)
read_only_fields = ('created_by',)
depth = 1
def validate(self, attrs):
images_list = attrs.pop("images")
images = []
for image in images_list:
images.append({
"image": image,
})
attrs["images"] = images
return attrs
So, I was able to get it to work with #ARJMP's suggestion.
# views.py
class CreatePostViewSet(generics.CreateAPIView):
# authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,)
queryset = Post.objects.order_by('id')
serializer_class = CreatePostSerializer
def create(self, request, *args, **kwargs):
data = {}
print(request.data)
images = [{'image': i} for i in request.data.pop('images', [])]
serializer = self.get_serializer(
data={'content': request.data['content'], 'images': images})
serializer.is_valid(raise_exception=True)
post = serializer.save(created_by=request.user)
# self.perform_create(serializer)
data['post'] = serializer.data
return Response(data,
status=status.HTTP_201_CREATED,
headers=self.get_success_headers(serializer.data))
# serializers.py
class CreatePostSerializer(serializers.ModelSerializer):
images = PostImageSerializer(many=True, required=False)
class Meta:
model = Post
fields = ('id', 'content', 'images',
'is_private', 'created_by',)
read_only_fields = ('view_count', 'created',)
depth = 1
def create(self, validated_data):
images = validated_data.pop('images', [])
p = Post.objects.create(**validated_data)
for im in images:
pi = PostImage.objects.create(image=im['image'], post=p)
return p
My thing is this seems rather convoluted to get it to work. A lot of manipulating it myself. I was really hoping to leverage more of the "magic" stuff that gets done with ModelSerializer and CreateAPIView.
Are there better approaches to doing this?

Changing Return Type on Creation in Serializer

I have the following two serializers:
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
from radio.models import Program
model = Program
fields = ('id', 'title')
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program', 'program_data',)
They are based on the following models:
class Program(models.Model):
title = models.CharField(max_length=64)
class UserRecentlyPlayed(models.Model):
user = models.ForeignKey(User)
program = models.ForeignKey(Program)
What I'm trying to do is the following: On create, I want to be able create a new instance of UserRecentlyPlayed in the following manner:
{
"user": "...user id ....",
"program": "....program id...."
}
However, when I return a list, I would like to return the following:
[{
"id": "... id .... ",
"user": ".... user id .....",
"program": {"id": "...program id...", "title": "...title..." }
}]
These are called in the following view:
class RecentlyPlayed(generics.ListCreateAPIView):
serializer_class = UserRecentlyPlayedSerializer
This, unfortunately is not working. What is the correct magic for this?
You can rename your program_data in your serializer to program or you can specify source for your nested serializer.
That should return the output of list as you'd like.
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program',)
or
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True, source='program')
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program_data',)
And to support same json input for create, the easiest way is create another serializer for input:
class UserRecentlyPlayedSerializerInput(serializers.ModelSerializer):
program = serializers.PrimaryKeyRelatedField(queryset=Program.objects.all())
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program',)
And use it in your view when request is POST/PUT/PATCH:
class RecentlyPlayed(generics.ListCreateAPIView):
serializer_class = UserRecentlyPlayedSerializer
def get_serializer_class(self):
if self.request.method.lower() == 'get':
return self.serializer_class
return UserRecentlyPlayedSerializerInput
While this works great for a "get", I would like to see that same
result after a create. I still see {"program": "...id...."
For this, you have to change slightly the implementation of create method in your view
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = serializer.save()
headers = self.get_success_headers(serializer.data)
oser = UserRecentlyPlayedSerializer(instance)
return Response(oser.data, status=status.HTTP_201_CREATED,
headers=headers)
Firstly create a property named program_data in your model
class Program(models.Model):
title = models.CharField(max_length=64)
class UserRecentlyPlayed(models.Model):
user = models.ForeignKey(User)
program = models.ForeignKey(Program)
#property
def program_data(self):
return self.program
Then in your serializer you do not need to change anything following, it will remain same as below
class ProgramSerializer(serializers.ModelSerializer):
class Meta:
from radio.models import Program
model = Program
fields = ('id', 'title')
class UserRecentlyPlayedSerializer(serializers.ModelSerializer):
program_data = ProgramSerializer(read_only=True)
class Meta:
model = UserRecentlyPlayed
fields = ('id', 'user', 'program', 'program_data',)
Ok, I went in a slightly different direction and it works. Instead of using the ListCreateAPIView, I created my own class using ListModeMixin, CreateModelMixin and GenericAPIView. The magic was in overriding the def list class. I also implemented a "return_serializer_class" attribute. That's what did it.
class RecentlyPlayed(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
serializer_class = UserRecentlyPlayedSerializer
return_serializer_class = ProgramSerializer
parser_classes = (JSONParser, MultiPartParser)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.create(request, *args, **kwargs)
return self.list(request, *args, **kwargs)
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.return_serializer_class(queryset, many=True)
return Response({'recently_played': serializer.data})

How to save a modelSerializer that has relations? - django

I want to save a sent json data to db by django-rest-framework.
the problem is, not saving the relation and returns error.
The bellow snippet is my models:
class Profile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='profile', on_delete=models.CASCADE)
name = models.CharField(max_length=30)
family = models.CharField(max_length=50)
class Klass(models.Model):
title = models.CharField(max_length=50)
description = models.CharField(max_length=500)
teacher = models.ForeignKey(Profile, related_name='teacher', on_delete=models.CASCADE)
I use below serializer for serializing/deserializing the Klass model.
class ProfileSerializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('pk', 'name', 'family')
class KlassSerializer(serializers.ModelSerializer):
teacher = ProfileSerializer()
class Meta:
model = Klass
fields = ('id', 'title', 'description', 'teacher')
now when I prepare a JSON object and send it to the view, it returns error. the below is the view class:
class KlassView(APIView):
"""for SELECT, INSERT Queries"""
def get(self, request, pk):
# somthing
#csrf_exempt
def post(self,request, pk=None):
"""For Creating A Class"""
serializer = KlassSerializer(data=request.data)
if serializer.is_valid():
teacher = ProfileSerializer(request.data['teacher']['pk'])
serializer.teacher = teacher.data
serializer.save()
return Response({'data': serializer.data})
else:
return Response({'data': serializer.errors})
and the error is:
The .create() method does not support writable nested fields by default.
Write an explicit .create() method for serializer mainp.serializers.KlassSerializer, or set read_only=True on nested serializer fields.
How can I save relation in KlassSerializer in order to save to db?
At first change your serializer like below:
class KlassSerializer(serializers.ModelSerializer):
# teacher = ProfileSerializer() # No need to this!
class Meta:
model = Klass
# fields = ('id', 'title', 'description', 'teacher')
fields = ('id', 'title', 'description') # Omit teacher
Then get profile from requested user and pass it to your serializer:
def post(self,request, pk=None):
"""For Creating A Class"""
serializer = KlassSerializer(data=request.data)
if serializer.is_valid():
teacher = ProfileSerializer(request.data['teacher']['pk'])
serializer.teacher = teacher.data
serializer.save(teacher=request.user.profile) # Retrieve teacher and stroe
return Response({'data': serializer.data})
else:
return Response({'data': serializer.errors})
Just override the create method of ModelSerializer in KlassSerializer.
class KlassSerializer(serializers.ModelSerializer):
teacher = ProfileSerializer()
class Meta:
model = Klass
fields = ('id', 'title', 'description', 'teacher')
def create(self, validated_data):
profile = Profile.objects.filter(pk=validated_data['teacher']['pk'])
if profile:
k = Klass()
k.teacher = profile
...

Serializing Foreign Key Objects with Django-HVAD TranslatedFields

I see there are several questions on SO about serialization on Django already, but I'm having additional complexity because my fields are translated (using django-hvad).
What I have kinda works but I imagine it's horribly inefficient, and since I'm new to Django, am wondering if there's a better way.
What I'm doing now is getting each one of the three models I need (Survey, SurveyQuestion, SurveyAnswer), serializing each individually, and then knitting them together before converting to JSON.
The effect of this is working, as seen in the django shell:
>>> qs = models.SurveyQuestion.objects.language('en').all().filter(survey=1)
>>> for q in qs:
... res.append({'question': q, 'answers' : models.SurveyAnswer.objects.language('en').all().filter(question=q.pk)})
...
>>> res = []
>>> res
[{'question': <SurveyQuestion: Who is the best Beatle?>, 'answers': [<SurveyAnswer: Paul McCartney>, <SurveyAnswer: George Harrison>, <SurveyAnswer: Ringo Starr>]}, {'question': <SurveyQuestion: Which album from The Beatles was the best?>, 'answers': [<SurveyAnswer: Yellow Submarine>, <SurveyAnswer: Revolver>, <SurveyAnswer: The White Album>]}]
Here's the relevant code. Let me know if you need to see more:
views.py
class SurveyDetail(APIView):
"""
Retrieve, update or delete a survey instance.
"""
def get_object(self, pk):
try:
user_language = self.request.GET.get('language')
return models.Survey.objects.language(user_language).get(pk=pk)
except models.Survey.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
def get_related_questions(self, pk):
try:
user_language = self.request.GET.get('language')
return models.SurveyQuestion.objects.language(user_language).all().filter(survey=pk)
except models.SurveyAnswer.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
def get_related_answers(self, pk):
try:
user_language = self.request.GET.get('language')
return models.SurveyAnswer.objects.language(user_language).all().filter(question=pk)
except models.SurveyAnswer.DoesNotExist:
return HttpResponse(status=status.HTTP_404_NOT_FOUND)
def get(self, request, pk, format=None):
survey = self.get_object(pk)
questions = self.get_related_questions(pk)
res = []
for q in questions:
res.append({
'question': SurveyQuestionSerializer(q).data,
'answers' : SurveyAnswerSerializer(self.get_related_answers(q.pk), many=True).data
})
resp_obj = {
'survey' : SurveySerializer(survey).data,
'data' : res
}
return JSONResponse(resp_obj)
def put(self, request, pk, format=None):
survey = self.get_object(pk)
serializer = SurveySerializer(survey, data=data)
if serializer.is_valid():
serializer.save()
return JSONResponse(serializer.data)
return JSONResponse(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
survey = self.get_object(pk)
survey.delete()
return HttpResponse(status=status.HTTP_204_NO_CONTENT)
serializers.py
class SurveySerializer(TranslatableModelSerializer):
class Meta:
model = models.Survey
fields = ['pk', 'title', 'description', 'slug']
class SurveyQuestionSerializer(TranslatableModelSerializer):
# survey = SurveySerializer(required=False)
class Meta:
model = models.SurveyQuestion
fields = ['pk', 'title', 'content', 'slug', 'survey', 'is_multi_select', 'has_other_field', 'required']
class SurveyAnswerSerializer(TranslatableModelSerializer):
# question = SurveyQuestionSerializer(required=False)
class Meta:
model = models.SurveyAnswer
fields = ['pk', 'title', 'slug', 'question']
I figured it out myself. Apparently, it's no different than on untranslated models, I just didn't have it right before.
Below is the corrected code which works properly. Note the order of each of the serializers and also the inclusion of the reverse foreign key fields as serializers.
serializers.py
class SurveyAnswerSerializer(TranslatableModelSerializer):
class Meta:
model = models.SurveyAnswer
fields = ['pk', 'title', 'slug', 'question']
class SurveyQuestionSerializer(TranslatableModelSerializer):
answers = SurveyAnswerSerializer(many=True)
class Meta:
model = models.SurveyQuestion
fields = ['pk', 'title', 'content', 'slug', 'survey', 'is_multi_select', 'type', 'scale_min','scale_max', 'has_other_field', 'required', 'answers']
class SurveySerializer(TranslatableModelSerializer):
questions = SurveyQuestionSerializer(many=True)
class Meta:
model = models.Survey
fields = ['pk', 'title', 'description', 'slug', 'questions']
views.py
class SurveyDetail(APIView):
...
def get(self, request, pk, format=None):
survey = self.get_object(pk)
serializer = SurveySerializer(survey)
return JSONResponse(serializer.data)
Edit:
turns out this doesn't completely work. In this scenario, the Survey objects are correctly translated, but the objects referenced by the foreign keys, like SurveyQuestion and SurveyAnswer, are not translated. This means you get a mix of translated and untranslated results.