Required and Optional Field in Django Formset - python

I created a formset to replicate over two input fields, first name and last name. I wanted to make the first set of "first name" and "last name" field to be required while the second, and third set as optional. In another word, at least one of the three set input box must be filled out. Is there a way to do this? Do I have to make 2 separate form/formset to accommodate this?
The reason for my question is I wanted to have 3 sets of first name and last name field. If a user only fill out one of the 3 sets and click Submit, Django will throw a Key Error because the other 2 sets aren't filled in.
forms.py
from django import forms
class NameForm (forms.Form):
first_name = forms.CharField (max_length = 20, required = False)
last_name = forms.CharField (max_length = 20, required = False)
views.py
from django.shortcuts import render
from django.forms.formsets import formset_factory
from nameform.forms import NameForm
from nameform.addName import webform
# Create your views here.
def addname (request):
NameFormSet = formset_factory (NameForm, extra = 2, max_num = 5) # Set maximum to avoid default of 1000 forms.
if request.method == 'POST':
# Django will become valid even if an empty form is submitted. Adding initial data causes unbound form and
# trigger formset.errors
formset = NameFormSet (request.POST, initial = [{'first_name': 'John', 'last_name': 'Doe'}])
if formset.is_valid ():
location = request.POST ['site']
data = formset.cleaned_data
for form in data:
firstname = form ['first_name']
lastname = form ['last_name']
webform (firstname, lastname, location)
context = {'data': data, 'location': location}
return render (request, 'nameform/success.html', context)
else:
formset = NameFormSet ()
return render (request, 'nameform/addname.html', {'formset': formset})
Here are the steps that I've tried:
If I removed the "required = False", it would make every form in the formset required. KeyError will be flagged if the user only filled out the first set and leave the second set blank.
Making this formset as optional like above will also throw a KeyError if user submitted a blank form.

Django formset in this case works in a peculiar way - forms without data have empty cleaned_data but treated as valid. So you have to manually check existence of form data:
# at imports section of views.py
from django.forms.util import ErrorList
# at addname()
actual_data = []
for form in data:
firstname = form.get('first_name')
lastname = form.get('last_name')
if firstname and lastname:
webform (firstname, lastname, location)
actual_data.append(form)
if actual_data:
context = {'data': actual_data, 'location': location}
return render(request, 'nameform/success.html', context)
formset._non_form_errors = ErrorList(['Enter at least one person name.'])

you need to inherit from BaseFormSet and override "clean" method to implement manual validation .
the django docs has an example : https://docs.djangoproject.com/en/1.7/topics/forms/formsets/#custom-formset-validation

Related

Manually set model fields in ModelForm

