Setting up router with APIView and viewset in Django Rest Framework - django

It's my first question on Stackoverflow !
I'm new to Django and following some tutorials.
I'm trying to understand if there is a way to set up the routing of an API from different view classes like APIView and viewsets.ModelViewSet (please tell me if I do not use the good wording)
In views I have :
from rest_framework import viewsets
from post.models import UniquePost
from .serializers import UniquePostSerializers
from rest_framework.views import APIView
class UniquePostViewSet(viewsets.ModelViewSet):
serializer_class = UniquePostSerializers
queryset = UniquePost.objects.all()
class FileUploadView(APIView):
some code here but no queryset nor serialized data...and no model
In urls I have :
from post.api.views import UniquePostViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from post.api.views import FileUploadView
router = DefaultRouter()
router.register('UniquePost', UniquePostViewSet, base_name='uniquepostitem')
router.register('demo', FileUploadView, base_name='file-upload-demo')
urlpatterns = router.urls
But it seems that I can register FileUploadView this way. Because I don't have a queryset to render.
I have : AttributeError: type object 'FileUploadView' has no attribute 'get_extra_actions'
I realized that (well I think) that I can use APIView for FileUploadView (and add ".as_view()) but I think I have to rewrite UniquePostViewSet using APIView as well and defining exactly what I want to see in details like POST, PUT etc...
My question is : Is there a way to use DefaultRouter router.register and insert a view that inherit from APIView (and a view that inherit from viewsets.ModelViewset) at the same time ?
Hope all of this is clear and thank you so much for your help !!!

Something like this should work.
from post.api.views import UniquePostViewSet
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from post.api.views import FileUploadView
router = DefaultRouter()
router.register('UniquePost', UniquePostViewSet, base_name='uniquepostitem')
urlpatterns = [
path('demo',FileUploadView.as_view(),name='demo'),
]
urlpatterns += router.urls

urls.py
router = routers.DefaultRouter()
router.register(r'users', UsersViewSet, basename ='users')
This happens when there is no queryset attribute in the viewset.

Related

Class-based Django API only GET method. It's possible?

I'm building a class-based only GET method API with Django and Rest Framework. However, looking on internet, it seems to be impossible to do that.
Here is my urls.py:
from django.urls import path, include
from rest_framework import routers
from . import views
api_router = routers.DefaultRouter()
api_router.register(r"get_foo_id", views.GetFooIdViewSet)
urlpatterns = [
path( "", include(api_router.urls)),
]
views.py
from app.models import *
from app.serializers import *
from rest_framework import viewsets
from django_filters.rest_framework import DjangoFilterBackend
class GetFooIdViewSet(viewsets.ModelViewSet):
queryset = Foos.objects.all()
serializer_class = FoosSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['foo_id']
It's really impossible?
You can allow only GET(list) requests in urls.py.
Try to do it:
path('your_path/', views.GetFooIdViewSet.as_view({
'get': 'list'
})),

'Access-Control-Allow-Origin' issue even though I've set up the settings.py correctly?

amateur developer here. Trying to follow this tutorial, where in the settings.py I have
CORS_ALLOWED_ORIGINS = ['http://localhost:8080']
as per the video.
However, when I try to access the server from my front-end, I get the error
Access to XMLHttpRequest at 'http://127.0.0.1:8000/engine' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Appreciate there are many similar posts on SO, but I couldn't understand why I'm having this issue whereas the guy who made the tutorial does not. This is the rest of my code:
models.py
from django.db import models
from django.utils import timezone
import datetime
class Engine(models.Model):
date = models.DateField(default=datetime.datetime(2024,1,1))
serializers.py
from rest_framework import serializers
from .models import Engine
class EngineSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Engine
fields = ('id', 'date')
views.py
from django.shortcuts import render
from .models import Engine
from .serializers import EngineSerializer
from rest_framework import viewsets
from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated
class EngineViewSet(viewsets.ModelViewSet):
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAuthenticated,)
queryset = Engine.objects.all()
serializer_class = EngineSerializer
urls.py
from django.contrib import admin
from django.urls import path, include
from backend_app.views import EngineViewSet
from rest_framework import routers
router = routers.DefaultRouter()
router.register('engine', EngineViewSet)
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(router.urls))
]
'http://localhost:8080' and 'http://127.0.0.1:8080' are not the same. They may point to exactly the same code and functions, but they are different for such matter.
Put both options inside the list:
CORS_ALLOWED_ORIGINS = ['http://localhost:8080', 'http://127.0.0.1:8000']
I'm not sure about ports, though.
Some more help is to found HERE.

