Error 405 on form submission

Hi,

Using the following code, I get an error 405 in the inspector. Any idea what could be wrong here? I’ve tried different things but with no success so far.

Thanks in advance for your suggestions.

views.py

def adherent_view(request):

    if request.method == 'POST':
        form = AdherentForm(request.POST)
        if form.is_valid():
            qdem01_genre = form.cleaned_data['qdem01_genre']
            qdem01rx_genre_precis = form.cleaned_data['qdem01rx_genre_precis']
            qdem02_naissance = form.cleaned_data['qdem02_naissance']
            qdem03_diplome = form.cleaned_data['qdem03_diplome']
            qdem04_csp = form.cleaned_data['qdem04_csp']
        
            obj = Questionnaire(
                qdem01_genre = qdem01_genre, 
                qdem01rx_genre_precis = qdem01rx_genre_precis, 
                qdem02_naissance = qdem02_naissance,
                qdem03_diplome = qdem03_diplome,
                qdem04_csp = qdem04_csp
            )

            obj.save()
            form2 = AdherentForm(request.POST, instance=obj)
            form2.save(commit=False)
            form2.save_m2m()
            return redirect('')
    
    else:
        form = AdherentForm()

    context = {'form':form}
    return render(request, 'form1/adherent.html',context)


class MerciView(TemplateView):
    template_name = 'form1/merci.html'

urls.py

urlpatterns = [
    path('adherent/', views.adherent_view, name='adherent'),
    path('merci/', views.MerciView.as_view(), name='merci'),
    ]

forms.py

class AdherentForm(forms.ModelForm):
  qdem05_statut = forms.ModelMultipleChoiceField(
                    queryset=Statut.objects.all(),
                    widget=forms.CheckboxSelectMultiple,
                    label=('label')
  )
  class Meta:
    model = Questionnaire
    # fields = ['qdem01_genre', 'qdem01rx_genre_precis', 'qdem02_naissance', 'qdem03_diplome', 'qdem04_csp', 'qdem05_statut', 'qdem07_revenus', 'qdem08_patrimoine', 'qdem09_territoire', 'qdem10_departement', 'qdem10b_pays', 'qcon02_consent', 'qdem11_discrim1', 'qdem12_discrim2', 'qtraj01_avant_adh', 'qtraj01_avant_sym', 'qtraj02_adhes_adh', 'qtraj02_adhes_sym', 'qtraj03_deseng_mil', 'qtraj04_deseng_adh', 'qtraj05_deseng_cause', 'qtraj06_primo_eng', 'qtraj07_integ_sent', 'qtraj08a_mandat']
    exclude = ['created_at','edited_at']
    # widgets = {
    #   'qdem05_statut': forms.CheckboxSelectMultiple
    # }
    
  def __init__(self, *args, **kwargs):
    kwargs.setdefault('label_suffix', '')
    super(AdherentForm, self).__init__(*args, **kwargs)

adherent.html

{% extends 'form1/base.html' %}
{% block content %}
{% load static %}

<form action="{% url 'merci' %}" method="post" id="form" name="form">

  {% csrf_token %}
  
  <div id="form_all">

    {{ form.non_field_errors }}

    <div name="genre" class="questionWrapper" id="genre">
      <p>{{ form.qdem01_genre.errors }}</p>
      <p>{{ form.qdem01_genre.label_tag }}</p>      
      <p>{{ form.qdem01_genre }}</p>
    </div>
    
    <div name="genre_preciser" class="questionWrapper" id="genre_preciser">
      <p>{{ form.qdem01rx_genre_precis.errors }}</p>
      <p>{{ form.qdem01rx_genre_precis }}</p>
    </div>

    <div name="naissance" class="questionWrapper" id="naissance">
      <p>{{ form.qdem02_naissance.errors }}</p>
      <p>{{ form.qdem02_naissance.label_tag }}</p>
      <p>{{ form.qdem02_naissance }}</p>
    </div>

    <div name="diplome" class="questionWrapper" id="diplome">
      <p>{{ form.qdem03_diplome.errors }}</p>
      <p>{{ form.qdem03_diplome.label_tag }}</p>
      <p>{{ form.qdem03_diplome }}</p>
    </div>
    
    <div name="csp" class="questionWrapper" id="csp">
      <p>{{ form.qdem04_csp.errors }}</p>
      <p>{{ form.qdem04_csp.label_tag }}</p>
      <p>{{ form.qdem04_csp }}</p>
    </div>
    
    <div name="statut" class="questionWrapper" id="statut">
      <p>{{ form.qdem05_statut.errors }}</p>
      <p>{{ form.qdem05_statut.label_tag }}</p>
      <p>{{ form.qdem05_statut }}</p>
    </div>
 
  </div>

  <input type="submit" value="Envoyer">

