Django Rest Framework: How to include application namespace when defining HyperlinkedRelatedField - python

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

Related

Django error: 404 Page not found on get method

I am trying to get serialized data from endpoint localhost:8000/v1/test/uuid, but hitting a 404 error - what is wrong with below?
views.py
from uuid in response, get site object
class Test(APIView):
def get(self, request, *args, **kwargs):
uuid = kwargs.get('uuid')
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)
urls.py
from django.conf.urls import re_path
from rest_framework.routers import DefaultRouter
from site_config import views
router = DefaultRouter()
router.register(
r'site',
views.SiteViewSet,
basename='site')
urlpatterns = [
re_path(r'^v1/', include(router.urls)),
re_path('test/<uuid:uuid>/', views.Test.as_view(), name='test'),
]
models.py
site id as the pk
class Site(models.Model):
"""
Model that represents a Site
"""
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True)
domain_name = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(editable=False, auto_now_add=True)
modified = models.DateTimeField(editable=False, auto_now=True)
serializers.py
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
fields = [
'uuid',
'domain_name'
]
FYI - the endpoint was working when the views.py was returning all Sites, but it isn't working when I try to filter on uuid.
views.py (previously working version)
class Test(APIView):
def get(self, request, *args, **kwargs):
resp = {'site': None}
site = Site.objects.all()
resp['site'] = SiteSerializer(site, many=True).data
return Response(resp)
Error Message on Browser:
Page not found (404)
Request Method: GET
Request URL: http://localhost:8000/v1/test/7c018183-c952-4040-a450-e3cb58f09745/
Using the URLconf defined in site_config.urls, Django tried these URL patterns, in this order:
^v1/ ^site/$ [name='site-list']
^v1/ ^site\.(?P<format>[a-z0-9]+)/?$ [name='site-list']
^v1/ ^site/(?P<uuid>[^/.]+)/$ [name='site-detail']
^v1/ ^site/(?P<uuid>[^/.]+)\.(?P<format>[a-z0-9]+)/?$ [name='site-detail']
^v1/ ^$ [name='api-root']
^v1/ ^\.(?P<format>[a-z0-9]+)/?$ [name='api-root']
test/<uuid:uuid> [name='test']
urls.py
As this is a routing problem the first place to have a look should be the urls.py.
Without recreating the app it looks like there are potentially three problems there:
Analysis
1. re_path
re_path is used, a regular Django path expression is provided. The django.urls documentation has some examples that speak for themselves.
2. the path itselt
The URL path starts with v1/ while the provided configuration starts with test/.
3. the order
As the re_path for ^v1/ matches anything beginning with v1/ the order in the pattern is important.
Anything that should available in that path must either be listed before the regex match, or be registered in the router.
Fix
urlpatterns = [
path('v1/test/<uuid:uuid>/', views.Test.as_view(), name='test'),
re_path(r'^v1/', include(router.urls)),
]
FYI - the endpoint was working when the views.py was returning all
Sites, but it isn't working when I try to filter on uuid.
As I do not have the breakpoint analysis for your views.py code I assume that your get method inside
class Test(APIView):
def get(self, request, *args, **kwargs):
uuid = kwargs.get('uuid')
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)
is not getting resolved by the patterns in your urls.py or the uuid is not getting through.
If the case is former here what you can do is a direct mapping of the UUID inside the get method and avoiding the kwarg.get() by doing this
class Test(APIView):
def get(self, request, uuid, *args, **kwargs):
uuid = uuid
resp = {'site': None}
site = Site.objects.get(uuid=uuid)
resp['site'] = SiteSerializer(site).data
return Response(resp)

DRF Post to ViewSet without writing into Model

