How to make customize detail view set using ModelViewSet? - python

I am new to django.
I have a model:
class Class(models.Model):
name = models.CharField(max_length=128)
students = models.ManyToManyField("Student")
def __str__(self) -> str:
return self.name
Now I want to create API to display the students in a particular class,the detail view, by giving the name of the class as a parameter using ModelViewClass.
Currently, I have following viewset written:
class ClassViewSet(ModelViewSet):
serializer_class = serializers.ClassSerializer
queryset = models.Class.objects.all()
How to do that?

You can use the #action(...) decorator to create a custom view.
from rest_framework.decorators import action
class ClassViewSet(ModelViewSet):
serializer_class = serializers.ClassSerializer
queryset = models.Class.objects.all()
#action(detail=True, methods=['get'], url_path='students', url_name='students')
def students(self, request, *args, **kwargs):
class_ = self.get_object()
students = class_.students.all()
serializer = serializers.StudentSerializer(students, many=True)
return Response(serializer.data)
If you are not using the DRF routers, you can specify the custom route as below,
urlpatterns = [
path('classes/', views.ClassViewSet.as_view({'get': 'list'})),
path('classes/<int:pk>/', views.ClassViewSet.as_view({'get': 'retrieve'})),
path('classes/<int:pk>/students/', views.ClassViewSet.as_view({'get': 'students'})),
]

Related

ViewSet and additional retrieve URL

I have a django model Donation that I expose as a ViewSet. Now I want to add an additional URL to a second model Shop where a related instance of Donation can be retrieved via the parameter order_id and custom actions can be executed.
# models.py
class Donation(models.Model):
id = models.AutoField(primary_key=True)
order_id = models.StringField(help_text='Only unique in combination with field `origin`')
origin = models.ForeignKey('Shop', on_delete=models.PROTECT)
class Shop(models.Model):
id = models.AutoField(primary_key=True)
# views.py
class DonationViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
viewsets.GenericViewSet):
def retrieve(self, request, *args, **kwargs):
if kwargs['pk'].isdigit():
return super(DonationViewSet, self).retrieve(request, *args, **kwargs)
else:
shop_id = self.request.query_params.get('shop_id', None)
order_id = self.request.query_params.get('order_id', None)
if shop_id is not None and order_id is not None:
instance = Donations.objects.filter(origin=shop_id, order_id=order_id).first()
if instance is None:
return Response(status=status.HTTP_404_NOT_FOUND)
return Response(self.get_serializer(instance).data)
return Response(status=status.HTTP_404_NOT_FOUND)
#action(methods=['post'], detail=True)
def custom_action(self, request, *args, **kwargs):
pass
class ShopViewSet(viewsets.ModelViewSet):
pass
# urls.py
router = routers.DefaultRouter()
router.register(r'donations', DonationViewSet)
router.register(r'shops', ShopViewSet)
router.register(r'shops/(?P<shop_id>[0-9]+)/donations/(?P<order_id>[0-9]+)', DonationViewSet)
My goal is to have http://localhost:8000/donations point at the entire DonationViewSet. Also I would like to lookup an individual donation, by its combination of shop_id and order_id like follows http://localhost:8000/shops/123/donations/1337/ and also executing the custom action like follows http://localhost:8000/shops/123/donations/1337/custom_action/. The problem I have is that the second url returns an entire queryset, not just a single instance of the model.
You can also use drf-nested-routers, which will have something like this:
from rest_framework_nested import routers
from django.conf.urls import url
# urls.py
router = routers.SimpleRouter()
router.register(r'donations', DonationViewSet, basename='donations')
router.register(r'shops', ShopViewSet, basename='shops')
shop_donations_router = routers.NestedSimpleRouter(router, r'', lookup='shops')
shop_donations_router.register(
r'donations', ShopViewSet, basename='shop-donations'
)
# views.py
class ShopViewSet(viewsets.ModelViewSet):
def retrieve(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
#action(detail=True, methods=['PUT'])
def custom_action(self, request, pk=None, donations_pk=None):
# pk for shops, donations_pk for donations
This is not tested! But in addition to what you already have, this will support:
donations/
donations/1337/
shops/123/donations/1337/
shops/123/donations/1337/custom_action
You can add urls by simply appending to the router's urls in the config like so. If all you want to do is add a single action from a view for one specifc url, and dont need all of the actions/urls for the viewset
# urls.py
urlpatterns = [
path('some_path', my_lookup_view)),
] # typical django url convention
urlpatterns += router.urls
# views.py
#api_view(['POST'])
def my_looup_view(request, shop_id, order_id):
# ...some lookup...
pass
You'll want to
derive from GenericViewSet since you're using models anyway
override get_object() instead with your custom lookup logic:
from rest_framework import mixins
from rest_framework.generics import get_object_or_404
from rest_framework.viewsets import GenericViewSet
class MyModelViewSet(
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet,
):
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
lookup_value = self.kwargs["pk"]
if lookup_value.isdigit():
obj = get_object_or_404(queryset, pk=lookup_value)
else:
obj = get_object_or_404(queryset, third_party_service_id=lookup_value)
self.check_object_permissions(self.request, obj)
return obj
#action(methods=["post"], detail=True)
def custom_action(self, request, *args, **kwargs):
thing = self.get_object()
# urls.py
router.register(r"mymodels", MyModelViewSet)
This should let you do
mymodels/ to list models,
mymodels/123/ to get a model by PK,
mymodels/kasjdfg/ to get a model by third-party service id,
mymodels/123/custom_action/ to run custom_action on a model by PK
mymodels/kasjdfg/custom_action/ to run custom_action on a model by service id,

