Django REST framework - multiple lookup fields? - python

I have a model that more or less looks like this:
class Starship(models.Model):
id = models.UUIDField(default=uuid4, editable=False, primary_key=True)
name = models.CharField(max_length=128)
hull_no = models.CharField(max_length=12, unique=True)
I have an unremarkable StarshipDetailSerialiser and StarshipListSerialiser (I want to eventually show different fields but for now they're identical), both subclassing serializers.ModelSerializer. It has a HyperlinkedIdentityField that refers back to the (UU)ID, using a home-brew class very similar to the original HyperlinkedIdentityField but with capability to normalise and handle UUIDs:
class StarshipListSerializer(HyperlinkedModelSerializer):
uri = UUIDHyperlinkedIdentityField(view_name='starships:starship-detail', format='html')
class Meta:
model = Starship
fields = ('uri', 'name', 'hull_no')
Finally, there's a list view (a ListAPIView) and a detail view that looks like this:
class StarshipDetail(APIView):
"""
Retrieves a single starship by UUID primary key.
"""
def get_object(self, pk):
try:
return Starship.objects.get(pk=pk)
except Starship.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
vessel = self.get_object(pk)
serializer = StarshipDetailSerialiser(vessel, context={'request': request})
return Response(serializer.data)
The detail view's URL schema is currently invoking the view based on the UUID:
...
url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail'),
...
I now want users to be able to navigate and find the same vessel not just by UUID but also by their hull number, so that e.g. vessels/id/abcde1345...and so on.../ and vessels/hull/H1025/ would be able to resolve to the same entity. And ideally, regardless of whether one arrived at the detail view from ID or hull number, the serialiser, which is used with slight alterations in lists as well, should be able to have the ID hyperlinked to the ID-based link and the hull hyperlinked to a hull number based link (vessels/hull/H1025/). Is this at all possible? And if so, how would I go about it?

1. Add the new routes
# in urls.py
urlpatterns = [
...,
url(r'vessels/id/(?P<pk>[0-9A-Fa-f\-]+)/$', StarshipDetail.as_view(), name='starship-detail-pk'),
url(r'vessels/hull/(?P<hull_no>[0-9A-Za-z]+)/$', StarshipDetail.as_view(), name='starship-detail-hull'),
]
Tweak the regex for hull_no as you want. Note that I gave distinct names to each route, starship-detail-pk and starship-detail-hull.
2. Add the hull field in the serializer
# in serializers.py
class StarshipListSerialiser(HyperlinkedModelSerializer):
uri = UUIDHyperlinkedIdentityField(view_name='starship-detail-pk', format='html')
hull_no = UUIDHyperlinkedIdentityField(view_name='starship-detail-hull', format='html', lookup_field='hull_no')
class Meta:
model = Starship
fields = ('uri', 'name', 'hull_no')
3. Modify the view so it can also resolve objects based on hull
# in serializers.py
from django.shortcuts import get_object_or_404
from rest_framework.views import APIView, Response
from starwars.serializers import StarshipDetailSerialiser
from starwars.models import Starship
class StarshipDetail(APIView):
def get(self, request, pk=None, hull_no=None, format=None):
lookup = {'hull_no': hull_no} if pk is None else {'pk': pk}
vessel = get_object_or_404(Starship, **lookup)
serializer = StarshipDetailSerialiser(vessel, context={'request': request})
return Response(serializer.data)
That should be enough to get you going with the detail view:
As a final note, you should be aware that it's not RESTful for the same resource to be available at two different URLs like this. Perhaps, as an alternate design decision, you might like to consider just defining the "one true route" for a resource, and adding in a "convenience" redirect from the other locator to the canonical URL.

Related

URL doesn't exist after creating

Here's my views.py:
class LanguageViewSet(viewsets.ModelViewSet):
queryset = Language.objects.all().order_by('name')
serializer_class = LanguageSerializer
class FrameworkViewSet(viewsets.ModelViewSet):
queryset = Framework.objects.all()
serializer_class = FrameworkSerializer
class SelectedLanguageViewSet(viewsets.ModelViewSet):
queryset = Language.objects.all()
serializer_class = FrameworkSerializer
def get_queryset(self):
request = self.request
language_id = request.query_params.get('language_id')
language = Language.objects.filter(id=language_id)
self.queryset = language.framework_set.all()
return self.queryset
And my urls.py:
router = routers.DefaultRouter()
router.register(r'languages', views.LanguageViewSet)
router.register(r'frameworks', views.FrameworkViewSet)
router.register(r'language/<int:language_id>', views.SelectedLanguageViewSet)
urlpatterns = [
path('', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework'))
]
However, only languages and frameworks work. language doesn't exists. Is this because of the get_queryset? I even tried removing the <int:language_id> in the url params but it still won't show up.
EDIT:
Forgive my naiveness, I'm quite new to django and DRF
Update
What I meant to do was like this (without DRF):
In the views.py:
def frameworks_from_language(request, language_id):
language = Language.objects.get(pk=language_id)
if language == None:
# Do some stuffs
frameworks = language.framework_set.all()
template = 'app/language.html'
context = {
'frameworks': frameworks
}
return render(request, template, context)
And in urls.py:
path('language/<int:language_id>', views.frameworks_from_language, name='getframeworks')
So the whole flow would be:
User clicks a language
Fire a get request based on the id of the selected language
Return an object of frameworks that is related to the language
This is working on a normal template-based Django. However I have no idea how to execute something like this on DRF, with all the viewsets etc.
Yes, it's because of your get_queryset (and your URL definitions).
At first, drop <int:language_id> from URL prefix, as DRF router generates the list and detail URL endpoints for you automatically. Moreover, as you're using Regex path language/<int:language_id> is taken literally (<int:language_id> has a meaning while using path, not re_path).
In your SelectedLanguageViewSet.get_queryset, you're trying to return all Framework instances related with a certain Language (you thought you would take that from language_id query param). The viewset is for Language model, and at most you should do some filtering on the default queryset inside get_queryset; absolutely don't return a whole different queryset from another model. What will happen (after fixing your URL) when you pass /language/1/? (Will 1 be a Language ID or a Framework ID ? Hint: as per you current design it would refer a Framework instance ID).
FWIW, the URL captures come as kwargs attribute in a viewset instance (i.e. self.kwargs), not via the query string.
Answer to edits:
To implement that in DRF, you can define a serializer for language with only the frameworks field:
from viewsets import ReadOnlyModelViewSet
class SelectedLanguageViewSet(ReadOnlyModelViewSet):
queryset = Language.objects.all()
serializer_class = LangaugeRelationSerializer
only list and retrieve actions should be supported, hence inherited from ReadOnlyModelViewSet.
Now, the serializer (with only one field -- frameworks):
class LangaugeRelationSerializer(serializers.ModelSerializer):
frameworks = FrameworkSerializer(source='framework_set', many=True)
class Meta:
model = Language
fields = ('frameworks',)
By default ModelSerializer sets the related fields as PrimaryKeyRelatedField, so the related objects are represented as their primary keys. If you want that instead of using the FrameworkSerializer:
class LangaugeRelationSerializer(serializers.ModelSerializer):
frameworks = serializers.PrimaryKeyRelatedField(source='framework_set', many=True)
class Meta:
model = Language
fields = ('frameworks',)
As PrimaryKeyRelatedField is the default, you can also define the source in Meta.extra_kwargs (to save you from writing the field definition yourself):
class LangaugeRelationSerializer(serializers.ModelSerializer):
class Meta:
model = Language
fields = ('frameworks',)
extra_kwargs = {
'frameworks': {
'source': 'framework_set',
'many': True,
},
}
Have you tried passing url as /language/id-of-language in the place of id-of-language pass integer i.e. the id for language in the db.

How can I create a view that retrieves all objects with matching primary key in url?

I have two models that form a one-to-many relationship. One pallet has multiple images associated with it.
models.py
class Pallet(models.Model):
pallet_id = models.IntegerField(primary_key=True)
def __str__(self):
return str(self.pallet_id)
class Meta:
ordering = ('pallet_id',)
class Image(models.Model):
created = models.DateTimeField(auto_now_add=True)
pallet = models.ForeignKey(Pallet, on_delete=models.CASCADE)
image = models.FileField(upload_to="")
def __str__(self):
return str(self.created)
I'm trying to create a view where I get all images associated with a particular pallet_id from the url.
serializers.py
class ImageSerializer(serializers.HyperlinkedModelSerializer):
pallet = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Image
fields = '__all__'
class PalletSerializer(serializers.ModelSerializer):
class Meta:
model = Pallet
fields = '__all__'
urls.py
urlpatterns = [
url(r'^pallets/', include([
url(r'^(?P<pk>[0-9]+)/$', views.PalletDetail.as_view(), name='pallet-detail'),
])),
]
I think the issue is in the views.py with the PalletDetail class. I am confused on how to write the view based on the primary key from the URL. I've tried to use **kwargs['pk'] but does using this make it a function-based view? If so, would it be bad form to mix class-based and function-based views? How can I get similar behavior from class-based views?
I'm really struggling with the views here:
views.py
class PalletDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Image.objects.prefetch_related('pallet').all()
serializer_class = ImageSerializer
In Function based view
pass pk parameter
example:
def Example_function(request, pk):
queryset = Model.objects.filter(id=pk)
In Class-Based views
get url parameter
pk = self.kwargs.get('pk')
queryset = Model.objects.filter(id=pk)
You're going about it the wrong way. You want to return a list of images belonging to the pallet so what you need is a list view for the images and not a pallet detail view. The url should look like this:
/pallets/<pallet_id>/images/
In this way the url is self-descriptive and by REST standards you can easily see that you are requesting for images belonging to a pallet and not the pallet itself
In urls.py
urlpatterns = [
url(r'^pallets/', include([
url(r'^(?P<pallet_pk>[0-9]+)/images/$', views.PalletImagesListView.as_view(), name='pallet-images-list'),
])),
]
In views, you have to overide the get_queryset() method, so that it only returns the images for the pallet specified
class PalletImagesListView(generics.ListAPIView):
serializer_class = ImageSerializer
def get_queryset(self):
return Image.objects.prefetch_related('pallet').filter(pallet__pk=self.kwargs.get('pallet_pk'))
So now you can request the first pallet's images with /pallets/1/images/
You are using the serializer and view in a wrong way.
The problem with yours PalletDetail view is that you are using the wrong queryset. The queryset should return Pallet objects, since the lookup will be on that queryset (default lookup field is 'pk'). Also, you will then need to use the serializer for Pallet since that is supposed to handle the data for the Pallet object.
class PalletDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Pallet.objects.all()
serializer_class = PalletSerializer
Have you tried filter_by() method
queryset = Model.query.filter_by(id=pk).first()

Update and delete in same Api view without passing id in Url

How to perform crud operation in One URL End point in django rest framework?
Currently i am having 2 url end points
url(r'^recipient/$', views.RecipientView.as_view()), # in this APiview im performing get all and post
url(r'^recipient/(?P<pk>[0-9]+)/$', views.RecipientDetail.as_view()), # in this APiview im performing retrieve, update delete.
Now the requirement is i have remove 2nd url and perform all operations in first api view?
I am new to django framework can anyone please help me achieve this?
Below is my code.
View.py
class RecipientView(APIView):
def get(self, request, format=None):
Recipients = Recipient.objects.all()
serializer = RecipientSerializer(Recipients, many=True)
return Response(serializer.data)
def post(self, request, format=None):
serializer = RecipientSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
"""
class RecipientDetail(APIView):
def get_object(self, pk):
try:
return Recipient.objects.get(pk=pk)
except Recipient.DoesNotExist:
raise Http404
def get(self, request, pk, format=None):
Recipient = self.get_object(pk)
serializer = RecipientSerializer(Recipient)
return Response(serializer.data)
def put(self, request, pk, format=None):
Recipient = self.get_object(pk)
serializer = RecipientSerializer(Recipient, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, pk, format=None):
Recipient = self.get_object(pk)
Recipient.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
"""
model.py
class Recipient(models.Model):
recipient = models.CharField(max_length=32, blank=False, null=False)
def __str__(self):
"""returns the model as string."""
return self.racipi
ent
serializer.py
class RecipientSerializer(serializers.ModelSerializer):
class Meta:
model = Recipient
fields = '__all__'
I am not able to update and delete in the same view please needed help?
You can avoid passing the ID in the URL with a POST request. Supply the ID and some kind of "action" verb, e.g. action=delete in the body of the request.
That's not considered RESTful though, partly because the HTTP DELETE and PUT verbs perfectly describes the requested operations, but also because POST is considered a non-idempotent method, meaning that the server state will change with each successful request. Being idempotent, duplicate DELETE/PUT (and GET for that matter) requests will leave the server in the same state.
It's not a major hassle to have a second route and view to implement the REST API so it's best to leave it as it is.
Your 2nd URL is receiving a parameters that can be used to fetch data object from the database and then perform any action on that particular instance. If you see the class RecipientDetail, you'll see all the methods are accepting a parameter called pk that relates to the object you want to fetch from database.
But your 1st URL is for generic actions like Create New Object or List All Objects and it is not good to use these endpoints to do instance specific actions.
You can read more about REST API standard enpoints to know details. Here is a reference link:
https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
The easiest way is to use DRF's ViewSet. It already provides the basic CRUD operation for you so you can just create the view in something like this:
# views.py
from rest_framework import viewsets
from .models import Recipient
from .serializers import RecipientSerializer
class RecipientViewSet(viewsets.ModelViewSet):
"""
A viewset for viewing and editing recipient instances.
"""
serializer_class = RecipientSerializer
queryset = Recipient.objects.all()
Since we are using ModelViewSet, it already provides actions like get, list, update, etc. as you can see in the documentation.
You can then use routers in your urls.py like this:
# urls.py
from rest_framework.routers import DefaultRouter
from myapp.views import RecipientViewSet
router = DefaultRouter()
router.register(r'recipients', RecipientViewSet)
urlpatterns = router.urls
The url above will generate a url that is like what you wrote in you question:
# Add recipient
POST /recipients/
# Get list of recipients
GET /recipients/
# Get recipient detail
GET /recipients/:recipient_id/
# Update recipient
PUT/PATCH /recipients/:recipient_id/
# Delete recipient
DELETE /recipients/:recipient_id/
Please take note that this is a simplified version and you can even create your own urls pattern with your specified actions.
UPDATE:
Thanks to mhawke for clarification. As what mhawke said in the comment, this may not be what the OP wanted, if you just want to avoid passing the ID in the url, then you can follow mawke's answer, and yes it is not considered RESTful.

django rest framework: Get url parameter value in a model property

I have a Product model and one propery in it is "my_test_fn". This is called from my serializer. My requirement is, I want to do some calculations based on the filter passing through the url. How can I get the url parameter values in a model property?
I want to get "filters" value in my_test_fn
models.py
class Product(AbstractProduct):
product_id = models.UUIDField(default=uuid.uuid4, editable=False)
##more fields to go
def my_test_fn(self):
filters = self.request.query_params.get('filters', None)
return {"key":"value"}
serializer.py
class MySerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ('id','product_id','sku', 'title', 'my_test_fn',)
views.py
class ProductDetailConfiguration(viewsets.ViewSet):
lookup_field = 'product_id'
def retrieve(self, request, product_id=None):
queryset = Product.objects.filter(product_id=product_id)[0]
serializer = ProductConfigurationCustomSerializer(queryset, context={'request': request})
return Response(serializer.data)
API url:
http://127.0.0.1:8000/api/v1/product-configuration/2FC2AA43-07F5-DCF4-9A74-C840FDD8280A?filters=5
This logic belongs in the serializer, not the model. You can access it there via self.context['request'].
I guess what you want is not possible (have the my_fn on the model itself).
You would need to use a SerializerMethodField, so you will have access to the object, but to the request (and the various parameters of it) as well.

Representing more than single object/list CRUD ops with Django REST Framework ViewSets

I've been writing a game picking style webapp with Django and recently decided to implement my views as API endpoints with DRF, to give me more flexibility when it comes to frontend approaches. I have basic serializers and ViewSets for each of my models, and I can browse them normally with the (excellent) browsable API. Here are a couple:
class SheetSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer(read_only = True)
league = LeagueSerializer(read_only = True)
picks = serializers.HyperlinkedRelatedField(
source='pick_set',
many=True,
view_name='pick-detail',
read_only = True
)
class Meta:
model = Sheet
fields = ('url', 'id', 'league_week', 'user', 'league', 'picks')
class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Game
fields = ('url', 'home_team', 'away_team', 'week', 'home_team_score', 'away_team_score')
class PickSerializer(serializers.HyperlinkedModelSerializer):
sheet = SheetSerializer()
game = GameSerializer()
class Meta:
model = Pick
fields = ('url', 'sheet', 'amount', 'spread', 'pick_type', 'pick_team', 'game')
With respective ViewSets:
class PickViewset(viewsets.ModelViewSet):
queryset = Pick.objects.all()
serializer_class = PickSerializer
class GameViewset(viewsets.ModelViewSet):
queryset = Game.objects.all()
serializer_class = GameSerializer
class SheetViewset(viewsets.ModelViewSet):
queryset = Sheet.objects.all()
serializer_class = SheetSerializer
What I'm currently having trouble with is how to represent more complex endpoints than single-object or list of same-type object CRUD operations. For example, I currently have a regular Django view for matchups which pulls the users Sheet (collection of Picks), another users Sheet, and displays the nested Picks against each other. I'm also planning to display other data on the same page from the other users in whatever League they're a part of. The implementation for the user + opponent data in vanilla Django looks like this:
class MatchupDetail(DetailView):
template_name = 'app/matchups.html'
context_object_name = 'pick_sheet'
def get_object(self):
#logic to find and return object
def get_opponent(self,username,schedule,week, **kwargs):
#logic to find and return the opponent in the matchup
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
#logic to pull the opponents details and set them in the context
I've been struggling on how to represent this in a ViewSet. With a regular Django view, it's easy to write the get_object (or get_list) as well as get_context_data, include the user's data + any other desired objects from the database, and pass all of that to the template. Currently, the (early) API endpoint version of the above looks like this:
class MatchupViewset(viewsets.ReadOnlyModelViewSet):
serializer_class = SheetSerializer
def get_queryset(self):
user = self.request.user
return Sheet.objects.filter(user=self.request.user)
def list(self, request, format=None):
sheets = self.get_queryset()
serializer = SheetSerializer(sheets, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk, format=None):
sheet = Sheet.objects.get(user=self.request.user, league_week=pk)
serializer = SheetSerializer(sheet, context={'request':request})
return Response(serializer.data)
This code works, but only returns a limited subset of objects- a single users Sheet(s). My aim in this Viewset is to return a list of all the matchups (whi are just a collection of user Sheets) for a given league with list() and given league + week with retrieve(), but I'm not sure how to return that data. With the normal Django context variable, extra data you include can be named in any manner you like- how is the goal of collecting and returning an arbitrary number of potentially different objects accomplished with DRF? edit: this wasn't very clear initially- I've gotten nested representation working, but I'm wondering if there's a way to name/label the different serialized objects in the manner that information in the normal context can be set with any name
Say I want to pass the requesting users serialized Sheet, along with their opponents Sheet and all the other players Sheets for this particular league and week. Will the logic to determine which belongs to the user, which belongs to their opponent, and which are the other players' have to live in the frontend code?
Also, how would I configure the router so that the list() method always requires a URL parameter (for the league)? Anytime I change the registration from
router.register(r'matchup', MatchupViewset, base_name = 'matchup')
to something like
router.register(r'matchup/(?P<league>[0-9]+)/$', MatchupViewset, base_name = 'matchup')
the endpoint disappears from the browsable API root.
how is the goal of collecting and returning an arbitrary number of potentially different objects accomplished with DRF?
I'm not sure what the question is exactly here. I assume you'll want nested representations which is explained here.
the endpoint disappears from the browsable API root.
This is because the browsable API doesn't know how to resolve the extra kwargs. Since no url matches, he won't display the link

Categories

Resources