views:

389

answers:

2

Hello,

I am currently having the problem that when I use the login_required decorator from django.contrib.auth.decorators on any of my views, my POST parameters do not arrive at the protected view whenever the decorator did redirect (to the login page) and back again to the protected view. Suggestions about how to work around this (preferably maintaining the convenience of the login_required decorator and the POST method) appreciated!

This page appears to be a contested Django ticket about the matter. Although the bug/enhancement was framed in terms of template vs. view logic instead of just making parameters accessible to a view, which is my problem.

+1  A: 

There's no easy way to do this, not to mention the fact that you want to use the "login_required" decorator. You can do your own view that checks for the is_authenticated method and does the right thing, like serializing the POST data and passing around, but this is very error prone.

The easy workaround is to change your form to do GET instead of POST.

Tiago
What's right/wrong with a solution like having @login_required passing all POST parameters to the protected view? Isn't that relatively straightforward to implement?
DGGenuine
It's a "two phase process". First you have to store the POST parameters somewhere while the user is entering his login information in the login view. After that, you'll have to check the user login/passowrd, login the user and the do another post to the right view(the first one).
Tiago
I guess those two phases seem pretty common-sense to me, but admittedly I'm not writing the backend so..thanks for cluing me in.
DGGenuine
A: 

I have developed the following solution using sessions that I find acceptable. Dealing with redirects and substituting views is tricky, and this method seems to be the best balance of not fiddling with the framework and not fighting the HTTP protocol while gaining the desired functionality. The negative aspect of this method is the extra work required in each protected view checking the session variables.

  1. Create a custom decorator (login_required2, below) that returns the requested view if the user is authenticated and the project's login view otherwise.
  2. The login view:
    1. Stores the original POST parameters in a session variable.
    2. Stores the original HTTP_REFERER in a session variable
    3. If the user correctly authenticates, returns the view corresponding to the requested path (the requested path remains identical throughout the login process, and is identical to the path the user requested when they were originally passed the login view instead.)
  3. Any views protected thus must check the session variables before they use either the request's POST or META['HTTP_REFERER']

Code follows:

def login_view(request):    
    from django.conf import settings
    from django.core.urlresolvers import resolve

    USERNAME_FIELD_KEY = 'username'
    PASSWORD_FIELD_KEY = 'password'

    message = '' #A message to display to the user
    error_message = '' #An error message to display to the user

    #If the request's path is not the login URL, the user did not explicitly request 
    # the login page and we assume this view is protecting another.
    protecting_a_view = request.path != settings.LOGIN_URL

    post_params_present = bool(request.POST)

    #Any POST with username and password is considered a login attempt, regardless off what other POST parameters there may be
    login_attempt = request.POST and request.POST.has_key(USERNAME_FIELD_KEY) and request.POST.has_key(PASSWORD_FIELD_KEY)

    if protecting_a_view:
        message = 'You must login for access.'
        if not request.session.has_key(ACTUAL_REFERER_KEY):
            #Store the HTTP_REFERER if not already
            request.session[ACTUAL_REFERER_KEY] = request.META.get(HTTP_REFERER_KEY)

    if protecting_a_view and post_params_present and not login_attempt: 
        #Store the POST parameters for the protected view
        request.session[FORWARDED_POST_PARAMS_KEY] = request.POST

    if login_attempt:
        form = LoginForm(request.POST)
        if form.is_valid():
            username = form.cleaned_data[USERNAME_FIELD_KEY]
            password = form.cleaned_data[PASSWORD_FIELD_KEY]
            user = auth.authenticate(username=username, password=password)
            if user is not None:
                if user.is_active:
                    auth.login(request, user)
                    if protecting_a_view:
                        actual_view, actual_args, actual_kwargs = resolve(request.path) #request.path refers to the protected view
                        return actual_view(request, *actual_args, **actual_kwargs)
                    else:
                        HttpResponseRedirect('/')
                else:
                    message = 'That account is inactive.'
            else:
                error_message = 'That username or password is incorrect.'
    else:
        form = LoginForm()

    context_dict = {
        'form': form,
        'message': message,
        'error_message': error_message,
    }
    return render_to_response2('my_app/login.html', context_dict)

@login_required2
def protected_view(request):
    post_params = {}

    if request.POST:
        post_params = request.POST
    elif request.session.has_key(FORWARDED_POST_PARAMS_KEY):
        post_params = request.session[FORWARDED_POST_PARAMS_KEY]
        del request.session[FORWARDED_POST_PARAMS_KEY]
    if post_params:
        #Process post_params as if it were request.POST here:
        pass

    #assuming this view ends with a redirect.  Otherwise could render view normally
    if request.session.has_key(ACTUAL_REFERER_KEY):
        redirect_location = request.session.get(ACTUAL_REFERER_KEY)
    elif request.META.get(HTTP_REFERER_KEY) != request.path:
        redirect_location = request.META.get(HTTP_REFERER_KEY)
    else:
        redirect_location = ROOT_PATH
    return HttpResponseRedirect(redirect_location)

def login_required2(view_func):
    """
    A decorator that checks if the request has an authenticated user.
    If so it passes the request to the view.
    Otherwise, it passes the request to the login view, which is responsible
    for recognizing that the request was originally for another page and forwarding
    state along (GET, POST).

    See django.contrib.auth.decorators for how Django's auth decorators mesh 
    using _CheckLogin.  This decorator bypasses that for my ease of creation.
    """
    def login_required_decoration(request, *args, **kwargs):
        if request.user.is_authenticated():
            return view_func(request, *args, **kwargs)
        else:
            from django.conf import settings
            from django.core.urlresolvers import resolve

            login_url = settings.LOGIN_URL
            login_view, login_args, login_kwargs = resolve(login_url)
            #Here the user gets a login view instad of the view they requested
            return login_view(request, *login_args, **login_kwargs)
    return login_required_decoration
DGGenuine