3

I have a model with a integer field wich will increment on user click, like a "vote this" button.

The button only shows on the detail view. To increment the vote count it sends an ajax POST. The problem is that django returns a 405 (method not allowed) error even before executing the view. What can be causing this?

Here is my code:

views.py (doesn't get executed)

@require_POST
def vote_proposal(request, space_name):

    """
    Increment support votes for the proposal in 1.
    """
    prop = get_object_or_404(Proposal, pk=request.POST['propid'])
    proposal_form = VoteProposal(request.POST or None, instance=prop)

    if request.method == "POST" and request.is_ajax:
        if proposal_form.is_valid():
            vote = proposal_form.cleaned_data['propid']
            vote.support_votes += 1
            vote.save()
            msg = "The vote has been saved."
        else:
            msg = "The vote didn't pass validation."
    else:
        msg = "An error has ocurred."

    return HttpResponse(msg)

jQuery code:

<script type="text/javascript">
    function upvote(proposal) {
        var request = $.ajax({
            type: "POST",
            url: "../add_support_vote/",
            data: { propid: proposal }
        });

        request.done(function(msg) {
            var cur_votes = $("#votes span").html();
            var votes = cur_votes += 1;
            $("#votes span").html().fadeOut(1000, function(){
                $("#votes span").html(votes).fadeIn();
            });
        });

        request.fail(function(jqXHR, textStatus) {
            $("#jsnotify").notify("create", {
                title:"Couldn't vote the proposal",
                text:"There has been an error." + textStatus,
                icon:"alert.png"
            });
        })
     }
</script>

urls.py

urlpatterns = patterns('e_cidadania.apps.proposals.views',

    url(r'^$', ListProposals.as_view(), name='list-proposals'),

    url(r'^add/$', 'add_proposal', name='add-proposal'),

    url(r'^(?P<prop_id>\w+)/edit/$', 'edit_proposal', name='edit-proposal'),

    url(r'^(?P<prop_id>\w+)/delete/$', DeleteProposal.as_view(), name='delete-proposal'),

    url(r'^(?P<prop_id>\w+)/', ViewProposal.as_view(), name='view-proposal'),

    url(r'^add_support_vote/', 'vote_proposal'),

)

Template

<div id="votes">
    <span style="font-size:30px;text-align:center;">
        {{ proposal.support_votes }}
    </span><br/>
    <button onclick="upvote({{ proposal.id }})" class="btn small">{% trans "support" %}</button>
</div>
Oscar Carballal
  • 7,598
  • 13
  • 43
  • 66
  • may be you missed this: https://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ajax – Ahsan Oct 19 '11 at 14:20
  • I have that included, and the POST event works fine on other functions in the platform, that's why i'm a bit confused – Oscar Carballal Oct 20 '11 at 14:06
  • 2
    Couldn't the problem be caused by the relative URL `url: "../add_support_vote/",` in `$.ajax`? I can imagine that another view that doesn't allow POST might be called instead of `vote_proposal()` depending on the location of the page from which you trigger the Ajax call. – Jakub Roztocil Jan 02 '12 at 12:11
  • 1
    I would guess the relative url as well ... currently this vote will only work from view-proposal page. What does the chrome/safari console say when you click on the vote button? Also, you have a race condition: if multiple people vote at the same time some of the votes could be lost. Use fields instead: vote.support_votes = F("support_votes") + 1 instead. – Rob Osborne Jan 02 '12 at 20:04
  • @jkbr and Rob Osborne You were right, the view-proposal URL was messing with the vote_proposal. Just changing the order worked fine (and after that I used fields, for the sake of the race condition). If both of you put your comments as answers I'll give you the bounty :) – Oscar Carballal Jan 03 '12 at 08:40
  • @OscarCarballal glad to hear that it helped :) Posted it as an answer. – Jakub Roztocil Jan 03 '12 at 13:06

3 Answers3

6

Couldn't the problem be caused by the relative URL url: "../add_support_vote/" in $.ajax? I can imagine that another view that doesn't allow POST might be called instead of vote_proposal() depending on the location of the page from which you trigger the Ajax call.

