creating or editing multiple objects of the same type in a single HTML form • Can be used for regular Forms, but most useful for ModelForms • When used with Models, ModelFormSets can edit or create related objects • For example, adding multiple players to a team • When using with Models, FormSets are defined using a “factory” method • Factories (in this use) dynamically construct classes that can be instantiated in a view • Necessary because ModelFormSets have 2 initialization processes • Creating the ModelFormSet class requires specifying the Model to be used • Instantiating the ModelFormSet class requires specifying the collection of instances to be used 3
creation factories • modelformset_factory - used to manage a collection of objects of the same type that aren’t related to each other • inlineformset_factory - used to manage a collection of objects that all share a ForeignKey relationship with specific related object • Create a ModelFormSet to manage players associated with Teams: 4 from django.forms.models import modelformset_factory AuthorFormSet = modelformset_factory(Author) from django.forms.models import inlineformset_factory BookFormSet = inlineformset_factory(Author, Book) author = Author.objects.get(name=u'Mike Royko') formset = BookFormSet(instance=author) TeamPlayerFormSet = inlineformset_factory( models.Team, models.Player, extra=20, max_num=20)
like ModelForms, except they are a collection of Forms instead of a single one • To use a ModelFormSet in a view, use it (in conjunction with) just as a ModelForm, with a GET and POST component • ModelFormSets have an is_valid() method to check to see if the data is complete for all Models • ModelFormSets have a save() method to save the data to the database and return the collection of instances • When instantiating a ModelFormSet, specify extra and max_num parameters to provide a larger list of available entries (default is 3) • Most important difference is to provide a queryset instead of an instance to the ModelFormSet • ModelFormSets support adding and deleting instances as well automatically (as long as the template is rendered properly) 5
the related object that is the ForeignKey for the collection of Model instances • In many cases, an InlineFormSet is used for editing instead of creating • In create view, redirect to edit view on success to enter FormSet data • When using an InlineFormSet, queryset is provided automatically via ForeignKey field to related instance • Any existing related objects will be included for editing • Any additional rows of data entered will be automatically assigned the related object as the ForeignKey 6
is usually as a table, with one ModelForm per row • Properly rendering a FormSet includes: • Rendering FormSet management_form • Iterating over each form in the formset and for each: • Rendering a row of fields • Rendering errors for each form and for each form field • Rendering hidden fields • Rendering form.id to enable Django to track which rows represent which instances • Rendering DELETE field • Rendering any supplemental non-form data for each form 8
may be necessary to customize fields in the Forms • Because Forms are created by FormSets, which are themselves wrapped in factories, customizing Forms and FormSets can seem complicated • FormSet factories provide a way to specify customized Form and FormSet classes when using the factory method • FormSet factories also enable configuration of fields to use from the specified Models • Customizing GameStatisticForm and GameStatisticFormSet allows Player choice ModelField to be customized based on FormSet Team instance 10 GameStatisticModelFormSet = inlineformset_factory( parent_model=models.Roster, model=models.Statistic, form=GameStatisticForm, formset=GameStatisticFormSet, extra=15, max_num=20, fields=( 'player', 'at_bats', 'runs', 'rbis', 'walks', 'strikeouts', 'singles', 'doubles', 'triples', 'home_runs', ))
accept a Team to use for Player selection super(StatisticForm, self).__init__(*args, **kwargs) widget_overrides = ( 'at_bats', 'runs', 'singles', 'doubles', 'triples', 'home_runs', 'rbis', 'walks', 'strikeouts', ) # override the class of the widget_overrides fields for s in widget_overrides: self.fields[s].widget.attrs['class'] = \ 'input-mini right' class GameStatisticForm(StatisticForm): def __init__(self, *args, **kwargs): self.team = kwargs.pop('team') super(GameStatisticForm, self).__init__(*args, **kwargs) # use the team to override the players field queryset self.fields['player'].queryset = self.team.players.all()
ModelFormSets in views, Django can get confused about which submitted form fields belong to which FormSet • Forms in each FormSet are identified by form.field.id, but by default, FormSets will use the same field names for all FormSets of the same type • To handle this, use a form prefix argument when instantiating FormSets • Prefixes allow the FormSet rendering to uniquely identify FormSet forms and fields from one another • Be sure to specify the same prefix for GET and POST usage! home_statistic_formset = forms.GameStatisticModelFormSet( request.POST, prefix='home', instance=game.home_roster) away_statistic_formset = forms.GameStatisticModelFormSet( request.POST, prefix='away', instance=game.away_roster)
button from Player List page, Team isn’t filled out • When using “Add Player” button from Team View page, Team isn’t filled out, but should be preselected • Supporting pre-filled form fields can be handled by using GET URL parameters • On “Add Player” button on Team View page, add team_id parameter to URL 15 <a class="btn" href="{% url player_create %}? team_id={{ team.id }}">Add Player</a>
user clicks “Cancel” on a form, there should always be a default page to which they are taken • For Player Create/Edit pages, this should be Player List page • If there’s a second path to enter the create or edit workflow, it may make sense to support returning the user to a different page • For “Add Player” from Team View page, user should return to Team View instead of Player List on success or cancel • On “Add Player” button on Team View page, add next parameter: • On “Cancel” button on Add Player page, specify different URL: 16 <a class="btn" href="{% url player_create %}? team_id={{ team.id }}&next={% url team_view team_id=team.id %}"> Add Player</a> <a class="btn" href="{% if next_url %}{{ next_url }}{% else %}{% url player_list %}{% endif %}">Cancel</a>
and “team_id” GET parameters in player_create view: 17 def player_create(request): # if there's a team_id specified, use that team as the preset team for # this player team = models.Team() if 'team_id' in request.GET: try: team = models.Team.objects.get(pk=request.GET['team_id']) except models.Team.DoesNotExist, e: # Team doesn't exist, so just use an empty team pass player = models.Player(team=team) # if provided get the "next" url to send the user to on success or cancel next_url = request.GET.get('next') if request.method == 'POST': player_form = forms.PlayerForm(request.POST, instance=player) if player_form.is_valid(): player = player_form.save() messages.success(request, 'Player {0} created'.format(player)) if next_url: return redirect(next_url) return redirect('player_view', player_id=player.id) else: player_form = forms.PlayerForm(instance=player) return TemplateResponse(request, 'softball/player/create.html', { 'player': player, 'player_form': player_form, 'next_url': next_url, })