How to do a class based delete view that allows DELETE method in django 3.1? - django

In Django 3.1, the typical DeleteView accepts GET and POST.
See https://docs.djangoproject.com/en/3.1/ref/class-based-views/generic-editing/#deleteview
and I reproduce below:
A view that displays a confirmation page and deletes an existing object. The given object will only be deleted if the request method is POST. If this view is fetched via GET, it will display a confirmation page that should contain a form that POSTs to the same URL.
How do I do a DELETEView that's class based view and also accepts DELETE method?

Tldr; I chose to use 303 at the server side so that it can correct redirect to the list view
Long story short is here https://stackoverflow.com/a/24375475/80353
In this SO answer which applies to Spring (a Java Framework), the question had the same issue as me.
Send a DELETE
then server side want to redirect using 302
302 will use precedent method and list typically don't accept DELETE as precedent method. Only POST, GET, and HEAD as precedent method
This seems like a web framework issue. But it's not. It appears to a convention most sensible web frameworks adopt.
There are 3 solutions with drawbacks:
1. override the convention
Allow the backend web framework to accept DELETE as precedent method for 302.
Con: Not nice by convention
2. Let client handle redirection
send back a 200 then client will redirect back to list view
Con: This results in two requests and htmx-delete doesn't work that way. It will send a DELETE method request and then take whatever comes back and immediately swap. I like this so I want to keep this. One request to settle this rather than two.
3. Use 303 for the redirection
After successful delete, do a 303 redirect to list view (I chose this)
Con: 303 doesn't work with HTTP/1.0 and older browsers. But that's not a problem in the year 2021 and will continue to be less of a problem going forward.
In the end I wrote my own deleteview
from django.views.generic.detail import BaseDetailView
class DeleteThingView(BaseDetailView):
http_method_names = ["delete"]
model = Thing
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.object.delete()
response = redirect(reverse_lazy("things-list"))
response.status_code = 303
return response

Related

Is there an alternative to using a serializer for drf-spectacular's extend_schema request param?

I'm using drf-spectacular to generate swagger/redoc API documentation.
One of the most useful features is the ability to test requests via the generated swagger html page, but I'd like to enforce the application/x-www-form-urlencoded content-type so that when the request is received by my Django endpoints, the request.data has the encoded data instead of it ending up as part of a query string. drf-spectacular always seems to default to query strings e.g. /objects/action/?key=value
The only way I've figured out how to do this is to use a serializer in conjunction with the request content-type e.g.
#extend_schema(
request={'application/x-www-form-urlencoded': DoTheActionInputsSerializer},
responses={200: DoTheActionOutputsSerializer},
methods=["POST"]
)
#action(methods=['post'], detail=False)
def do_the_action(self, request, *args, **kwargs):
...
This works great, but it does require a lot of small serializers that may only have one or two attributes. Is there an alternative way of achieving this inside the extend_schema decorator?
I was hoping something like the following would work, but doesn't
request={'application/x-www-form-urlencoded': {'schema': {'foo_id': OpenApiTypes.INT}}},
I think the documentation answers that question. You can use inline_serializer for those small one-off cases. It allows you to do this:
#extend_schema(responses={
'2XX': SimpleSerializer,
'401': inline_serializer('Error1', fields={'detail': serializers.CharField()}),
})
This works the same for #extend_schema(request=inline_serializer(...))
As a matter of last resort, you can also put a raw schema dict into request or response.
Note: If you want application/x-www-form-urlencoded to be detected automatically, just do
#action(methods=['post'], detail=False, parser_classes=[parsers.FormParser])

Choosing correct Django delete view approach

