Django REST - grab paramater value when part of the URL - django

I have the following urls.py:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from location import views
router = DefaultRouter()
router.register('city', views.CityViewSet, 'city')
app_name = 'location'
urlpatterns = [
path('', include(router.urls)),
]
when hitting the url /api/location/city/25/ I get all the details for that object instance (with the id of 25) as expected.
My question how do I grab the id number in my viewset?
I know how to do it with just regular query parameters /?id=25, but can't seem to figure out how to do it when it's part of the URL.

URL captures are available as the kwargs attribute (which is a dict) of the viewset instance. So from inside any viewset method, you can access them via self.kwargs.
In this case, as you're retrieving the instance (GET on detail), you can get the pk (primary key) via:
class CityViewSet(ModelViewSet):
...
def retrieve(self, request, *args, **kwargs):
pk = self.kwargs['pk']
Note that, I've assumed your lookup_url_kwarg is pk (the default); if you have something different you need to access by that key name as you can imagine.

Related

Django DRF, how to properly register custom URL patterns with DRF actions

Background
I have a ModelViewSet that defines several custom actions. I am using the default router in my urls.py to register the URLs. Right now, my views use the default created routes like ^images/$, ^images/{pk}/$.
In order for users to use the API using resource names with which they are familiar, I have adapted the viewset to accept multiple parameters from the URL, using the MultipleFieldLookupMixin strategy described in the docs to allow for the pattern images/{registry_host}/{repository_name}/{tag_name}.
I've created the get_object method in my viewset like so:
class ImageViewSet(viewsets.ModelViewSet):
...
def get_object(self):
special_lookup_kwargs = ['registry_host', 'repository_name', 'tag_name']
if all(arg in self.kwargs for arg in special_lookup_kwargs):
# detected the custom URL pattern; return the specified object
return Image.objects.from_special_lookup(**self.kwargs)
else: # must have received pk instead; delegate to superclass
return super().get_object()
I've also added a new URL path pattern for this:
urls.py
router = routers.DefaultRouter()
router.register(r'images', views.ImageViewSet)
# register other viewsets
...
urlpatterns = [
...,
path('images/<str:registry_host>/<path:repository_name>/<str:tag_name>', views.ImageViewSet.as_view({'get': 'retrieve',})),
path('', include(router.urls)),
]
Problem
The above all works as intended, however, I also have some extra actions in this model viewset:
#action(detail=True, methods=['GET'])
def bases(self, request, pk=None):
...
#action(detail=True, methods=['GET'])
def another_action(...):
... # and so on
With the default patterns registered by DRF, I could go to images/{pk}/<action> (like images/{pk}/bases) to trigger the extra action methods. However I cannot do this for images/{registry_host}/{repository_name}/{tag_name}/<action>. This is somewhat expected because I never registered any such URL and there's no reasonable way DRF could know about this.
I'm guessing that I can add all these paths manually with an appropriate arguments to path(...) but I'm not sure what that would be.
urlpatterns = [
...,
path('.../myaction', ???)
]
Questions
As a primary question, how do I add the actions for my URL?
However, I would like to avoid having to add new URLS every time a new #action() is added to the view. Ideally, I would like these to be registered automatically under the default path images/{pk}/<action> as well as images/{registry_host}/{repository_name}/{tag_name}/<action>.
As a secondary question, what is the appropriate way to achieve this automatically? My guess is perhaps a custom router. However, it's unclear to me how I would implement adding these additional routes for all extra (detail) actions.
Using django/drf 3.2
Another approach I see is to (ab)use url_path with kwargs like:
class ImageViewSet(viewsets.ModelViewSet):
#action(detail=False, methods=['GET'], url_path='<str:registry_host>/<path:repository_name>)/<str:tag_name>/bases')
def bases_special_lookup(...):
# detail=False to remove <pk>
#action(detail=True, methods=['GET'])
def bases(...):
...
#action(detail=False, methods=['GET'], url_path='<str:registry_host>/<path:repository_name>)/<str:tag_name>')
def retrieve_special_lookup(...):
# detail=False to remove <pk>
def retrieve(...):
...
This will create these urls:
images/<str:registry_host>/<path:repository_name>/<str:tag_name>/bases
images/<pk>/bases
images/<str:registry_host>/<path:repository_name>/<str:tag_name>
images/<pk>
Theres probably a better way of doing this, but you could try something like this:
router = routers.DefaultRouter()
router.register(r'images', views.ImageViewSet)
# register other viewsets
...
urlpatterns = [
...,
path('images/<str:registry_host>/<path:repository_name>/<str:tag_name>', views.ImageViewSet.as_view({'get': 'retrieve',})),
path('images/<str:registry_host>/<path:repository_name>/<str:tag_name>/<str:action>', views.ImageViewSet.as_view({'get': 'retrieve',})),
path('', include(router.urls)),
]
and then in your viewset:
class ImageViewSet(viewsets.ModelViewSet):
...
def get_object(self):
special_lookup_kwargs = ['registry_host', 'repository_name', 'tag_name']
if all(arg in self.kwargs for arg in special_lookup_kwargs):
action = getattr(self, self.kwargs.pop('action', ''), None)
if action:
#not sure what your action is returning so you may not want to return this
return action(self.request)
# detected the custom URL pattern; return the specified object
return Image.objects.from_special_lookup(**self.kwargs)
else: # must have received pk instead; delegate to superclass
return super().get_object()