I have a model with a foreign key and a unique constraint as follows:
class Menu(models.Model):
tournament = models.ForeignKey(Tournament, on_delete=models.CASCADE)
name = models.CharField(max_length=128)
date_menu = models.DateField()
class Meta:
constraints = [
models.UniqueConstraint(fields=['tournament', 'name', 'date_menu'], name="unique_name_menu")
]
I would like to create a form to add instance of Menu. However the value of tournament is set by the URL of the page. I do not want the user to be able to set it.
For this I use a modelForm, excluding the tournament field :
class MenuForm(forms.ModelForm):
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
exclude = ['tournament']
Here is my view :
def add_menu(request, tournament_slug):
tournament = get_object_or_404(Tournament, slug=tournament_slug)
form = MenuForm(request.POST or None)
if form.is_valid():
menu_id = form.save(commit=False)
menu_id.tournament = Tournament.objects.get(pk=1)
menu_id.save() # I get the integrity error only here
return HttpResponseRedirect(reverse('admin'))
return render(request, "view.html", {'form': form, 'formset': formset, "tournament": tournament})
My problem is that when I call the .is_valid() function on this form the uniqueness condition cannot be checked as the tournament field is not set. As a result I get an integrity error when calling the save function in the view.
The question is : how can link the Menu instance created by the form to add the tournament field before checking if it's valid? If it's not the right way of doing it, how can I check the uniqueness of the model instance and return the corresponding errors to the template when needed?
I tried including the tournament field as hidden field in the view, it works but I don't know if that's the best way of doing it...
You should simply instantiate the form with an unsaved instance of Menu so your view should be like:
def add_menu(request, tournament_slug):
tournament = get_object_or_404(Tournament, slug=tournament_slug)
if request.method == 'POST':
form = MenuForm(request.POST, instance=Menu(tournament=tournament))
if form.is_valid():
menu_id = form.save()
return HttpResponseRedirect(reverse('admin'))
else:
form = MenuForm(instance=Menu(tournament=tournament))
return render(request, "view.html", {'form': form, "tournament": tournament})
Also the form calls _get_validation_exclusions() and excludes fields not present in the form from validation. You can try to override validate_unique to overcome this:
class MenuForm(forms.ModelForm):
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
exclude = ['tournament']
def validate_unique(self):
exclude = self._get_validation_exclusions()
if 'tournament' in exclude:
exclude.remove('tournament') # Make sure `tournament` gets validated
try:
self.instance.validate_unique(exclude=exclude)
except ValidationError as e:
self._update_errors(e)
Note: I changed your view structure to avoid using MenuForm(request.POST or None) which is an antipattern. (Forms
can be valid even if nothing is sent in the POST data, with the way
you write such forms would be considered invalid).
Edit: As discussed in the comments perhaps the option of a hidden and disabled field is much better than overriding the forms validate_unique method:
class MenuForm(forms.ModelForm):
tournament = forms.ModelChoiceField(
queryset=Tournament.objects.all(),
widget=forms.HiddenInput(),
disabled=True
)
date_menu = forms.DateField(initial=datetime.datetime.now())
class Meta:
model = Menu
fields = ['tournament', 'name', 'date_menu']

How to redirect from FormView to a ListView after form validation

In my Django application, I would like to display a form where a user enters a name to search for Persons from an external data source (not model). I am using class-based generic views and have a working application (code attached below) with a minor inconvenience - I would like to see if there is a better way to do this.
First, here's how I have done it:
I have a Form with 3 fields (first, second and last name) and a clean() where I check if at least one field is populated
A FormView which renders this form, and form_valid() method which does nothing at the moment (reasons will become clear shortly)
An entry in urls.py to render this view and display the form in a template. The form is being submitted to a ListView with GET, and not the FormView itself (sad!)
A ListView where I define get_queryset(self) because data comes from an external source, not a Model; and I use self.request.GET.get('first_name', '') etc. to retrieve query string values, make a connection to the external data source and get a Pandas dataframe which I convert to a list of records and render, paginated, in the template.
An entry in urls.py for the List View and render the template of search results.
Everything works but, hopefully, the problem is apparent. The FormView is being used only to display the form, but the form submits to the ListView where data is retrieved to be displayed. This means that my form_valid() in the FormView and consequently clean() from the form aren't even used - I can work around it by using Javascript based validation but I would like to be able to use the FormView to its full potential.
So, how do I redirect to ListView with the form input, after form validation?
Here's my simple, and working, code:
urls.py
...
path('search/name', form_views.NameSearchView.as_view(), name='search-name'),
path('search/results', list_views.SearchResultsList.as_view(), name='search-results'),
...
forms.py
class NameSearchForm(forms.Form):
last_name = forms.CharField(label='Last Name', required=False)
first_name = forms.CharField(label='First Name', required=False)
second_name = forms.CharField(label='Second Name', required=False)
def clean(self):
cleaned_data = super().clean()
first_name = cleaned_data['first_name'].strip()
second_name = cleaned_data['second_name'].strip()
last_name = cleaned_data['last_name'].strip()
# At least one field should be filled to search
if not (first_name or second_name or last_name):
raise ValidationError(
_('Fill in a name to search!'),
code='invalid',
params={
'first_name': first_name,
'second_name': second_name,
'last_name': last_name,
})
form_views.py
class NameSearchView(FormView):
template_name = 'app/search_name.html'
form_class = NameSearchForm
# This is clearly wrong as it does nothing
success_url = '.'
def form_valid(self, form):
self.form = form
return HttpResponseRedirect(self.get_success_url())
list_views.py
class SearchResultsList(ListView):
template_name = 'app/search_results.html'
paginate_by = 10
def get_queryset(self):
first_name = self.request.GET.get('first_name', '').strip()
second_name = self.request.GET.get('second_name', '').strip()
last_name = self.request.GET.get('last_name', '').strip()
conn = create_connection() # This abstraction creates a database connection
query = '''
SELECT i.first_name first_name,
i.second_name second_name,
i.last_name last_name,
i.info
FROM db.person_tbl i
WHERE i.name_type = 'primary'
'''
params = []
terms = [first_name, second_name, last_name]
term_keys = ['first', 'sec', 'last']
for i, term in enumerate(terms):
if term:
query += f' AND i.srch_{term_keys[i]}_name = ?'
params.append(term)
# this abstraction gets the pandas dataframe
object_list = conn.get_data(query, params=params)\
.sort_values(by='last_name')\
.to_dict('records')
return object_list
I would like to be able to submit POST to the FormView itself, validate the form and then display the search results, paginated, through a ListView.
Any help would be greatly appreciated!