Format url by user name

I have a small messaging API built with DRF which sends messages between users in the system.
My messages view contains several extra actions:
class MessagesViewSet(ModelViewSet):
"""
A simple ViewSet for viewing and editing the messages
associated with the user.
"""
authentication_classes = [TokenAuthentication, ]
permission_classes = [IsAuthenticated]
serializer_class = MessageSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = [MessageFields.MARK_READ]
def get_user(self):
user = self.request.user
return user
def get_queryset(self):
return Message.objects.filter(sent_to=self.get_user())
#action(detail=True)
def sent_messages(self, request, pk):
"""
Return all messages sent by the user.
"""
queryset = Message.objects.filter(sender=self.get_user())
serialized_data = MessageSerializer(queryset, many=True)
return Response(serialized_data.data, status=HTTP_200_OK)
#action(detail=True)
def last_50_messages(self, request, pk):
"""
Return the user's 50 last messages
"""
queryset = Message.objects.filter(sent_to=self.get_user())
serialized_data = MessageSerializer(queryset, many=True)
return Response(serialized_data.data, status=HTTP_200_OK)
Urls file:
from .views import MessagesViewSet
messages_router = DefaultRouter()
messages_router.register(r'messages', MessagesViewSet, basename='messages')
urlpatterns = [
url('', include(messages_router.urls))
]
Right now the only way to access the two custom methods is opening one of the message instances and then add it to the URL line and it'll work.
How can format the url for each method so it will be via the username?
right now:
http://127.0.0.1:8000/api/messages/1/sent_messages/
I looking for something like:
http://127.0.0.1:8000/api/messages/#request.user.username/sent_messages/
You have to change lookup_field value in ModelViewSet like this:
class MessagesViewSet(ModelViewSet):
...
lookup_field = "username"
...
But be careful, API like retrieve will be work with username lookup too, not pk.
To use both (username, lookup) check hook here:
class MultipleFieldLookupORMixin(object):
"""
Actual code http://www.django-rest-framework.org/api-guide/generic-views/#creating-custom-mixins
Apply this mixin to any view or viewset to get multiple field filtering
based on a `lookup_fields` attribute, instead of the default single field filtering.
"""
def get_object(self):
queryset = self.get_queryset() # Get the base queryset
queryset = self.filter_queryset(queryset) # Apply any filter backends
filter = {}
for field in self.lookup_fields:
try: # Get the result with one or more fields.
filter[field] = self.kwargs[field]
except Exception:
pass
return get_object_or_404(queryset, **filter) # Lookup the object
class RetrieveUserView(MultipleFieldLookupORMixin, generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
lookup_fields = ('account', 'username')

How to Query model filed elements in the same URL

I am writing a single model application in DRF. My model looks like this:
class Superhero(models.Model):
squad_name = models.CharField(max_length=100)
hometown = models.CharField(max_length=30)
formed = models.DateField()
active = models.BooleanField()
members = JSONField()
My viewset looks like this:
class SuperheroViewSet(viewsets.ViewSet):
"""
A simple ViewSet for listing or retrieving superheros.
"""
serializer_class = SuperheroSerializer
def list(self, request):
"""list superhero object"""
queryset = Superhero.objects.filter()
serializer = SuperheroSerializer(queryset, many=True)
return Response(serializer.data)
def retrieve(self, request, pk=None):
queryset = Superhero.objects.filter()
superhero = get_object_or_404(queryset, pk=pk)
serializer = SuperheroSerializer(superhero)
return Response(serializer.data)
and finally, my router is:
router = DefaultRouter()
router.register(r'superhero', SuperheroViewSet, basename='superhero')
urlpatterns = router.urls
Now how do I set a URL,so I would query the members field like:
//superhero/{id}/members to get specific id members. I tried drf nested URL but didn't work. The url I have works for superhero/ and superhero/{id}.
You should use detailed viewset action.
Your code would looks smth like this:
from rest_framework.decorators import action
from rest_framework.shortcuts import get_object_or_404
from rest_framework.response import Response
class SuperheroViewSet():
...
#action(detail=True, methods=['get'], url_path='members')
def get_superhero_members(self, request, pk=None):
superhero = get_object_or_404(self.get_queryset(), pk=pk)
members = <get members of your hero>
return Response(members)
You should also probably use custom serializer for members and in response return: return Response(CustomSerializer(members).data)

Authentication User in Django Class Based Views

I'm trying to refactor my django project.
Therefore I want to refactor from:
#api_view([GET, POST])
#permission_classes((IsAuthenticated, VehiclePermissions, ))
def inactive_vehicle_view(request):
if request.method == "GET":
con = CBaseUtil.get_prod_instance()
vehicle_bo = VehicleBO()
dongle_dao = OBDDongleDAO(con)
since_days = int(request.GET.get("since", 28))
vehicles = vehicle_bo.find_vehicles_by_user_context(request.user.details)
return Response(vehicles, status=status_code, headers=get_headers(request))
To a class based view like this:
class InactiveVehicleView(View):
#authentication_classes((BasicAuthentication, WebsiteAuthentication))
#permission_classes((IsAuthenticated, VehiclePermissions, ))
def dispatch(self, *args, **kwargs):
return super(InactiveVehicleView, self).dispatch(*args, **kwargs)
def get(self, request):
con = CBaseUtil.get_prod_instance()
vehicle_bo = VehicleBO()
dongle_dao = OBDDongleDAO(con)
since_days = int(request.GET.get("since", 28))
vehicles = vehicle_bo.find_vehicles_by_user_context(request.user.details)
return Response(vehicles, status=status_code, headers=get_headers(request))
The issue I'm facing is that I can't get the user details like in the old version by request.user.details as the WSGI-Request does not contain an attribute user. I guess I did something wrong with the decorators but I can't figure it out.
FYI the view is mapped in the urls like this:
url(r'^vehicles/inactive/?$', InactiveVehicleView.as_view())
Does anyone have an idea what I did wrong with the authentication and/or the decorators?
According to the Django Rest Framework Authentication Docs, your view should subclass APIView, and set authentication_classes and permission_classes as attributes instead of using the decorators.
from rest_framework.views import APIView
class InactiveVehicleView(APIView):
authentication_classes = (BasicAuthentication, WebsiteAuthentication)
permission_classes = (IsAuthenticated, VehiclePermissions)
def get(self, request):
...

Django admin list display optimize queryset

In User model i have a method:
#cached_property
def income(self):
return PartnerIncome.objects.all().aggregate(Sum('income'))['income__sum']*self.share_of_profit
PartnerIncome model:
class PartnerIncome(models.Model):
title = models.CharField(max_length=255)
income = models.FloatField(default=0)
Now i want to show 'inccome' in list_display=('income', ) at admin panel, but each object make extra query to database. How i can make PartnerIncome.objects.all().aggregate(Sum('income')) as a global variable for admin change list...
How about just overriding the get_queryset method in your ModelAdmin?
class UserAdmin(admin.ModelAdmin):
list_display = ('income', ...)
def income(self, obj):
return obj.income
def get_queryset(self, request):
queryset = super(UserAdmin, self).get_queryset(request)
# you logic here to `annotate`the queryset with income
return queryset
Documentation on aggregation:
https://docs.djangoproject.com/en/dev/topics/db/aggregation/
Without me really understanding your models and business logic.. here's a snippet from my own code as an example:
class AuthorAdmin(admin.ModelAdmin):
list_display = ('__str__', 'books_count',)
def books_count(self, obj):
return obj.books_count
def get_queryset(self, request):
return super(AuthorAdmin, self).get_queryset(
request).annotate(books_count=Count('books'))

Categories