DRF how to use DjangoModelPermissions on function-based views? - django-rest-framework

I separated by view functions instead of using viewset/queryset. Question is, how can I restrict permissions to my view functions based on user-group permissions?
Sample code:
#api_view(['GET', 'POST'])
#permission_classes([DjangoModelPermissions])
def some_list(request):
"""
List all something, or create a new something.
"""
{...code here...}
Error:
Cannot apply DjangoModelPermissions on a view that does not set .queryset or have a .get_queryset() method.

Ended up making my own custom DjangoModelPermissions without checking queryset to get model but rather creating multiple child class for each specific models.
from rest_framework import permissions
from django.apps import apps
class BaseCustomModelPermissions(permissions.BasePermission):
model_cls = None
perms_map = {
'GET': [],
'OPTIONS': [],
'HEAD': [],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
authenticated_users_only = True
def get_required_permissions(self, method):
"""
Given a model and an HTTP method, return the list of permission
codes that the user is required to have.
"""
kwargs = {
'app_label': self.model_cls._meta.app_label,
'model_name': self.model_cls._meta.model_name
}
if method not in self.perms_map:
raise exceptions.MethodNotAllowed(method)
return [perm % kwargs for perm in self.perms_map[method]]
def has_permission(self, request, view):
# Workaround to ensure DjangoModelPermissions are not applied
# to the root view when using DefaultRouter.
if getattr(view, '_ignore_model_permissions', False):
return True
if not request.user or (
not request.user.is_authenticated and self.authenticated_users_only):
return False
perms = self.get_required_permissions(request.method)
return request.user.has_perms(perms)
Sample Child class:
class SomeModelPermissions(BaseCustomModelPermissions):
model_cls = apps.get_model('my_app', 'its_Model')

Related

Override permission and authentication classes in ViewSet list method

I am trying to set specific authentication and permission classes on the ListModelMixin of a viewset. I tried the following but it is not working:
def list(self, request):
self.authentication_classes = (apiauth.SessionAuthentication, )
self.permission_classes = (permissions.IsAuthenticated, )
return super(TestViewSet, self).list(request)
Am i doing something wrong ?
#henriquesalvaro's answer does not work for me.
For some reason action is not part of self in get_authenticators(self)
So if self.action == 'list' does not work for me.
I had to write the following code to get the action.
def get_authenticators(self):
authentication_classes = [TokenAuthentication]
action_map = {key.lower(): value for key,
value in self.action_map.items()}
action_name = action_map.get(self.request.method.lower())
if action_name =="list":
return [YourPermission]
return authentication_classes
When your request reaches the list function, it means it has already passed both the authentication and the permission phases.
If you're using the same classes for permission and authentication for all actions on your ViewSet you should define them on your class declaration:
class MyViewSet(viewsets.ViewSet):
authentication_classes = (apiauth.SessionAuthentication,)
permission_classes = (permissions.IsAuthenticated,)
If you're trying to use different authentication/permission classes for the list action, you can override the get_authenticators and get_permissions methods:
class MyViewSet(viewsets.ViewSet):
...
def get_authenticators(self):
if self.action == 'list':
# Set up here
return YourAuthenticators
return super().get_authenticators()
def get_permissions(self):
if self.action == 'list':
# Set up here
return YourPermissions
return super().get_permissions()

Cannot generate post request for multiple data

I am trying to take input multiple data object in post request, but getting such error.
non_field_errors: [ Invalid data. Expected a dictionary, but got a list. ]
models.py
class OrderProduct(BaseModel):
product = models.ForeignKey(Product,on_delete=models.CASCADE)
order = models.ForeignKey(Order,on_delete=models.CASCADE)
order_product_price = models.FloatField(blank=False,null=False,default=0) # product may belong to offer do the price
order_product_qty = models.FloatField(default=1)
serializers.py
class OrderProductSerializer(serializers.ModelSerializer):
def update(self,instance,validated_data):
product = self.validated_data.pop('product')
order = self.validated_data.pop('order')
instance.orderproduct_qty =
self.validated_data.get('orderproduct_qty',instance.orderproduct_qty)
instance.product = product
instance.order = order
instance.save()
return instance
class Meta:
model = OrderProduct
fields = '__all__'
views.py
def post(self,request,*args,**kwargs):
if request.data['contact_number'] == '':
request.POST._mutable =True
request.data['contact_number'] = request.user.mobile_number
request.POST._mutable = False
serializer = OrderSerializer(data=request.data,many=isinstance(request.data,list),context={'request': request})
print(serializer)
if serializer.is_valid():
serializer.save(user = request.user,created_by = request.user)
return Response(serializer.data,status=status.HTTP_200_OK)
else:
return Response(serializer.errors,status=status.HTTP_400_BAD_REQUEST)
urls.py
path('orderproduct/',views.OrderProductList.as_view()),
When you call serializer.save(). It's only perform create() action which is only create one and accept dictionary data type only. If you want to save multiple data like that, you will have to override the create function of the serializer class. You can do something similar like this or run a for loop.
serializers.py
def create(self, validate_data):
# Get the data objects you need to perform bulk create
order_products = OrderProduct.objects.bulk_create(validate_data)
return order_products
views.py
if serializer.is_valid(raise_exception=True):
# Replace the serializer.save() by this line to trigger the create method in serializer
self.perform_create(serializer)
return Response(...)

How to solve django.url.exceptions.NoReverseMatch with kwargs?

I am trying to update an object that has already been created.
Following is my urls.py:
from django.conf.urls import url
from django.urls import include, path
from rest_framework import routers
from . import admin_views, temp_views, views
app_name = "transactions"
router = routers.SimpleRouter()
router.register(r"transactions", views.TransactionViewSet)
router.register(r"offerings", views.OfferingViewSet)
router.register(r"bank_accounts", views.BankAccountViewSet)
router.register(r"merchants", views.MerchantViewSet)
Following is my views.py:
class MerchantViewSet(GetPrefixedIDMixin, viewsets.ModelViewSet):
"""POST support for /merchants/."""
print ("in MerchantViewSet")
queryset = models.Merchant.objects.all()
serializer_class = serializers.CreateMerchantSerializer
lookup_field = "id"
lookup_value_regex = f"{models.Merchant.id_prefix}_[a-f0-9]{32}"
permission_classes = [permissions.MerchantPermission]
def get_queryset(self):
"""Filter the queryset based on the full merchant name or starting with a letter."""
queryset = models.Merchant.objects.all()
search_param = self.request.query_params.get("search", None)
if search_param:
if search_param.startswith("^"):
queryset = queryset.filter(name__istartswith=search_param[1:])
else:
queryset = queryset.filter(name__icontains=search_param)
return queryset
Following is the test I am trying to write:
class MerchantsViewSetTest(tests.BigETestCase): # noqa
#classmethod
def setUpClass(cls): # noqa
super(MerchantsViewSetTest, cls).setUpClass()
cls.application = tests.get_application()
tests.create_group("merchant")
cls.consumer_user = tests.create_consumer()
cls.admin = tests.create_administrator()
cls.merchant_geraldine = models.Merchant.objects.create(
name="Test Account 1",
contact_name="Geraldine Groves",
contact_email="geraldine#example.com",
contact_phone_number="+35310000000",
)
cls. merchant_barbara = models.Merchant.objects.create(
name="Account 2",
contact_name="Barbara",
contact_email="barbara#example.com",
contact_phone_number="+35310000432",
)
def test_edit_merchant(self): # noqa
# url = reverse("bige_transactions:merchant-list", kwargs={"id": self.merchant_geraldine.prefixed_id},)
url = reverse("bige_transactions:merchant-list", kwargs={"id": self.merchant_geraldine.prefixed_id})
# payload
data = {"name": "Edited"}
# verify anonymous cannot edit a user
self.put(url, data, status_code=401)
Following is my permissions.py:
class MerchantPermission(BasePermission): # noqa
def _has_get_permission(self, request, view): # noqa
access_token = get_access_token(request)
# we allow the request if the user is an admin
if is_administrator(access_token.user):
return True
return False
def _has_put_permission(self, request, view): # noqa
print ("entered put permissions")
access_token = get_access_token(request)
# we allow the request if the user is an admin
if is_administrator(access_token.user):
print ("Admin detected!")
return True
return False
def has_permission(self, request, view): # noqa
# is it a supported method
method = request.method.lower()
if method in view.http_method_names:
if method == "post":
return True
elif method == "get":
return self._has_get_permission(request, view)
elif method == "put":
return self._has_put_permission(request, view)
# always deny by default
raise exceptions.MethodNotAllowed(request.method)
So basically what I am trying to do is to add a functionality whereby I can update a merchant's detail. From my understanding from reading the Django Rest Framework documentation, the router.register method in my urls.py should set up all the URL's for create, list, update, retrieve and destroy. With my current test method, I get :
django.urls.exceptions.NoReverseMatch: Reverse for 'merchant-list' with keyword arguments '{'id': 'merch_b6c983ec082b4c7eb321b335fe9b122c'}' not found. 1 pattern(s) tried: ['merchants/$']
I am able to create, list and search users (didn't add those methods here as they are not relevant and don't want to make this question longer than it needs to be). I changing the url to
url = reverse("bige_transactions:merchant-list") + "?id=" + self.merchant_geraldine.prefixed_id
which does print out the URL, and I do not get the reverse() method error anymore, but I get a 404 error. So I am kind of stuck trying to implement this and I cannot find any examples on how to use the update() method that is built in ModelViewSet other than mentions of that it can be used.
When you register your viewset using router.register(r"merchants", views.MerchantViewSet) behind the scene you will get two different reverse urls:
merchants-list -> actual url will be ^merchants/$
merchants-detail -> actual url will be ^merchants/<pk>/$
In your case since you defined lookup_field="id" if might be <id> instead of <pk>. So your code needs to use the detail url like this:
url = reverse("bige_transactions:merchant-detail", kwargs={'id': self.merchant_geraldine.prefixed_id})
For more info check related docs

How get request user data in django rest to representation function

I need to access request.user in to_representation function, I tried self.context['request'] but self.context is empty dictionary. Is there anyway to access request.user or any way that I can push this data to this function?
def to_representation(self, obj):
print(self.context)
#output is an empty dictionary {}
UPDATE
class RetrieveView(generics.RetrieveAPIView):
def get(self, request, *args, **kwargs):
uid = kwargs.get('uid')
try:
item = self.model.nodes.get(uid=uid)
except Exception as e:
# error response
serializer = self.serializer_class(item)
return HttpSuccessResponse(SuccessResponse(serializer.data).to_json(), status=status.HTTP_200_OK).send()
class TopicRetrieveView(single_result.RetrieveView):
model = Topic
serializer_class = topic.TopicSerializer
ALL CODES are from django rest framwork Generic views generic.py
serializer_class is attribute we set in class definition or we need to override get_serializer_class function. It will handle in this function:
def get_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.serializer_class`.
You may want to override this if you need to provide different
serializations depending on the incoming request.
(Eg. admins get full serialization, others get basic serialization)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
get_serializer_class will used in get_serializer function:
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
and context will fill by get_serializer_context function.
def get_serializer_context(self):
"""
Extra context provided to the serializer class.
"""
return {
'request': self.request,
'format': self.format_kwarg,
'view': self
}
So correct usage is serializer = self.get_serializer(item) because it will use serializer_class for serializing item and fill context with extra information that may be helpful. serializer = self.serializer_class(item) can be used for just serializing item with no more extra information.

How to Not allow the PUT method at all but allow PATCH in a DRF ViewSet?

PUT and PATCH are both part of the same mixin (The UpdateModelMixin).
So if I extend it like so:
class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
Both PUT and PATCH are allowed. I want to not allow PUT at all for my app (since PATCH already does the work, and I want to limit object creation using just POST). One way is to create a permission:
class NoPut(permissions.BasePermission):
"""
PUT not allowed.
"""
message = 'You do not have permission to complete the action you are trying to perform.'
def has_object_permission(self, request, view, obj):
if view.action == "update":
return False
return True
And to give this permission to all my ViewSets which allow PATCH. Is this the best way to do it? Is there a more preferred way?
Edit: After looking at the answer provided by #wim, will this be a fine solution (everything kept the same except the mapping for put was removed):
from rest_framework.routers import SimpleRouter
class NoPutRouter(SimpleRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes.
# Generated using #list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
# put removed
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes.
# Generated using #detail_route decorator on methods of the viewset.
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
]
or would I need to redefine other methods in SimpleRoute (e.g. __init()__, get_routes(), _get_dynamic_routes(), get_method_map() etc.) in order for it to work correctly?
If you want to use builtin mixins.UpdateModelMixin, limit to PATCH and disable swagger from showing PUT you can use http_method_names
class UserViewSet(mixins.UpdateModelMixin, GenericViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
http_method_names = ["patch"]
Instead of using mixins.UpdateModelMixin just define your own mixin that would perform patch only:
class UpdateModelMixin(object):
"""
Update a model instance.
"""
def partial_update(self, request, *args, **kwargs):
partial = True
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(serializer.data)
def perform_update(self, serializer):
serializer.save()
A simple and straight forward approach:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
http_method_names = ['get', 'post', 'patch'] # <---------
Like this the PUT method will not be allowed.
I think a superior solution would be to use a custom router and disable the route for PUT. Then use your custom router for the viewsets.
class SimpleRouter(BaseRouter):
routes = [
# List route.
Route(
url=r'^{prefix}{trailing_slash}$',
mapping={
'get': 'list',
'post': 'create'
},
name='{basename}-list',
initkwargs={'suffix': 'List'}
),
# Dynamically generated list routes.
# Generated using #list_route decorator
# on methods of the viewset.
DynamicListRoute(
url=r'^{prefix}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
# Detail route.
Route(
url=r'^{prefix}/{lookup}{trailing_slash}$',
mapping={
'get': 'retrieve',
'put': 'update',
'patch': 'partial_update',
'delete': 'destroy'
},
name='{basename}-detail',
initkwargs={'suffix': 'Instance'}
),
# Dynamically generated detail routes.
# Generated using #detail_route decorator on methods of the viewset.
DynamicDetailRoute(
url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
name='{basename}-{methodnamehyphen}',
initkwargs={}
),
]
^ The router implementation looks something like that. So you just need to inherit the SimpleRouter, or perhaps the DefaultRouter, and defines the routes class attribute how you want it. You can remove the mapping for 'put' in the Route(mapping={...}) completely, or you can define your own action to handle it and return the appropriate 400-something resonse.
Similar to #linovia's answer but using standard mixin:
from rest_framework.exceptions import MethodNotAllowed
class UpdateModelMixin(mixins.UpdateModelMixin, viewsets.GenericViewSet):
"""
update:
Update Model
"""
def update(self, *args, **kwargs):
raise MethodNotAllowed("POST", detail="Use PATCH")
def partial_update(self, request, *args, **kwargs):
# Override Partial Update Code if desired
return super().update(*args, **kwargs, partial=True)
Here's the solution I'm using:
class SomeViewSet(
mixins.UpdateModelMixin,
...
):
#swagger_auto_schema(auto_schema=None)
def update(self, request, *args, **kwargs):
"""Disabled full update functionality"""
partial = kwargs.get('partial', False) # This must be .get() not .pop()
if not partial:
raise exceptions.MethodNotAllowed(request.method)
return super(SomeViewSet, self).update(request, *args, **kwargs)
This will also disable it in drf-yasg UIs.
Solution similar to #EbramShehata's but for drf-spectacular (OpenAPI 3). This will disallow full updates (PUT) and also exclude that from the generated OpenAPI 3 schema.
class SomeViewSet(
mixins.UpdateModelMixin,
...
):
#extend_schema(exclude=True)
def update(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""Disallow full update (PUT) and allow partial update (PATCH)."""
if kwargs.get("partial", False): # Use .get() instead of .pop()
return super().update(request, args, kwargs)
raise MethodNotAllowed(request.method)

Resources