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()
Related
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'),
I'm using something like this to populate inlineformsets for an update view:
formset = inline_formsetfactory(Client, Groupe_esc, form=GroupEscForm, formset=BaseGroupEscInlineFormset, extra=len(Groupe.objects.all()))
(Basically I need as many extra form as there are entries in that table, for some special processing I'm doing in class BaseGroupEscInlineFormset(BaseInlineFormset)).
That all works fine, BUT if I pull my code & try to makemigrations in order to establish a brand new DB, that line apparently fails some django checks and throws up a "no such table (Groupe)" error and I cannot makemigrations. Commenting that line solves the issues (then I can uncomment it after making migration). But that's exactly best programming practices.
So I would need a way to achieve the same result (determine the extra number of forms based on the content of Groupe table)... but without triggering that django check that fails. I'm unsure if the answer is django-ic or pythonic.
E.g. perhaps I could so some python hack that allows me to specific the classname without actually importing Groupe, so I can do my_hacky_groupe_import.Objects.all(), and maybe that wouldn't trigger the error?
EDIT:
In forms.py:
from .models import Client, Groupe
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def get_form_kwargs(self, index):
""" this BaseInlineFormset method returns kwargs provided to the form.
in this case the kwargs are provided to the GroupEsForm constructor
"""
kwargs = super().get_form_kwargs(index)
try:
group_details = kwargs['group_details'][index]
except Exception as ex: # likely this is a POST, but the data is already in the form
group_details = []
return {'group_details':group_details}
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False)
The issue as already outlined is that your code is written at the module level and it executes a query when the migrations are not yet done, giving you an error.
One solution as I already pointed in the comment would be to write the line to create the formset class in a view, example:
def some_view(request):
GroupeEscFormset = inlineformset_factory(
Client,
Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
extra=len(Groupe.objects.all()),
can_delete=False
)
Or if you want some optimization and want to keep this line at the module level to not keep recreating this formset class, you can override the __init__ method and accept extra as an argument (basically your indirect way to call Model.objects.all()):
class BaseGroupEscInlineFormset(BaseInlineFormSet):
def __init__(self, *args, extra=3, **kwargs):
self.extra = extra
super().__init__(*args, **kwargs)
...
GroupeEscFormset = inlineformset_factory(Client, Groupe_esc,
form=GroupeEscForm,
formset=BaseGroupEscInlineFormset,
can_delete=False)
# In your views:
def some_view(request):
formset = GroupeEscFormset(..., extra=Groupe.objects.count()) # count is better if the queryset is needed only to get a count
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
I need to pass id from the url slug. I am using generic views. This is my code for urls.py:
path('category/<int:pk>/details/',
CategoryDetailView.as_view(),
name='category-details'),
and I need to pass the <int:pk> value into views.py, so I can filter my queryset with this id.
My views.py code:
class CategoryDetailView(DetailView):
model = Category
def get_context_data(self, *, object_list=Expense.objects.get_queryset(), **kwargs):
queryset = object_list
return super().get_context_data(
summary_per_year_month = summary_per_year_month(queryset.filter(category_id= <int:pk> ))
)
You can access values from the URL in self.kwargs.
queryset.filter(category_id=self.kwargs['pk'])
Note that your get_context_data is the other way round than normal. Typically, you call super() and then add to the context dict. It looks like your way will work, but it will seem odd to other Django users. You could try writing it as follows:
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
queryset=Expense.objects.get_queryset()
context['summary_per_year_month'] = summary_per_year_month(queryset.filter(category_id=self.kwargs['pk']))
return context
Yes, the path parameters are stored in self.kwargs, a dictionary that maps the name of the parameter to the value. So you can make use of:
class CategoryDetailView(DetailView):
model = Category
def get_context_data(self, *args, **kwargs):
summary=summary_per_year_month(
Expense.objects.filter(category_id=self.kwargs['pk'])
)
return super().get_queryset(*args, **kwargs, summary_per_year_month=summary)
You use self.kwargs.get('pk').
class CategoryDetailView(DetailView):
model = Category
def get_context_data(self, *, object_list=Expense.objects.get_queryset(), **kwargs):
queryset = object_list
return super().get_context_data(
summary_per_year_month = summary_per_year_month(queryset.filter(category_id=self.kwargs.get('pk')))
)
Something that really helped me learn Django was to add breakpoints (pdb) in my code, then run dir() on each object I came across.
For example, dir(self) will tell you what properties and methods 'self' has (ie, kwargs, model, request, etc). Then you can start experimenting around with these properties: self.kwargs, self.request, self.model, etc, see what they return.
Soon enough, you would find out that self.kwargs returns a dictionary of arguments that includes 'pk', which you can access using get(). That's how you can access 'pk'.
To me, this simple trick unlocked most of my understanding of Django and python.
I'm going to convert all my APIs into gRPC calls. At the moment I was able to transfer all the ViewSet into gRPC(sample code added end of this question). But ModelViewSet, it get an error like this.
Traceback (most recent call last):
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/grpc/_server.py", line 435, in _call_behavior
response_or_iterator = behavior(argument, context)
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/servicers/tenant/main.py", line 15, in get_tenant
data = ClientViewSet().list(request=original_request, )
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/common/lib/decorators.py", line 128, in wrapper_format_response
final_data = call_func(func, self, request, transaction, exception, *args, **kwargs)
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/common/lib/decorators.py", line 99, in call_func
return func(self, request, *args, **kwargs)
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/api_v1/viewsets.py", line 471, in list
data = super().list(request, *args, **kwargs).data
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/rest_framework/mixins.py", line 38, in list
queryset = self.filter_queryset(self.get_queryset())
File "/home/wasdkiller/PycharmProjects/ocsa/dataengine-service/venv/lib/python3.6/site-packages/rest_framework/generics.py", line 158, in filter_queryset
queryset = backend().filter_queryset(self.request, queryset, self)
AttributeError: 'ClientViewSet' object has no attribute 'request'
So my viewsets.py look like this (format_response decorator convert it to Response object)
class ClientViewSet(viewsets.ModelViewSet):
queryset = Client.objects.all()
serializer_class = ser.ClientSerializer
#format_response(exception=False)
def list(self, request, *args, **kwargs):
data = super().list(request, *args, **kwargs).data
# data = {}
return data, True, HTTPStatus.OK, 'data retrieve successfully'
When I call this as an API, it works perfectly. But I want to do the same thing without calling an API. Here how I was solving it,
from django.http import HttpRequest
from rest_framework.request import Request
# creating request object
django_request = HttpRequest()
django_request.method = 'GET'
drf_request = Request(django_request)
data = ClientViewSet().list(request=drf_request)
print(f'data: {data.data}')
The problem with the super() function in the ClientViewSet, but if I uncomment data = {} and comment out the calling super() function, it works both API and the above method. I go through inside the DRF code base where error occurred, when calling the API the self object has the request object and above method it doesn't exist.
In short, you need to call as_view() on the viewset and map the http verbs, and then call that function with a proper HttpRequest.
All views in django end up being functions, DRF is sugar on top of that. A "ViewSet" doesn't exist in the normal way, as a standalone class, after routing is complete.
django_request = HttpRequest()
django_request.method = 'GET'
my_view = ClientViewSet.as_view({'get': 'list', 'post':'create'})
data = my_view(request=django_request)
print(f'data: {data.data}')
If you want the detail routes (users/37, ...), then you need to create a new view function mapping to those viewset functions. This can't be the same view function because http get now needs to point to a different function on the viewset than in the list case. See routers.py source for whats going on and what is mapped where.
# map http 'get' to the 'retrive' function on the viewset
my_view = ClientViewSet.as_view({'get': 'retrieve', ...})
# pass in the kwarg the URL routing would normally extract
client_pk = 9329032
data = my_view(request=django_request, pk=client_pk)
If you want to see what all the mappings for your viewset are, then you an print them out using this snippet:
router = SimpleRouter()
router.register("client", ClientViewSet, basename="client")
for url in router.urls: # type: URLPattern
print(f"{url.pattern} ==> {url.callback}")
for verb, action in url.callback.actions.items():
print(f" {verb} -> {action}")
# output will be something like this
^client/$ ==> <function ClientViewSet at 0x11b91c280>
get -> list
^client/(?P<pk>[^/.]+)/$ ==> <function ClientViewSet at 0x11b91c3a0>
get -> retrieve
put -> update
patch -> partial_update
delete -> destroy
The kwargs you pass to this view will depend on the settings in your ViewSet for things like lookup_url_kwarg, but in most cases they will be simple.