Jakub Roztocil
  • 14,586
  • 3
  • 46
  • 50
1

Oscar unfortunately this small research didn't found the issue, but hope it will help you to clarify how to fix your code to get it work.

In main urls.py I've created two views first one for the button, the second one in testap for ajax call handler

from django.conf import settings
from django.conf.urls.defaults import patterns, include, url    
from django.views.generic.simple import direct_to_template

urlpatterns = patterns(''    
    url(r'^$', direct_to_template , {'template':'test.html'}),
    url(r'^test/', include('testapp.urls')),
)    

if settings.DEBUG:
    urlpatterns += patterns(
        '',
        url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT, 'show_indexes': True}),
)

My template with button test.html a bit simplified for test purposes. Also csrf hook has been added to prevent 403 CSRF verification error:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<script src="{{ MEDIA_URL }}js/jquery-1.6.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
    $(document).ajaxSend(function(event, xhr, settings) {
        function getCookie(name) {
            var cookieValue = null;
            if (document.cookie && document.cookie != '') {
                var cookies = document.cookie.split(';');
                for (var i = 0; i < cookies.length; i++) {
                    var cookie = jQuery.trim(cookies[i]);
                    // Does this cookie string begin with the name we want?
                    if (cookie.substring(0, name.length + 1) == (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }
        function sameOrigin(url) {
            // url could be relative or scheme relative or absolute
            var host = document.location.host; // host + port
            var protocol = document.location.protocol;
            var sr_origin = '//' + host;
            var origin = protocol + sr_origin;
            // Allow absolute or scheme relative URLs to same origin
            return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
                (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
                // or any other URL that isn't scheme relative or absolute i.e relative.
                !(/^(\/\/|http:|https:).*/.test(url));
        }
        function safeMethod(method) {
            return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
        }

        if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
            xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
        }
    });

       function upvote(proposal) {
           var request = $.ajax({
               type: "POST",
               url: "../test/add_support_vote/",
               data: { propid: proposal }
           });

           request.done(function(msg) {
               var cur_votes = $("#votes span").html();
               var votes = cur_votes += 1;
               $("#votes span").html().fadeOut(1000, function(){
                   $("#votes span").html(votes).fadeIn();
               });
           });

           request.fail(function(jqXHR, textStatus) {
               $("#jsnotify").notify("create", {
                   title:"Couldn't vote the proposal",
                   text:"There has been an error." + textStatus,
                   icon:"alert.png"
               });
           })
        }
   </script>
</head>
<body>
<div id="votes">
    <button onclick="upvote(1)" class="btn small">support</button>
</div>
</body>
</html>

urls.py from testapp looks like

from django.conf.urls.defaults import *
from .views import vote_proposal

urlpatterns = patterns('',
    url(r'^add_support_vote/', vote_proposal),

)

and views.py which maximally simplified to localize the issue

from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST

@require_POST
def vote_proposal(request):
    return HttpResponse('ok')

And it's works. I've got 200 HTTP response.


Also final small suggestion to use resolve to get view function which handle url from ajax call:

from django.core.urlresolvers import resolve
resolve('/test/add_support_vote/')
# returns ResolverMatch(func=<function vote_proposal at 0x2b17230>, args=(), kwargs={}, url_name='testapp.views.vote_proposal', app_name='None', namespace='')
Alexey Savanovich
  • 1,815
  • 10
  • 19
  • +1 on "resolve" and the answer. I didn't know about that function it. Unfortunately, the issue was the URL, for some reason the "view-proposal" view was messing with the add_support_vote. Just changing the order it worked :) – Oscar Carballal Jan 03 '12 at 08:35
0

I was getting a 405 error because I was trying to POST to a TemplateView which doesn't have a POST method, if that helps anyone.

So I changed it to a FormView instead (which has a POST method), and it worked.

Aaron Lelevier
  • 16,606
  • 11
  • 62
  • 101
  • Just for info: I had problem with localization: probably ajax request to /ajaxurl/ made a redirect to /en/ajaxurl/ (and this redirect was GET instead of the original POST) – mirek Apr 17 '19 at 11:45