AttributeError 'UserViewset' object has no attribute 'action' - python

I've been using the Django rest framework, and I'm trying to customize the get_parsers method in my UserViewset, I looked at the docs and found a similar use case with permission classes in docs, and I tried to customize the get_parsers like this
class UserViewset(viewsets.ModelViewSet):
serializer_class = UserSerializer
# Redefine get_parsers, so that only the update methods have form-data media type
def get_parsers(self):
if self.action == 'update' or self.action == 'partial_update':
parser_classes = [FormParser, MultiPartParser]
else:
parser_classes = [JSONParser]
return [parser() for parser in parser_classes]
but I keep getting the error: AttributeError at /api/auth/users/: 'UserViewset' object has no attribute 'action'
I tried the use case found in the docs and it worked perfectly.
What am I doing wrong here ?

So, thanks to fb.com/laidani.basset #bdbd and #willem-van-onsem and u/vikingvynotking, I've been able to fix it with 2 different solutions:
The first is to override initialize_request like this, the idea is to set the request parsers to the instance of which parser you want; the drop back is when you're using swagger or any API doc, it'll not differentiate between the method parsers and it'll be just one parser, in my case it's JSONParser:
def initialize_request(self, request, *args, **kwargs):
request = super().initialize_request(request, *args, **kwargs)
print(request.method)
if request.method in ['PUT', 'PATCH']:
request.parsers = [FormParser(), MultiPartParser()]
else:
request.parsers = [JSONParser()]
self.action = self.action_map.get(request.method.lower())
return request
The second solution is to override initialize_request so that the self.action is called before calling the request, and then use the get_parsers method as you like, in this case, the swagger will differentiate between the parsers of each method:
def initialize_request(self, request, *args, **kwargs):
self.action = self.action_map.get(request.method.lower())
return super().initialize_request(request, *args, **kwargs)

You should cast your viewset to .as_view() for example;
path('users/', UserViewset.as_view(), name='users'),

Related

How to re-use GET call of another class-based api view

I have a hard time trying to re-use a get call from an existing APIView in another APIVIew.
I have a class-based DRF view:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SomethingList.as_view()), #legacy url
# in views.py
class SomethingList(generics.ListCreateAPIView):
queryset = Something.objects.all()
serializer_class = SomethingSerializer
# override get, because of some required custom action
def get(self, request, *args, **kwargs):
# do some custom actions (scan folder on filesystem)
...
return super().get(request, *args, **kwargs)
The above view both provides a get (list) and post (create) API interface. As intended. I've augmented it with DRF-spectacular information (not shown here) to generate my swagger docs.
Now, I have another (legacy) URL defined that should do exactly the same as the get (list) call above. Currently, this legacy url also points to the SomethingList.
But ... the legacy URL should NOT provide the post (create) interface, and I want to mark it as 'deprecated' in swagger using drf-spectacular. So I figured I need a separate class to restrict to get() and add the #extend_schema decorator
So I though of re-using the existing SomethingList.get functionality as follows:
# in urls.py
path('api/something', views.SomethingList.as_view()),
path('api/similarsomething', views.SimilarSomethingList.as_view()), # ! points to new class
# in views.py
class SomethingList(generics.ListCreateAPIView):
...
class SimilarSomethingList(generics.ListAPIView): #ListAPIView only!
#extend_schema(summary="Deprecated and other info..")
def get(self, request, *args, **kwargs):
view = SomethingList.as_view()
return view.get(request, *args, **kwargs)
However, this doesn't work. I get AttributeError: 'function' object has no attribute 'get'
I tried a couple of variations, but couldn't get that working either.
Question:
How can I reuse the get() call from another APIView? Should be simple, so I'm likely overlooking something obvious.
Set http_method_names to the class view.
class SomethingList(generics.ListCreateAPIView):
http_method_names = ['get', 'head']
reference: https://stackoverflow.com/a/31451101/13022138

Django: test TemplateView based views triggered by url pattern?

