Django inlineformsetfactory - What is it good for? - django

Sorry for a newbie question but...
Can someone shed some light on what is the use case for inlineformset_factory?
I have followed example from Django documentation:
#Models
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
author = models.ForeignKey(Author)
title = models.CharField(max_length=100)
#View
def jojo(request):
BookFormSet = inlineformset_factory(Author, Book)
author = Author.objects.get(name=u'Mike Royko')
formset = BookFormSet(instance=author)
return render_to_response('jojo.html', {
'formset': formset,
})
#jojo.html
<form action="" method="POST">
<table>
{{ formset }}
</table>
<input type="submit" value="Submit" />
</form>
But it only displays book fields.
My understanding was that formset would display Book form with inline Author form just
like Django Admin. On top of that I can't easily pass initial values to formset?
Then how is it better then using two separate AuthorForm and BookForm?
Or am i missing something obvious?

inlineformset_factory only provides multiple forms for the nested elements, you need a separate form at the top if you want a form for the main model.
Here is an example of a working inlineformset_factory with the main form embedded at the top:
views.py
from django.shortcuts import get_object_or_404, render_to_response
from django.forms.models import inlineformset_factory
from django.http import HttpResponseRedirect
from django.template import RequestContext
from App_name.models import * #E.g. Main, Nested, MainForm, etc.
. . .
#login_required
def Some_view(request, main_id=None, redirect_notice=None):
#login stuff . . .
c = {}
c.update(csrf(request))
c.update({'redirect_notice':redirect_notice})#Redirect notice is an optional argument I use to send user certain notifications, unrelated to this inlineformset_factory example, but useful.
#Intialization --- The start of the view specific functions
NestedFormset = inlineformset_factory(Main, Nested, can_delete=False, )
main = None
if main_id :
main = Main.objects.get(id=main_id)#get_object_or_404 is also an option
# Save new/edited Forms
if request.method == 'POST':
main_form = MainForm(request.POST, instance=main, prefix='mains')
formset = NestedFormset(request.POST, request.FILES, instance=main, prefix='nesteds')
if main_form.is_valid() and formset.is_valid():
r = main_form.save(commit=False)
#do stuff, e.g. setting any values excluded in the MainForm
formset.save()
r.save()
return HttpResponseRedirect('/Home_url/')
else:
main_form = MainForm(instance=main, prefix='mains') #initial can be used in the MainForm here like normal.
formset = NestedFormset(instance=main, prefix='nesteds')
c.update({'main_form':main_form, 'formset':formset, 'realm':realm, 'main_id':main_id})
return render_to_response('App_name/Main_nesteds.html', c, context_instance=RequestContext(request))
template.html
{% if main_form %}
<form action="." method="POST">{% csrf_token %}
{{ formset.management_form }}
<table>
{{main_form.as_table}}
{% for form in formset.forms %}
<table>{{ form }}</table>
{% endfor %}
</table>
<p><input type="submit" name="submit" value="Submit" class="button"></p>
</form>
{% endif %}

The beauty of the inlineformset_factory (and modelformset_factory) is the ability to create multiple model instances from a single form. If you were to simply 'use two separate forms' the id's of the form's fields would trample each other.
The formset_factory functions know how many extra form(sets) you need (via the extra argument) and sets the id's of the fields accordingly.

inlineformset_factory creates a list of forms.
This can be used when the same form needs to be repeated at the page, for example:
Upload multiple photo's with a description.
Invite multiple members by email
Fill a calendar grid per hour
Fill a list of books for the author.
With some JavaScript code, you can add a "add another row" functionality as well.

Related

Bind dynamic choices to ModelForm in Django

