Django-forms: Raise forms.ValidationError not working - django

I am beginner in Django and recently studied form-validation. I implemented the code but was unable to raise ValidationError for some constraints.
Here are my subsequent file content.
forms.py
from django import forms
from django.core import validators
class formClass(forms.Form):
name = forms.CharField(max_length=128)
email = forms.EmailField(max_length=256)
text = forms.CharField(widget=forms.Textarea)
catchBot = forms.CharField(required=False, widget=forms.HiddenInput,
validators=[validators.MaxLengthValidator(0)])
def clean(self):
cleaned_data = super(formClass, self).clean()
t = self.cleaned_data.get('name')
if t[0].lower() != 'd':
raise forms.ValidationError('Name must start with d.')
return cleaned_data
views.py
from django.shortcuts import render
from formApp import forms
from django.http import HttpResponseRedirect
def formNameView(request):
formObj = forms.formClass()
formDict = {'form': formObj}
if request.method == 'POST':
formObj = forms.formClass(request.POST)
if formObj.is_valid():
# SOME CODE
print("NAME: " + formObj.cleaned_data['name'])
print("EMAIL: " + formObj.cleaned_data['email'])
return HttpResponseRedirect('/users')
return render(request, 'formApp/forms.html', context=formDict)
My valid input works great, but it doesn't happen with my invalid input.
for example: if name = 'Alex', it should raise an error. But it doesn't.
Could someone please help me in it?
EDIT:
[Added forms.html and validators callable.]
Previously, I used validators callable to raise ValidationError instead of clean() method. But the results were same.
Here is my code:
def checkForD(value):
if value[0].lower() != 'd':
raise forms.ValidationError('Name must start with d.')
.
.
.
# in my formClass()
name = forms.CharField(max_length=128, validators[checkForD])
...
Forms.html
<body>
<div class='container'>
<div class='jumbotron'>
<h3>Welcome to the form page.</h3>
<h2>Please insert the form.</h2>
</div>
<form method="post">
{{form.as_p}}
{% csrf_token %}
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</div>
</body>

In your POST block you've redefined formObj to be the bound form, but you haven't replaced the instance in the context dict - so what is passed to the template is the empty unbound form, and no errors will be shown on that template.
The easiest fix would be to move the definition of the dict to the end of the function:
formDict = {'form': formObj}
return render(request, 'formApp/forms.html', context=formDict)
Now the correct instance will be used and the errors will show.

You can try this: don't define the formDict in post, directly define it in return render
return render(request, 'formApp/forms.html', {'form': formObj})

Related

How to create a record from request GET data (form does not pass the validation)

