Call Method in Model Class Based view - python

I have method in my model that I want to be callable in the API.
model:
class Booking(models.Model):
PENDING = 'PN'
ACCEPTED = 'AC'
DENIED = 'DN'
BOOKING_STATUS_CHOICES = [
(PENDING, 'Pending'),
(ACCEPTED, 'Accepted'),
(DENIED, 'Denied')
]
createdDate = models.DateTimeField(auto_now_add=True)
comments = models.CharField(max_length=120)
location = models.TextField()
date = models.DateTimeField()
operator = models.ForeignKey("Business", on_delete=models.CASCADE)
status = models.CharField(max_length=2, choices=BOOKING_STATUS_CHOICES,default=PENDING)
def __str__(self):
return self.comments
def acceptBooking(self):
self.status = self.ACCEPTED
def denyBooking(self):
self.status = self.DENIED
serializer:
class BookingSerializer(serializers.ModelSerializer):
class Meta:
model = Booking
fields = ('createdDate', 'comments', 'location', 'date', 'operator', 'status')
views:
class BookingView(viewsets.ModelViewSet):
serializer_class = BookingSerializer
queryset = Booking.objects.all()
filter_backends = [filters.SearchFilter]
search_fields = ['createdDate', 'comments', 'location', 'date', 'operator']
I would like to call acceptBooking or denyBooking. What's the best practice for achieving this?
Thanks!

One option is to add extra actions to your view. So someone can accept or deny a booking by doing one of these HTTP requests:
PUT /bookings/1/accept/
PUT /bookings/1/deny/
You can achieve this with something like:
# models.py
class Booking(models.Model):
...
def accept(self):
self.status = self.ACCEPTED
self.save()
def deny(self):
self.status = self.DENIED
self.save()
# views.py
from rest_framework.decorators import action
from rest_framework.response import Response
class BookingView(viewsets.ModelViewSet):
...
#action(detail=True, methods=['put', 'patch'])
def accept(self, request, pk=None):
booking = self.get_object()
booking.accept()
return Response({'status': booking.status})
#action(detail=True, methods=['put', 'patch'])
def deny(self, request, pk=None):
booking = self.get_object()
booking.deny()
return Response({'status': booking.status})

It looks like your only problem is that you are missing the self.save() from the model function. The serializer will automatically save the statuses for you without the functions needed.
If that's not what your looking for and you want to call the functions then as #Ram mentioned, look at how to call the model fields. For you this would be:
class BookingView(viewsets.ModelViewSet):
serializer_class = BookingSerializer
queryset = Booking.objects.all()
filter_backends = [filters.SearchFilter]
search_fields = ['createdDate', 'comments', 'location', 'date', 'operator']
def perform_update(self, serializer):
serializer.save()
if <accept_check>:
serializer.instance.acceptBooking()
if <deny_check>:
serializer.instance.denyBooking()
Also you must save the changes on model functions like:
def acceptBooking(self):
self.status = self.ACCEPTED
self.save()
def denyBooking(self):
self.status = self.DENIED
self.save()
Please note functions should use underscores, so acceptBooking should be accept_booking.

Related

Does using/calling method having calculating queryset in another method hits the database multiple times