I've always written data into database when posting via Django Rest Framework endpoints. This time I would like to process received data and send it somewhere else without writing into DB. I switched from ModelViewSet to ViewSet, I can issue GET request OK but receiving Bad Request 400 when I curl or POST via DRF URL. Here's a working minimal code (removed need for authentication etc):
urls.py
from django.urls import path, include
from .views import ContactView
from rest_framework import routers
router = routers.DefaultRouter()
router.register('message', ContactView, basename='message')
urlpatterns = [
path('', include(router.urls)),
]
serializers.py
from rest_framework import serializers
class ContactSerializer(serializers.Serializer):
text = serializers.CharField(max_length=250)
views.py
from rest_framework.response import Response
from .serializers import ContactSerializer
from rest_framework import viewsets
class ContactView(viewsets.ViewSet):
def list(self, request):
return Response('Got it')
def create(self, request):
serializer = ContactSerializer(data=request.data)
if serializer.is_valid():
return Response(serializer.data)
else:
return Response('Invalid')
Would greatly appreciate your suggestions.
You can use GenericAPIView for get or post request and do some logic in validate method, for example do something with signals or edit something. Also u can use #detailt_route or #list_route for any ModelViewSet for write special url for instance, example for edit extra data.
how i did rarely:
in urls.py
urlpatterns = [
url('v1/message', ContactAPIView.as_view(), name='message'),
]
in view.py
class ContactAPIView(GenericAPIView):
serializer_class = ContactSerializer
permission_classes = ()
def post(self, request, *args, **kwargs):
serializer_class = self.get_serializer_class()
serializer = serializer_class(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
data = {"status": True}
return Response(data)
in serializers.py
class ContactSerializer(serializers.Serializer):
text = serializers.TextField()
def validate(self, attrs):
write some logic
you are getting this error because you are using Viewsets which uses DefaultRouter to register routers for you. What it does is that it creates 2 urls for your viewset
message
message/id
so in your current case; i.e. viewset you need to send some dummy number in your url to access this post function (which is not a good approach).
So, you should use any class which parent doesn't include ViewSetMixin (which gives functionality of Router Registration) like in your case inherit your view from these classes
ListModelMixin
CreateModelMixin
GenericAPIView

Why can't django's reverse() match an email parameter?

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'[^/]+'

DRF (django-rest-framework) action decorator not working

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__()

Django - URL not found using generic views

I am writing a basic Events app which contains of two modules(apps) so far : users and events.
I am using Django 2.1 with Python 3.6 on Ubuntu 16.04
So far, I've been able to handle users, but on events, I can't use Update, Detail and Delete generic views. All of them return 404.
My views.py:
class EventListView(ListView):
model = EventModel
template_name = 'event_list.html'
queryset = EventModel.objects.order_by('start_date_time')
class EventUpdateView(UpdateView):
model = EventModel
fields = ['event_type','start_date_time'
]
template_name = 'event_update.html'
class EventDeleteView(DeleteView):
model = EventModel
template_name = 'event_delete.html'
success_url = reverse_lazy('event_list')
class EventDetailView(DetailView):
model = EventModel
template_name = 'event_detail.html'
My urls.py (in project folder):
urlpatterns = [
path('', include('pages.urls')),
path('admin/', admin.site.urls),
path('users/', include('users.urls')),
path('users/', include('django.contrib.auth.urls')),
path('events/', include('events.urls')),
]
My urls.py (in events app):
urlpatterns = [
path('', views.EventListView.as_view(), name='event_list'),
path('<int:id>', views.EventDetailView.as_view(), name='event_detail'),
path('<int:id>/edit/', views.EventUpdateView.as_view(), name='event_update'),
path('<int:id>/delete/', views.EventDeleteView.as_view(), name='event_delete'),
]
What am I doing wrong? I've been searching the whole day and still have no idea how this might be wrong.
Note that the first line works (EventListView) but the other lines don't. By the way, I am using the book Django for Beginners. Most of the code here is identical to the code in the book.
Update
I don't use namespace in this application, the rest of urls.py is only some basic imports :
from django.urls import path
from . import views
The urls.py for the Project is like above, except it has include and admin as well.
The examples of URLs giving 404 error:
http://127.0.0.1:8000/events/1/
http://127.0.0.1:8000/events/1/edit/
PS I thought edit and delete give me 404, but actually the error is :
ImproperlyConfigured at /events/1/edit/
EventUpdateView is missing a QuerySet. Define EventUpdateView.model, EventUpdateView.queryset, or override EventUpdateView.get_queryset().)
In short: you defined a models (with s) attribute, but it should be model (without s).
Well the error actually already explains the problem:
ImproperlyConfigured at /events/1/edit/ EventUpdateView is missing a QuerySet.
Define EventUpdateView.model, EventUpdateView.queryset,
or override EventUpdateView.get_queryset().)
In your EventUpdateView you did not specify a model attribute, you wrote models, and for Django that is an entirely different attribute. So you should rename it to:
class EventListView(ListView):
model = EventModel
template_name = 'event_list.html'
queryset = EventModel.objects.order_by('start_date_time')
class EventUpdateView(UpdateView):
model = EventModel
fields = ['event_type','start_date_time'
]
template_name = 'event_update.html'
class EventDeleteView(DeleteView):
model = EventModel
template_name = 'event_delete.html'
success_url = reverse_lazy('event_list')
class EventDetailView(DetailView):
model = EventModel
template_name = 'event_detail.html'
For the EventListView, that did not matter, since you also defined a queryset attribute, and so Django took that one, but I would update it anyway.
Furthermore in the urls.py, you need to specify a pk parameter by default:
urlpatterns = [
path('', views.EventListView.as_view(), name='event_list'),
path('<int:pk>', views.EventDetailView.as_view(), name='event_detail'),
path('<int:pk>/edit/', views.EventUpdateView.as_view(), name='event_update'),
path('<int:pk>/delete/', views.EventDeleteView.as_view(), name='event_delete'),
]
Finally in the template you wrote something like:
{% url 'event_update' event.id %}
But apparently there was no event identifier, as a result the event.id is the string_if_invalid (by default the empty string), which is not an integer (well at least not if you did not specify that), and hence it can not find a relevant URL. After some discussion, it turned out that the correct identifier was object, so the correct url is something like:
{% url 'event_update' pk=object.id %}
The same of course should happen with other {% url ... %} calls.

Categories