I'm trying to bind a dynamic list of choices to a ModelForm. The form is rendered correctly. However, when using the form with a POST Request, I get an empty form back. My goal is to save that form into the database (form.save()). Any help would be much appreciated.
Model
I'm using a multiple choice select field ( https://github.com/goinnn/django-multiselectfield )
from django.db import models
from multiselectfield import MultiSelectField
class VizInfoModel(models.Model):
tog = MultiSelectField()
vis = MultiSelectField()
Forms
class VizInfoForm(forms.ModelForm):
class Meta:
model = VizInfoModel
fields = '__all__'
def __init__(self,choice,*args,**kwargs):
super(VizInfoForm, self).__init__(*args,**kwargs)
self.fields['tog'].choices = choice
self.fields['vis'].choices = choice
View
Choices are passed from the view when instantiating the form.
def viz_details(request):
options = []
headers = request.session['headers']
for header in headers :
options.append((header, header))
if request.method == 'POST':
form = VizInfoForm(options, request.POST)
#doesnt' get into the if statement since form is empty!
#choices are not bounded to the model although the form is perfectly rendered
if form.is_valid():
form.save()
return HttpResponseRedirect('/upload')
else:
#this works just fine
form = VizInfoForm(options)
return render(request, 'uploads/details.html', {'form': form})
Template
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<p>Choose variables to toggle between</p>
{{ form.tog }}
<br></br>
<p>Choose variable to be visualized</p>
{{ form.vis }}
<br></br>
<button type="submit">Submit</button>
</form>
You're saying Django doesn't get into your if request.method == 'POST' block.
This tells us that you're not sending your request through the POST method. Your template probably has an error in it, maybe you haven't specified the method on your form, or you made your button to just be a link instead of a submit ?
Show your template so we can say more, unless this was enough to solve your question !

Pagination in Django TemplateView

I have a template where I want to display a large list of data, (received from external API rather than DB).
Although I'm aware this is easily done is ListView, however as I'm not pulling data from the database, TemplateView seems the best choice but what would the best way to display the list of data and paginate it?
Currently I have:
View
class QuotesResultsView(TemplateView):
template_name = 'site/quotes.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['quotes'] = self.request.session['quotes']['data']
return context
Template:
....
<div class="about">
{% for quote in quotes %}
<h3>Supplier:{{ quote.supplierName }}</h3>
<div>
<p>Annual Cost: {{ quote.newSpend }}</p>
<p>Savings: {{ quote.newSavings }}</p>
</div>
<button class="btn btn-cta-primary">Proceed</button>
{% endfor %}
</div><!--//about-->
You still can inherit ListView and override get_queryset() method, which must return an iterable. This will enable you to use pagination as usually
class QuotesResultsView(ListView):
template_name = 'site/quotes.html'
paginate_by = settings.QUOTES_PER_PAGE
context_object_name = 'quotes'
def get_queryset(self):
# Looks like your data is already an iterable
# if not convert it to iterable and return
return self.request.session['quotes']['data']
Don't forget to set QUOTES_PER_PAGE in your settings and import it in your views.py
Then in template your can use standard pagination snippet from docs.

Render Django formset as array