I'm working on Django website and I have problem in figuring out correct/good way to handle delete view. From what I found out there are two ways to approach it:
1
class ObjectDeleteView(DeleteView):
model = Object
def get_success_url(self):
objectid = self.kwargs['object_id']
object = Object.objects.get(id = objectid)
container = object.container
containerid = container.id
url = reverse('Containers:showContainerContent', args=[containerid])
return url
def get_object(self):
return get_object_or_404(Object, pk=self.kwargs['object_id'])
2
def objectDelete(request, object_id):
object = Object.objects.get(id = object_id)
container = object.container
containerid = container.id
url = reverse('Containers:showContainerContent', args=[containerid])
return HttpResponseRedirect(url)
From what I can tell both are doing exactly the same thing - once object is deleted present user with page under Containers:showContainerContent.
The big difference I am experiencing is error I am getting when running unit test for this (simple call of the website and check of response code). With option 1 I end up getting error
django.template.exceptions.TemplateDoesNotExist: ContainerApp/object_confirm_delete.html
Which I understand - I don't have this template, this is default template for DeleteView, hence error is correct. The thing is I don't want to have any extra page. Just redirect user and that's it.
Also, I tested changing return url to return HttpResponseRedirect(url) in option 1, but result is the same.
What should I do here? Should I just continue with option 2? What are or might be the drawbacks for this approach?
There is a major difference between two class based delete view and function based view (the way you declared it).
CBV accepts get, post and delete http methods. When you send a get request to class based view, it does not delete the object. Instead it renders template with object to be deleted in context. This is basically used to have confirmation. For example you can send a get request and it will render a template with text "Do you really want to delete?" or "Please confirm blah blah..". And if you send a post or delete request, it will actually delete the object and redirect to next page.
FBV, on the other hand, give you full control over what you want to do. And as you declared it, it will accept any request type and delete the object and redirect to next page because you have not done any request type check in your view which is not a great idea IMHO. You should not allow deletion on get requests. They should be idempotent. There are plenty of otherthings that CBV provides. For example in case the object does not exist your FBV will crash. CBV, on contrary, will return proper 404 response if object does not exist.
So I think there is no bad in using FBV, but make is strong and secure enough that it handles every case (what if object does not exist?, what about confirmation?, GET should be idempotent only allow deletion with post? etc etc). Or simply use CBV.

Django - Redirect to another domain from View

I'm trying to redirect from mydomain.com to google.com.
There are a couple of answers on stackoverflow that asume the following is working:
return HttpResponseRedirect('google.com')
or
return redirect('google.com')
But it doesn't it. This just redirects the page to itself and appends the google.com part so it comes out like this:
www.mydomain.com/google.com
What throws a 404 of course..
My view now looks like the following:
class MyView(TemplateView):
def get(self, request, *args, **kwargs):
return HttpResponseRedirect('google.com')
Can anyone give me insights in what I'm doing wrong?
They answers are in some sense correct: you do a redirect. But now the web browser needs to perform the redirect.
Usually paths that are not prepended with two consecutive slashes are assumed to be local: so that means it stays at the same domain.
In case you want to go to another domain, you need to add a protocol, or at least two consecutive slashes (such that the old protocol is reused):
return HttpResponseRedirect('https://google.com') # use https
or:
return HttpResponseRedirect('//google.com') # "protocol relative" URL
After all you only return a redirect answer to the browser. The browser can decide not to follow the redirect (some browsers do), or can interpret it in any way they like (although that means that the browser does not really does what we can expect it to do). We can not force a browser to follow the redirect.

How to follow Django redirect using django-pytest?