Can a django formset that dynamically adds fields have persistent data?

I am making a formset in python/django and need to dynamically add more fields to a formset as a button is clicked. The form I'm working on is for my school asking students who they would like to disclose certain academic information to, and the button here allows them to add more fields for entering family members/people they want to disclose to.
I have the button working to the point where the extra fields show up, and you can add as many as you like. Problem is, the data that was previously entered into the already existing fields gets deleted. However, only the things in the formset get deleted. Everything else that was filled out earlier in the form stays persistent.
Is there any way to make the formset keep the data that was entered before the button was pressed?
form.py:
from django import forms
from models import Form, ParentForm, Contact
from django.core.exceptions import ValidationError
def fff (value):
if value == "":
raise ValidationError(message = 'Must choose a relation', code="a")
# Create your forms here.
class ModelForm(forms.ModelForm):
class Meta:
model = Form
exclude = ('name', 'Relation',)
class Parent(forms.Form):
name = forms.CharField()
CHOICES3 = (
("", '-------'),
("MOM", 'Mother'),
("DAD", 'Father'),
("GRAN", 'Grandparent'),
("BRO", 'Brother'),
("SIS", 'Sister'),
("AUNT", 'Aunt'),
("UNC", 'Uncle'),
("HUSB", 'Husband'),
("FRIE", 'Friend'),
("OTHE", 'Other'),
("STEP", 'Stepparent'),
)
Relation = forms.ChoiceField(required = False, widget = forms.Select, choices = CHOICES3, validators = [fff])
models.py
from django.db import models
from django import forms
from content.validation import *
from django.forms.models import modelformset_factory
class Contact(models.Model):
name = models.CharField(max_length=100)
class Form(models.Model):
CHOICES1 = (
("ACCEPT", 'I agree with the previous statement.'),
)
CHOICES2 = (
("ACADEMIC", 'Academic Records'),
("FINANCIAL", 'Financial Records'),
("BOTH", 'I would like to share both'),
("NEITHER", 'I would like to share neither'),
("OLD", "I would like to keep my old sharing settings"),
)
Please_accept = models.CharField(choices=CHOICES1, max_length=200)
Which_information_would_you_like_to_share = models.CharField(choices=CHOICES2, max_length=2000)
Full_Name_of_Student = models.CharField(max_length=100)
Carthage_ID_Number = models.IntegerField(max_length=7)
I_agree_the_above_information_is_correct_and_valid = models.BooleanField(validators=[validate_boolean])
Date = models.DateField(auto_now_add=True)
name = models.ManyToManyField(Contact, through="ParentForm")
class ParentForm(models.Model):
student_name = models.ForeignKey(Form)
name = models.ForeignKey(Contact)
CHOICES3 = (
("MOM", 'Mother'),
("DAD", 'Father'),
("GRAN", 'Grandparent'),
("BRO", 'Brother'),
("SIS", 'Sister'),
("AUNT", 'Aunt'),
("UNC", 'Uncle'),
("HUSB", 'Husband'),
("FRIE", 'Friend'),
("OTHE", 'Other'),
("STEP", 'Stepparent'),
)
Relation = models.CharField(choices=CHOICES3, max_length=200)
def __unicode__(self):
return 'name: %r, student_name: %r' % (self.name, self.student_name)
and views.py
from django.shortcuts import render
from django.http import HttpResponse
from form import ModelForm, Parent
from models import Form, ParentForm, Contact
from django.http import HttpResponseRedirect
from django.forms.formsets import formset_factory
def create(request):
ParentFormSet = formset_factory(Parent, extra=1)
if request.POST:
Parent_formset = ParentFormSet(request.POST, prefix='Parent_or_Third_Party_Name')
if 'add' in request.POST:
list=[]
for kitties in Parent_formset:
list.append({'Parent_or_Third_Party_Name-0n-ame': kitties.data['Parent_or_Third_Party_Name-0-name'], 'Parent_or_Third_Party_Name-0-Relation': kitties.data['Parent_or_Third_Party_Name-0-Relation']})
Parent_formset = ParentFormSet(prefix='Parent_or_Third_Party_Name', initial= list)
form = ModelForm(request.POST)
if form.is_valid() and Parent_formset.is_valid():
form_instance = form.save()
for f in Parent_formset:
if f.clean():
(obj, created) = ParentForm.objects.get_or_create(name=f.cleaned_data['name'], Relation=f.cleaned_data['Relation'])
return HttpResponseRedirect('http://Google.com')
else:
form = ModelForm()
Parent_formset = ParentFormSet(prefix='Parent_or_Third_Party_Name')
return render(request, 'content/design.html', {'form': form, 'Parent_formset': Parent_formset})
def submitted(request):
return render(request, 'content/design.html')
Thank you in advance!
I've had trouble with dynamically adding fields in Django before and this stackoverflow question helped me:
dynamically add field to a form
To be honest, I'm not entirely sure what you mean by "persistent" in your case - are the values of your forms being removed as you add inputs? Are you sure it isn't something with your JS?
A coworker of mine finally figured it out. Here is the revised views.py:
from django.shortcuts import render
from django.http import HttpResponse
from form import ModelForm, Parent
from models import Form, ParentForm, Contact
from django.http import HttpResponseRedirect
from django.forms.formsets import formset_factory
def create(request):
ParentFormSet = formset_factory(Parent, extra=1)
boolean = False
if request.POST:
Parent_formset = ParentFormSet(request.POST, prefix='Parent_or_Third_Party_Name')
if 'add' in request.POST:
boolean = True
list=[]
for i in range(0,int(Parent_formset.data['Parent_or_Third_Party_Name-TOTAL_FORMS'])):
list.append({'name': Parent_formset.data['Parent_or_Third_Party_Name-%s-name' % (i)], 'Relation': Parent_formset.data['Parent_or_Third_Party_Name-%s-Relation' % (i)]})
Parent_formset = ParentFormSet(prefix='Parent_or_Third_Party_Name', initial= list)
form = ModelForm(request.POST)
if form.is_valid() and Parent_formset.is_valid():
form_instance = form.save()
for f in Parent_formset:
if f.clean():
(contobj, created) = Contact.objects.get_or_create(name=f.cleaned_data['name'])
(obj, created) = ParentForm.objects.get_or_create(student_name=form_instance, name=contobj, Relation=f.cleaned_data['Relation'])
return HttpResponseRedirect('http://Google.com')
else:
form = ModelForm()
Parent_formset = ParentFormSet(prefix='Parent_or_Third_Party_Name')
return render(request, 'content/design.html', {'form': form, 'Parent_formset': Parent_formset, 'boolean':boolean})
def submitted(request):
return render(request, 'content/design.html')
Thank you for your input, those of you who answered :)
I was once trying to do something like this, and was directed to django-crispy-forms by a man much wiser than I. I never finished the project so I can't offer more help than that, but it could be a starting point.
If your formset does not show the input you made before that means it does not see model's queryset. Add queryset to formset arguments to resolve this. For example:
formset = SomeModelFormset(queryset=SomeModel.objects.filter(arg_x=x))