Converting to using URL router is causing a ImproperlyConfigured exception

I have been working through the Django Rest Framework tutorial and on the very last step I am encountering the error:
Exception Type: ImproperlyConfigured.
Exception Value:
Could not resolve URL for hyperlinked relationship using view name "snippet-detail". You may have failed to include the related model in your API, or incorrectly configured the lookup_field attribute on this field.
When trying to view either /settings/ or /users/ (visiting any user pages yields the same exception but with "user-detail" in place of "snippet-detail") as well as any specific indices of them, etc. All that works is root and login.
All my code thus far has been working fine and I'm very confused as to why copy-pasting from the tutorial would yield such catastrophic results
In comparing my snippets files with those available on the tutorial's repo I have not been able to find any significant difference (all that I've found is inconsistencies in whitespace). That being said, here is the code I'm using.
snippets/views.py:
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework import generics, permissions
from django.contrib.auth.models import User
from snippets.permissions import IsOwnerOrReadOnly
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework import renderers
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework import permissions
from rest_framework import viewsets
class UserViewSet(viewsets.ReadOnlyModelViewSet):
"""
This viewset automatically provides `list` and `retrieve` actions.
"""
queryset = User.objects.all()
serializer_class = UserSerializer
class SnippetViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `highlight` action.
"""
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
#action(detail=True, renderer_classes=[renderers.StaticHTMLRenderer])
def highlight(self, request, *args, **kwargs):
snippet = self.get_object()
return Response(snippet.highlighted)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
snippets/urls.py:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet,basename="snippets")
router.register(r'users', views.UserViewSet,basename="users")
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]
snippets/serializers.py:
from django.contrib.auth.models import User
from rest_framework import serializers
from snippets.models import Snippet
class SnippetSerializer(serializers.HyperlinkedModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
highlight = serializers.HyperlinkedIdentityField(
view_name='snippet-highlight', format='html')
class Meta:
model = Snippet
fields = ('url', 'id', 'highlight', 'owner', 'title', 'code',
'linenos', 'language', 'style')
class UserSerializer(serializers.HyperlinkedModelSerializer):
snippets = serializers.HyperlinkedRelatedField(
many=True, view_name='snippet-detail', read_only=True)
class Meta:
model = User
fields = ('url', 'id', 'username', 'snippets')
tutorial/urls.py:
"""tutorial URL Configuration
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('snippets.urls')),
]
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
Router was being passed the wrong base names (plural forms of snippet and user rather than singular). Thanks to #IainShelvington for the answer in the comments!
To elaborate:
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet,basename="snippets")
router.register(r'users', views.UserViewSet,basename="users")
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]
should have been
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from snippets import views
# Create a router and register our viewsets with it.
router = DefaultRouter()
router.register(r'snippets', views.SnippetViewSet,basename="snippet")
router.register(r'users', views.UserViewSet,basename="user")
# The API URLs are now determined automatically by the router.
urlpatterns = [
path('', include(router.urls)),
]

Django: Testing LoginView throwing AssertionError