In setting up a ArchiveIndexView in Django I am able to successfully display a list of items in a model by navigating to the page myself.
When going to write the test in pytest to verify navigating to the page "checklist_GTD/archive/" succeeds, the test fails with the message:
> assert response.status_code == 200
E assert 301 == 200
E + where 301 = <HttpResponsePermanentRedirect status_code=301, "text/html; charset=utf-8", url="/checklist_GTD/archive/">.status_code
test_archive.py:4: AssertionError
I understand there is a way to follow the request to get the final status_code. Can someone help me with how this done in pytest-django, similar to this question? The documentation on pytest-django does not have anything on redirects. Thanks.
pytest-django provides both an unauthenticated client and a logged-in admin_client as fixtures. Really simplifies this sort of thing. Assuming for the moment that you're using admin_client because you just want to test the redirect as easily as possible, without having to log in manually:
def test_something(admin_client):
response = admin_client.get(url, follow=True)
assert response.status_code == 200
If you want to log in a standard user:
def test_something(client):
# Create user here, then:
client.login(username="foo", password="bar")
response = client.get(url, follow=True)
assert response.status_code == 200
By using follow=True in either of these, the response.status_code will equal the return code of the page after the redirect, rather than the access to the original URL. Therefore, it should resolve to 200, not 301.
I think it's not documented in pytest-django because the option is inherited from the Django test client it subclasses from (making requests).
UPDATE:
I'm getting downvoted into oblivion but I still think my answer is better so let me explain.
I still think there is a problem with Shacker's answer, where you can set follow=True and get a response code of 200 but not at the URL you expect. For example, you could get redirected unexpectedly to the login page, follow and get a response code of 200.
I understand that I asked a question on how to accomplish something with pytest and the reason I'm getting downvoted is because I provided an answer using Django's built-in TestCase class. However, the correct answer for the test is/was more important to me at the time than exclusively using pytest. As noted below, my answer still works with pytest's test discovery so I think the answer is still valid. After all, pytest is built upon Django's built-in TestCase. And my answer asserts the response code of 200 came from where I expected it to come from.
The best solution would be to modify pytest to include the expected_url as a parameter. If anyone is up for doing this I think it would be a big improvement. Thanks for reading.
ORIGINAL CONTENT:
Answering my own question here. I decided to include final expected URL using the built-in Django testing framework's assertRedirects and verify that it (1) gets redirected initially with 302 response and (2) eventually succeeds with a code 200 at the expected URL.
from django.test import TestCase, Client
def test_pytest_works():
assert 1==1
class Test(TestCase):
def test_redirect(self):
client = Client()
response = client.get("/checklist_GTD/archive/")
self.assertRedirects(response, "/expected_redirect/url", 302, 200)
Hat tip to #tdsymonds for pointing me in the right direction. I appreciated Shacker's answer but I have seen in some scenarios the redirect result being 200 when the page is redirected to an undesirable URL. With the solution above I am able to enforce the redirect URL, which pytest-django does not currently support.
Please note: This answer is compliant with the auto-discover feature of pytest-django and is thus not incompatible (it will auto-discover both pytest-django and Django TestCase tests).

Django: Passing a request directly (inline) to a second view

I'm trying to call a view directly from another (if this is at all possible). I have a view:
def product_add(request, order_id=None):
# Works. Handles a normal POST check and form submission and redirects
# to another page if the form is properly validated.
Then I have a 2nd view, that queries the DB for the product data and should call the first one.
def product_copy_from_history(request, order_id=None, product_id=None):
product = Product.objects.get(owner=request.user, pk=product_id)
# I need to somehow setup a form with the product data so that the first
# view thinks it gets a post request.
2nd_response = product_add(request, order_id)
return 2nd_response
Since the second one needs to add the product as the first view does it I was wondering if I could just call the first view from the second one.
What I'm aiming for is just passing through the request object to the second view and return the obtained response object in turn back to the client.
Any help greatly appreciated, critism as well if this is a bad way to do it. But then some pointers .. to avoid DRY-ing.
Thanx!
Gerard.
My god, what was I thinking. This would be the cleanest solution ofcourse:
def product_add_from_history(request, order_id=None, product_id=None):
""" Add existing product to current order
"""
order = get_object_or_404(Order, pk=order_id, owner=request.user)
product = Product.objects.get(owner=request.user, pk=product_id)
newproduct = Product(
owner=request.user,
order = order,
name = product.name,
amount = product.amount,
unit_price = product.unit_price,
)
newproduct.save()
return HttpResponseRedirect(reverse('order-detail', args=[order_id]) )
A view is a regular python method, you can of course call one from another giving you pass proper arguments and handle the result correctly (like 404...). Now if it is a good practice I don't know. I would myself to an utiliy method and call it from both views.
If you are fine with the overhead of calling your API through HTTP you can use urllib to post a request to your product_add request handler.
As far as I know this could add some troubles if you develop with the dev server that comes with django, as it only handles one request at a time and will block indefinitely (see trac, google groups).