Django initial value of choicefield

I'm having a strange problem where I can't seem to set the initial value of one of the fields in my forms in django.
My model field is:
section = models.CharField(max_length=255, choices=(('Application', 'Application'),('Properly Made', 'Properly Made'), ('Changes Application', 'Changes Application'), ('Changes Approval', 'Changes Approval'), ('Changes Withdrawal', 'Changes Withdrawal'), ('Changes Extension', 'Changes Extension')))
My form code is:
class FeeChargeForm(forms.ModelForm):
class Meta:
model = FeeCharge
# exclude = [] # uncomment this line and specify any field to exclude it from the form
def __init__(self, *args, **kwargs):
super(FeeChargeForm, self).__init__(*args, **kwargs)
self.fields['received_date'] = forms.DateField(('%d/%m/%Y',), widget=forms.DateTimeInput(format='%d/%m/%Y', attrs={'class': 'date'}))
self.fields['comments'].widget.attrs['class']='html'
self.fields['infrastructure_comments'].widget.attrs['class']='html'
My view code is:
form = FeeChargeForm(request.POST or None)
form.fields['section'].initial = section
Where section is a url var passed to the function. I've tried:
form.fields['section'].initial = [(section,section)]
With no luck either :(
Any ideas what I'm doing wrong or is there a better way to set the default value (before a form submit) of this choice field from a url var?
Thanks in advance!
Update: It seems to be something to do with the URL variable.. If I use:
form.fields['section'].initial = "Changes Approval"
It works np.. If I HttpResponse(section) it's outputs correctly tho.
The problem is use request.POST and initial={'section': section_instance.id}) together. This happens because the values of request.POST always override the values of parameter initial, so we have to put it separated. My solution was to use this way.
In views.py:
if request.method == "POST":
form=FeeChargeForm(request.POST)
else:
form=FeeChargeForm()
In forms.py:
class FeeChargeForm(ModelForm):
section_instance = ... #get instance desired from Model
name= ModelChoiceField(queryset=OtherModel.objects.all(), initial={'section': section_instance.id})
---------- or ----------
In views.py:
if request.method == "POST":
form=FeeChargeForm(request.POST)
else:
section_instance = ... #get instance desired from Model
form=FeeChargeForm(initial={'section': section_instance.id})
In forms.py:
class FeeChargeForm(ModelForm):
name= ModelChoiceField(queryset=OtherModel.objects.all())
UPDATE
Try escaping your url. The following SO answer and article should be helpful:
How to percent-encode URL parameters in Python?
http://www.saltycrane.com/blog/2008/10/how-escape-percent-encode-url-python/
Try setting the initial value for that field as follows and see if that works:
form = FeeChargeForm(initial={'section': section})
I assume you're going to be doing a lot of other things when the user posts the form, so you could separate the POST form from the standard form using something like:
if request.method == 'POST':
form = FeeChargeForm(request.POST)
form = FeeChargeForm(initial={'section': section})