Quesiton
I'm building a testing suite for my login process and immediately ran into a hiccup. I believe the issue is that LoginView is a 'class' while the code is testing it as a function. What is the proper way to assert that the URL resolved to the LoginView?
urls.py
from . import views
from users.views import *
from django.urls import path
from django.contrib.auth.views import LoginView, LogoutView
from users.forms import LoginForm
urlpatterns = [
path('', views.user_home_view, name='user_home'),
path('sign_up', views.SignUpView.as_view()),
path('login', LoginView.as_view(authentication_form=LoginForm), name='login'),
path('logout', LogoutView.as_view(), name='logout')
]
tests.py
from django.test import SimpleTestCase
from django.urls import reverse, resolve
from django.contrib.auth.views import LoginView, LogoutView
from users.forms import LoginForm
from users.views import *
# Create your tests here.
class UrlTestCase(SimpleTestCase):
def test_login_url_resolved(self):
url = reverse('login')
self.assertEquals(resolve(url).func, LoginView)
Testing Results (./manage.py test)
AssertionError: <function LoginView at 0x7f970ca05320> != <class 'django.contrib.auth.views.LoginView'>
This is because you are not getting back instance of LoginView class but appropriate method through as_view() entry point method
You can access class through attribute view_class which is set in as_view() method as documented
The returned view has view_class and view_initkwargs attributes.
Solution
self.assertEquals(resolve(url).func.view_class, LoginView)
See this: django how to assert url pattern resolves to correct class based view function

Django - reverse nested url with drf-nested-routers

I configured my api url as
localhost:port/app_name/students/{student_id}/macro/{macro_id}/lto
using drf-nested-routers extension. Basically, each students has some macro categories assigned, that in turns have some Long Term Objectives (LTOs). I've tested it using curl and Postman and everything seems to work.
Now I need to write a more precise test case for my LTO model.
This is my urls.py
from django.urls import path, re_path
from django.conf.urls import include
from rest_framework import routers
from app_name.views.views import UserViewSet, StudentViewSet, MacroViewSet, LTOViewSet, MacroAssignmentViewSet
from rest_framework_nested import routers as nested_routers
# application namespace
app_name = 'app_name'
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, basename='user')
router.register(r'macro', MacroViewSet, basename='macro')
router.register(r'macro-assignments', MacroAssignmentViewSet, basename='macro-assignment')
student_router = routers.DefaultRouter()
student_router.register(r'students', StudentViewSet, basename='student')
lto_router = nested_routers.NestedSimpleRouter(student_router, r'students', lookup='student')
lto_router.register(r'macro/(?P<macro_pk>.+)/lto', LTOViewSet, basename='lto')
urlpatterns = [
re_path('^', include(router.urls)),
re_path('^', include(student_router.urls)),
re_path('^', include(lto_router.urls)),
]
The issue is that I cannot use the reverse() method correctly to get the url of my LTOViewSet to test it.
self.url = reverse('app_name:student-detail:lto', {getattr(self.student, 'id'), getattr(self.macro, 'id')})
This gives the following error
django.urls.exceptions.NoReverseMatch: 'student-detail' is not a registered namespace inside 'app_name'
In other test cases, I use very similar sentences and those work fine
self.list_url = reverse('app_name:student-list')
reverse('app_name:student-detail', {post_response.data['id']})
So here's the minimally reproducible example:
# main/viewsets.py
from rest_framework.viewsets import ModelViewSet
from django.contrib.auth.models import User, Group
class StudentViewSet(ModelViewSet):
model = User
class LTOViewSet(ModelViewSet):
model = Group
# main/urls.py
from django.urls import re_path, include
from rest_framework import routers
from rest_framework_nested import routers as nested_routers
from .viewsets import StudentViewSet, LTOViewSet
# application namespace
app_name = "main"
student_router = routers.DefaultRouter()
student_router.register(r"students", StudentViewSet, basename="student")
lto_router = nested_routers.NestedSimpleRouter(
student_router, r"students", lookup="student"
)
lto_router.register(r"macro/(?P<macro_pk>.+)/lto", LTOViewSet, basename="lto")
urlpatterns = [
re_path("^", include(student_router.urls)),
re_path("^", include(lto_router.urls)),
]
reverse('main:lto-detail', args=(1,1,1))
Out[5]: '/api/students/1/macro/1/lto/1/'
So indeed your error was passing just the router basename not a final endpoint to reverse and because of the nesting we were thrown off by student-detail not reversing (which I still don't get).