I have a model and i need to create form with multiple instances in it. To be more specific: i need to render my ModelForm inside regular form with square brackets next to it's fields names. Something like this in magicworld:
forms.py
class ManForm(ModelForm):
class Meta:
model = Man
fields = ['name', 'age']
class PeopleForm(forms.Form):
# modelless form
people = ??? # array of ManForm instances or something
form.html
<form action="/people/create/">
{{ form }}
</form>
output
<form action="/people/create/">
<input type="text" name="name[0]"/>
<input type="text" name="age[0]"/>
</form>
To tell you the truth, i don't know how to approach this problem at all. I tried modelformset_factory, but all i've got is <input type="text" name="form-0-name"/>
As discussed in the comments, you need a formset.
def create_people(request):
PeopleFormSet = modelformset_factory(Man, form=ManForm)
if request.method == 'POST':
formset = PeopleFormSet(request.POST)
if formset.is_valid():
for form in formset:
... do something with individual form
else:
formset = PeopleFormSet()
return render(request, template_name, {'formset': formset}
For using formsets in function based views see #Daniel Roseman 's answer or read up here.
For class based views there is no built in generic view for this. According to this ticket they decided to let third-party-packages handle that. You can use django-extra-views for that.

Django filter ModelFormSet field choices... different from limiting the Formset's queryset

I understand that it is possible to override the default queryset 'used' by the modelformset. This just limits the objects for which a form is created.
I also found a Stack Overflow question about filtering ForeignKey choices in a Django ModelForm, but not a ModelForm Set and about limiting available choices in a Django formset, but not a Model FormSet. I have included my version of this code below.
What I want to do is render a ModelFormSet, for a school class ('teachinggroup' or 'theclass' to avoid clashing with the 'class' keyword) with one field limited by a queryset. This is for a teacher's class-editing form, to be able to reassign pupils to a different class, but limited to classes within the same cohort.
My models.py
class YearGroup(models.Model):
intake_year = models.IntegerField(unique=True)
year_group = models.IntegerField(unique=True, default=7)
def __unicode__(self):
return u'%s (%s intake)' % (self.year_group, self.intake_year)
class Meta:
ordering = ['year_group']
class TeachingGroup(models.Model):
year = models.ForeignKey(YearGroup)
teachers = models.ManyToManyField(Teacher)
name = models.CharField(max_length=10)
targetlevel = models.IntegerField()
def __unicode__(self):
return u'Y%s %s' % (self.year.year_group, self.name)
class Meta:
ordering = ['year', 'name']
My views.py
def edit_pupils(request, teachinggroup):
theclass = TeachingGroup.objects.get(name__iexact = teachinggroup)
pupils = theclass.pupil_set.all()
PupilModelFormSet = modelformset_factory(Pupil)
classes_by_year = theclass.year.teachinggroup_set.all()
choices = [t for t in classes_by_year]
# choices = [t.name for t in classes_by_year] #### I also tried this
if request.method == 'POST':
formset = PupilModelFormSet(request.POST,queryset=pupils)
if formset.is_valid():
formset.save()
return redirect(display_class_list, teachinggroup = teachinggroup)
else:
formset = PupilModelFormSet(queryset=pupils)
for form in formset:
for field in form:
if 'Teaching group' == field.label:
field.choices = choices
return render_to_response('reassign_pupils.html', locals())
As you can see, I am limiting the choices to the queryset classes_by_year, which is only classes which belong to the same year group. This queryset comes out correctly, as you can see in the rendered page below, but it doesn't affect the form field at all.
My template
{% for form in formset %}
<tr>
{% for field in form.visible_fields %}
<td> {# Include the hidden fields in the form #}
{% if forloop.first %}
{% for hidden in form.hidden_fields %}
{{ hidden }}
{% endfor %}
{% endif %}
<p><span class="bigtable">{{ field }}</span>
{% if field.errors %}
<p><div class="alert-message error">
{{field.errors|striptags}}</p>
</div>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</table>
<input type="submit" value="Submit changes"></p>
</form>
{{ choices }} <!-- included for debugging -->
The page renders with all teaching groups (classes) visible in the select widget, but the tag at the bottom of the page renders as: [<TeachingGroup: Y8 82Ma2>, <TeachingGroup: Y8 82Ma3>], accurately showing only the two classes in Year 8.
Note that I've also read through James Bennett's post So you want a dynamic form as recommended by How can I limit the available choices for a foreign key field in a django modelformset?, but that involves modifying the __init__ method in forms.py, and yet the only way I know how to create a ModelFormSet is with modelformset_factory, which doesn't involve defining any classes in forms.py.
Further to help from Luke Sneeringer, here is my new forms.py entry. After reading Why do I get an object is not iterable error? I realised that some of my problems came from giving a tuple to the field.choices method, when it was expecting a dictionary. I used the .queryset approach instead, and it works fine:
class PupilForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PupilForm, self).__init__(*args, **kwargs)
thepupil = self.instance
classes_by_year = thepupil.teaching_group.year.teachinggroup_set.all()
self.fields['teaching_group'].queryset = classes_by_year
class Meta:
model = Pupil
As best as I can tell, you've actually put all the pieces together except one. Here's the final link.
You said you read the dynamic form post, which involves overriding the __init__ method in a forms.Form subclass, which you don't have. But, nothing stops you from having one, and that's where you can override your choices.
Even though modelformset_factory doesn't require an explicit Form class (it constructs one from the model if none is provided), it can take one. Use the form keyword argument:
PupilModelFormset = modelformset_factory(Pupil, form=PupilForm)
Obviously, this requires defining the PupilForm class. I get the impression you already know how to do this, but it should be something like:
from django import forms
class PupilForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(PupilForm, self).__init__(*args, **kwargs)
self.fields['teaching_group'].choices = ______ # code to generate choices here
class Meta:
model = Pupil
The last problem you might have is that a modelformset_factory just takes the class, which means that the constructor will be called with no arguments. If you need to send an argument dynamically, the way to do it is to make a metaclass that generates the form class itself, and call that metaclass in your modelformset_factory call.
You can accomplish this by setting field choices of a form in a formset is in the forms init and overwriting the self.fields['field_name'].choices. This worked fine for me but I needed more logic in my view after the formset was initialized. Here is what works for me in Django 1.6.5:
from django.forms.models import modelformset_factory
user_choices = [(1, 'something'), (2, 'something_else')] # some basic choices
PurchaserChoiceFormSet = modelformset_factory(PurchaserChoice, form=PurchaserChoiceForm, extra=5, max_num=5)
my_formset = PurchaserChoiceFormSet(self.request.POST or None, queryset=worksheet_choices)
# and now for the magical for loop and override each desired fields choices
for choice_form in my_formset:
choice_form.fields['model'].choices = user_choices
I wasn't able to find the answer for this but tried it out and it works in Django 1.6.5. I figured it out since formsets and for loops seem to go so well together :)

How to make a customizable user survey in Django

I am making a system for a company which among other things must hold information about the satisfactory level about various things, I have made it work fine using a fixed model with fixed questions and answers, but I am sure that they will need to change or add questions.
So I want to make a system where users can make custom evaluation schemas that consists of custom questions defined by them. How do I go about making such a design?
Right now my model is this, but wrong:
RATING_CHOICES = ((0, u"Good"), (1, u"Bad"), (2, u"Dunno"),)
class EvaluationScheme(models.Model):
title = models.CharField(max_length=200)
class Evaluation(models.Model):
doctor = models.CharField(max_length=200)
agency = models.CharField(max_length=200)
scheme = models.ForeignKey(EvaluationScheme)
class EvaluationQuestion(models.Model):
question = models.CharField(max_length=200)
evaluation = models.ForeignKey(EvaluationScheme)
def __unicode__(self):
return self.question
class EvaluationAnswer(models.Model):
evaluation = models.ForeignKey(Evaluation)
question = models.ForeignKey(EvaluationQuestion)
answer = models.SmallIntegerField(choices=RATING_CHOICES)
This is sort of what I want, except that the EvaluationScheme is useless, since you still have to chose all questions and answers yourself - it does not display a list of only the questions related to the schema of choice.
I think your models are fine. I used the Django admin to create an EvaluationScheme with EvaluationQuestions, then I created an Evaluation and I was able to answer its questions. Here's the code I used to go with your models:
# forms.py:
from django.forms.models import inlineformset_factory
import models
AnswerFormSet = inlineformset_factory(models.Evaluation,
models.EvaluationAnswer, exclude=('question',),
extra=0, can_delete=False)
# views.py
from django.http import HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
import models, forms
def prepare_blank_answers(evaluation):
for question in evaluation.scheme.evaluationquestion_set.all():
answer = models.EvaluationAnswer(evaluation=evaluation,
question=question)
answer.save()
def answer_form(request, id):
evaluation = get_object_or_404(models.Evaluation, id=id)
if len(evaluation.evaluationanswer_set.all()) == 0:
prepare_blank_answers(evaluation)
if request.method == 'POST':
formset = forms.AnswerFormSet(request.POST, instance=evaluation)
if formset.is_valid():
formset.save()
return HttpResponse('Thank you!')
else:
formset = forms.AnswerFormSet(instance=evaluation)
return render_to_response('answer_form.html',
{'formset':formset, 'evaluation':evaluation})
# answer_form.html:
<html><head></head><body>
Doctor: {{ evaluation.doctor }} <br>
Agency: {{ evaluation.agency }}
<form method="POST">
{{ formset.management_form }}
<table>
{% for form in formset.forms %}
<tr><th colspan="2">{{ form.instance.question }}</th></tr>
{{ form }}
{% endfor %}
</table>
<input type="submit">
</form>
</body></html>
Have you checked django-survey? It's pretty neat.
Django-crowdsourcing is a fork of django-survey that is actively maintained as of 2012 and targets Django 1.2+.
Not a django expert so you might wish to wait for a more experience person to answer but you could try something like:
EvaluationQuestions.objects.filter(evaluationscheme__title="myscheme").select_related()
Could also put the relationships the other way around, depends how you need to access the data.
class EvaluationScheme(models.Model):
title = models.CharField(max_length=200)
evaluations = models.ManyToMany(Evaluation)
questions = models.ManyToMany(EvaluationQuestions)