Django Rest Framework router - exclude URL for prefix

I am using DRF router with a prefix + include like this:
router = BulkRouter()
router.register(r"myfeature", MyfeatureViewSet, base_name="myfeature")
urlpatterns = [
url(r"^api/1/", include(router.urls)),]
This allows me to hit the both api/1 and api/1/myfeature URLS. How can I prevent the first URL from returning 200's? The response of that endpoint is a list of everything registered under the router which I don't want to make easily obtainable.
from rest_framework.response import Response
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
class UserViewSet(ModelViewSet):
def list(self, request, *args, **kwargs):
return Response(data='Not allow', status=status.HTTP_400_BAD_REQUEST)
Two steps:
1) Wire a view directly to the prefix url (use the $)
urlpatterns = [
url(r"^api/1/$", some_view,
url(r"^api/1/", include(router.urls)),]
2) Define the view.
There is a one-liner that Django provides - django.views.defaults.page_not_found. However, in newer versions of Django, this requires an exception parameter so you can't do it in a one-liner. Thus, we have something like this:
def page_not_found_custom(request):
return page_not_found(request, None)
urlpatterns = [
url(r"^api/1/$", page_not_found_custom,
url(r"^api/1/", include(router.urls)),]

Validate pk as int in drf viewset retrieve url

Code looks as follows:
class UserViewSet(ViewSet):
# ... Many other actions
def list(self):
# list implementation
def retrieve(self, request, pk):
# manual pk int validation
router = DefaultRouter()
router.register(r"users", UserViewSet, basename="users")
urlpatterns = router.urls
Right now pk is not validated as int therefore a request to db is made, which I want to avoid. Is there any way I can add that type of validation in urls?
I can achieve that without using router like this:
urlpatterns = [
path('users/<int:pk>/', UserViewSet.as_view({'get': 'retrieve'}),
# many other actions have to be added seperately
]
But I have many actions in my viewset and all of them have to be added separately. Is there a cleaner way to do so or a package?
Use lookup_value_regex attribute as,
class UserViewSet(ViewSet):
lookup_value_regex = '\d+'
...

API url routing - Django/Postman

I am messing around with a backend Django API tutorial (Django 2.1) and I am having trouble pulling 'profile' information via Postman. My assumption is that I am not correctly stating my url in my urls.py.
Here is my project urls.py:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/v1/', include('conduit.apps.authentication.urls'), name='authentication'),
path('api/v1/', include('conduit.apps.profiles.urls'), name='profiles')
]
Here is my profiles.urls.py:
from .views import ProfileRetrieveAPIView
urlpatterns = [
path('profiles/<username:username>/', ProfileRetrieveAPIView.as_view())
]
I think my issue has to do with how I am implementing / to the end of my path. My only other relevant experience with this kind of mechanism was on prior projects where I was using something like this for unique blog post url routing (which I have done successfully):
".../<slug:slug>/"
Now, here is my relevant class-based view for the above url:
class ProfileRetrieveAPIView(RetrieveAPIView):
permission_classes = (AllowAny,)
renderer_classes = (ProfileJSONRenderer,)
serializer_class = ProfileSerializer
def retrieve(self, request, username, *args, **kwargs):
try:
profile = Profile.objects.select_related('user').get(
user__username=username
)
except Profile.DoesNotExist:
raise ProfileDoesNotExist
serializer = self.serializer_class(profile)
return Response(serializer.data, status=status.HTTP_200_OK)
You can see in my retrieve function, I am working with a username attribute. This is what I think I am trying to match up with my url path. I am guessing that I am probably not understanding how to correctly associate the url path variable (that terminology doesn't sound right) with my view. Thanks!
Also - the tutorial I am following is having me make a GET request in postman. The collection I downloaded as part of the tutorial has the following url in populated by default:
http://127.0.0.1:8000/api/v1/profiles/celeb_harry
Where is the 'celeb_' prefacing my username ('harry') coming from. I am not seeing that in any of my .py files (renderers, serializers, views, urls, etc)
You need to set lookup_field in you View. For example:
class ProfileRetrieveAPIView(RetrieveAPIView):
lookup_field = 'username'
path('profiles/<username>/', ProfileRetrieveAPIView.as_view())
What happens is that, inside get_object method of the view, based on lookup_field, a get_object_or_404 is executed. Please see the implementation in here for understanding how RetrieveAPIView works.

Django Rest Framework Routing 2 things with regex

I'm setting up an API Endpoint using Django Rest Framework viewsets and routers, and I'm trying to get the url to accept two values: first, to filter objects by a user_id, and then by the object's id. (In my case, the objects are from a model called Request.) For example, mysite.com/api/requests/1A/ would return all Request objects for user 1A, and mysite.com/api/requests/1A/23/ would return Request object with pk=23 for user 1A.
Right in my urls.py:
# urls.py
from django.conf.urls import url, include
from rest_framework import routers
from . import views
router = routers.DefaultRouter()
router.register(r'requests/(?P<user_id>.+?)(?=\/)', viewset=views.RequestsByUser, base_name='request')
urlpatterns = [
url(r'^', include(router.urls)),
]
# views.py
class RequestsByUser(viewsets.ModelViewSet):
serializer_class = RequestsSerializer
def get_queryset(self):
u_id = self.kwargs['user_id']
return Request.objects.filter(user_id=u_id)
This works well for listing all Request objects when the url is only passed the user_id. But when I try to also pass the object's id example: mysite.com/api/requests/1A/23/, rest framework returns an empty result.
So the url will properly filter by user_id, but won't properly serve the detailed view of an object when given its primary key (object_id). (It looks like the proper page for a detailed view, except it's missing the data for the object.)
Django debugging says that the following four url patterns are in my URLConf:
^api/ ^ ^test/(?P<user_id>.+?)(?=\/)/$ [name='request-list']
^api/ ^ ^test/(?P<user_id>.+?)(?=\/)\.(?P<format>[a-z0-9]+)/?$ [name='request-list']
^api/ ^ ^test/(?P<user_id>.+?)(?=\/)/(?P<pk>[^/.]+)/$ [name='request-detail']
^api/ ^ ^test/(?P<user_id>.+?)(?=\/)/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$ [name='request-detail']
I've read through the Django Rest Framework docs for url routing several times, and I feel like I must be missing something. My understanding is the router will automatically create url routing for detailed views based on primary keys, and it looks like it's doing that in the URL Conf. Is my regular expression configured wrong, or maybe something else?
Try something like this:
settings.py
INSTALLED_APPS = [
...
'rest_framework',
'django_filters',
...
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
}
serializers.py
import django_filters.rest_framework
class MyModelSerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = ('id', 'MyField', 'MyFavoriteField','OtherField')
class MyModelListView(generics.ListAPIView):
queryset = MyModel.objects.all()
serializer_class = MyModelSerializer
filter_backends = (django_filters.rest_framework.DjangoFilterBackend,)
filter_fields = ('id', 'MyField','MyFavoriteField',)
urls.py:
path('service_name/', MyModelListView.as_view(), name="something_name"),
GET:
http://localhost:8070/services/service_name/?id=123&MyField=My%20Field%20Value
More Info:
https://www.django-rest-framework.org/api-guide/filtering/#filtering