I am confused when I try to insert record from a "GET" request
I will try to explain what I want to do.
I am creating an application to take inventory of assets.
I have 3 tables in my database.
I have a main table called
fixed asset("ActFijo") where all the assets of my company are registered.
Another call Inventory ("Inventario"), which stores the name of each inventory
and another call Inventory_detail ("Inventario_detalle"), where the details or assets in which they are being counted are stored to verify that the equipament or furniture is not being stolen in that location.
From the main table ("ActFijo") I have to search for the furniture or asset and store it in the detail table ("Inventario_detalle")
I'm confused I don't know how to work on a GET request and then do a POST all in one request
Do I have to write my code in parts in a GET request and then POST?
Or can I do everything in the GET request?
This is the code I have so far
I don't know if it's ok, please I need guidance
For example my code does not pass the validation of the form.
if form.is_valid():
I am trying to print, But I don't see any validation error, it doesn't print anything
print(form.errors)
Views.py
from django.shortcuts import redirect, render
from .form import InventarioDetalle_Form, InventarioForm
from .models import ActFijo, Inventario, Inventario_detalle
# Create your views here.
def inventario_home_view(request):
if request.method == "GET":
inv = Inventario.objects.all()
context = {"inventarios": inv}
return render(request, "inventario/index.html", context)
def inventario_crear_view(request):
if request.method == "POST":
form = InventarioForm(request.POST)
if form.is_valid():
form.save()
return redirect("inventario-home")
else:
form = InventarioForm()
inv = Inventario.objects.all()
context = {"formulario": form, "inventarios": inv}
return render(request, 'inventario/crear.html', context)
def inventario_detalle_view(request, inventario):
if request.method == "GET":
# Obtener el valor del input "Buscar"
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
# Buscar el activo en la bd por el campo codigo
try:
activo = ActFijo.objects.get(codigo=codigo_activo)
# print(activo)
except ActFijo.DoesNotExist:
activo = None
if activo:
form = InventarioDetalle_Form(instance=activo)
# print(form)
print(form.errors)
if form.is_valid():
instance = form.save(commit=False)
instance.inventario_id = inventario
instance.save()
else:
print(
"This request does not pass the validation")
else:
print(
"The element does not exist")
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)
form.py:
from django import forms
from .models import Inventario, Inventario_detalle
class InventarioForm(forms.ModelForm):
class Meta:
model = Inventario
fields = '__all__'
class InventarioDetalle_Form(forms.ModelForm):
class Meta:
model = Inventario_detalle
fields = '__all__'
url.py
from django.urls import path
from django import views
from . import views
urlpatterns = [
path("", views.inventario_home_view, name="inventario-home"),
path("create/", views.inventario_crear_view,
name="inventario-create"),
path('detail/<int:inventario>',
views.inventario_detalle_view, name="inventario-detail"),
]
detail.html
{% extends "core/base.html" %} {% block content%}
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="titulo mt-5">
<h1>Inventario Detalle</h1>
</div>
<form method="get">
<input type="text" class="form-control" placeholder="Buscar Activo" name="buscar" />
</form>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<th>Codigo</th>
<th>Descripcion</th>
<th>Accion</th>
</tr>
</thead>
<tbody>
{% for i in item %}
<tr>
<td>{{i.codigo}}</td>
<td>{{i.descripcion}}</td>
<td><button type="button" class="btn btn-danger">Eliminar</button></td>
</tr>
</tbody>
{% endfor %}
</tbody>
</table>
</div>
<div>{{request.GET}}</div>
</div>
</div>
</div>
{% endblock %}
The problem is here
def inventario_detalle_view(request, inventario):
if request.method == "GET":
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
try:
activo = ActFijo.objects.get(codigo=codigo_activo)
print(activo)
except ActFijo.DoesNotExist:
activo = None
if activo:
form = InventarioDetalle_Form(instance=activo)
print(form.errors)
if form.is_valid():
instance = form.save(commit=False)
instance.inventario_id = inventario
instance.save()
else:
print(
"This request does not pass the validation")
else:
print(
"The element does not exist")
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)
I believe you don't see any validation errors on the form because for the GET request, you are not passing in anything. The only thing you're passing into the form is the model instance and you're running form.is_valid on it which does not make sense. You dont need to use the form at all. Use this instead.
def inventario_detalle_view(request, inventario):
if request.method == "GET":
codigo_activo = request.GET.get("buscar")
print("[CODIGO ACTIVO]:", codigo_activo)
try:
activo = ActFijo.objects.get(codigo=codigo_activo) # get activo object
activo.inventario_id = inventario # update object
activo.save() # save changes
print(activo)
except ActFijo.DoesNotExist:
# you can do anthing here
# maybe redirect with a message..
pass
context = {"item": Inventario_detalle.objects.all()}
return render(request, "inventario/detalle.html", context)

Display form content on same template django

