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+'
...
Related
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()
I got a DjangoREST APIView that supports Read and Create operations. Something like this:
class FirebaseUser(APIView):
...
get(request):
...
post(request):
...
urls.py:
...
path('user/', views.FirebaseUser.as_view()),
...
I need an API that would accept a read request with user id as url param
GET .../api/user/<userId>
But for create operation there's no user ID yet and I need something like this
POST .../api/user/
What is the best way to make my APIView treat url params differently depending on method?
You can define a ModelViewSet like this in your views.py:
from rest_framework import viewsets
class FirebaseUserViewSet(viewsets.ModelViewSet):
queryset = FirebaseUser.objects.all() # or whatever should your queryset be
serializer_class = FirebaseUserSerializer
Then, in your urls.py you register the viewset:
from django.urls import path
from rest_framework import routers
router = routers.DefaultRouter()
router.register(r'user', FirebaseUserViewSet)
urlpatterns = [
path('', include(router.urls)),
]
This will create a few new API endpoints and you'll be able to do all the CRUD operations.
I suggest reading a bit more about ModelViewSets in the official docs.
Also, if you require only certain operations, for example only read and create you may consider extending only certain mixins from rest_framework.mixins (read more here).
So, I came up with using ViewSet instead of APIView.
This is how it looks now:
urls.py
path('user/', views.FirebaseUser.as_view({'post': 'create'})),
path('user/<str:pk>', views.FirebaseUser.as_view({'patch': 'update', 'delete': 'destroy'})),
views.py
class FirebaseUser(ViewSet):
authentication_classes = [...]
permission_classes = [...]
#staticmethod
def create(request):
...
#staticmethod
def update(request: Request, pk=None):
uid = pk
...
#staticmethod
def destroy(request: Request, pk=None):
uid = pk
...
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.
I am working on a simple investment-tracker app, which should get stock prices from an api and display them nicely for the user. I am having trouble, however to pass the necessary data through to the API call.
views.py
class PortfolioData(APIView):
authentication_classes = []
permission_classes = []
def get(self, request, tickers ,format=None):
# how do I pass the tickers?
stock_data = get_multiple_stock_details(tickers) # returns JSON response
return Response(stock_data)
#login_required
def portfolio(request):
user = request.user
user_portfolio = Portfolio.objects.filter(user=user).first()
return render(request, 'app/portfolio.html', {'portfolio':user_portfolio})
urls.py
urlpatterns = [
path('', views.home, name="homepage"),
path('api/portfolio/data/', views.PortfolioData.as_view(),
name="portfolio-data"),
path('portfolio/', views.portfolio, name='portfolio'),
]
On the frontend I would make an ajax call to my PortfolioData view, in order to be able to process the data on the frontend. My biggest issue is how to pass the needed parameters.
I tried to get the ticker symbols from the frontend using jQuery and then pass that to the endpoint url but I am not sure if this is the best way to go about this.
You can try something like this,,,
urls.py
'''
You should bind two different url with same view. One with dynamic variable and another without it.
'''
urlpatterns = [
path('', views.home, name="homepage"),
path('api/portfolio/', views.PortfolioData.as_view(), name='api_portfolio'), # use unique name for each URL
path('api/portfolio/<tickers>/', views.PortfolioData.as_view(), name='api_portfolio_data'), # use unique name for each URL
path('portfolio/', views.portfolio, name='portfolio'),
]
views.py
class PortfolioData(APIView):
authentication_classes = []
permission_classes = []
def get(self, request, tickers=None ,format=None):
if tickers is None:
# GET /api/portfolio/
print('No tickers parameter in URL')
# implement your logic when tickers is not passed.
return Response(JSON_DATA)
# GET /api/portfolio/tickers1/
# implement your logic when tickers is passed.
stock_data = get_multiple_stock_details(tickers) # returns JSON response
return Response(stock_data)
Now, when make HTTP GET request (Does not matter, it's AJAX call or not).
http://localhost:8000/api/portfolio/
tickers variable will be None this case.
http://localhost:8000/api/portfolio/ticker1/
tickers variable will be ticker1 str this case.
Hope, it helps you.
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