I'm using a DRF ViewSet to manage user accounts:
class UserViewSet(ModelViewSet):
lookup_field = 'email'
queryset = User.objects.all()
And have a testcase like:
from django.urls import reverse
from .base import BaseApiTestCase
class UsersTestCase(BaseApiTestCase):
def test_get_user_account(self):
# ...Create a test user, login as test user...
response = self.client.get(
reverse('api:users-detail', kwargs={'email': 'test#user.com'}),
content_type='application/json'
)
self.assertStatusCode(response, 200)
I get the error:
django.urls.exceptions.NoReverseMatch: Reverse for 'users-detail' with keyword arguments '{'email': 'test#user.com'}' not found. 2 pattern(s) tried: ['api/users/(?P<email>[^/.]+)\\.(?P<format>[a-z0-9]+)/?$', 'api/users/(?P<email>[^/.]+)/$']
It's my understanding that the [^/.]+ regex should match test#user.com.
Although reverse() should do this for me, I've also tried url-encoding the # symbol, as in:
reverse('api:users-account', kwargs={'email': 'test%40user.com'}),
Running manage.py show_urls reveals that the URL is available:
...
/api/users/<email>/ api.views.users.UserViewSet api:users-detail
...
Why can't django's reverse() system find the url match?
EDIT:
I'm using ViewSets as normal with DRF's routers, so urls.py isn't super relevant, but for posterity here's the relevant part:
from rest_framework import routers, permissions
from api import views
router = routers.DefaultRouter()
router.register(r'users', views.users.UserViewSet, base_name='users')
The problem is not the #, it is the .. That's because the default regex the DRF router uses for parameters is [^/.]+, which specifically excludes dots.
You should be able to override this by setting lookup_value_regex on the viewset:
class UserViewSet(ModelViewSet):
lookup_field = 'email'
lookup_value_regex = r'[^/]+'
Related
My django application has a namespace defined in the app_name variable, in urls.py.
It seems like this namespace needs to be specified in the view_name argument of HyperlinkedRelatedField for HyperlinkedRelatedField to successfuly retrieve the relevant url router.
To avoid repeating this namespace, I'd like to import the namespace into the serializers module. However I get an import error when doing so.
extract from my app/urls.py:
...
app_name = 'viewer'
...
api_router = DefaultRouter()
api_router.register('year', api_views.YearViewSet, 'api_year')
api_router.register('month', api_views.MonthViewSet, 'api_month')
...
urlpatterns = [
...
path('api/', include(api_router.urls)),
]
api_views.py
...
class YearViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet):
queryset = Year.objects.all().order_by('-date')
serializer_class = YearSummarySerializer
lookup_field = 'slug'
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = YearDetailSerializer(instance=instance)
return Response(serializer.data)
#action(detail=True)
def months(self, request, *args, **kwargs):
serializer = YearMonthsSerializer(instance=self.get_object(), context={'request': request})
return Response(serializer.data)
...
serializers.py
...
from .urls import app_name
...
class YearMonthsSerializer(serializers.HyperlinkedModelSerializer):
month_set = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name= app_name + ':api_month-detail',
lookup_field='slug'
)
class Meta:
model = Year
fields = ['month_set']
...
When I manually enter the app_name ('viewer') the serializer works as intended, however when I try to import app_name from .urls, python throws an ImportError
ImportError: cannot import name 'app_name' from 'cbg_weather_viewer.viewer.urls'
I don't understand why I get this import error as I already use relative imports models, views, etc. successfully.
I don't understand what I am doing wrong. What should I do instead?
Edit
I understand that I cannot include url as it will create a circular reference.
After doing some research, it seems that the request contains the app_name value as documented here: https://docs.djangoproject.com/en/2.2/ref/urlresolvers/#django.urls.ResolverMatch
However, I don't know how to access the request directly in the YearMonthSerializer class. I could retrieve it using self.request in a class method, but not directly from the class itself. Any suggestion?
Temporary solution
I have deported the app namespace to the apps.py module and imported it both urls.py and serializers.py as follow:
apps.py
app_namespace = 'viewer' # Used in urls
urls.py
from .apps import app_namespace
app_name = app_namespace
serializers.py
from .apps import app_namespace
def get_view_name(view_name):
return f'{app_namespace}:{view_name}'
...
month_set = serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name= get_view_name('api_month-detail'),
lookup_field='slug'
)
put namespace in the path when you include URLs like this
without namespace don't retrieve app_name
try this
urlpatterns = [
...
path('api/', include(api_router.urls,namespace="viewer")),
]
The documentation of the restframework states following, which worked for me:
You may use include with an application namespace:
urlpatterns = [
path('forgot-password/', ForgotPasswordFormView.as_view()),
path('api/', include((router.urls, 'app_name'))),
]
Or both an application and instance namespace:
urlpatterns = [
path('forgot-password/', ForgotPasswordFormView.as_view()),
path('api/', include((router.urls, 'app_name'), namespace='instance_name')),
]
See Django's URL namespaces docs and the include API reference for
more details.
Pay attention to the double parenthese with the include statement in the first example:
include((router.urls, 'app_name'))
I have this class view, that works perfectly for creating and listing objects of SiteGroup:
But I need a method to perform several operations on a single SiteGroup object, and objects associated with them. Therefore, I have tried to create a method, decorated with #action (as suggested by the docs).
According to the docs, this will autogenerate the intermediate url. Nevertheless, it doesn't work.
When I try to access (given that 423 is an existing SiteGroup Object):
http://127.0.0.1:8000/api/site-groups/423/replace_product_id/?product_id=0x345
the url is not found.
I also tried generating myself the URL in the urls.py, with no luck.
Can someone tell me where the problem is? I've browsed through all the docs, and found no clue. Thanks a lot.
class SiteGroupDetail(generics.ListCreateAPIView):
queryset = SiteGroup.objects.all()
parser_classes = (MultiPartParser, FormParser, JSONParser)
serializer_class = SiteGroupSerializer
authentication_classes = (authentication.TokenAuthentication,)
#action(detail=True, methods=['post'], url_path='replace_product_id', permission_classes=[IsSuperUser], url_name='replace_product_id')
def replace_product_id(self, request, pk=None, device_type=None):
serializer = SiteGroupSerializer(data=request.data)
product_id = self.request.query_params.get('product_id', None)
print("replace_product", product_id, device_type, pk, flush=True)
return Response({"hello":product_id})
My urls.py
from django.conf.urls import url, include
from api import views, routers
router = routers.SimpleRouter()
router.register(r'users', views.UserViewSet)
router.register(r'groups', views.GroupViewSet)
urlpatterns = [
url(r'^', include(router.urls)),
enter code here
url(r'^site-groups/', views.SiteGroupDetail.as_view()),
url(r'^site-groups/(?P<pk>[0-9]+)/$', views.SiteGroupDetail.as_view()),
]
For one thing the router should be calling
super(OptionalSlashRouter, self).__init__()
What you have now calls __init__ of SimpleRouter's parent, skipping the logic in SimpleRouter.__init__
Change that and see if it starts working
Actually as you're using python 3 it could be just
super ().__init__()
I'm trying to add an API using the Django REST framework to an existing codebase which uses the Django OAuth2 toolkit. The existing views make use of the fact that the OAuth2 toolkit's backend modifies the behavior of Django's login_required decorator so that it uses OAuth2 authentication. The function-based views look similar to the one in the tutorial example:
from django.contrib.auth.decorators import login_required
from django.http.response import HttpResponse
#login_required()
def secret_page(request, *args, **kwargs):
return HttpResponse('Secret contents!', status=200)
I'm trying to adapt the example given on https://django-oauth-toolkit.readthedocs.io/en/latest/rest-framework/getting_started.html to my situation, but I'm finding it's not an exact correspondence, because the example uses the DRF's ModelViewSet classes whereas my current view uses a (less-integrated) generic class-based view:
from rest_framework import generics
from ..models import Session
from ..serializers import SessionSerializer
class SessionDetail(generics.UpdateAPIView):
queryset = Session.objects.all()
serializer_class = SessionSerializer
where I've set the default permission class to IsAuthenticated in settings.py:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
This should allow a Session object to be updated using PATCH requests, but it appears to return a 403 Forbidden response for use cases similar to the ones for the current views decorated with login_required.
How can I update the SessionDetail view so that it behaves the same as a function view decorated with login_required? I suspect that I need to use the TokenHasReadWriteScope permission class, since this appears to be the only one for which the required_scopes is optional. (The views decorated with login_required don't provide required_scopes either).
Would it be like this:
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
from oauth2_provider.contrib.rest_framework import OAuth2Authentication, TokenHasReadWriteScope
from ..models import Session
from ..serializers import SessionSerializer
class SessionDetail(generics.UpdateAPIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenHasReadWriteScope]
queryset = Session.objects.all()
serializer_class = SessionSerializer
Also, would I have to update my REST_FRAMEWORK setting with a DEFAULT_AUTHENTICATION_CLASSES like this:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
)
}
Right now if I run the server and try to make a patch request with the browsable API, I get a 401 Unauthorized error:
Also if I Log in using a username and password that I just created using python manage.py createsuperuser, it goes straight back to this 401 page instead of one with a form to make a PATCH request as I'd expect.
Any ideas on how to fix this?
This might be a simple one but I have been on this for hours, I must be missing something. Here we go:
urls.py:
urlpatterns = [
# Event patterns
url('^$', views.BuddyProgram.as_view(), name='buddy_program'),
url('^dashboard/$', views.BuddyDashboard.as_view(), name='buddy_dashboard'),
url('^thank-you/$', views.BuddyFinal.as_view(), name='final'),
url('^completed_intro/$', views.CompletedIntro.as_view(), name='buddy_completed_intro'),
url('^completed_passive_track/$', views.CompletedPassiveTrack.as_view(), name='buddy_completed_passive_track'),
url('^about/$', views.BuddyAbout.as_view(), name='buddy_about'),
url('^list/$', views.Buddies.as_view(model=BuddyProfile), name='buddies'),
url('^signup/$', views.BuddySignupView.as_view(), name='buddy_signup'),
# url(r'^(?P<question_id>[0-9]+)/$', views.detail, name='detail'),
url(r'^(?P<buddy_id>[0-9]+)/$', views.Buddy.as_view(model=BuddyProfile), name='buddy'),
]
views.py:
class BuddyFinal(TemplateView):
template_name = 'buddy/thank_you.html'
class BuddySignupView(SignupView):
template_name = 'buddy/buddy_create.html'
success_url = reverse('final') # profile specific success url
form_class = BuddySignupForm
profile_class = BuddyProfile # profile class goes here
def form_valid(self, form):
response = super(BuddySignupView, self).form_valid(form)
profile = self.profile_class(user=self.user)
profile.save()
return response
and the error I get:
django.core.urlresolvers.NoReverseMatch: Reverse for 'final' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []
As your URLs aren't loaded yet when importing the BuddySignupView (and thus executing reverse), Django cannot find the URL.
You should use reverse_lazy instead: https://docs.djangoproject.com/en/1.11/ref/urlresolvers/#reverse-lazy
In your views.py file:
from django.core.urlresolvers import reverse_lazy
class BuddySignupView(SignupView):
template_name = 'buddy/buddy_create.html'
success_url = reverse_lazy('final') # profile specific success url
reverse_lazy only reverse the URL name at "runtime" (when Django actually needs the value) instead of "import time" when everything may not be available yet.
This error is coming because in any template file you are accecsing url(thank-you or signup) in wrong format. make sure about this url in html.
I have a browsable API:
restaurant_router = DefaultRouter()
restaurant_router.register(r'rooms', RoomsViewSet)
restaurant_router.register(r'printers', PrintersViewSet)
restaurant_router.register(r'shifts', ShiftsViewSet)
urlpatterns = patterns('',
url(r'^$', api_root),
url(r'^restaurant/$',
RestaurantView.as_view(),
name='api_restaurants_restaurant'),
url(r'^restaurant/', include(restaurant_router.urls)),
)
In the api_root I can link to the named route:
#api_view(('GET',))
def api_root(request, format=None):
return Response({
'restaurant': reverse('api_restaurants_restaurant', request=request, format=format),
})
Or I can use the browsable API generated by the DefaultRouter, as explained in the documentation:
The DefaultRouter class we're using also automatically creates the API
root view for us, so we can now delete the api_root method from our
views module.
What do I do if I want to mix ViewSets and normal Views, and show everything in the same API root? The DefaultRouter is only listing the ViewSets it controls.
You can define your views as ViewSets with only one method.
So you can register it in router and it will be in one space with ViewSets.
http://www.django-rest-framework.org/api-guide/viewsets/
Doesn't look like there's a simple way to do that using the DefaultRouter, you'd have to build your own router. If it's any consolation the DefaultRouter's logic for generating the APIRoot view is fairly simple and you could probably easily roll your own, similar router based on the DefaultRouter class (e.g. Modify the ApiRoot class implementation to fetch additional URLs to include, you can do this any number of ways e.g. pass them into your Router's constructor):
https://github.com/tomchristie/django-rest-framework/blob/86470b7813d891890241149928d4679a3d2c92f6/rest_framework/routers.py#L263
From http://www.django-rest-framework.org/api-guide/viewsets/:
A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as .get() or .post(), and instead provides actions such as .list() and .create()
Which means we can extend your ViewSets:
def other_rooms_view(request):
return Response(...)
class RoomsViewSet(ViewSet):
...
def list(self, request):
return other_rooms_view(request)
restaurant_router = DefaultRouter()
restaurant_router.register(r'rooms', RoomsViewSet)