Short question:
What's the appropriate way to access a url path parameter (not a url query parameter) in a Django DRF serializer?
Context:
A have a Django DRF api, where I have a url similar to :
/api/blogposts/:blogid/comments
that is handled by a generic view:
class MyCommentsView(generics.ListCreateAPIView):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
(blog and comment are just example resources; i am working with different ones)
This view allows via a POST call to create new comments.
For legacy reasons, the request will contain the blog_id also as a request body parameter.
In my Serializer I want to check if the :blogid in the url is identical to the blog_id in the body. Currently I do the following, but the path splitting and element selecting feels very fragile (e.g. if I restructure my url in urls.py I need to adjust my custom parsing) . So, is there a better way?
class CommentSerializer(serializers.ModelSerializer):
class Meta:
model = Comment
fields = [..., 'blog_id', ... ]
...
def validate_blog_id(self, value):
# Better way for next line?
blogid_in_url = int(self.context['request'].path.split('/')[-2])
if blogid_in_url != value:
...
Note:
my question is about a url path parameter, not a url query parameter (like blogid in api/blogposts?blogid=55). I know a url query parameter can be accessed via self.context['request'].query_params.
I can't say it is a good way but suggest alternatively:
You can access your url parameters in your serializer like this:
self.context.get('request').parser_context.get('kwargs').get(
'blog_id') # your url parameter name here
Another way, you can override create method of ListCreateAPIView;
def create(self, request, *args, **kwargs):
blog_id = kwargs.get('blog_id') # your url parameter name here
serializer = self.get_serializer(data=request.data,
context={'blog_id': blog_id})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
Related
I have a Django application which under /api/v1/crm/ticket can create tickets via a POST call. Now I want to be able to send different types of tickets (more then the one in the example code) to the same endpoint having a "dynamic" serializer depending on the data send. The endpoint should select the right "model" depending on the data properties existing in the request data.
I tried Django db.models but did not get them to work as I write the tickets to another external system and just pass them through, so no database table is existing and the model lacks the necessary primary key.
Can you help me out how to add more ticket types having the same endpoint?
Code
class TicketAPIView(CreateAPIView):
serializer_class = TicketSerializer
permission_classes = (IsAuthenticated,)
class TicketSerializer(serializers.Serializer):
title = serializers.CharField(max_length=256)
description = serializers.CharField(max_length=2048)
type = serializers.ChoiceField(TICKET_TYPES)
def create(self, validated_data):
if validated_data['type'] == 'normal':
ticket = TicketPOJO(
validated_data['title'],
validated_data['description'],
)
...
else:
raise Exception('Ticket type not supported')
return ticket
Files
/my-cool-app
/apps
/crm
/api
/v1
/serializers
serializers.py
__init.py
urls.py
views.py
/clients
/ticket
provider.py
/user
provider.py
/search
/config
Since your models are different for each of your ticket type, I would suggest you create an individual serializer that validates them for each different model with one generic view.
You can override the get_serializer method in your view to select an appropriate serializer depending upon the type of ticket. Something like this
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
type = self.request.data.get("type", '')
if type === 'normal':
return NormalTicketSerializer(*args, **kwargs)
elif type == 'abnormal':
return AbnormalTicketSerializer(*args, **kwargs)
else:
raise ParseError(detail='Ticket type not supported') # This will return bad request response with status code 400.
Hope this helps.
I'm new to the Django Framework and one thing bothers me.
I want a simple Rest Call:
www.abc.com/users/1/cantonments/1/
If i use 'pk' in the url pattern everything works out of the box (pk, pk1, pk2....).
But i have some permission functionality which expects the parameters in kwargs in the form 'upk' and 'cpk' for user and cantonment. So if i change pk to upk everything breaks. Somehow the url needs ONE pk.
This works:
url(r'^users/(?P<pk>[0-9]+)/cantonments/(?P<cpk>[0-9]+)/$',
views.CantonmentDetail.as_view()),
This doesnt:
url(r'^users/(?P<upk>[0-9]+)/cantonments/(?P<cpk>[0-9]+)/$',
views.CantonmentDetail.as_view()),
Is there any way to have an url pattern that does not need one entry with pk?
P.S. The error:
Expected view CantonmentDetail to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
EDIT:
My view is simple:
# Authenticated User can show Cantonment Detail
class CantonmentDetail(generics.RetrieveAPIView):
serializer_class = serializers.CantonmentSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Cantonment.objects.filter(pk=self.kwargs['cpk'])
Edit2:
I changed get_queryset to get object and it works.
def get_object(self):
queryset = self.filter_queryset(self.get_queryset())
obj = queryset.get(pk=self.kwargs['cpk'])
return obj
Edit3:
Using
lookup_url_kwarg = "cpk"
in the class works as well.
You can send optional pk using get method with your url like
www.abc.com/users/1/cantonments/?&upk=1
and url should be
url(r'^users/(?P<pk>[0-9]+)/cantonments/$',
views.CantonmentDetail.as_view()),
and views.py
def view_name(request, pk=None):
upk = request.GET.get('upk')
May be in your view you are accessing pk variable
urls.py
url(r'^users/(?P<upk>[0-9]+)/cantonments/(?P<cpk>[0-9]+)/$',
views.CantonmentDetail.as_view()),
views.py
class your_class_name(ListView):
def view_name(self):
upk=self.kwargs['upk']
cpk=self.kwargs['cpk']
print upk, cpk
...
Hope this is helps you
The upk doesn't make any difference to the lookup (because a primary key identifies a single object by design).
So for the view, the lookup_field needs to be set to 'cpk' and everything works.
Did you changed your view with the new names of the variables?
If you have url like this:
url(r'^users/(?P<upk>[0-9]+)/cantonments/(?P<cpk>[0-9]+)/$',
views.CantonmentDetail.as_view()),
You shouls update your view like this:
def view_name(request, upk=None, cpk=None):
...
I use the following serializer in most requests such as GET, POST etc:
class PrescriptionSerializer(serializer.ModelSerializer):
tags = TagSerializer()
setting = SettingSerializer()
But, I want to map setting field to SettingUpdateSerializer() if request.action is UPDATE(=PUT/PATCH). Without diving PrescriptionGetSerializer and PrescriptionUpdateSerialzer and using them accordingly, is there a way to dynamically map serializer-nesting field to other serializer, as below?
class PrescriptionSerializer(serializer.ModelSerializer):
tags = TagSerializer()
setting = SettingUpdateSerializer()
I though about using self.fields.pop on __init__, but this way it is only possible by using different different field names such as update_setting and get_setting.
Thanks for the help in advance.
I think the most clear solution is creating two separate serializer. And chose what serializer to use in a view layer depends on the http verb. If you use viewset it is easy to implement in a get_serializer_class method.
class SomeViewSet(viewsets.ModelViewset):
def get_serializer_class(self):
if self.action === 'update':
return UpdatePrescriptionSerializer
return PrescriptionSerializer
Now when you'll call a get_serializer in actions methods you'll get serializer depends on the action.
But you could also do something like you said:
class PrescriptionSerializer(serializer.ModelSerializer):
def __init__(self, *args, **kwargs):
super(PrescriptionSerializer, self).__init__(*args, **kwargs)
if self.context['request'].method == 'PUT':
self.fields['setting'] = SettingUpdateSerializer()
else:
self.fields['setting'] = SettingSerializer()
tags = TagSerializer()
Just ensure that you pass a request to serializer context. If you use get_serializer method in a viewset then it is already passed.
I use a variable in the base of my API url, identical to the setup found in the docs for Django REST Framework:
/api/<brand>/states/<state_pk>/
Everything after the base brand slug is a standard API format, and so I use ModelViewSets to generate all my list and detail views for my objects. Everything in the API is filtered by the brand, so this setup makes sense.
simplified project/urls.py
urlpatterns = patterns(
'',
url(r'^v2/(?P<brand_slug>\w+)/', include(router.urls, namespace='v2')),
)
simplified api/urls.py
router = routers.DefaultRouter()
router.register(r'states', StateViewSet)
router.register(r'cities', CityViewSet)
I also need hypermedia links for all models, and this is where I've run into problems. The REST framework doesn't know how to grab this brand variable and use it to generate correct links. Attempting to solve this problem by following the docs leaves me with 2 setbacks:
While the docs explain how to overwrite the HyperlinkRelatedField class, they never say where to put THAT class so that it works with my Serializers.
There's no mention on how to actually get the brand variable from the URL into the HyperlinkRelatedField class.
What are the missing elements here?
So, I figured it out.
Getting the URL variable into the Serializer
To do this, you need to overwrite the get_serializer_context() method for your ModelViewSet, and send in the variable from your kwargs
class BrandedViewSet(viewsets.ModelViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
context['brand_slug'] = self.kwargs.get('brand_slug')
return context
Then, you can just extend all of your ModelViewSets with that class:
class StateViewSet(BrandedViewSet):
queryset = State.objects.all()
serializer_class = StateSerializer
What's nice is that even though you've injected the Serializer with this variable, it's ALSO accessible from the HyperlinkedRelatedField class, via self.context, and that's how the next part is possible.
Building a Custom Hypermedia link with extra URL variables
The docs were correct in overwriting get_url():
class BrandedHyperlinkMixin(object):
def get_url(self, obj, view_name, request, format):
""" Extract brand from url
"""
if hasattr(obj, 'pk') and obj.pk is None:
return None
lookup_value = getattr(obj, self.lookup_field)
kwargs = {self.lookup_url_kwarg: lookup_value}
kwargs['brand_slug'] = self.context['brand_slug']
return reverse(
view_name, kwargs=kwargs, request=request, format=format)
Except, you'll notice I'm grabbing the variable from the context I set in part 1. I was unable to get the context from the object as the docs suggested, and this method turned out to be simpler.
The reason it's a mixin is because we need to extend TWO classes for this to work on all the url hyperlinks and not just the related field hyperlinks.
class BrandedHyperlinkedIdentityField(BrandedHyperlinkMixin,
serializers.HyperlinkedIdentityField):
pass
class BrandedHyperlinkedRelatedField(BrandedHyperlinkMixin,
serializers.HyperlinkedRelatedField):
pass
class BrandedSerializer(serializers.HyperlinkedModelSerializer):
serializer_related_field = BrandedHyperlinkedRelatedField
serializer_url_field = BrandedHyperlinkedIdentityField
Now we can safely extend our serializer and the hyperlinks show the brand variable!
class StateSerializer(BrandedSerializer):
class Meta:
model = State
fields = ('url', 'slug', 'name', 'abbrev', )
After resolving some of my troubles while converting from django-rest-framwork 0.3.2 to the lates 2.1.9 I cannot see to fix this one (which i agree with a blog of Reinout.... it's a real pain in the ...)
I had this code:
class ApiSomeInputView(View):
form = ApiSomeForm
permissions = (IsAuthenticated, )
resource=SomeResource
def get(self, request):
"""
Handle GET requests.
"""
return "Error: No GET request Possible, use post"
def post(self, request, format=None):
some_thing = self.CONTENT['some_thing']
# check if something exist:
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
#Note exludes are set in SomeResource
data = Serializer(depth=4).serialize(something)
return Response(status.HTTP_200_OK, data)
Now I have followed the tutorial and saw how you can do this different (maybe even prettier). By using slug in the url.
However.... I want to keep things backward compatible for the client side software... so I want to have this without putting the value of the query in the url. The client side uses json data and ContentType json in the header of a post.
In the first version of django rest framwork, I even got a nice browsable form in which to fill in the values for this query
My question: how to get this done in the latest version?
I can't seem to get a form in the views.... where I can fill in values and use in the proces
maybe good to post what I have tried until sofar...
first I changed the ModelResource in a Serializer:
class SomethingSerializer(HyperlinkedModelSerializer):
class Meta:
model = Something
#exclude = ('id',)
depth = 4
and than the view changed in to:
class ApiSomeInputView(APIView):
permissions = (IsAuthenticated, )
def post(self, request, format=None):
some_thing = request.DATA['some_thing']
# check if something exist: .... well actually this above already does not work
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise _404_SOMETHING_NOT_FOUND
serializer = SomethingSerializer(something)
return Response(status.HTTP_200_OK, serializer.data)
Note: Bases upon the accepted answer (by Tom Christie) I als put an answer in which I show how I got it working (in more detail).
When you're inheriting from APIView, the browseable API renderer has no way of knowing what serializer you want to use to present in the HTML, so it falls back to allowing you to post a plain JSON (or whatever) representation.
If you instead inherit from GenericAPIView, set the serializer using the serializer_class attribute, and get an instance of the serializer using the get_serializer(...) method - see here, then the browseable API will use a form to display the user input.
Based upon the answer of Tom Christie (which I'll accept as the answer). I got it working:
I made an extra serializer which defines the field(s) to be shown to fill in for the post and shown using the GenericAPIView... (correct me if I Am wrong Tom, just documenting it here for others... so better say it correct)
class SomethingSerializerForm(Serializer):
some_thing = serializers.IntegerField()
And with this serializer and the other one I aready had.
And a view:
class ApiSomeInputView(GenericAPIView):
permissions = (IsAuthenticated, )
model = Something
serializer_class = SomethingSerializerForm
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA)
if not serializer.is_valid():
raise ParseError(detail="No valid values")
some_thing = request.DATA['some_thing']
something = get_object_or_none(Something,some_field=int(some_thing))
if not something:
raise Http404
serializer = SomethingSerializer(something)
return Response(serializer.data)
Above is working, and exactly the same as before....
I still got the feeling I Am abusing the Serializer class as a Form.