</form>
{% endblock %}

{% block script %}

<script src="{% static 'genre.js' %}"></script>

{% endblock %}

and finally merci.html

{% extends 'form1/base.html' %}

{% block content %}

Merci pour votre participation !

{% endblock %}

This form is being submitted through the browser directly, without any JavaScript / AJAX?

(What is the “inspector”?)

What does your server log show for the request?

Ken,

Yes, as far as I understand it, it is submitted directly through the browser. There’s a Javascript file loaded at the end, whose content is as follows:

genre.js

document.addEventListener("DOMContentLoaded", function() {
  const genre  = document.querySelector("[name='qdem01_genre']");
  const genrePreciser = document.querySelector("[name='qdem01rx_genre_precis']").closest(".questionWrapper");
  genrePreciser.classList.add('hidden');
  genre.addEventListener('change', (event) => {
    if (event.target.value === "Autre") {
      genrePreciser.classList.remove('hidden');
    } else {
      genrePreciser.classList.add('hidden');
    }
  });
});

Regarding the “inspector”, I was reffering to the console within Firefox development tools, my bad. I might be a bit tired :upside_down_face:

Here’s the server log:

Method Not Allowed (POST): /form1/merci/
Method Not Allowed: /form1/merci/
[13/Oct/2023 16:10:16] "POST /form1/merci/ HTTP/1.1" 405 0

What does your MerciView view look like? (That’s the one throwing the error as shown in the log.)

It’s a very simple (=incomplete) view. I intend to develop it but I’m not there yet…

views.py

merci.html

{% extends 'form1/base.html' %}

{% block content %}

Merci pour votre participation !

{% endblock %}

And that’s the problem.

You’re trying to submit a form to a view that isn’t designed / intended to accept a form.
(More technically, you’re trying to POST data to that view - but the view isn’t configured to accept that POST action.)

The issue is fundamentally caused by the line:
<form action="{% url 'merci' %}" method="post" id="form" name="form">

The common case is that you post a form to the view that rendered that form.

1 Like

Right! Makes sense.

So here it would rather be like:
<form action="{% url 'adherent' %}" method="post" id="form" name="form">
Edit: it seems to work according to the server log:

[13/Oct/2023 18:19:17] "POST /form1/adherent/ HTTP/1.1" 200 11706
[13/Oct/2023 18:19:17] "GET /static/style.css HTTP/1.1" 200 107
[13/Oct/2023 18:19:17] "GET /static/genre.js HTTP/1.1" 200 564

However nothing gets posted in the database.

Also, how could I define a success_url? My view is a now a function, not a FormView (which is how it started).

That’s effectively the purpose of this:

The success_url in your CBV is the definition of the URL to which you will be sent after successful posting of the submitted form.

Note: You’re never saving the AdherentForm. You’re binding it, but not saving it. You’re actually creating a separate instance of the Questionnaire, which isn’t needed when you’re using a ModelForm.

You don’t need any of this:

Depending upon whether this is a new Questionnaire being created or an edit of an existing instance, you can do:

...
if form.is_valid():
    form.save()
    return redirect(...)
...

I suggest you review the docs at Working with forms for the general flow of events in FBVs, and then the docs for the save() method for ModelForms.

Thanks for all those detailed explanations Ken.

This whole (apparently useless :face_with_peeking_eye:) part was created cause I struggled to add a question with multiple choices to the form. I tried many methods but none worked, so I ended up writing this based on various examples. It looks like I need to start over.

In my specific case, may I ask what method you would recommend to implement a form with single-answer elements + one or several multiple-answer elements?

Here’s my models.py:

ANNEE_NAISSANCE = []
for r in range(1920, (datetime.datetime.now().year+1)):
    ANNEE_NAISSANCE.append((r,r))
   
class Statut(models.Model):
    name = models.CharField(
        max_length=100,
        unique=True,
        blank=False
    )
    
    def __str__(self):
        return self.name


class Questionnaire(models.Model):
    qcon01_consent = models.BooleanField(
        _('je consens à répondre à ce questionnaire.'),
        blank=False,        
    )
    class Qclenche(models.TextChoices):
        ADHERENT = 'obfuscated', _('obfuscated')
        EX_ADHERENT = 'obfuscated', _('obfuscated')    
        SYMPATHISANT = 'obfuscated', _('obfuscated')
    qclenche = models.CharField(
        _('Êtes-vous :'),
        choices=Qclenche.choices,
        max_length=100,
        null=False,
        default='obfuscated',
        blank=False
    )
    class Genre(models.TextChoices): 
        FEMININ = 'Féminin', _('Féminin')
        MASCULIN = 'Masculin', _('Masculin')
        NON_BINAIRE = 'Non binaire', _('Non binaire')
        NON_PRECISE = 'Non précisé', _('Je ne souhaite pas le préciser')
        AUTRE = 'Autre', _('Autre (préciser)')
    qdem01_genre = models.CharField(
        _('Quel est votre genre ? *'),
        max_length=24,
        choices=Genre.choices,
        null=True,
        blank=False  
    )
    qdem01rx_genre_precis = models.CharField(
        _('Préciser :'),
        max_length=200,
        null=True,
        blank=True
    )
    qdem02_naissance = models.IntegerField(
        _('Quelle est votre année de naissance ? *'), 
        choices=ANNEE_NAISSANCE,
        null=True,
        blank=False
    )
    class Diplome(models.TextChoices):
        SANS_DIPLOME = 'Sans diplôme', _('Sans diplôme')
        BEPC = 'BEPC', _('BEPC, brevet élémentaire, brevet des collèges, DNB')
        BEP_CAP = 'BEP/CAP', _('BEP/CAP')
        BACCALAUREAT = 'Baccalauréat', _('Baccalauréat')
        DEUG = 'DEUG', _('DEUG, BTS, DUT, DEUST')
        LICENCE = 'Licence', _('Licence, licence professionnelle')
        MAITRISE = 'Maîtrise', _("Maîtrise, Master, diplôme d'études approfondies, diplôme d'études supérieures spécialisées, diplôme d'ingénieur")
        DOCTORAT = 'Doctorat', _('Doctorat')
    qdem03_diplome = models.CharField(
        _('Quel est le diplôme le plus élevé que vous ayez obtenu ? *'),
        max_length=230,
        choices=Diplome.choices,
        null=True,
        blank=False
    )
    class CSP(models.TextChoices):
        AGRI = 'AgriculteurICE', _('AgriculteurICE')
        ARTISTE = 'Artiste', _('Artiste')
        ARTISAN = 'Artisan', _('Artisan, CommerçantE, chefFE d\'entreprise')
        LIBERAL = 'Profession libérale', _('Profession libérale')
        CADRE_PRIVE = 'Cadre privé', _('Cadre du privé')
        CADRE_PUBLIC = 'Cadre public', _('Autre cadre du public ou fonctionnaire de catégorie A')
        PROF_SUP = 'ProfesseurE, profession scientifique', _('ProfesseurE, profession scientifique')
        PROF_PRIM = 'ProfesseurE des écoles', _('professeurE des écoles, instituteurICE et assimiléE')
        AUTRES_INTERM = 'Autres professions intermédiaires', _('Autre profession intermédiaire ou fonctionnaire de catégorie B')
        POLITIQUE = 'Personnel politique ou éluE', _('Personnel politique ou éluE')
        EMPLOYE = 'EmployéE', _('EmployéE')
        OUVRIER = 'OuvrierE', _('OuvrierE')
        RETRAITE = 'RetraitéE', _('RetraitéE')
        SANS_EMPLOI = 'Sans emploi', _('Sans emploi')
        AUTRE_FONCT = 'Autre fonctionnaire de catégorie C', _('Autre fonctionnaire de catégorie C')
        ETUDIANT = 'ÉtudiantE, lycéenNE', _('ÉtudiantE, lycéenNE')
        AUTRE = 'Autre', _('Autre')
    qdem04_csp = models.CharField(
        _('Quelle est votre catégorie socio-professionnelle actuelle ? *'),
        choices=CSP.choices,
        max_length=100,
        null=True,
        blank=False
    )
    qdem05_statut = models.ManyToManyField(
        Statut, 
        verbose_name="liste de statuts"
    )