Django set default form values

I have a Model as follows:
class TankJournal(models.Model):
user = models.ForeignKey(User)
tank = models.ForeignKey(TankProfile)
ts = models.IntegerField(max_length=15)
title = models.CharField(max_length=50)
body = models.TextField()
I also have a model form for the above model as follows:
class JournalForm(ModelForm):
tank = forms.IntegerField(widget=forms.HiddenInput())
class Meta:
model = TankJournal
exclude = ('user','ts')
I want to know how to set the default value for that tank hidden field. Here is my function to show/save the form so far:
def addJournal(request, id=0):
if not request.user.is_authenticated():
return HttpResponseRedirect('/')
# checking if they own the tank
from django.contrib.auth.models import User
user = User.objects.get(pk=request.session['id'])
if request.method == 'POST':
form = JournalForm(request.POST)
if form.is_valid():
obj = form.save(commit=False)
# setting the user and ts
from time import time
obj.ts = int(time())
obj.user = user
obj.tank = TankProfile.objects.get(pk=form.cleaned_data['tank_id'])
# saving the test
obj.save()
else:
form = JournalForm()
try:
tank = TankProfile.objects.get(user=user, id=id)
except TankProfile.DoesNotExist:
return HttpResponseRedirect('/error/')
You can use Form.initial, which is explained here.
You have two options either populate the value when calling form constructor:
form = JournalForm(initial={'tank': 123})
or set the value in the form definition:
tank = forms.IntegerField(widget=forms.HiddenInput(), initial=123)
Other solution: Set initial after creating the form:
form.fields['tank'].initial = 123
If you are creating modelform from POST values initial can be assigned this way:
form = SomeModelForm(request.POST, initial={"option": "10"})
https://docs.djangoproject.com/en/1.10/topics/forms/modelforms/#providing-initial-values
I had this other solution (I'm posting it in case someone else as me is using the following method from the model):
class onlyUserIsActiveField(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(onlyUserIsActiveField, self).__init__(*args, **kwargs)
self.fields['is_active'].initial = False
class Meta:
model = User
fields = ['is_active']
labels = {'is_active': 'Is Active'}
widgets = {
'is_active': forms.CheckboxInput( attrs={
'class': 'form-control bootstrap-switch',
'data-size': 'mini',
'data-on-color': 'success',
'data-on-text': 'Active',
'data-off-color': 'danger',
'data-off-text': 'Inactive',
'name': 'is_active',
})
}
The initial is definded on the __init__ function as self.fields['is_active'].initial = False
As explained in Django docs, initial is not default.
The initial value of a field is intended to be displayed in an HTML . But if the user delete this value, and finally send back a blank value for this field, the initial value is lost. So you do not obtain what is expected by a default behaviour.
The default behaviour is : the value that validation process will take if data argument do not contain any value for the field.
To implement that, a straightforward way is to combine initial and clean_<field>():
class JournalForm(ModelForm):
tank = forms.IntegerField(widget=forms.HiddenInput(), initial=123)
(...)
def clean_tank(self):
if not self['tank'].html_name in self.data:
return self.fields['tank'].initial
return self.cleaned_data['tank']
If you want to add initial value and post other value you have to add the following :
or None after request.POST
form = JournalForm(request.POST or None,initial={'tank': 123})
If you want to add files or images also
form = JournalForm(request.POST or None,request.FILES or None,initial={'tank': 123})
I hope this can help you:
form.instance.updatedby = form.cleaned_data['updatedby'] = request.user.id
I also encountered the need to set default values in the form during development. My solution is
initial={"":""}
form=ArticleModel(request.POST)
if form.has_changed():
data = {i: form.cleaned_data[i] for i in form.changed_data}
data.update({key: val for key, val in init_praram.items() if key not in form.changed_data})
use form.has_changed ,if form.fields is required you can use this method
How I added the initial to the form:
I read #Sergey Golovchenko answer.
So I just added it to the form in if request.method == 'POST':.
But that's not where you place it, if you want to see what value it got before posting the form.
You need to put it in the form where the else is.
Example here from views.py
def myForm(request):
kontext = {}
if request.method == 'POST':
# You might want to use clean_data instead of initial here. I found something on a stack overflow question, and you add clean data to the Forms.py, if you want to change the post data. https://stackoverflow.com/questions/36711229/django-forms-clean-data
form = myModelForm(request.POST, initial={'user': request.user})
if form.is_valid():
form.save()
return redirect('/')
else:
# you need to put initial here, if you want to see the value before you post it
form = myModelForm(initial={'user': request.user})
kontext['form'] = form
return render(request, 'app1/my_form.html', kontext)

Categories