So I am working on django, I have a simple form with one text field named "sujet", here's the form.py code:
from django import forms
class ContactForm(forms.Form):
sujet = forms.CharField(max_length=100,
widget=forms.TextInput(
attrs={
'id': 'id_sujet',
'style': 'border-color: blue;',
'placeholder': 'Write your name here'
}
)
)
The template "contact.html" where the user can fill the form is the following:
<form id="myForm" action="{% url 'contact' %}" method="post">
{{ form.sujet }}
<input type="submit" value="Submit" />
</form>
<div id="response">
Sujet : <span id="sujeet"></span>
{{ sujet }}
</div>
In my "views.py" I want my contact function to render the variable "sujet" to the template so I can diplay it on "contact.html" after filling and submitting the form (the intended usage of this is later to display the result of the database query that concerns sujet and not the variable itself but I am still working on that), here's the code of the view function :
def contact(request):
form = ContactForm(request.POST or None)
if form.is_valid():
sujet = form.cleaned_data['sujet']
envoi = True
return redirect(request.META['HTTP_REFERER'], {'sujet':sujet})
else:
return render(request, 'voirkpi/contact.html', locals())
Here is the code of my "urls.py" :
from django.urls import path
from django.conf.urls import url
from . import views
urlpatterns = [
path('accueil', views.ligneerabstat),
path('spark', views.kpitest),
path('testchart', views.testchart),
path('contact', views.contact, name='contact'),
#path('test', views.test, name='test'),
url('test', views.test, name='test')
]
My problem is that after clicking submit sujet is empty and doesn't show anything, I did make this work with simple javascript, but that's not what I want as the intended usage of this is as I said later to query a database and return in that span the result of the query according to the "sujet" filled.
Any help would be very much appreciated, thank you.
Thanks for the help. You're right Resley Rodrigues, I solved my problem by changing my contact function in "views.py" like so:
def contact(request):
form = ContactForm(request.POST or None)
if form.is_valid():
sujet = form.cleaned_data['sujet']
envoi = True
form = ContactForm()
return render(request,'voirkpi/contact.html',locals(), {'sujet' : sujet})
else:
return render(request, 'voirkpi/contact.html', locals())

Django custom form validation in ListView

I am using a ListView to set a form and to show results. However i am not sure how can I make form validation and having the same form with errors in case form.is_valid() is not True.
this is my code
forms.py
class InsolventiForm(forms.Form):
anno_validator = RegexValidator(r'[0-9]{4}', 'L\'anno deve essere un numero di 4 caratteri')
anno = forms.CharField(label='Anno', required=True, max_length=4,validators=[anno_validator])
def clean_anno(self):
anno = self.cleaned_data['anno']
return anno
views.py
from .forms import InsolventiForm
class InsolventiView(LoginRequiredMixin, ListView):
template_name = 'insolventi.html'
model = Archivio
form_class = InsolventiForm
def get(self, request):
import datetime
if self.request.GET.get('anno'):
form = self.form_class(self.request.GET)
if form.is_valid():
date = '31/12/'+self.request.GET.get('anno')
dateTime = datetime.datetime.strptime(date, "%d/%m/%Y")
dateC = '01/01/'+self.request.GET.get('anno')
dateTimeC = datetime.datetime.strptime(dateC, "%d/%m/%Y")
context = Archivio.objects.filter(~Q(quoteiscrizione__anno_quota__exact=self.request.GET.get('anno')) \
& Q(data_iscrizione__lte=dateTime) \
& (Q(cancellato__exact=False) | (Q(cancellato__exact=True) & (Q(data_canc__gte=dateTimeC)))))
self.request.session['insolventi_queryset'] = serialize('json', context)
return render(request, self.template_name, {'form':form})
else: return redirect(reverse('insolventi'))
return render(request, self.template_name, {'form':self.form_class()})
this is my template and I am displaying the form manually.
insolventi.html
<form method="get" action="">
{% for field in form %}
{{ field.errors }}
{{ field.as_widget() }}
{% endfor %}
<input type="submit" value="Ricerca" />
</form>
Even if there are errors and form.is_valid() is returning False (giving me a redirect to the same view) on the template I never get {{ form.errors }}.
I don't know what is missing!
I am thinking: Because i use the input of the form to get the query in JSON with django rest and post it on the same template with DataTables, maybe I do not need to use a ListView ??
You should not be redirecting if there are errors since redirecting will lose all the form data.
Try removing the line:
else: return redirect(reverse('insolventi'))
and letting it fall through to the render() line.
Hi can you try this post
custom form validation
also refer django document
django custom validation as per document

Django: Passing value from template to view