I’m sorry, I’m not sure I understand your question.

Making a guess as to what I think you may be asking, the answer would depend upon the intent of what you mean by “several multiple-answer elements.”

In the general case, multiple selections are typically modelled as a many-to-many relationship with the tables being referenced. But there are so many different ways to handle things like this that it’s difficult to identify a “one-size-fits-all” solution covering all possibilities.

Yes, sorry about that. I meant: multiple choices, not multiple answer.
I did model it as a many-to-many relationship indeed (see models.py on my previous post, I edited the message to add it later).

According to your previous feedback, my adherent_view now looks like that:

def adherent_view(request):

    if request.method == 'POST':
        form = AdherentForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('/merci/')
    else:
        form = AdherentForm()

    context = {'form':form}
    return render(request, 'form1/adherent.html',context)

Does that look correct?

I’m running into an issue similar to the previous one: when I submit the form, nothing gets saved in the database and the page doesn’t change.

Ok, let’s take a step back for a moment and examine your models.

You’ve defined a many-to-many field to Statut - what’s in that model?

When you say “multiple answers”, what are you referring to?

What specifically are you looking at when you say that nothing is being saved?

When you submit data, are you being redirected to the new page, or are you remaining on the same page?

Remember that form.save() returns the instance of the model being saved. The statement new_object = form.save() gives you access to that new instance. You can then add a couple of print statements after the save to verify that the data has been saved.

The model is defined as follows:

class Statut(models.Model):
    name = models.CharField(
        max_length=100,
        unique=True,
        blank=False
    )

The database contains the following table:

id name
2 Fonctionnaire à temps plein
3 Fonctionnaire à temps partiel
4 En CDI, à temps plein
5 En CDI, à temps partiel
6 En CDD, à temps plein
7 En CDD, à temps partiel
8 IntermittentE du spectacle
9 Intérimaire / Extra
10 ChômeurEUSE au régime général
11 Bénéficiaire du RSA
12 Bénéficiaire de l’AAH
13 Je vis principalement d’une rente patrimoniale ou actionnariale
14 J’ai deux employeurs ou plus
15 Micro-entreprise / auto-entreprise
1 ChefFE d’entreprise

I filled it up manually in PostgreSQL.

I’m referring to the possibility for the user to send several answer simultaneously to qdem05_statut, by checking as many boxes as they want among the list constituted by queryingStatut. If a user checks three boxes, I want to record the three of them in the db.

I’m refreshing the database tables opened in DBeaver. I don’t see any new item appearing.

I’m remaining on the same page.

If you’re remaining on the same page, then your form has errors from the submission. This makes me think that you’re not rendering form errors in the form.

If you don’t want to render the errors on the page, you can print them so you can at least see them in the console. See The Forms API | Django documentation | Django

I thought I was rendering them in the template :thinking:

adherent.html:

{% extends 'form1/base.html' %}

{% block content %}

{% load static %}

