Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Lesson 18 - FormSets

Lesson 18 - FormSets

Dana Spiegel

December 18, 2012
Tweet

More Decks by Dana Spiegel

Other Decks in Technology

Transcript

  1. Django FormSets • FormSets are collections of Forms that enable

    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
  2. Django FormSets (cont’d.) • For ModelFormSets, there are 2 FormSet

    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)
  3. Using a ModelFormSet in a View • ModelFormSets work just

    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
  4. Using a InlineFormSet in a View • For InlineFormSets, provide

    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
  5. Example: Team Edit with Players 7 def team_edit(request, team_id): try:

    team = models.Team.objects.get(pk=team_id) except models.Team.DoesNotExist: raise Http404 if request.method == 'POST': team_form = forms.TeamForm(request.POST, instance=team) player_formset = forms.TeamPlayerFormSet( request.POST, instance=team) if team_form.is_valid() and player_formset.is_valid(): team = team_form.save() messages.success(request, 'Team {0} updated'.format(team.name)) player_formset.save() return redirect('team_view', team_id=team.id) else: team_form = forms.TeamForm(instance=team) player_formset = forms.TeamPlayerFormSet(instance=team) return TemplateResponse(request, 'softball/team/edit.html', { 'team': team, 'record': team.record(), 'team_form': team_form, 'player_formset': player_formset, })
  6. Using a ModelFormSet in a Template • Presentation of FormSets

    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
  7. Example: Team Edit Template 9 <tbody> {{ player_formset.management_form }} {%

    for form in player_formset %} {% if form.non_field_errors %} <tr class="error"> <td colspan="15"> {{ form.non_field_errors|join:", "|escape }} </td> </tr> {% endif %} <tr class="{% if form.non_field_errors %}error{% endif %}"> <td class="center {% if form.DELETE.errors %}error{% endif %} "> {% for hidden in form.hidden_fields %} {{ hidden }} {% endfor %} {{ form.id }} {{ form.DELETE }} {% if form.DELETE.errors %} <br/> <span class="text-error"> {{ form.DELETE.errors|join:", " }} </span> {% endif %} </td> {% for field in form.visible_fields %} {% if field.name != "DELETE" %} <td class="{{ field.name }} {% if field.errors %}error{% endif %}"> {{ field }} {% if field.errors %} <br/> <span class="text-error"> {{ field.errors|join:", " }} </span> {% endif %} </td> {% endif %} {% endfor %} <td class="right">{{ form.instance.at_bats|default:"-" }}</td> ... </tr> {% endfor %} </tbody>
  8. Customizing Forms and FormSets • When using FormSet factories, it

    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', ))
  9. Example: StatisticForm 11 class StatisticForm(ModelForm): def __init__(self, *args, **kwargs): #

    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()
  10. Example: GameStatisticFormSet 12 class GameStatisticFormSet(BaseInlineFormSet): def __init__(self, *args, **kwargs): super(GameStatisticFormSet,

    self).__init__(*args, **kwargs) if 'queryset' not in kwargs: kwargs['queryset'] = \ self.instance.player_statistics.all() def _construct_form(self, index, **kwargs): # override _construct_form to add self.team to the # StatisticForm upon creation kwargs['team'] = self.instance.team return super(GameStatisticFormSet, self)._construct_form(index, **kwargs) 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', ))
  11. Using Multiple ModelFormSets 13 • When using multiple FormSets or

    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)
  12. Example: game_edit 14 def game_edit(request, game_id): try: game = models.Game.objects.get(pk=game_id)

    except models.Player.DoesNotExist: raise Http404 initial = { 'home_team': game.home_roster.team_id, 'away_team': game.away_roster.team_id, } if request.method == 'POST': game_form = forms.GameForm(request.POST, instance=game, initial=initial) 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) if game_form.is_valid() and home_statistic_formset.is_valid() and away_statistic_formset.is_valid(): game = game_form.save() # if home_team or away_team were changed, clear the statistics if 'home_team' in game_form.changed_data: game.home_roster.team = game_form.cleaned_data['home_team'] game.home_roster.save() game.home_roster.player_statistics.all().delete() else: home_statistic_formset.save() if 'away_team' in game_form.changed_data: game.away_roster.team = game_form.cleaned_data['away_team'] game.away_roster.save() game.away_roster.player_statistics.all().delete() else: away_statistic_formset.save() if 'home_team' in game_form.changed_data or 'away_team' in game_form.changed_data: return redirect('game_edit', game_id=game.id) messages.success(request, 'Game {0} updated'.format(game)) return redirect('game_view', game_id=game.id) else: game_form = forms.GameForm(instance=game, initial=initial) home_statistic_formset = forms.GameStatisticModelFormSet(prefix='home', instance=game.home_roster) away_statistic_formset = forms.GameStatisticModelFormSet(prefix='away', instance=game.away_roster) return TemplateResponse(request, 'softball/game/edit.html', { 'game': game, 'game_form': game_form, 'home_statistic_formset': home_statistic_formset, 'away_statistic_formset': away_statistic_formset, })
  13. Pre-filling Fields for Create Views • When using “Add Player”

    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>
  14. Send User to Other Page from Form • When 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>
  15. Send User to Other Page from Form • Use “next”

    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, })