I HAD this situation:
Clicking on a html submit button, I call views.stream_response which "activates" views.stream_response_generator which "activates" stream.py and return a StreamingHttpResponse and I see a progressive number every second up to m at /stream_response/:
1
2
3
4
5
6
7
8 //e.g. my default max value for m
stream.py
from django.template import Context, Template
import time
def streamx(m):
lista = []
x=0
while len(lista) < m:
x = x + 1
time.sleep(1)
lista.append(x)
yield "<div>%s</div>\n" % x #prints on browser
print(lista) #print on eclipse
return (x)
views.py
def stream_response(request): // unified the three functions as suggested
if request.method == 'POST':
form = InputNumeroForm(request.POST)
if form.is_valid():
m = request.POST.get('numb', 8)
resp = StreamingHttpResponse(stream.streamx(m))
return resp
forms.py
from django.db import models
from django import forms
from django.forms import ModelForm
class InputNumero(models.Model):
m = models.IntegerField()
class InputNumeroForm(forms.Form):
class Meta:
models = InputNumero
fields = ('m',)
urls.py
...
url(r'^homepage/provadata/$', views.provadata),
url(r'^stream_response/$', views.stream_response, name='stream_response'),
...
homepage/provadata.html
<form id="streamform" action="{% url 'stream_response' %}" method="POST">
{% csrf_token %}
{{form}}
<input id="numb" type="number" />
<input type="submit" value="to view" id="streambutton" />
</form>
If I delete "8" and use only m = request.POST.get('numb') I obtain:
ValueError at /stream_response/ The view homepage.views.stream_response didn't return an HttpResponse object.
It returned None instead.
So, if I try to submit, it takes only the default value 8 (and works) but it not takes my form input. What is it wrong?
-->UPDATE: with #Tanguy Serrat suggestions:
views.py
def stream_response(request):
form = InputNumeroForm()
if request.method == 'POST':
form = InputNumeroForm(data=request.POST)
if form.is_valid():
#Accessing the data in cleaned_data
m = form.cleaned_data['numero']
print("My form html: %s" % form)
print ("My Number: %s" % m) #watch your command line
print("m = ", m)
resp = StreamingHttpResponse(stream.streamx(m))
return resp
#If not post provide the form here in the template :
return render(request, 'homepage/provadata.html', {'form': form,})
forms.py
class InputNumeroForm(forms.Form):
numero = models.IntegerField()
homepage/provadata.py
<form action="/stream_response/" method="POST">
{% csrf_token %}
{{form}} <!--issue: does not appear on the html !!!!!-->
<input type="number" name="numero" /> <!--so I write this-->
<input type="submit" value="to view" />
</form>
If I give as input e.g. 7 from keyboard:
KeyError at /stream_response/
'numero'
WHILE
If I write m = request.POST.get('numero'), in command line I have:
...
My form html:
My Number: 7
m = 7
...
while len(lista) < m:
TypeError: unorderable types: int() < str()
EDIT : ModelForm part removed, no need to save the data in DB so using classic form this way :
Method 1 : Classic Form without Model using Django
in your forms.py
from django import forms
class InputNumeroForm(forms.Form):
numero = forms.IntegerField()
in your views.py
from django.shortcuts import render
def stream_response(request):
form = InputNumeroForm()
if request.method == 'POST':
form = InputNumeroForm(data=request.POST)
if form.is_valid():
#Accessing the data in cleaned_data
m = form.cleaned_data['numero']
print "My Number %s" % m #watch your command line
resp = StreamingHttpResponse(stream.streamx(m))
return resp
#If not post provide the form here in the template :
return render(request, 'homepage/provadata.html', {
'form': form,
})
in your template :
<form id="streamform" action="{% url 'stream_response' %}" method="POST">
{% csrf_token %}
{{ form }}
<input type="submit" value="to view" id="streambutton" />
</form>
To clarify a little there are two types of form in Django :
Classic Forms which do not require saving in a database
Model Forms which will allow you to create forms which are based on a Database Model i.e. (you can add a row to your db or edit one)
Here you don't need to save your number in your database so you use classical forms:
https://docs.djangoproject.com/en/1.8/topics/forms/
Method 2 : Not using the Django Form
For very simple forms like your case :
in views.py
from django.shortcuts import render
def stream_response(request):
if request.method == 'POST':
if request.POST.get('numero', False):
m = int(request.POST['numero'])
print "My Number %s" % m #watch your command line
resp = StreamingHttpResponse(stream.streamx(m))
return resp
return render(request, 'homepage/provadata.html')
in your template :
<form id="streamform" action="{% url 'stream_response' %}" method="POST">
{% csrf_token %}
<input type="number" name="numero" />
<input type="submit" value="to view" id="streambutton" />
</form>
I see a handful of issues with the code you posted:
Your form field is missing the 'name' attribute, and thus the value set for this field will not be passed to your code. This is why your request.POST.get('numb') returns None unless you provide your default (8).
Try this:
<input id="numb" name="numb" type="number" />
Also, I noticed in your form you use models.IntegerField - why are you trying to use a model field here?
A better approach for you may be to add the numb field to your form and then retrieve the value from the form's cleaned data: form.cleaned_data['numb'] instead of the POST data.
Hopefully this gets you going.
Django views MUST return a HttpResponse object. Considering that the other two functions you're referencing have only one line each, it would serve you better to just put everything in one function, or at least move the StreamingHttpResponse to the main function.
Edit: The StreamingResponseHttp must be given an iterable so, going back to the stream.py, try taking out the return and print functions (I've taken out excess stuff for experimentation's sake). This worked for me in the shell.
def streamx(m):
lista = []
x=0
while len(lista) < m:
x = x + 1
time.sleep(1)
lista.append(x)
yield "<div>%s</div>\n" % x #prints on browser
#c = Context({'x': x})
#yield Template('{{ x }} <br />\n').render(c)
def stream_response(request):
if request.method == 'POST':
form = InputNumeroForm(request.POST)
if form.is_valid():
m = request['numb']
resp = StreamingHttpResponse(streamx(m))
return resp