<form action="{% url 'adherent' %}" method="post" id="form" name="form">

  {% csrf_token %}

  <div id="form_all">

    {{ form.non_field_errors }}

    <div name="genre" class="questionWrapper" id="genre">
      <p>{{ form.qdem01_genre.errors }}</p>
      <p>{{ form.qdem01_genre.label_tag }}</p>      
      <p>{{ form.qdem01_genre }}</p>
    </div>
    
    <div name="genre_preciser" class="questionWrapper" id="genre_preciser">
      <p>{{ form.qdem01rx_genre_precis.errors }}</p>
      <p>{{ form.qdem01rx_genre_precis }}</p>
    </div>

    <div name="naissance" class="questionWrapper" id="naissance">
      <p>{{ form.qdem02_naissance.errors }}</p>
      <p>{{ form.qdem02_naissance.label_tag }}</p>
      <p>{{ form.qdem02_naissance }}</p>
    </div>

    <div name="diplome" class="questionWrapper" id="diplome">
      <p>{{ form.qdem03_diplome.errors }}</p>
      <p>{{ form.qdem03_diplome.label_tag }}</p>
      <p>{{ form.qdem03_diplome }}</p>
    </div>
    
    <div name="csp" class="questionWrapper" id="csp">
      <p>{{ form.qdem04_csp.errors }}</p>
      <p>{{ form.qdem04_csp.label_tag }}</p>
      <p>{{ form.qdem04_csp }}</p>
    </div>
    
    <div name="statut" class="questionWrapper" id="statut">
      <p>{{ form.qdem05_statut.errors }}</p>
      <p>{{ form.qdem05_statut.label_tag }}</p>
      <p>{{ form.qdem05_statut }}</p>
    </div>

    <div name="csp_ex" class="questionWrapper" id="csp_ex">
      <p>{{ form.qd em06_csp_ex.errors }}</p>
      <p>{{ form.qdem06_csp_ex.label_tag }}</p>
      <p>{{ form.qdem06_csp_ex }}</p>
    </div>    
    
    <div name="revenus" class="questionWrapper" id="revenus">
      <p>{{ form.qdem07_revenus.errors }}</p>
      <p>{{ form.qdem07_revenus.label_tag }}</p>
      <p>{{ form.qdem07_revenus }}</p>
    </div>

    <div name="patrimoine" class="questionWrapper" id="patrimoine">
      <p>{{ form.qdem08_patrimoine.errors }}</p>
      <p>{{ form.qdem08_patrimoine.label_tag }}</p>
      <p>{{ form.qdem08_patrimoine }}</p>
    </div>

    <div name="territoire" class="questionWrapper" id="territoire">
      <p>{{ form.qdem09_territoire.errors }}</p>
      <p>{{ form.qdem09_territoire.label_tag }}</p>
      <p>{{ form.qdem09_territoire }}</p>
    </div>

    <div name="departement" class="questionWrapper" id="departement">
      <p>{{ form.qdem10_departement.errors }}</p>
      <p>{{ form.qdem10_departement.label_tag }}</p>
      <p>{{ form.qdem10_departement }}</p>
    </div>

    <div name="pays" class="questionWrapper" id="pays">
      <p>{{ form.qdem10b_pays.errors }}</p>
      <p>{{ form.qdem10b_pays.label_tag }}</p>
      <p>{{ form.qdem10b_pays }}</p>
    </div>

    <p>Les trois questions suivantes ont trait à votre expérience des discriminations. Elles nous permettront de comprendre si les personnes ayant vécu ou étant susceptibles de vivre des situations discriminantes sont également susceptibles de vivre une expérience particulière de la vie politique. Si vous pensez ne pas être concernéE : nous avons également besoin de vos réponses ! Consentez-vous à y répondre ? *</p>

    <div name="consent_2" class="questionWrapper" id="consent_2">
      <p>{{ form.qcon02_consent.errors }}</p>
      <p>{{ form.qcon02_consent }}</p>
    </div>

    <div name="discrim_1" class="questionWrapper" id="discrim_1">
      <p>{{ form.qdem11_discrim1.errors }}</p>
      <p>{{ form.qdem11_discrim1.label_tag }}</p>
      <p>{{ form.qdem11_discrim1 }}</p>
    </div>

    <div name="discrim_2" class="questionWrapper" id="discrim_2">
      <p>{{ form.qdem12_discrim2.errors }}</p>
      <p>{{ form.qdem12_discrim2.label_tag }}</p>
      <p>{{ form.qdem12_discrim2 }}</p>
    </div>

  </div>

  <input type="submit" value="Envoyer">

</form>

{% endblock %}

{% block script %}

<script src="{% static 'genre.js' %}"></script>
<script src="{% static 'consent2.js' %}"></script>

{% endblock %}

It’s tough for me to be sure because there’s a mismatch between the fields you are rendering in the template and what you have shown in the model. But these symptoms would be consistent with you not rendering all the Model fields defined in the form, and so the error would be that a non-nullable field wasn’t filled out. If that’s the case, you wouldn’t see an error because you’re not rendering that field.