How to solve django.url.exceptions.NoReverseMatch with kwargs? - django-rest-framework

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

Related

Override request.user in djangorestframework-simplejwt

There are two user models in my project:
class User(AbstractUser):
id = models.AutoField(primary_key=True)
email = models.EmailField(unique=True)
...
class ProjectMember(UserModel):
project = models.ForeignKey("project.Project")
I use djangorestframework-simplejwt for authorization and it gives me a User instance in request.user inside a view, here is an example:
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.views import APIView
class CurrentUserView(ListAPIView):
permission_classes = [IsAuthenticated]
def get(self, request: Request):
# there will be User instance
current_user = request.user
# Will raise an exception
current_user.project
# some other code here
...
I have a user, but I cannot access project, because it defined in ProjectMember and not accessible via User.
I found out that I can get ProjectMember instance by checking the special attribute:
def get(self, request: Request):
# there will be ProjectMember instance
current_user = request.user.projectmember
# I can access project now
current_user.project
# some other code here
...
But now I have to repeat this code in every view I use my current user. How can I override the request.user for it to be always a ProjectMember (if it is an instance of ProjectMember, of course)?
I found the solution while writing the question.
Override JWTAuthentication like this:
# users/auth/custom_auth.py
from rest_framework_simplejwt.authentication import JWTAuthentication
class CustomAuth(JWTAuthentication):
def authenticate(self, request):
user, access = super().authenticate(request)
if hasattr(user, 'projectmember'):
user = user.projectmember
return user, access
And in settings.py change:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication'
],
}
to path to new class (users/auth/custom_auth.py:CustomAuth in my case):
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'users.auth.custom_auth.CustomAuth'
],
}

DRF how to use DjangoModelPermissions on function-based views?

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')

How to do a search in the DRF only for the selected field?

I can’t understand how to do a search in the DRF only for the selected field.
The documentation on https://www.django-rest-framework.org/api-guide/filtering/ states that need to create a subclass to override the functions get_search_fields (). Everything is clear with this.
It is not clear how to make a request.
If just do a search:
http://... /api/v1/?search=keywords
In this case, it searches for all fields specified in:
search_fields = ['title', 'article']
What needs to be specified in the request in order to search in title field or article field?
http://... /api/v1/?search=keywords..?
Fragment of the views.py responsible for the search:
from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter):
def get_search_fields(self, view, request):
if request.query_params.get('title_only'):
return ['title']
elif request.query_params.get('article_only'):
return ['article']
return super(CustomSearchFilter, self).get_search_fields(view, request)
class MyListView(generics.ListAPIView):
serializer_class = MySerializer
filter_backends = [CustomSearchFilter, filters.OrderingFilter]
ordering_fields = ['id', 'title', 'article']
search_fields = ['title', 'article']
def get_queryset(self):
queryset = Blog.objects.all()
return queryset
You can use DjangoFilterBackend
Then you'll only need to specify the fields, something like this
class ProductList(generics.ListAPIView):
queryset = Product.objects.all()
serializer_class = ProductSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['category', 'in_stock']
Then you can filter using query params like this
http://example.com/api/products?category=clothing&in_stock=True
If you need more complex filter then have a look at django-filter documentations : https://django-filter.readthedocs.io/en/master/ref/filters.html#lookup-expr
Reference : https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend
How:
http://127.0.0.1:8000/api/v1/?search=Sometitle&title_only=title_only
Why:
# filters.py
from rest_framework import filters
class CustomSearchFilter(filters.SearchFilter):
""""Dynamically change search fields based on request content."""
def get_search_fields(self, view, request):
"""
Search only on the title or the article body if the query parameter,
title_only or article_only is in the request.
"""
# request.query_params is a more correctly named synonym for request.GET
# request.GET a dictionary-like object containing
# all given HTTP GET parameters
# Get the value of the "title_only" item
# How does the querydict looks like
# <QueryDict: {'search': ['Sometitle'], 'title_only': ['title_only']}>
# if exist return search_fields with ['title'] only
if request.query_params.get('title_only'):
return ['title']
elif request.query_params.get('article_only'):
return ['article']
return super(CustomSearchFilter, self).get_search_fields(view, request)
Links:
query_params
django.http.HttpRequest.GET

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)

DRF: Pagination without queryset

I am trying to make use of Django Rest Framework's pagination mechanisms in my case without success.
class TransactionView(viewsets.ViewSet):
serializer_class = TransactionSerializer
def list(self, request):
# fetching data from external API...
serializer = self.serializer_class(data=list_of_json, many=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
else:
return Response(serializer.errors)
class TransactionSerializer(serializers.Serializer):
# Serializer (transaction's) fields ...
def create(self, validated_data):
return APITransaction(**validated_data)
class APITransaction(object):
def __init__(self, arg1, arg2, ...):
self.arg1 = arg1
...
The problem is that registering the pagination_class (like I have done for the rest of my resources which are represented by Models), doesn't work since the data are created/fetched on the fly, thus I don't have a Model/queryset.
Any ideas on how I could use DRF's pagination mechanism?
Here's the class I wound up creating and using locally for this sort of thing. (Thanks to stelios above for the initial clues.) Of course the contents of "data" must be JSONable.
from typing import List, Any
from collections import OrderedDict
from django.core.paginator import Paginator
from django.http.response import JsonResponse
from rest_framework.request import Request
class ListPaginator:
def __init__(self, request: Request):
# Hidden HtmlRequest properties/methods found in Request.
self._url_scheme = request.scheme
self._host = request.get_host()
self._path_info = request.path_info
def paginate_list(self, data: List[Any], page_size: int, page_number: int) -> JsonResponse:
paginator = Paginator(data, page_size)
page = paginator.page(page_number)
previous_url = None
next_url = None
if self._host and self._path_info:
if page.has_previous():
previous_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.previous_page_number())
if page.has_next():
next_url = '{}://{}{}?limit={}&page={}'.format(self._url_scheme, self._host, self._path_info, page_size, page.next_page_number())
response_dict = OrderedDict([
('count', len(data)),
('next', next_url),
('previous', previous_url),
('results', page.object_list)
])
return JsonResponse(response_dict, status=200, safe=False)
You can't reuse existing DRF's pagination because they are supposed to work with queryset.
However, you may roll your own class by inheriting BasePagination though I haven't done myself.

Resources