Related
I'm using Django Rest Framework and OAuthTookit.
I want that the scope provided by the token should be HTTP Method specific. For eg:- GET, PUT, DELETE of the same APIView should have different scopes.
Following are my APIs.
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScope]
required_scopes = ['scope1']
serializer_class = ModelSerializer
queryset = Model.objects.all()
Currently, the scope is set at the class level, which means to access all the GET, PUT & DELETE method, the token should have scope1.
I want that there should be different scope for different HTTP methods. How can I set different scope for different methods?
To handle this case, I think you need to implement a new permission class, something like this:
class TokenHasScopeForMethod(TokenHasScope):
def has_permission(self, request, view):
token = request.auth
if not token:
return False
if hasattr(token, "scope"):
# Get the scopes required for the current method from the view
required_scopes = view.required_scopes_per_method[request.method]
return token.is_valid(required_scopes)
And use it in your view like this:
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScopeForMethod]
required_scopes_per_method = {'POST': ['post_scope'], 'GET': ['get_scope']}
serializer_class = ModelSerializer
queryset = Model.objects.all()
Perhaps You can use TokenMatchesOASRequirements permission class
class SongView(views.APIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [["read"]],
"POST": [["create"], ["post", "widget"]],
"PUT": [["update"], ["put", "widget"]],
"DELETE": [["delete"], ["scope2", "scope3"]],
}
I like #clément-denoix answer, but would tweak it a bit.
Instead of reloading TokenHasScope.has_permission I would suggest to redefine
TokenHasScope.get_scopes as it is smaller method and perfectly fit for what you need. Also original has_permission method has additional logic that I prefer to preserve.
Something like this should do the trick:
from django.core.exceptions import ImproperlyConfigured
from oauth2_provider.contrib.rest_framework import TokenHasScope
class TokenHasScopeForMethod(TokenHasScope):
def get_scopes(self, request, view):
try:
scopes = getattr(view, "required_scopes_per_method")
return scopes[request.method]
except (AttributeError, KeyError):
raise ImproperlyConfigured(
"TokenHasScope requires the view to define the required_scopes_per_method attribute"
)
I have a view that I want to add to my django-restframework api that does not relate to any model. Though I'm using 'rest_framework.permissions.DjangoObjectPermissions' in DEFAULT_PERMISSION_CLASSES.
class EnumChoices(views.APIView):
def get(self, request):
enums = {}
return Response(enums)
Now Django complains about my view:
AssertionError at /api/enums/
Cannot apply DjangoModelPermissions on a view that does not have `.queryset` property or overrides the `.get_queryset()` method.
I need the permission class for almost all other views and do not want to get rid of it. How can I get around the mandatory attributes for the one view?
You can add a view-specific permission logic to overwrite the model permission check. Create a BasePermission class object and add it to your views permission_classes attribute. Don't forget IsAuthenticated unless you want to allow anonymous users too.
class EnumChoices(views.APIView):
class EnumPermission(permissions.BasePermission):
def has_permission(self, request, view):
# whatever permission logic you need, e.g.
return request.user.has_perm("planning.view_enums")
permission_classes = (permissions.IsAuthenticated, EnumPermission)
def get(self, request):
enums = {}
return Response(enums)
Now the view will ensure the user is authenticated and has the view_enums permission.
More info here: http://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
I have a subscription model that looks like this
class Subscription(models.Model):
name = models.CharField(max_length=100)
quantity = models.IntegerField(max_length=20)
stripe_id = models.CharField(max_length=100)
user = models.ForeignKey(User)
I would like to create an endpoint that allows POST, PATCH, DELETE, GET
So I did the following things
views.py
class SubscriptionDetail(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
permission_classes = (IsAuthenticated,)
queryset = Subscription.objects.all()
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
#how do I write create and delete?
urls.py
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/$', subscription, name='something'),
Questions
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
The easiest way is to do it this way.
keep the models class the same
views.py
from rest_framework import viewsets
#impost serializer and model class for subscription
class SubscriptionViewSet(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
def get_queryset(self):
queryset = Subscription.objects.all()
#if you need to get subscription by name
name = self.request.QUERY_PARAMS.get('name', None)
if name is not None:
queryset = queryset.filter(name=name)
return queryset
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
# django will handle get, delete,patch, update for you ....
# for customization you can use def update or def create ... to do whatever you need
# def create(self, validated_data):
# you can handle the email here
# and something like subscription= Subscription (name=validated_data['name'],vendor=validated_data['quantity']...)
# subscription.save()
# it will save whatever you want
urls.py
#use the router to handle everything for you
from django.conf.urls import patterns, include, url
from rest_framework import routers
#import your classes
router = routers.DefaultRouter()
router.register(r'subscription', views.SubscriptionViewSet,base_name='subscription')
urlpatterns = patterns('',
url(r'^', include(router.urls)),
)
For the creation of an Object you must implement the create function as described in the official documentation, found here. For patching you could use the partial argument from within you view class:
SubscriptionSerializer(subscription, data={'something': u'another', partial=True)
For deletion of the a Subscription, that could be done when you get the delete call as so in your view class:
if request.METHOD == 'DELETE':
subscription = Subscription.objects.get(pk=pk)
subscription.delete()
See this tutorial for complete example
Further more I think that you should include the "id" field in the SubscriptionSerialiser Meta class, otherwise it will be difficult to do the updates/deletions. I hope this helped a little.
Cheers,
Tobbe
When you want to use a method that allow make these operations you have to use a #detail_route() where you can say as well which methods will you use, like in the docs is said:
#detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
...
So to be able to use them you should add the next decorator
#detail_route(methods=['post', 'patch'])
To add another parameters you can do it for the .save() parameter. You just have to indicate the name of this and them just override your .save() model to check if that email belongs or not to the user that is trying to do the subscription. Here I paste you what the Django Rest docs says:
" Passing additional attributes to .save()
...
You can do so by including additional keyword arguments when calling .save(). For example:
serializer.save(owner=request.user)
Here I leave you the link for more information:
http://www.django-rest-framework.org/api-guide/serializers/#passing-additional-attributes-to-save
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
The error is caused because unlike create request, patch/update require a pk to know which object to update. That is why you have to supply the pk value for it. So, your url for PUT, DELETE andPATCH must have at least named parameter like this -
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/(?<pk>(\d+))$', subscription, name='something'),
an example url will be - rest-auth/subscription/10 where 10 is the pk or id of the object. Django Rest Framework will then load the object internally to be updated.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
To add custom parameters, first declare the property in serializer, it is better to keep it required=False, so that other request does not throw error -
class SubscriptionSerializer(serializers.ModelSerializer):
custom_field = serialiers.BooleanField(required=False)
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
so far this is enough for the django rest framework to accept the field custom_field and you will find the value in update method. To get the value pop it from the attributes supplied by the framework like this -
def update(self, instance, validated_data):
custom_field = validated_data.pop('custom_field', None)
if custom_field is not None:
# do whatever you like with the field
return super().update(instance, validated_data)
# for python < 3.0 super(SubscriptionSerializer, self).update(instance, validated_data)
When you overrided (I don't know if that's the proper conjugation of overriding a method) the update method, you stopped the ability to PUT or PATCH and object. Your new method only prints out "In update" but doesn't save the instance. Look at the update method from the serializer.ModelSerializer object:
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Notice the last few lines where the instance is saved with the values and then returned. Remove your update method on the SubscriptionSerializer object. This let's your parent object's create, update, retrieve, and delete methods do their magic which supports PATCH and PUT updates. The next problem is that your urls.py is using the Django rather than the REST framework router. Change it to this:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'subscription', SubscriptionDetail)
That should solve the patch update problem.
I don't think you can add an email field in your patch method without the attribute on the subscription model. That's just a guess on my part, and I may be wrong. Does the email field map to anything on any object? Can you use a ForeignKey to map it?
I hope that works for you, good luck!
In view.py you just need set the class with:
class SubscriptionDetail(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
generics.GenericAPIView):
and add this to fix .lookup_field :
def update(self, request, *args, **kwargs):
log.error("OBJ update kwargs= %s , data = %s" % (kwargs, str(request.data)))
pk = request.data.get('id')
if (kwargs.get('pk') is not None):
kwargs['pk'] = request.data.get('id')
self.kwargs['pk'] = request.data.get('id')
return super().update(request, *args, **kwargs)
and add support to methods do you want :
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
# def get(self, request, *args, **kwargs):
# return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
# def patch(self, request, *args, **kwargs):
# return self.partial_update(request, *args, **kwargs)
#
# def delete(self, request, *args, **kwargs):
# return self.destroy(request, *args, **kwargs)
only tweak that remains is get for list or get for retrieve on element but should be easy now add something if we have one pk we may call self.retrieve else we may call self.list
Since my model's custom save method takes request.user as an argument I'm unable to do POST/PUT requests.
TypeError at /api/obsadmin/observation/23
save() takes at least 2 arguments (1 given)
I'm using SessionAuthentication() and have included the CSRF token.
Here's the relevant model part:
def save(self, user, owner=None, *args, **kwargs):
self.updated_by = user.id
super(ObsModel, self).save(*args, **kwargs)
And the resource:
class ObservationResource2(ModelResource):
comments = fields.ToManyField(CommentResource2, 'comments', full=True, null=True)
class Meta:
queryset = Observation.objects.filter(is_verified=True)
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
resource_name = 'observation'
always_return_data = True
I've just achieved this same end goal by using the built-in hydrate methods to modify the data prior to saving. The current request is available in bundle.request inside the hydrate methods. See the docs here.
I have a Friend model exposed via FriendResource that I want to link to the creating Django user via a user ForeignKey field.
My example Resource code:
class FriendResource(ModelResource):
class Meta:
queryset = Friend.objects.all()
resource_name = 'friend'
excludes = ['slug',]
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
always_return_data = True
def get_object_list(self, request):
return super(FriendResource, self).get_object_list(request).filter(user=request.user)
def hydrate(self, bundle):
bundle.obj.user = bundle.request.user
return bundle
Hope that helps!
You could override the default save() method on your ModelResource subclass. Looking at the default implementation shows that save() is called with a bundle object which has both the request and the object to be saved.
Unfortunately, there's no easy way to change this without copying most of that code because changing a Django model's save() signature is fairly uncommon. You might be able to do something like this, although I'd recommend testing it carefully:
from functools import partial
try:
old_save = bundle.obj.save
bundle.obj.save = partial(old_save, user=bundle.request.user)
return super(FooResource, self).save(bundle)
finally:
bundle.obj.save = old_save
References:
obj_create: docs source
obj_update: docs source
save: source
Using Django REST Framework, I want to limit which values can be used in a related field in a creation.
For example consider this example (based on the filtering example on https://web.archive.org/web/20140515203013/http://www.django-rest-framework.org/api-guide/filtering.html, but changed to ListCreateAPIView):
class PurchaseList(generics.ListCreateAPIView)
model = Purchase
serializer_class = PurchaseSerializer
def get_queryset(self):
user = self.request.user
return Purchase.objects.filter(purchaser=user)
In this example, how do I ensure that on creation the purchaser may only be equal to self.request.user, and that this is the only value populated in the dropdown in the form in the browsable API renderer?
I ended up doing something similar to what Khamaileon suggested here. Basically I modified my serializer to peek into the request, which kind of smells wrong, but it gets the job done... Here's how it looks (examplified with the purchase-example):
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
def get_fields(self, *args, **kwargs):
fields = super(PurchaseSerializer, self).get_fields(*args, **kwargs)
fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
return fields
class Meta:
model = Purchase
permitted_objects is a function which takes a user and a query, and returns a filtered query which only contains objects that the user has permission to link to. This seems to work both for validation and for the browsable API dropdown fields.
Here's how I do it:
class PurchaseList(viewsets.ModelViewSet):
...
def get_serializer(self, *args, **kwargs):
serializer_class = self.get_serializer_class()
context = self.get_serializer_context()
return serializer_class(*args, request_user=self.request.user, context=context, **kwargs)
class PurchaseSerializer(serializers.ModelSerializer):
...
def __init__(self, *args, request_user=None, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
self.fields['user'].queryset = User._default_manager.filter(pk=request_user.pk)
The example link does not seem to be available anymore, but by reading other comments, I assume that you are trying to filter the user relationship to purchases.
If i am correct, then i can say that there is now an official way to do this. Tested with django rest framework 3.10.1.
class UserPKField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
user = self.context['request'].user
queryset = User.objects.filter(...)
return queryset
class PurchaseSeriaizer(serializers.ModelSerializer):
users = UserPKField(many=True)
class Meta:
model = Purchase
fields = ('id', 'users')
This works as well with the browsable API.
Sources:
https://github.com/encode/django-rest-framework/issues/1985#issuecomment-328366412
https://medium.com/django-rest-framework/limit-related-data-choices-with-django-rest-framework-c54e96f5815e
I disliked the style of having to override the init method for every place where I need to have access to user data or the instance at runtime to limit the queryset. So I opted for this solution.
Here is the code inline.
from rest_framework import serializers
class LimitQuerySetSerializerFieldMixin:
"""
Serializer mixin with a special `get_queryset()` method that lets you pass
a callable for the queryset kwarg. This enables you to limit the queryset
based on data or context available on the serializer at runtime.
"""
def get_queryset(self):
"""
Return the queryset for a related field. If the queryset is a callable,
it will be called with one argument which is the field instance, and
should return a queryset or model manager.
"""
# noinspection PyUnresolvedReferences
queryset = self.queryset
if hasattr(queryset, '__call__'):
queryset = queryset(self)
if isinstance(queryset, (QuerySet, Manager)):
# Ensure queryset is re-evaluated whenever used.
# Note that actually a `Manager` class may also be used as the
# queryset argument. This occurs on ModelSerializer fields,
# as it allows us to generate a more expressive 'repr' output
# for the field.
# Eg: 'MyRelationship(queryset=ExampleModel.objects.all())'
queryset = queryset.all()
return queryset
class DynamicQuersetPrimaryKeyRelatedField(LimitQuerySetSerializerFieldMixin, serializers.PrimaryKeyRelatedField):
"""Evaluates callable queryset at runtime."""
pass
class MyModelSerializer(serializers.ModelSerializer):
"""
MyModel serializer with a primary key related field to 'MyRelatedModel'.
"""
def get_my_limited_queryset(self):
root = self.root
if root.instance is None:
return MyRelatedModel.objects.none()
return root.instance.related_set.all()
my_related_model = DynamicQuersetPrimaryKeyRelatedField(queryset=get_my_limited_queryset)
class Meta:
model = MyModel
The only drawback with this is that you would need to explicitly set the related serializer field instead of using the automatic field discovery provided by ModelSerializer. i would however expect something like this to be in rest_framework by default.
In django rest framework 3.0 the get_fields method was removed. But in a similar way you can do this in the init function of the serializer:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
def __init__(self, *args, **kwargs):
super(PurchaseSerializer, self).__init__(*args, **kwargs)
if 'request' in self.context:
self.fields['purchaser'].queryset = permitted_objects(self.context['view'].request.user, fields['purchaser'].queryset)
I added the if check since if you use PurchaseSerializer as field in another serializer on get methods, the request will not be passed to the context.
First to make sure you only allow "self.request.user" when you have an incoming http POST/PUT (this assumes the property on your serializer and model is named "user" literally)
def validate_user(self, attrs, source):
posted_user = attrs.get(source, None)
if posted_user:
raise serializers.ValidationError("invalid post data")
else:
user = self.context['request']._request.user
if not user:
raise serializers.ValidationError("invalid post data")
attrs[source] = user
return attrs
By adding the above to your model serializer you ensure that ONLY the request.user is inserted into your database.
2) -about your filter above (filter purchaser=user) I would actually recommend using a custom global filter (to ensure this is filtered globally). I do something for a software as a service app of my own and it helps to ensure each http request is filtered down (including an http 404 when someone tries to lookup a "object" they don't have access to see in the first place)
I recently patched this in the master branch so both list and singular views will filter this
https://github.com/tomchristie/django-rest-framework/commit/1a8f07def8094a1e34a656d83fc7bdba0efff184
3) - about the api renderer - are you having your customers use this directly? if not I would say avoid it. If you need this it might be possible to add a custom serlializer that would help to limit the input on the front-end
Upon request # gabn88, as you may know by now, with DRF 3.0 and above, there is no easy solution.
Even IF you do manage to figure out a solution, it won't be pretty and will most likely fail on subsequent versions of DRF as it will override a bunch of DRF source which will have changed by then.
I forget the exact implementation I used, but the idea is to create 2 fields on the serializer, one your normal serializer field (lets say PrimaryKeyRelatedField etc...), and another field a serializer method field, which the results will be swapped under certain cases (such as based on the request, the request user, or whatever). This would be done on the serializers constructor (ie: init)
Your serializer method field will return a custom query that you want.
You will pop and/or swap these fields results, so that the results of your serializer method field will be assigned to the normal/default serializer field (PrimaryKeyRelatedField etc...) accordingly. That way you always deal with that one key (your default field) while the other key remains transparent within your application.
Along with this info, all you really need is to modify this: http://www.django-rest-framework.org/api-guide/serializers/#dynamically-modifying-fields
I wrote a custom CustomQueryHyperlinkedRelatedField class to generalize this behavior:
class CustomQueryHyperlinkedRelatedField(serializers.HyperlinkedRelatedField):
def __init__(self, view_name=None, **kwargs):
self.custom_query = kwargs.pop('custom_query', None)
super(CustomQueryHyperlinkedRelatedField, self).__init__(view_name, **kwargs)
def get_queryset(self):
if self.custom_query and callable(self.custom_query):
qry = self.custom_query()(self)
else:
qry = super(CustomQueryHyperlinkedRelatedField, self).get_queryset()
return qry
#property
def choices(self):
qry = self.get_queryset()
return OrderedDict([
(
six.text_type(self.to_representation(item)),
six.text_type(item)
)
for item in qry
])
Usage:
class MySerializer(serializers.HyperlinkedModelSerializer):
....
somefield = CustomQueryHyperlinkedRelatedField(view_name='someview-detail',
queryset=SomeModel.objects.none(),
custom_query=lambda: MySerializer.some_custom_query)
#staticmethod
def some_custom_query(field):
return SomeModel.objects.filter(somefield=field.context['request'].user.email)
...
I did the following:
class MyModelSerializer(serializers.ModelSerializer):
myForeignKeyFieldName = MyForeignModel.objects.all()
def get_fields(self, *args, **kwargs):
fields = super(MyModelSerializer, self).get_fields()
qs = MyModel.objects.filter(room=self.instance.id)
fields['myForeignKeyFieldName'].queryset = qs
return fields
I looked for a solution where I can set the queryset upon creation of the field and don't have to add a separate field class. This is what I came up with:
class PurchaseSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Purchase
fields = ["purchaser"]
def get_purchaser_queryset(self):
user = self.context["request"].user
return Purchase.objects.filter(purchaser=user)
def get_extra_kwargs(self):
kwargs = super().get_extra_kwargs()
kwargs["purchaser"] = {"queryset": self.get_purchaser_queryset()}
return kwargs
The main issue for tracking suggestions regarding this seems to be drf#1985.
Here's a re-usable generic serializer field that can be used instead of defining a custom field for every use case.
class DynamicPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
"""A PrimaryKeyRelatedField with ability to set queryset at runtime.
Pass a function in the `queryset_fn` kwarg. It will be passed the serializer `context`.
The function should return a queryset.
"""
def __init__(self, queryset_fn=None, **kwargs):
assert queryset_fn is not None, "The `queryset_fn` argument is required."
self.queryset_fn = queryset_fn
super().__init__(**kwargs)
def get_queryset(self):
return self.queryset_fn(context=self.context)
Usage:
class MySerializer(serializers.ModelSerializer):
my_models = DynamicPrimaryKeyRelatedField(
queryset_fn=lambda context: MyModel.objects.visible_to_user(context["request"].user)
)
# ...
Same works for serializers.SlugRelatedField.