I want to call a post of a class as shown below by instantiating a view as shown below., i need to write a unit test that calls the post method of this class and not through URL.
class SequencingRequestSpreadsheetView(GenericAPIView):
parser_classes = (JSONParser,)
serializer_class = SequencingRequestSerializer
permission_classes = (IsBioUser, )
suffix = '.xls'
path = settings.SEQUENCE_REQUEST_SUBMISSION
def post(self, request, format=None, simulation_mode = False):
I need to know how do I create a request object and pass it to this function.
iam instantiating this view class and I tried passing a request data as json and also tried dictionary but did not work.
how do I create a request object and pass it to this method.
resp = SequencingRequestSpreadsheetView().post(request)
You can use RequestFactory for achieve what you want.
factory = RequestFactory()
# Build a post request.
request = factory.post(post_url, data, ...)
# Note here that, I don't call the class view directly
# with:
# SequencingRequestSpreadsheetView().post(request)
# instead I get the view with as_view(), and then pass
# a post request to it.
view = SequencingRequestSpreadsheetView.as_view()
response = view(request, ...)
See Making requests here to get a better understanding on how RequestFactory works.
Related
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'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.
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()
I'm using Django Rest Framework and OAuthTookit.
I want that the scope provided by the token should be HTTP Method specific. For eg:- GET, PUT, DELETE of the same APIView should have different scopes.
Following are my APIs.
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScope]
required_scopes = ['scope1']
serializer_class = ModelSerializer
queryset = Model.objects.all()
Currently, the scope is set at the class level, which means to access all the GET, PUT & DELETE method, the token should have scope1.
I want that there should be different scope for different HTTP methods. How can I set different scope for different methods?
To handle this case, I think you need to implement a new permission class, something like this:
class TokenHasScopeForMethod(TokenHasScope):
def has_permission(self, request, view):
token = request.auth
if not token:
return False
if hasattr(token, "scope"):
# Get the scopes required for the current method from the view
required_scopes = view.required_scopes_per_method[request.method]
return token.is_valid(required_scopes)
And use it in your view like this:
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScopeForMethod]
required_scopes_per_method = {'POST': ['post_scope'], 'GET': ['get_scope']}
serializer_class = ModelSerializer
queryset = Model.objects.all()
Perhaps You can use TokenMatchesOASRequirements permission class
class SongView(views.APIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [["read"]],
"POST": [["create"], ["post", "widget"]],
"PUT": [["update"], ["put", "widget"]],
"DELETE": [["delete"], ["scope2", "scope3"]],
}
I like #clément-denoix answer, but would tweak it a bit.
Instead of reloading TokenHasScope.has_permission I would suggest to redefine
TokenHasScope.get_scopes as it is smaller method and perfectly fit for what you need. Also original has_permission method has additional logic that I prefer to preserve.
Something like this should do the trick:
from django.core.exceptions import ImproperlyConfigured
from oauth2_provider.contrib.rest_framework import TokenHasScope
class TokenHasScopeForMethod(TokenHasScope):
def get_scopes(self, request, view):
try:
scopes = getattr(view, "required_scopes_per_method")
return scopes[request.method]
except (AttributeError, KeyError):
raise ImproperlyConfigured(
"TokenHasScope requires the view to define the required_scopes_per_method attribute"
)
I'm using Django (1.10.5) together with DRF (3.5.4) to create API. I've defined a view to handle file upload and call the foo method with the data passed in the post request and the path to temporary file where the uploaded file is being stored.
class FooView(APIView):
parser_classes = (FormParser, MultiPartParser)
def post(self, request, format=None):
serializer = NewFooSerializer(data=request.data)
if serializer.is_valid():
file_obj = serializer.validated_data["my_file"]
name = serializer.validated_data["name"]
foo_id = my_module.foo(name=name, my_file=file_obj.temporary_file_path())
result_url = reverse('api:foo', kwargs={"foo_id": foo_id})
return Response({"result_url": result_url}, status=status.HTTP_202_ACCEPTED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
My serializer to validate incoming data looks like this
class NewFooSerializer(serializers.Serializer):
name = serializers.CharField()
my_file = serializers.FileField()
Basically I'd like to test this view without the filesystem interaction using mock library. My current test method is defined below
#mock.patch('my_module.foo')
def test_post_should_invoke_foo_method(self, mocked_foo):
mocked_foo.return_value = "123456"
self.client.post(self.VIEW_URL,
{'my_file': self.file_mock,
'name': 'Test file'
}, format='multipart')
mocked_new.assert_called_with(name="Test file", my_file="/some/dummy/path"
This one of course raises AssertionError
AssertionError: Expected call: foo(my_file='/some/dummy/path', name='Test file')
Actual call: foo(my_file='/tmp/tmp0VrYgR.upload', name=u'Test file')
I couldn't find any solution how to mock TemporaryUploadedFile so I tried to mock tempfile.NamedTemporaryFile which is being used by this class. So I've added these lines before TestCase
named_temporary_file_mock = mock.MagicMock(spec=tempfile.NamedTemporaryFile)
named_temporary_file_mock.name = mock.MagicMock()
named_temporary_file_mock.name.return_value = "/some/dummy/path"
and then decorate my test method with
#mock.patch('tempfile.NamedTemporaryFile', named_temporary_file_mock)
But unfortunately this results in the same error as previous version. What I might be missing? Should I take another approach to avoid interaction with the filesystem?