I created ticket #14770 (http://code.djangoproject.com/ticket/14770)
because I ran into a situation this week where I only had access to
the request, but needed to set a cookie. As the ticket says, I did
solve this problem with a middleware, but I couldn't think of a reason
not to include this ability in core. So, I wrote up a patch complete
with docs and tests. Any feedback you might have would be very much
appreciated.
Thanks,
Paul McLanahan
Ugh, I'm -1 on this. It confuses requests and responses, and abstracts
away a *very* important aspect of how HTTP actually works. Doing this
is semantically unclean and binds together two parts of the
request/response cycle that are separate by design. I don't at all
like the idea of obscuring the truth about how HTTP actually works.
There's a tiny chance I might be able to be convinced to change my
mind, but you'll have to back up and explain exactly what it is you're
trying to do that's impossible without this hack.
Jacob
There are just situations where you need to have the ability to cause
a cookie to be set, but can't yet create the response object. In my
case I was creating a small framework for registering functions that
would check for and set messages via the messages framework. If I
called the view and passed the request and response to these
functions, the messages they'd set wouldn't show until the next
response because the page would already have been rendered. If I could
stick to only passing in a request then I could put off getting the
response object until later, my functions could set cookies, and the
messages would show up at the right time.
In David Cramer's post on the subject[0] he mentions needing this when
building an authentication service. I'm sure there are many other use
cases and at least as many hacks that have been put into place to get
around this limitation.
I'm certainly not married to this implementation. It made sense to me
to offer methods with the exact same signature on the request as the
response, but I can see you point about confusion. Since they
accomplish the same outcome, I don't think any confusion would result
in bugs, but it could, so I understand.
Would you be more open to a completely separate cookie handling
component that the HttResponse.set_cookie and
HttResponse.delete_cookie called for backward compatibility? I have no
idea how such a thing would work or maintain state through a
request-response cycle, but if an alternate new API were created
through which one always interacted with cookies, perhaps that would
assuage some confusion concerns.
I mainly just need to be able to set a cookie from anywhere in a view,
or in any decorator or function that deals with the view process. The
only way I've found to do that is to use a middleware that
monkeypatches the request and then copies whatever was added there to
the resopnse on its way out. If there's another way, I'm very happy to
hear it. The thing I like most about attempting contributions like
this to Django is the learning experience.
Thanks for the feedback!
Paul
[0] http://www.davidcramer.net/code/62/set-cookies-without-a-response-in-django.html
Okay, but you've said this twice without giving any examples. Which
makes it very easy for me to argue that in fact any trying to set a
cookie without a response object around is just a badly-designed or
-factored application. I believe that argument, so you're going to
need to show concrete examples.
> In my
> case I was creating a small framework for registering functions that
> would check for and set messages via the messages framework. If I
> called the view and passed the request and response to these
> functions, the messages they'd set wouldn't show until the next
> response because the page would already have been rendered. If I could
> stick to only passing in a request then I could put off getting the
> response object until later, my functions could set cookies, and the
> messages would show up at the right time.
I'm really not following this "next response because the page has
already been rendered" thing -- if you create the reponse object, pass
it into your utility functions, then return it that creates the
cookies just fine.
> In David Cramer's post on the subject[0] he mentions needing this when
> building an authentication service. I'm sure there are many other use
> cases and at least as many hacks that have been put into place to get
> around this limitation.
David's post uses this example::
def my_view(request):
request.COOKIES.set([args])
...
return response
How in any way is that better than::
def my_view(request):
response = HttpResponse(...)
response.COOKIES.set(...)
...
return response
Okay, it saves one line of code, but in the Python world you don't get
bonus points for shorter more obscure code.
> Would you be more open to a completely separate cookie handling
> component that the HttResponse.set_cookie and
> HttResponse.delete_cookie called for backward compatibility? I have no
> idea how such a thing would work or maintain state through a
> request-response cycle, but if an alternate new API were created
> through which one always interacted with cookies, perhaps that would
> assuage some confusion concerns.
Again, *why*? Cookies are just an HTTP response header. What purpose
does hiding HTTP's semantics serve?
> I mainly just need to be able to set a cookie from anywhere in a view,
> or in any decorator or function that deals with the view process. The
> only way I've found to do that is to use a middleware that
> monkeypatches the request and then copies whatever was added there to
> the resopnse on its way out. If there's another way, I'm very happy to
> hear it.
* Views::
def my_view(request):
response = HttpResponse(...)
...
response.write(...)
...
response.COOKIES.set(...)
...
return response
* Decorators::
def my_decorator(viewfunc):
def _inner(request, *args, **kwargs):
response = viewfunc(request, *args, **kwargs)
response.COOKIES.set(...)
return response
return _inner
* Arbitrary functions::
def some_function(response):
response.COOKIES.set(...)
...
It's possible I'm missing something, but I really can't see that some
sort of magic global cookie handling adds enough utility to outweigh
its unintuitiveness.
Of course, you're certainly free to implement some magic middleware
thing in your own code -- it's easy enough to hang something off the
request object then transfer it to the response on the way out. It
certainly wouldn't pass a code review if *I* was performing it, but
luckily your app doesn't have to live up to my standards :)
Jacob
I gather that the OP is trying to do something like this:
def some_view(request):
f(request)
return some_response(request)
def f(request, response):
response.COOKIES.set(...)
messages.add_message(request, messages.INFO, "Cookie set")
The problem then is that function f needs the response object in order
to set the cookie, but if the response object is passed into f, then
it is too late to add the message. This makes it difficult for the
same function to both set a cookie and add a message, unless it is
also responsible for creating the response.
An alternate solution might be to use delayed execution to set the cookie:
def some_view(request):
closure = f(request)
response = some_response(request)
closure(response)
return response
def f(request):
messages.add_message(request, messages.INFO, "Cookie set")
def closure(response):
response.COOKIES.set(...)
return closure
Although this could cause confusion if the message gets added, and
then the view fails in some way before the cookie actually gets set.
Cheers,
Ian
Jacob: I'm willing to accept that this situation is rare and that your
objections are valid. I can accomplish this via a middleware, and will
continue to do so. But I do still wish for a more simple and supported
way to deal with this situation. I can see Ian's delayed execution
pattern working just fine, but it's less readable in my opinion.
Thanks again,
Paul
Okay, I think we're narrowing in on things. Just so I'm clear, you've
got a view that sets a message towards the beginning and then displays
that message in the template *on the very same request*, yes?
So in essence you're doing something like::
def my_view(request):
set_some_messages(request)
response = render_to_response(...)
set_some_cookies(response)
return resp
But you'd like to collapse `set_some_messages()` and
`set_some_cookies()` into a single function, so something like::
def my_view(request):
response = render_to_response(...)
set_some_cookies_and_messages(request, response)
return response
Does that about some it up?
Because if so, the problem has nothing to do with cookies. It's
happening because the messages are being fetched in the template,
which you're rendering into a response "too early". This in fact has
nothing to do with the request/response cycle or anything, but just
with the fact that you're fetching messages from a template into a
response. But remember: you can get a response without rendering a
template, so you could do something like this::
def my_view(request):
response = HttpResponse()
set_some_cookies_and_messages(request, response)
response.write(render_to_string(...))
return response
render_to_response is a shortcut that collapses a bunch of steps for
the common case, but does so at the expense of inflexibility.
Jacob
PS: Oh, and BTW, the reason you're seeing this problem with messages,
specifically, is that messages really aren't designed to be produced
and consumed in the same view. The canonical use case for messages is
for use in the redirect-after-POST pattern::
def my_view(request):
form = MyForm(request.POST or None)
if form.is_valid():
form.save()
messages.add_message("It worked!")
return redirect('success')
...
If you're producing and consuming messages in the same request you
might want a different tool, or you'll need to work around the
assumptions that the messages framework makes.
Basically, yes.
> So in essence you're doing something like::
>
> def my_view(request):
> set_some_messages(request)
> response = render_to_response(...)
> set_some_cookies(response)
> return resp
>
> But you'd like to collapse `set_some_messages()` and
> `set_some_cookies()` into a single function, so something like::
>
> def my_view(request):
> response = render_to_response(...)
> set_some_cookies_and_messages(request, response)
> return response
>
> Does that about some it up?
It's a simplification, but yes.
> Because if so, the problem has nothing to do with cookies. It's
> happening because the messages are being fetched in the template,
> which you're rendering into a response "too early". This in fact has
> nothing to do with the request/response cycle or anything, but just
> with the fact that you're fetching messages from a template into a
> response. But remember: you can get a response without rendering a
> template, so you could do something like this::
>
> def my_view(request):
> response = HttpResponse()
> set_some_cookies_and_messages(request, response)
> response.write(render_to_string(...))
> return response
>
> render_to_response is a shortcut that collapses a bunch of steps for
> the common case, but does so at the expense of inflexibility.
Right. I do know that, but as I said before, this example is a
simplification. Just for completeness, I'll lay it all out for you.
I'm sure it won't change your mind, but hopefully it'll more clearly
demonstrate the dilema.
I have a set of views that I'd like to potentially show one or more
messages to the user. These messages will be created by one or more
functions that are registered into a module I wrote that finds these
special functions and provides a decorator. The decorator is applied
to the views where the messages should be displayed. These messages
are things like, "Your account will expire soon", and since they'll
keep displaying until the user renews, I provide a link for them to
hide the message. That hiding is accomplished by setting a cookie. I'm
also using a cookie to basically cache the fact that the user has
passed the test with no message and therefore doesn't need the check
for the remainder of their browser session. So, this decorator looks
something like:
def check_alerts(func):
def decor(request):
for alert in registered_alerts:
alert(request)
return func(request)
return decor
An example alert function might be:
def account_expiring(request):
if request.COOKIES.get('account_expiring') == 'no':
return
if request.user.get_profile().is_about_to_expire():
message.warning(request, "Your account will expire soon, yo")
else:
request.set_cookie('account_expiring', 'no')
That's obviously not exactly the code, but that's the idea. It works
great, and is the least convoluted thing I could come up with. I
thought of creating an empty response in the decorator, passing it to
the alert functions, then passing it to my decorated views and making
them use it instead of what they're already doing. But some are
generic views and some are not my code, so it'd be very difficult to
hack them all into accepting another argument, or using the decorator
to capture the response given by the view, then transfer cookies from
my fake response to the real one before returning it. All of that is
possible, but seemed more hacky to me than having the ability to set a
cookie from the request. I just thought that if I could get myself
into this situation then I'm sure many others have even more complex
systems, and since there'd already been a couple of solutions proposed
in the community, that perhaps it'd be useful for the framework to
have an easy solution.
> PS: Oh, and BTW, the reason you're seeing this problem with messages,
> specifically, is that messages really aren't designed to be produced
> and consumed in the same view. The canonical use case for messages is
> for use in the redirect-after-POST pattern::
>
> def my_view(request):
> form = MyForm(request.POST or None)
> if form.is_valid():
> form.save()
> messages.add_message("It worked!")
> return redirect('success')
> ...
>
> If you're producing and consuming messages in the same request you
> might want a different tool, or you'll need to work around the
> assumptions that the messages framework makes.
Yeah. Some of these follow that pattern, and some produce and consume
in the same view. The messages framework seems to handle all of this
just fine. Perhaps I'm missing something, but my tests are passing and
the site is functioning as I expect.
Paul