Should serializer be called multiple times? - django-rest-framework

So I have fairly many relations in models and now I'm trying to optimize db queries. One of the models i'm serializing is Thread model:
class Thread(models.Model):
participants = models.ManyToManyField(User, related_name='threads')
is_multi_participants = models.BooleanField(default=False)
creator = models.ForeignKey(User, verbose_name=_('creator'), on_delete=models.CASCADE)
last_sender = models.ForeignKey(User, verbose_name=_('last sender'), related_name='last_sender', on_delete=models.CASCADE)
prevent_reply = models.BooleanField(default=False, editable=False)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
title = models.CharField(max_length=1000, blank=True, null=True)
company = models.ForeignKey(to=Company, verbose_name=_('Company'),
on_delete=models.SET_NULL, null=True, blank=True)
class ThreadListSerializer(serializers.ModelSerializer):
subtitle = serializers.SerializerMethodField()
class Meta:
model = Thread
fields = ('id',
'subtitle',
)
def get_subtitle(self, obj):
print(traceback.extract_stack(None, 2)[0][2], f' {len(connection.queries)}')
return ''
class ThreadList(generics.ListAPIView):
pagination_class = ThreadPagination
def get_queryset(self):
**queryset filtering goes here**
return queryset
def list(self, request, *args, **kwargs):
queryset = self.get_queryset()
serializer = ThreadListSerializer(queryset, many=True, context={'request': request})
return Response(serializer.data)
Both view and serializer is simplified to debug one particular problem: while running this code in Pycharm (didn't matter in Debug or Run options) and making request from shell via curl, there are multiple calls of get_subtitle() function, thus resulting in many prints of to_representation *queries_number* to console. So the question is: is this a desired behavior? Should serializer be called as many times as queryset length?

Related

DRF - Add User to a model with ManyToManyField

I am trying to implement a feature to my backend and allow the owner of private "Group" to add other users by their usernames instead of ID's and allow them to add their images to FileField only once after they were added to the model. The code I have so far:
models.py
class Group(models.Model):
group_name = models.CharField(max_length=255)
group_text = models.TextField(max_length=360, blank=True)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL, related_name='owner_user', on_delete=models.SET(get_deleted_user), default=1)
created_on = models.DateTimeField(auto_now_add=True, null=True)
shared_to = models.ManyToManyField(UserProfile, blank=True, related_name='shared_to_user', null=True)
def __str__(self):
return self.group_name
def save(self, *args, **kwargs):
super(Group, self).save(*args, **kwargs)
class GroupImage(models.Model):
group_file = models.FileField(blank=True, null=True,
upload_to='media/covers/%Y/%m/%D/')
gallery_group = models.ForeignKey(Group, related_name='images', on_delete=models.CASCADE)
serializers.py
class GroupImageSerializer(serializers.ModelSerializer):
class Meta:
model = models.GroupImage
fields = ('group_file', )
class SharedToSerializer(serializers.ModelSerializer):
class Meta:
model = models.Group
fields = ('shared_to', )
class GroupSerializer(serializers.ModelSerializer):
images = GroupImageSerializer(many=True, read_only=True)
person = SharedToSerializer(many=True, read_only=True)
class Meta:
model = models.Group
fields = ('id', 'group_name', 'group_text', 'person', 'images')
def create(self, validated_data):
images_data = self.context.get('view').request.FILES
owner_id = self.context['request'].user.id
gallery_group = models.Group.objects.create(group_name=validated_data.get('group_name', 'no-
group_name'), group_text=validated_data.get('group_text'), owner_id=1)
for image_data in images_data.values():
models.GroupImage.objects.create(gallery_group=gallery_group,
group_file=image_data)
return gallery_group
views.py
class GroupCreateAPIView(generics.CreateAPIView):
queryset = models.Group.objects.all()
serializer_class = serializers.GroupSerializer
permission_classes = [AllowAny]
So if your only requirement is how to add users by their username and not their id. You should use SlugRelatedField. I also feel your serializer naming convention is quite confusing. Below is the serializer for Group model that can add users to a group.
class GroupSerializer(Serializer):
... other fields here
shared_to = models.SlugRelatedField(queryset = UserProfile.objects.all(), many=True, slug_field="username", allow_empty=True)
So first checkout SlugRelatedField. This basically is used to map to objects using a specific field of that object(username in this case). You will then get all the UserProfile instances in the shared_to field of the validated_data
property of the serializer which you can fetch in create method and add to you group. And then in the file upload api for your group you can check whether this user belongs to the group or not for permission checking.

Update method of serializer only creates and doesn't update (Django rest framework)

I'm having a hard time with nested serializers, especially updating them. So far I can update the lesson description and all the main fields before the nested serializer (like course description, title etc). Ideally I'd like to search using lesson_id, and not lesson.title like it is now, and update description and title at the same time. Is there a workaround for that?
My models.py
class Course (models.Model):
title = models.CharField (max_length=150)
description = models.CharField(max_length=250, default="No Description")
student = models.ManyToManyField(Student, related_name='courses', blank=True)
teacher = models.ManyToManyField(Teacher, related_name='teacher', blank=True)
def __str__(self):
return self.title
class Lesson (models.Model):
title = models.CharField(max_length=100)
description = models.TextField(default="No Description")
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='lessons')
def __str__(self):
return self.title
Serializers.py where the problem is
class CourseSerializer (serializers.ModelSerializer):
lessons = LessonSerializer(many=True, required=False)
#teacher = TeacherSerializer(many=True)
class Meta:
model = Course
fields = ('id', 'title', 'description', 'lessons') #to separate serializer with students for teachers later
def update(self, instance, validated_data):
lessons = validated_data.pop('lessons', [])
instance = super().update(instance, validated_data)
for lesson in lessons:
lesson, updated = Lesson.objects.update_or_create( defaults={'description': lesson["description"]}, title= lesson["title"])
#pk = instance.lessons_id doesn't work, I cannot get this id
instance.save()
return instance
Views
class CourseDetailDeleteView (generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAdminUserOrAuthenticatedOrReadOnly,)
queryset = Course.objects.all()
serializer_class = CourseSerializer
def update(self, request, *args, **kwargs):
serializer = CourseSerializer(instance=self.get_object(), data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
If your problem is that validated_data["lessons"][0]["id"] doesn't exists:
By default the id field of a ModelSerializer is read_only, so id is not included in validated_data in .create() or .update(). Thus you'll have to override that:
class LessonSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
....
## Optional: in case you don't want `id` getting explicitly set
def create(self, validated_data):
validated_data.pop("id", None)
return super().create(self, validated_data)
## Optional: in case you don't want `id` of the instance getting updated
def update(self, instance, validated_data):
validated_data.pop("id", None)
return super().update(self, instance, validated_data)
Another note:
There's no way to know if serializer.save() is even called in CourseDetailDeleteView.update. I would suggest to set serializer.is_valid(raise_exception=True) instead so it can return error messages and show you why it didn't save.

How to restrict fields when creating post request in DRF?

I am making a POST api using DRF. In that api, I need only few fields(name, size, customer_name, customer_address), but don't require this fields(status, ordered_time) because these fields I want to save these fields in run time as status='open' and ordered_time=DateTimeField.now()
views.py
class PizzaOrderCustomerView(APIView):
def post(self, request):
orders = request.data.get('orders')
# Create an article from the above data
serializer = ArticleSerializer(data=orders)
if serializer.is_valid(raise_exception=True):
article_saved = serializer.save()
return Response({"success": "Article '{}' created successfully".format(article_saved.name)})
models.py
class PizzaOrder(models.Model):
name = models.CharField(max_length=120)
size = models.CharField(max_length=10, choices=SIZE_CHOICE, default='MEDIUM')
customer_name = models.CharField(max_length=120)
customer_address = models.TextField()
ordered_time = models.DateTimeField(default=timezone.now, editable=False)
status = models.CharField(max_length=20, default='open', editable=False)
serializers.py
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = PizzaOrder
# fields = '__all__'
read_only_fields = ('status',)
But when I try to create an order, it needed status and ordered_time also. But it should save at the time of creating order automatically.
Suggest a good way to do it.
from rest_framework import viewsets, mixins
class PizzaViewsets(viewsets.ViewSet, mixins.CreateModelMixin):
model = PizzaOrder
serializer_class = OrderSerializer
queryset = model.objects.all(
serializer, it is always good practise to mention all fields instead of
all
class OrderSerializer(serializers.ModelSerializer):
class Meta:
model = PizzaOrder
fields = ('status','ordered_time','name', 'size', 'customer_name', 'customer_address',)
read_only_fields = ('status','ordered_time',)

json serialize serializer instance

I want to return some useful information after POST to my api. I have
# views.py
from .serializers import FlagInstanceSerializer
SomeViewSet(viewsets.ModelViewSet):
...
#action(detail=True, methods=['post'])
def flag(self, request, pk=None):
some_model = self.get_object()
flag_instance = flaggit.utils.flag(some_model, user=request.user, ip=None, comment=None)
serializer = FlagInstanceSerializer(data=flag_instance)
serializer.is_valid()
return Response(data=serializer.data)
model
# models.py
class FlagInstance(models.Model):
flag = models.ForeignKey(Flag, related_name='flags', on_delete=models.CASCADE)
user = models.ForeignKey(User, blank=True, null=True, on_delete=models.CASCADE)
ip = models.GenericIPAddressField(blank=True, null=True)
datetime = models.DateTimeField(auto_now_add=True)
flag_type = models.PositiveIntegerField(choices=FLAG_TYPES, default=1)
comment = models.TextField(blank=True, null=True)
def __str__(self):
return u'%s: %s' % (self.user, self.flag.content_object)
serializer
# serializers.py
from flaggit.models import FlagInstance
class FlagInstanceSerializer(serializers.ModelSerializer):
class Meta:
model = FlagInstance
fields = '__all__'
The rest docs show the following sanity check.
In [1]: from polls.serializers import FlagInstanceSerializer
In [2]: serializer = FlagInstanceSerializer()
In [3]: print(repr(serializer))
FlagInstanceSerializer():
id = IntegerField(label='ID', read_only=True)
ip = IPAddressField(allow_null=True, required=False)
datetime = DateTimeField(read_only=True)
flag_type = ChoiceField(choices=((1, 'Inappropriate'), (2, 'Move To Jobs'), (3, 'Move To Events'), (4, 'Move To Promotions')), required=False, validators=[<django.core.validators.MinValueValidator object>, <django.core.validators.MaxValueValidator object>])
comment = CharField(allow_blank=True, allow_null=True, required=False, style={'base_template': 'textarea.html'})
flag = PrimaryKeyRelatedField(queryset=Flag.objects.all())
user = PrimaryKeyRelatedField(allow_null=True, queryset=User.objects.all(), required=False)
The api responds with {}. I would like the api to respond with a json representation of a FlagInstance object.
You need to actually put an instance "into" the serializer to serialize the values into JSON. You are currently modifying a property that is used to deserialize the data into python objects.
Change your viewset method to this, replacing data with instance:
#action(detail=True, methods=['post'])
def flag(self, request, pk=None):
some_model = self.get_object()
flag_instance = flaggit.utils.flag(some_model, user=request.user, ip=None, comment=None)
serializer = FlagInstanceSerializer(instance=flag_instance)
return Response(data=serializer.data)

Passing argument from view to Custom RelatedField serializer

How can I pass an argument to a serializers.RelatedField class from views.py. I need to pass language_id to query Language.objects model within that RelatedField.
I am not sure if I took a right approach to this issue. What I want to achieve is to present information about genres associated to a movie from database model about depending on the language. The MovieGenre model has genre ID field which I want to replace with actual Genre name.
My serialiser.py
class GenreField(serializers.RelatedField):
def to_representation(self, value, language_id=1):
genre_name = GenresVideo.objects.get(genre_id=value, language_id=language_id)
return genre_name.name
class MovieGenresSerializer(serializers.ModelSerializer):
genre_id = GenreField(read_only=True)
class Meta:
model = MoviesGenres
As you see, here I query Language.objects with default value but I would like to pass it from views (language_id).
My views.py:
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer
def list(self, request, language_pk):
queryset = MoviesGenres.objects.all()
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, movie, language_pk):
queryset = MoviesGenres.objects.filter(movie=movie)
serializer = MovieGenresSerializer(queryset, many=True)
return Response(serializer.data)
And my urls.py:
router.register(r'lang', LanguagesViewSet, base_name='lang')
mov_gen = routers.NestedSimpleRouter(router, r'lang', lookup='language')
mov_gen.register(r'mg', MovieGenresTestViewSet, base_name='mg')
url(r'^api/', include(genre_spec.urls))
My models.py
class Languages(models.Model):
name = models.CharField(unique=True, max_length=255)
short_name = models.CharField(unique=True, max_length=4, blank=True, null=True)
active = models.BooleanField(default="")
class Meta:
managed = False
db_table = 'languages'
ordering = ('id',)
class GenresVideo(models.Model):
genre_id = models.IntegerField()
language = models.ForeignKey('Languages')
name = models.CharField(max_length=255, blank=True, null=True)
class Meta:
managed = False
db_table = 'genres_video'
unique_together = (('genre_id', 'language'),)
ordering = ('genre_id',)
class MoviesGenres(models.Model):
movie = models.ForeignKey(Movies)
genre_id = models.IntegerField()
class Meta:
managed = False
db_table = 'movies_genres'
unique_together = (('movie', 'genre_id'),)
Through the urls routes, I can get a correct response from API including the language_id. I just need to pass it to the view somehow.
Thanks a lot for help!
I'll try to answer to your first question, with the easiest implementation possible: SerializerMethodField. Because we will get the language id via the context passed to the serializer, we should either generate the context for the serializer, or let the framework do that for us.
Now to the problem at hand: you aren't filtering the queryset (MoviesGenres) by language per se. Thus, we can avoid overwriting the list and retrieve methods. Nevertheless, the router mechanism will inject in kwargs for the view method the language_pk parameter - that's the parameter that we will retrieve from within the serializer context:
class MovieGenresSerializer(serializers.ModelSerializer):
genre = searializers.SerializerMethodField()
class Meta:
model = MoviesGenres
def get_genre(self, instance):
# get the language id from the view kwargs
language_id = self.context['view'].kwargs['language_pk']
# get the genre
try:
genre_name = GenresVideo.objects.get(genre_id=instance.genre_id, language_id=language_id).name
except GenresVideo.DoesNotExist:
genre_name = None
# return the formatted output
return genre_name
class MovieGenresTestViewSet(viewsets.ModelViewSet):
lookup_field = 'movie'
queryset = MoviesGenres.objects.all()
serializer_class = MovieGenresSerializer

Resources