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)
Related
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'})),
]
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,
I'm trying to add filters to my function-based views. Using Class Based Views I was able to add filters to views. But I'm curious if I can add filtering to this function based views. I need to filter subject by it's name through a web search. How I can achieve that
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def TeacherSubject(request):
teacher = TeacherProfile.objects.get(user=request.user)
subject = Subject.objects.filter(author=teacher).order_by('-id')
paginator = PageNumberPagination()
paginator.page_size = 5
result_page = paginator.paginate_queryset(subject, request)
serializer = SubjectSerializer(result_page,many=True)
return paginator.get_paginated_response(serializer.data)
You can use django-filter. For example
# filters.py
class ProductFilter(django_filters.FilterSet):
class Meta:
model = Product
fields = ['price', 'release_date']
# views.py
...
from .filters import ProductFilter
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def product_list(request):
queryset = Product.objects.all()
filterset = ProductFilter(request.GET, queryset=queryset)
if filterset.is_valid():
queryset = filterset.qs
serializer = ProductSerializer(queryset, many=True)
return Response(serializer.data)
If you need raise Validation errors, use this
from .filters import ProductFilter
from django_filters.utils import translate_validation
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def product_list(request):
filterset = ProductFilter(request.GET, queryset=Product.objects.all())
if not filterset.is_valid():
raise translate_validation(filterset.errors)
serializer = ProductSerializer(filterset.qs, many=True)
return Response(serializer.data)
Pagination
from .filters import ProductFilter
from django_filters.utils import translate_validation
from rest_framework.pagination import PageNumberPagination
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def product_list(request):
paginator = PageNumberPagination()
paginator.page_size = 5
filterset = ProductFilter(request.GET, queryset=Product.objects.all())
if not filterset.is_valid():
raise translate_validation(filterset.errors)
queryset = paginator.paginate_queryset(filterset.qs, request)
serializer = ProductSerializer(queryset, many=True)
return paginator.get_paginated_response(serializer.data)
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')
I am trying to query on jQuery Datatables list.
Right now I can fetch all records from the table, but I want to fetch records to whom their parent id matches.
For example, users have posts, so in my case it would be fetch posts where user id is lets say 1.
So I am trying to implement same thing for jQuery Datatables.
I can see data is being posted but I can't figure out how to query along with datatables, so that datatables filters are not affected by this change.
My current code:
class PartRequestViewSet(CommonViewset, generics.RetrieveAPIView):
queryset = PartRequest.objects.filter(deleted_at=None)
serializer_class = PartRequestSerializer
def list(self, request, *args, **kwargs):
records = request.GET.get('records', None)
# ID of the part
part_number_id = request.GET.get('part_number_id', None)
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
page = self.paginate_queryset(queryset)
if page is not None and records is None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
Now in above code I can get the part_number_id from request, but how can I filter records using filter_queryset(), so that only parts_requests are given back in datatables where part_number_id is 1
Update
Current helper function filter_queryset that is used in above code.
def filter_queryset(self, queryset):
format = self.request.GET.get('format', None)
if format == 'datatables':
self.filter_backends += (DatatablesFilterBackend,)
else:
self.filter_backends += (DjangoFilterBackend,)
for backend in list(self.filter_backends):
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset
There is django-filter extension. In your case it may be something like the following:
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
from .models import Post
from .serializers import PostSerializer
class PostList(viewsets.ModelViewSet):
queryset = Post.objects.all()
serializer_class = PostSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['owner']
Then you can use get request like this: /posts?owner=3,
where 3 is user id and owner is ForeignKey in Post model to User.
The way I did is I added the following lines:
if part_number_id:
queryset = queryset.filter(part_number_id=part_number_id)
so the code looks like this now and it is working great. I'm open to better and more efficient options.
class PartRequestViewSet(CommonViewset, generics.RetrieveAPIView):
queryset = PartRequest.objects.filter(deleted_at=None)
serializer_class = PartRequestSerializer
def list(self, request, *args, **kwargs):
records = request.GET.get('records', None)
# ID of the part
part_number_id = request.GET.get('part_number_id', None)
queryset = self.get_queryset()
queryset = self.filter_queryset(queryset)
if part_number_id:
queryset = queryset.filter(part_number_id=part_number_id)
page = self.paginate_queryset(queryset)
if page is not None and records is None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)