Say I have the following url
path('clients/by_<str:order>', BrowseClients.as_view(), name='browse_clients')
and its corresponding view
#method_decorator(login_required, name='dispatch')
class BrowseClients(TemplateView):
template_name = "console/browse_clients.html"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['clients'] = Client.objects.filter(
owner=self.request.user.id).order_by(self.kwargs["order"])
context['form'] = AddClientForm()
return context
How can I test what is in the context?
class TestBrowseClientsView(TestCase, GeneralViewTest):
fixtures = ['users.yaml', 'clients.yaml']
def setUp(self):
self.request = RequestFactory().get('/console/clients/by_inscription')
self.request.user = User.objects.get(pk=1)
def test_return_client_ordered_by_inscription_date(self):
view = BrowseClients()
view.setup(self.request)
context = view.get_context_data()
Naively, I thought that view.setup(self.request) would "feed" .get_context_data() with the relevant kwargs based on the pattern found in self.request. But it does not seem to be the case.
======================================================================
ERROR: test_return_client_ordered_by_inscription_date (console.tests.TestBrowseClientsView)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/src/jengu/console/tests.py", line 164, in test_return_client_ordered_by_inscription_date
context = view.get_context_data()
File "/usr/src/jengu/console/views.py", line 34, in get_context_data
owner=self.request.user.id).order_by(self.kwargs["order"])
KeyError: 'order'
----------------------------------------------------------------------
Why is that the case? I managed to fix my problem by passing status and order explicitly but it looks a bit ad hoc:
def get_context_data(self, status, order, **kwargs):
def test_return_clients_ordered_by_parameter(self):
view = BrowseClients()
view.setup(self.request)
context = view.get_context_data("all", "inscription")
Among the different options mentioned here, which one is the more canonical? Am I taking a wrong path, explicitly using variables when defining get_context_data()?
If you want to check what will be in the context of a response, first you need to work with a response object (and you are not, you are just making an instance of your view, not getting the response generated by the view). I don't know about RequestFactory, but I'm sure you'll find out how to adapt my answer to your use case.
So, it would be something like:
def test_your_context(self):
user = User.objects.get(pk=1)
self.client.force_login(user) # because of the login_required decorator
response = self.client.get(reverse("browse_clients"))
assert response.context['your_context_key'] == "Anything you want to check"
Just a few things to go further:
the definition of your get_context_data method seems ok to me,
if you use Class Based View, I would recommand you to use also a Base view if you want to check if user is logged in or not (LoginRequiredMixin)
you gave a name to your url, so just use it instead of writing its raw form (that's what I did in my answer).
If you use the test client it will take care of running middleware and initialising the view.
When you use setup() and call the view directly, the URL handler does not run, so it's up to you to pass the kwargs.
def test_return_client_ordered_by_inscription_date(self):
view = BrowseClients()
view.setup(self.request, order='inscription')
context = view.get_context_data()

Pass request to django inherited classes

I am overriding some methods of a popular package, django-activity-stream (I think the package is mostly irrelevant to this question).
from app/urls.py I call TeamJSONActivityFeed
urlpatterns = [
...
url(_(r'^feeds/organization/(?P<organization_id>.+)$'), TeamJSONActivityFeed.as_view(name='organization_stream')),
...
]
TeamJSONactivityFeed then calls 'pass', which I am not too familiar with, and inherits from two other classes, OrganizationStreamMixin and JSONActivityFeed.
from rest_framework.authentication import TokenAuthentication
class TeamJSONActivityFeed(OrganizationStreamMixin, JSONActivityFeed):
"""
JSON feed of Activity for a custom stream. self.name should be the name of the custom stream as defined in the Manager
and arguments may be passed either in the url or when calling as_view(...)
"""
authentication_classes = (TokenAuthentication,)
pass
My issue is that I cannot seem to access/pass the request object in/to these inherited classes. How would I go about passing this in? Right now, self.request.user and request.user are AnonymousUser objects.
class OrganizationStreamMixin(object):
name = None
def get_object(self,request):
# this is printing Anonymous User
pprint(str(self.request.user))
pprint(str(request.user))
return
def get_stream(self):
return getattr(Action.objects, self.name)
def items(self, request, *args, **kwargs):
return self.get_stream()(*args[1:], **kwargs)
class JSONActivityFeed(AbstractActivityStream, View):
"""
Feed that generates feeds compatible with the v1.0 JSON Activity Stream spec
"""
def dispatch(self, request, *args, **kwargs):
for i, v in kwargs.items():
print (" ", i, ": ", v)
return HttpResponse(self.serialize(request, *args, **kwargs),
content_type='application/json')
def serialize(self, request, *args, **kwargs):
pprint(str(self.request.user))
items = self.items(request, *args, **kwargs)
return json.dumps({
'totalItems': len(items),
'items': [self.format(action) for action in items]
})
Note: I am a bit of a django/python noob, but I am sure I am calling this properly from the front end. Similar requests have access to the request user.
I think there's a bit of confusion. You do have access to the request object otherwise it would raise an error for trying to access .user on None. If you're concerned about it being an AnonymousUser instance, then authenticate before accessing that view. If you need to prevent AnonymousUser instances from being able to access that view, then wrap the view with the login_required decorator.
Edit
You're overriding the dispatch method without calling super. That could be the problem.

Django - Signature of method does not match signature of base method in class

I am learning Django and I am following a lynda.com course.
In one of there courses "building an elearning site", the have the following code:
class CourseModuleUpdateView(TemplateResponseMixin, View):
template_name = 'courses/manage/module/formset.html'
course = None
def get_formset(self, data=None):
return ModuleFormSet(instance=self.course,
data=data)
def dispatch(self, request, pk):
self.course = get_object_or_404(Course, id=pk, owner=request.user)
return super(CourseModuleUpdateView, self).dispatch(request, pk)
def get(self, request, *args, **kwargs):
formset = self.get_formset()
return self.render_to_response({'course': self.course,
'formset': formset})
def post(self, request, *args, **kwargs):
formset = self.get_formset(data=request.POST)
if formset.is_valid():
formset.save()
return redirect('manage_course_list')
return self.render_to_response({'course': self.course,
'formset': formset})
But I am getting an error message from PyCharm (my IDE) on:
def dispatch(self, request, pk):
And the error is:
Signature of method 'CourseModuleUpdateView.dispatch()' does not match signature of base method in class 'View' less... (Ctrl+F1)
This inspection detects inconsistencies in overriding method signatures.
Is there a way for me to troubleshoot the issue and see how to begin fixing the error? What is Pycharm even trying to tell me??
I am using python 3 and DJango 1.11
You're overriding a method dispatch of the parent class View whose signature is def dispatch(self, request, *args, **kwargs): which you can see from yours does not match.
Signature here means that the method arguments should match with parent class method you're overriding.
Firstly you have to understand that this is a warning, not error.
Secondly: every argument (except of request) that is passed to view by Django is extracted from URL, as defined in urlpatterns. Django is using *args and **kwargs internally in class-based views so you can pass any argument without need for overwriting dispatch method.
And back to our warning: this warning is raised by many of static code analysis tools (including one built into PyCharm) to inform you that something here may be wrong, because original dispatch method has different signature. It doesn't mean that this is wrong and that's why there are always options to mute those warnings on selected code lines. You should of course look at every warning your editor raises, but that doesn't mean that every warning should be fixed.
You can fix it of course using:
def dispatch(self, request, *args, **kwargs):
id = args[0] # or id = kwargs['id'] if it is passed as keyword argument
self.course = get_object_or_404(Course, id=pk, owner=request.user)
return super(CourseModuleUpdateView, self).dispatch(request, pk)
but you can also ignore that and use as is. Your usage has some benefits, for example automatic validation on method invoke that all required arguments have been passed. Usage with default method signature (as above) has benefit in not raising that warning in your editor. It is up to you to decide which one is better.

How to support all REST operations for an endpoint in django rest framework

I have a subscription model that looks like this
class Subscription(models.Model):
name = models.CharField(max_length=100)
quantity = models.IntegerField(max_length=20)
stripe_id = models.CharField(max_length=100)
user = models.ForeignKey(User)
I would like to create an endpoint that allows POST, PATCH, DELETE, GET
So I did the following things
views.py
class SubscriptionDetail(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
permission_classes = (IsAuthenticated,)
queryset = Subscription.objects.all()
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
#how do I write create and delete?
urls.py
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/$', subscription, name='something'),
Questions
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
The easiest way is to do it this way.
keep the models class the same
views.py
from rest_framework import viewsets
#impost serializer and model class for subscription
class SubscriptionViewSet(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
def get_queryset(self):
queryset = Subscription.objects.all()
#if you need to get subscription by name
name = self.request.QUERY_PARAMS.get('name', None)
if name is not None:
queryset = queryset.filter(name=name)
return queryset
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
# django will handle get, delete,patch, update for you ....
# for customization you can use def update or def create ... to do whatever you need
# def create(self, validated_data):
# you can handle the email here
# and something like subscription= Subscription (name=validated_data['name'],vendor=validated_data['quantity']...)
# subscription.save()
# it will save whatever you want
urls.py
#use the router to handle everything for you
from django.conf.urls import patterns, include, url
from rest_framework import routers
#import your classes
router = routers.DefaultRouter()
router.register(r'subscription', views.SubscriptionViewSet,base_name='subscription')
urlpatterns = patterns('',
url(r'^', include(router.urls)),
)
For the creation of an Object you must implement the create function as described in the official documentation, found here. For patching you could use the partial argument from within you view class:
SubscriptionSerializer(subscription, data={'something': u'another', partial=True)
For deletion of the a Subscription, that could be done when you get the delete call as so in your view class:
if request.METHOD == 'DELETE':
subscription = Subscription.objects.get(pk=pk)
subscription.delete()
See this tutorial for complete example
Further more I think that you should include the "id" field in the SubscriptionSerialiser Meta class, otherwise it will be difficult to do the updates/deletions. I hope this helped a little.
Cheers,
Tobbe
When you want to use a method that allow make these operations you have to use a #detail_route() where you can say as well which methods will you use, like in the docs is said:
#detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
...
So to be able to use them you should add the next decorator
#detail_route(methods=['post', 'patch'])
To add another parameters you can do it for the .save() parameter. You just have to indicate the name of this and them just override your .save() model to check if that email belongs or not to the user that is trying to do the subscription. Here I paste you what the Django Rest docs says:
" Passing additional attributes to .save()
...
You can do so by including additional keyword arguments when calling .save(). For example:
serializer.save(owner=request.user)
Here I leave you the link for more information:
http://www.django-rest-framework.org/api-guide/serializers/#passing-additional-attributes-to-save
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
The error is caused because unlike create request, patch/update require a pk to know which object to update. That is why you have to supply the pk value for it. So, your url for PUT, DELETE andPATCH must have at least named parameter like this -
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/(?<pk>(\d+))$', subscription, name='something'),
an example url will be - rest-auth/subscription/10 where 10 is the pk or id of the object. Django Rest Framework will then load the object internally to be updated.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
To add custom parameters, first declare the property in serializer, it is better to keep it required=False, so that other request does not throw error -
class SubscriptionSerializer(serializers.ModelSerializer):
custom_field = serialiers.BooleanField(required=False)
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
so far this is enough for the django rest framework to accept the field custom_field and you will find the value in update method. To get the value pop it from the attributes supplied by the framework like this -
def update(self, instance, validated_data):
custom_field = validated_data.pop('custom_field', None)
if custom_field is not None:
# do whatever you like with the field
return super().update(instance, validated_data)
# for python < 3.0 super(SubscriptionSerializer, self).update(instance, validated_data)
When you overrided (I don't know if that's the proper conjugation of overriding a method) the update method, you stopped the ability to PUT or PATCH and object. Your new method only prints out "In update" but doesn't save the instance. Look at the update method from the serializer.ModelSerializer object:
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Notice the last few lines where the instance is saved with the values and then returned. Remove your update method on the SubscriptionSerializer object. This let's your parent object's create, update, retrieve, and delete methods do their magic which supports PATCH and PUT updates. The next problem is that your urls.py is using the Django rather than the REST framework router. Change it to this:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'subscription', SubscriptionDetail)
That should solve the patch update problem.
I don't think you can add an email field in your patch method without the attribute on the subscription model. That's just a guess on my part, and I may be wrong. Does the email field map to anything on any object? Can you use a ForeignKey to map it?
I hope that works for you, good luck!
In view.py you just need set the class with:
class SubscriptionDetail(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
generics.GenericAPIView):
and add this to fix .lookup_field :
def update(self, request, *args, **kwargs):
log.error("OBJ update kwargs= %s , data = %s" % (kwargs, str(request.data)))
pk = request.data.get('id')
if (kwargs.get('pk') is not None):
kwargs['pk'] = request.data.get('id')
self.kwargs['pk'] = request.data.get('id')
return super().update(request, *args, **kwargs)
and add support to methods do you want :
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
# def get(self, request, *args, **kwargs):
# return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
# def patch(self, request, *args, **kwargs):
# return self.partial_update(request, *args, **kwargs)
#
# def delete(self, request, *args, **kwargs):
# return self.destroy(request, *args, **kwargs)
only tweak that remains is get for list or get for retrieve on element but should be easy now add something if we have one pk we may call self.retrieve else we may call self.list

Categories