Django: Can class-based views accept two forms at a time?

If I have two forms:
class ContactForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
class SocialForm(forms.Form):
name = forms.CharField()
message = forms.CharField(widget=forms.Textarea)
and wanted to use a class based view, and send both forms to the template, is that even possible?
class TestView(FormView):
template_name = 'contact.html'
form_class = ContactForm
It seems the FormView can only accept one form at a time.
In function based view though I can easily send two forms to my template and retrieve the content of both within the request.POST back.
variables = {'contact_form':contact_form, 'social_form':social_form }
return render(request, 'discussion.html', variables)
Is this a limitation of using class based view (generic views)?
Many Thanks
Here's a scaleable solution. My starting point was this gist,
https://gist.github.com/michelts/1029336
i've enhanced that solution so that multiple forms can be displayed, but either all or an individual can be submitted
https://gist.github.com/jamesbrobb/748c47f46b9bd224b07f
and this is an example usage
class SignupLoginView(MultiFormsView):
template_name = 'public/my_login_signup_template.html'
form_classes = {'login': LoginForm,
'signup': SignupForm}
success_url = 'my/success/url'
def get_login_initial(self):
return {'email':'dave#dave.com'}
def get_signup_initial(self):
return {'email':'dave#dave.com'}
def get_context_data(self, **kwargs):
context = super(SignupLoginView, self).get_context_data(**kwargs)
context.update({"some_context_value": 'blah blah blah',
"some_other_context_value": 'blah'})
return context
def login_form_valid(self, form):
return form.login(self.request, redirect_url=self.get_success_url())
def signup_form_valid(self, form):
user = form.save(self.request)
return form.signup(self.request, user, self.get_success_url())
and the template looks like this
<form class="login" method="POST" action="{% url 'my_view' %}">
{% csrf_token %}
{{ forms.login.as_p }}
<button name='action' value='login' type="submit">Sign in</button>
</form>
<form class="signup" method="POST" action="{% url 'my_view' %}">
{% csrf_token %}
{{ forms.signup.as_p }}
<button name='action' value='signup' type="submit">Sign up</button>
</form>
An important thing to note on the template are the submit buttons. They have to have their 'name' attribute set to 'action' and their 'value' attribute must match the name given to the form in the 'form_classes' dict. This is used to determine which individual form has been submitted.
By default, class-based views only support a single form per view. But there are other ways to accomplish what you need. But again, this cannot handle both forms at the same time. This will also work with most of the class-based views as well as regular forms.
views.py
class MyClassView(UpdateView):
template_name = 'page.html'
form_class = myform1
second_form_class = myform2
success_url = '/'
def get_context_data(self, **kwargs):
context = super(MyClassView, self).get_context_data(**kwargs)
if 'form' not in context:
context['form'] = self.form_class(request=self.request)
if 'form2' not in context:
context['form2'] = self.second_form_class(request=self.request)
return context
def get_object(self):
return get_object_or_404(Model, pk=self.request.session['value_here'])
def form_invalid(self, **kwargs):
return self.render_to_response(self.get_context_data(**kwargs))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if 'form' in request.POST:
form_class = self.get_form_class()
form_name = 'form'
else:
form_class = self.second_form_class
form_name = 'form2'
form = self.get_form(form_class)
if form.is_valid():
return self.form_valid(form)
else:
return self.form_invalid(**{form_name: form})
template
<form method="post">
{% csrf_token %}
.........
<input type="submit" name="form" value="Submit" />
</form>
<form method="post">
{% csrf_token %}
.........
<input type="submit" name="form2" value="Submit" />
</form>
Its is possible for one class-based view to accept two forms at a time.
view.py
class TestView(FormView):
template_name = 'contact.html'
def get(self, request, *args, **kwargs):
contact_form = ContactForm()
contact_form.prefix = 'contact_form'
social_form = SocialForm()
social_form.prefix = 'social_form'
# Use RequestContext instead of render_to_response from 3.0
return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))
def post(self, request, *args, **kwargs):
contact_form = ContactForm(self.request.POST, prefix='contact_form')
social_form = SocialForm(self.request.POST, prefix='social_form ')
if contact_form.is_valid() and social_form.is_valid():
### do something
return HttpResponseRedirect(>>> redirect url <<<)
else:
return self.form_invalid(contact_form,social_form , **kwargs)
def form_invalid(self, contact_form, social_form, **kwargs):
contact_form.prefix='contact_form'
social_form.prefix='social_form'
return self.render_to_response(self.get_context_data({'contact_form': contact_form, 'social_form': social_form}))
forms.py
from django import forms
from models import Social, Contact
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Submit, Button, Layout, Field, Div
from crispy_forms.bootstrap import (FormActions)
class ContactForm(forms.ModelForm):
class Meta:
model = Contact
helper = FormHelper()
helper.form_tag = False
class SocialForm(forms.Form):
class Meta:
model = Social
helper = FormHelper()
helper.form_tag = False
HTML
Take one outer form class and set action as TestView Url
{% load crispy_forms_tags %}
<form action="/testview/" method="post">
<!----- render your forms here -->
{% crispy contact_form %}
{% crispy social_form%}
<input type='submit' value="Save" />
</form>
Good Luck
I have used a following generic view based on TemplateView:
def merge_dicts(x, y):
"""
Given two dicts, merge them into a new dict as a shallow copy.
"""
z = x.copy()
z.update(y)
return z
class MultipleFormView(TemplateView):
"""
View mixin that handles multiple forms / formsets.
After the successful data is inserted ``self.process_forms`` is called.
"""
form_classes = {}
def get_context_data(self, **kwargs):
context = super(MultipleFormView, self).get_context_data(**kwargs)
forms_initialized = {name: form(prefix=name)
for name, form in self.form_classes.items()}
return merge_dicts(context, forms_initialized)
def post(self, request):
forms_initialized = {
name: form(prefix=name, data=request.POST)
for name, form in self.form_classes.items()}
valid = all([form_class.is_valid()
for form_class in forms_initialized.values()])
if valid:
return self.process_forms(forms_initialized)
else:
context = merge_dicts(self.get_context_data(), forms_initialized)
return self.render_to_response(context)
def process_forms(self, form_instances):
raise NotImplemented
This has the advantage that it is reusable and all the validation is done on the forms themselves.
It is then used as follows:
class AddSource(MultipleFormView):
"""
Custom view for processing source form and seed formset
"""
template_name = 'add_source.html'
form_classes = {
'source_form': forms.SourceForm,
'seed_formset': forms.SeedFormset,
}
def process_forms(self, form_instances):
pass # saving forms etc
It is not a limitation of class-based views. Generic FormView just is not designed to accept two forms (well, it's generic). You can subclass it or write your own class-based view to accept two forms.
Use django-superform
This is a pretty neat way to thread a composed form as a single object to outside callers, such as the Django class based views.
from django_superform import FormField, SuperForm
class MyClassForm(SuperForm):
form1 = FormField(FormClass1)
form2 = FormField(FormClass2)
In the view, you can use form_class = MyClassForm
In the form __init__() method, you can access the forms using: self.forms['form1']
There is also a SuperModelForm and ModelFormField for model-forms.
In the template, you can access the form fields using: {{ form.form1.field }}. I would recommend aliasing the form using {% with form1=form.form1 %} to avoid rereading/reconstructing the form all the time.
Resembles #james answer (I had a similar starting point), but it doesn't need to receive a form name via POST data. Instead, it uses autogenerated prefixes to determine which form(s) received POST data, assign the data, validate these forms, and finally send them to the appropriate form_valid method. If there is only 1 bound form it sends that single form, else it sends a {"name": bound_form_instance} dictionary.
It is compatible with forms.Form or other "form behaving" classes that can be assigned a prefix (ex. django formsets), but haven't made a ModelForm variant yet, tho you could use a model form with this View (see edit below). It can handle forms in different tags, multiple forms in one tag, or a combination of both.
The code is hosted on github (https://github.com/AlexECX/django_MultiFormView). There are some usage guidelines and a little demo covering some use cases. The goal was to have a class that feels as close as possible like the FormView.
Here is an example with a simple use case:
views.py
class MultipleFormsDemoView(MultiFormView):
template_name = "app_name/demo.html"
initials = {
"contactform": {"message": "some initial data"}
}
form_classes = [
ContactForm,
("better_name", SubscriptionForm),
]
# The order is important! and you need to provide an
# url for every form_class.
success_urls = [
reverse_lazy("app_name:contact_view"),
reverse_lazy("app_name:subcribe_view"),
]
# Or, if it is the same url:
#success_url = reverse_lazy("app_name:some_view")
def get_contactform_initial(self, form_name):
initial = super().get_initial(form_name)
# Some logic here? I just wanted to show it could be done,
# initial data is assigned automatically from self.initials anyway
return initial
def contactform_form_valid(self, form):
title = form.cleaned_data.get('title')
print(title)
return super().form_valid(form)
def better_name_form_valid(self, form):
email = form.cleaned_data.get('email')
print(email)
if "Somebody once told me the world" is "gonna roll me":
return super().form_valid(form)
else:
return HttpResponse("Somebody once told me the world is gonna roll me")
template.html
{% extends "base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ forms.better_name }}
<input type="submit" value="Subscribe">
</form>
<form method="post">
{% csrf_token %}
{{ forms.contactform }}
<input type="submit" value="Send">
</form>
{% endblock content %}
EDIT - about ModelForms
Welp, after looking into ModelFormView I realised it wouldn't be that easy to create a MultiModelFormView, I would probably need to rewrite SingleObjectMixin as well. In the mean time, you can use a ModelForm as long as you add an 'instance' keyword argument with a model instance.
def get_bookform_form_kwargs(self, form_name):
kwargs = super().get_form_kwargs(form_name)
kwargs['instance'] = Book.objects.get(title="I'm Batman")
return kwargs