I'm working on a DRF project to learn about ContentType models.
I created a post model and comment model(ContentType) and then added comments to the post. Everything was working fine until I added django-debug-tool and duplicated queries.
I have the following questions:
I've defined a method(children) and property(total_replies) on the comment model. Since total_replies just calling children method and count the size of queryset. Will it result in hitting the database two or more times in case I use the children method in some other methods or property?
If the database is hitting multiple times, what solution two improve performance?
After adding select_related the num of queries has been reduced drastically.
Before using select_related
After using select_related
Is it good to use select_related at all places where Foreignkey has been used?
Blog app
models.py
class Post(models.Model):
title = models.CharField(verbose_name=_("Post Title"), max_length=50)
content = models.TextField()
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='blog_posts')
category = models.ForeignKey(Category, on_delete=models.CASCADE)
def __str__(self):
return self.title
#property
def comments(self):
instance = self
#qs = Comment.objects.filter_by_instance(instance) #before
qs = Comment.objects.select_related('user').filter_by_instance(instance)
return qs
#property
def get_content_type(self):
instance = self
content_type = ContentType.objects.get_for_model(instance.__class__)
return content_type
serializers.py
class PostSerializer(serializers.ModelSerializer):
author = UserPublicSerializer(read_only=True)
status_description = serializers.ReadOnlyField(source='get_status_display')
class Meta:
model = Post
fields = (
'url', 'id', 'title', 'author',
'content', 'category', 'total_likes',
)
class PostDetailSerializer(serializers.ModelSerializer):
author = UserPublicSerializer(read_only=True)
status_description = serializers.ReadOnlyField(source='get_status_display')
comments = serializers.SerializerMethodField()
class Meta:
model = Post
fields = (
'url', 'id', 'title', 'author', 'content',
'category', 'comments', 'total_likes'
)
def get_comments(self, obj):
request = self.context.get('request')
comments_qs = Comment.objects.filter_by_instance(obj)
comments = CommentSerializer(comments_qs, many=True, context={'request':request}).data
return comments
class PostListCreateAPIView(generics.ListCreateAPIView):
serializer_class = serializers.PostSerializer
# queryset = Post.objects.all().order_by('-id') # before
queryset = Post.objects.select_related('author').order_by('-id')
name = 'post-list'
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
class PostRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
serializer_class = serializers.PostDetailSerializer
# queryset = Post.objects.all().order_by('-id') # before
queryset = Post.objects.select_related('author').order_by('-id')
name = 'post-detail'
permission_classes = [permissions.IsAuthenticatedOrReadOnly, account_permissions.IsStaffOrAuthorOrReadOnly]
def perform_update(self, serializer):
serializer.save(author=self.request.user)
Comment app
models.py
class CommentManager(models.Manager):
def all(self):
qs = super().filter(parent=None)
return qs
def filter_by_instance(self, instance):
content_type = ContentType.objects.get_for_model(instance.__class__)
object_id = instance.id
qs = super().filter(content_type=content_type, object_id=object_id).select_related('user').filter(parent=None)
return qs
class Comment(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='comments')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey(ct_field='content_type', fk_field='object_id')
parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE)
content = models.TextField()
objects = CommentManager()
def __str__(self):
if self.is_parent:
return f"comment {self.id} by {self.user}"
return f"reply {self.id} to comment {self.parent.id} by {self.user}"
def children(self):
return Comment.objects.select_related('user').filter(parent=self)
#property
def is_parent(self):
if self.parent is not None:
return False
return True
#property
def total_replies(self):
return self.children().count()
serializers.py
class CommentSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
user = UserPublicSerializer(read_only=True)
class Meta:
model = Comment
fields = ('url', 'user', 'id', 'content_type', 'object_id', 'parent', 'content', 'total_replies',)
class CommentChildSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
user = UserPublicSerializer(read_only=True)
class Meta:
model = Comment
fields = ('url', 'user', 'id', 'content',)
class CommentDetailSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='comment-detail', lookup_field='pk')
replies = serializers.SerializerMethodField()
class Meta:
model = Comment
fields = ('url', 'id', 'content_type', 'object_id', 'content', 'replies', 'total_replies',)
def get_replies(self, obj):
request = self.context.get('request')
if obj.is_parent:
return CommentChildSerializer(obj.children(), many=True, context={'request':request}).data
return None
views.py
class CommentListAPIView(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Comment.objects.select_related('user').order_by('-id')
name = 'comment-list'
serializer_class = serializers.CommentSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
class CommentDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = Comment.objects.select_related('user').all()
name = 'comment-detail'
serializer_class = serializers.CommentDetailSerializer
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Thanks in advance.
That's exactly what django docs says about select_related :
"Returns a QuerySet that will “follow” foreign-key relationships, selecting additional related-object data when it executes its query. This is a performance booster which results in a single more complex query but means later use of foreign-key relationships won’t require database queries."
They describe select_related as something complex but good in term of transactional db cost.

Search fields in custom action method

now i'm studying DRF and have to do project with photo albums. One of my tasks is to create custom #action "patch" method, using model field "title", but i can't understand how to add fields for search in custom methods. We can see them in base methods, like "get", "patch" and "put", but i can't find any info about how to add them to custom actions.
If anyone knows how to do it, please, tell me.
My model:
class PhotoAlbum(models.Model):
title = models.CharField(verbose_name='Название альбома', max_length=50, null=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, verbose_name='Автор')
created_at = models.DateTimeField(verbose_name='Дата создания', editable=False,
default=datetime.datetime.today())
class Meta:
verbose_name = 'Фотоальбом'
verbose_name_plural = 'Фотоальбомы'
def __str__(self):
return f'{self.title} Автор: {self.created_by} Дата: {self.created_at}'
My view:
def photo_album_view(request):
photo_albums = PhotoAlbum.objects.all()
context = {
'photo_albums': photo_albums,
}
return render(request, 'photo_album.html', context=context)
My viewset:
class AlbumFilter(django_filters.FilterSet):
title = django_filters.Filter(field_name='title')
class PhotoAlbumViewSet(viewsets.ModelViewSet):
queryset = PhotoAlbum.objects.all()
filterset_class = AlbumFilter
serializer_class = PhotoAlbumSerializer
pagination_class = ResultPagination
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
search_fields = ['title', ]
ordering_fields = ['created_at', ]
To answer the above
say you have a viewset class
// all imports
class AbcApi(viewsets.ModelViewset):
serializer_class = random
permission_classses = [IsAuthenticated]
search_fields = ["a_field", "b_field"]
filter_backends = [....]
#custom action
#action(detail=False)
def custom_action(self, *args, **kwargs):
""" to answer your question """
qset_ = **self.filter_queryset**(self.queryset())
#the filter bold automatically involves the class filters in the custom method
the answer is to use self.filter_queryset(..pass the get_queryset()) here as seen
If you are using ModelViewSet, I believe you are looking for one of these custom methods:
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
These functions allow you to add custom functionalities to your update method. Reference
Brief example:
def partial_update(self, request, pk=None):
serializer = UserPostSerializer(user, data=request.data, partial=True)
if serializer.is_valid():
try:
serializer.save()
except ValueError:
return Response({"detail": "Serializer not valid."}, status=400)
return Response({"detail": "User updated."})
else:
return Response(serializer.errors)

How can I pass an object selected by the user, to another view and retrieve a specific column from that object?

I'm trying to figure out the most efficient way to GET values inside my stock_list column from the current object that's being viewed by a user.
BucketDetail is used to retrieve the specific object selected by the user via item = self.kwargs.get('pk')
class BucketDetail(generics.RetrieveAPIView):
permission_classes = [IsAuthenticated]
serializer_class = BucketListSerializer
queryset = Bucket.objects.all()
def get_object(self, queryset=queryset, **kwargs):
item = self.kwargs.get('pk')
return get_object_or_404(Bucket, slug=item)
How can I pass the object instance, from BucketDetail to BucketData view, followed by getting the column, stock_list, from the current object instance?
class BucketData(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
stocks = Bucket.objects.get(stock_list)
...
data = response.json()
return Response(data, status=status.HTTP_200_OK)
above is what I have so far, stocks = Bucket.objects.get(stock_list) does not work like I thought.
models.py
class Bucket(models.Model):
category_options = (
('personal', 'Personal'),
('social', 'Social'),
)
class BucketObjects(models.Manager):
def get_queryset(self):
return super().get_queryset()
...
slug = models.SlugField(unique=True, blank=True)
stock_list = ArrayField(models.CharField(max_length=6,null=True),size=30,null=True)
...
objects = models.Manager()
bucketobjects = BucketObjects()
class Meta:
ordering = ('-created',)
def total_stocks_calc(self):
self.stock_count = Bucket.objects.aggregate(Sum('stock_list', distinct=True))
self.save()
You should use model pk or your unique slug to retrieve stock_list data within your BucketData view. You also should use a serializer for the data. Pass your slug or pk from urls file to your view. Try something like this (code is not tested and can be improved):
Serializer:
class StockListSerializer(ModelSerializer):
stock_list = serializers.ListField(child=serializers.CharField())
class Meta:
model = Bucket
fields = ("stock_list",)
View:
class BucketData(APIView):
permission_classes = [IsAuthenticated]
def get(self, request, *args, **kwargs):
slug = kwargs.get("slug")
bucket_object = get_object_or_404(Bucket, slug=slug)
serialzer = StockListSerializer(bucket_object.stock_list)
return Response(serialzer.data, status=status.HTTP_200_OK)

Return mean of values in models instead of every value

I am trying to make a Movie project in django. I would like to have one view for film's ratings that can take single rate_value but by GET returns mean of rating values for a film.
I have no idea where should I start. I tried some changes in view and serializer but didnt work. Here are:
views.py:
class RateListView(generics.ListCreateAPIView):
permission_classes = [AllowAny]
serializer_class = RateSerializer
lookup_url_kwarg = 'rateid'
def get_queryset(self):
rateid = self.kwargs.get(self.lookup_url_kwarg)
queryset = Rate.objects.filter(film = rateid)
# .aggregate(Avg('rate_value'))
if queryset.exists():
return queryset
else:
raise Http404
def post(self, request, *args, **kwargs):
serializer = RateSerializer(data = request.data)
if serializer.is_valid():
if Film.objects.filter(pk = self.kwargs.get(self.lookup_url_kwarg)).exists():
serializer.save(film_id = self.kwargs.get(self.lookup_url_kwarg))
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
raise Http404
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializer.py
class RateSerializer(serializers.ModelSerializer):
class Meta:
model = Rate
fields = ('rate_value',)
and model.py
class Rate(models.Model):
film = models.ForeignKey(Film, on_delete = models.CASCADE)
rate_value = models.IntegerField(validators = [
MinValueValidator(1),
MaxValueValidator(10)
]
)
def __str__(self):
return str(self.film.title) + str(self.rate_value)
Right now it returns single values correctly.
Try something like this:
serializers.py
class RateListSerializer(serializers.Serializer):
avg_rate_value = serializers.FloatField()
class RateSerializer(serializers.ModelSerializer):
class Meta:
model = Rate
fields = ("rate_value", "film")
views.py
class RateListView(generics.ListCreateAPIView):
permission_classes = [AllowAny]
def get_queryset(self):
if self.action == "list":
queryset = Film.objects.all()
film_id = self.request.query_params.get("film_id", None)
if film_id is not None:
queryset = queryset.filter(id=film_id)
return queryset.annotate(avg_rate_value=Avg("rate__rate_value"))
return Rate.objects.all()
def get_seializer_class(self):
if self.action == "list":
return RateListSerializer
return RateSerializer
class Film(models.Model:
...
#property
def mean_rating(self):
ratings = self.ratings.all().values_list('rate_value', flat=True)
if ratings:
return sum(ratings)/len(ratings)
# return a separate default value here if no ratings
class Rate(models.Model):
film = models.ForeignKey(Film, on_delete = models.CASCADE, related_name='ratings')
rate_value = models.IntegerField(validators = [
MinValueValidator(1),
MaxValueValidator(10)
]
)
...
class FilmSerializer(serializers.ModelSerializer):
class Meta:
model = Film
fields = ('id','name','mean_rating',)
Without getting into the architecture of your app, here's a pattern that can be used. The Film object has a computed property that calls ratings, the related_name supplied to a film rating, to get all related ratings and returns the mean.
Properties of models can be used as serializer fields as long as then provide serializable values.
For more on related_name for models in Django, see here - What is `related_name` used for in Django?

How to update a related model after another model is created?

I need to automatically update my Account's amount when a Transaction is created.
I have Transaction model's model.py:
class Transaction(models.Model):
user = models.ForeignKey(User, default=None)
account = models.ForeignKey(Account, default=None)
...
Its serializers.py:
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transaction
fields = ('id', 'user', 'account_id', 'category_id', 'name', 'amount', 'description', 'created_at')
def create(self, validated_data):
return Transaction.objects.create(**validated_data)
And its views.py:
class TransactionList(APIView):
def get(self, request):
user_id = request.user.pk
transactions = Transaction.objects.filter(user_id=user_id).order_by('-created_at')
serializer = TransactionSerializer(transactions, many=True)
return Response(serializer.data)
def post(self, request):
account_id = request.data['account_id']
category_id = request.data['category_id']
serializer = TransactionSerializer(data=request.data)
if serializer.is_valid():
serializer.save(user=request.user, account_id=account_id, category_id=category_id)
self.update_account(request)
return Response(serializer.data, status=HTTP_201_CREATED)
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
def update_account(self, request):
account_id = request.data['account_id']
category_id = request.data['category_id']
account = Account.objects.get(pk=account_id)
category = Category.objects.get(pk=category_id)
if category.type == 'expense':
account.amount = (account.amount - int(self.request['amount']))
else:
account.amount = (account.amount + int(self.request['amount']))
# Then what?
I thought of creating a custom method that will be executed within the condition if serializer is valid, that would get the account and category by their id. I could, so far, display the current values they have, such as amount and name, but after that, I don't know what to do. I'm imagining I need to use my AccountSerializer, but I'm not sure.
The easiest way would be to override the save method of your Transaction model:
class Transaction(models.Model):
user = models.ForeignKey(User, default=None)
account = models.ForeignKey(Account, default=None)
...
def save(self, *args, **kwargs):
super(Transaction, self).save(*args, **kwargs)
# Update self.account
See Django documentation for details.

Categories

Resources