14

I am working on adding more functionality to the polls app that is made in the official Django tutorial. One of the things I am working on is making Polls/Choices creatable by logged in users (instead of in an admin screen, where the tutorial leaves us).

I am looking to create a view where a user can create the a Poll, and then as well include some Choices to associate to the Poll. The Django Admin automagically does it, and I am not sure how to go about writing this out in a view.

To start with, these are my relevant files:

models.py

import datetime

from django.db import models
from django.utils import timezone


class Poll(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __unicode__(self):
        return self.question_text

    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

    was_published_recently.admin_order_field = 'pub_date'
    was_published_recently.boolean = True
    was_published_recently.short_description = 'Published recently?'

class Choice(models.Model):
    question = models.ForeignKey(Poll)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __unicode__(self):
        return self.choice_text

forms.py

from django import forms
from .models import Poll, Choice
from datetime import datetime

class PollForm(forms.ModelForm):
    question_text = forms.CharField(max_length=200, help_text="Please enter the question.")
    pub_date = forms.DateTimeField(widget=forms.HiddenInput(), initial = datetime.now())

    class Meta:
        model = Poll
        fields = ("__all__")

class ChoiceForm(forms.ModelForm):
    choice_text = forms.CharField(max_length=200, help_text="Please enter choices.")
    votes = forms.IntegerField(widget=forms.HiddenInput(),initial=0)
    exclude = ('poll',)

views.py

def add_poll(request):
    # A HTTP POST?
    if request.method == 'POST':
        form = PollForm(request.POST)

        # Have we been provided with a valid form?
        if form.is_valid():
            # Save the new category to the database.
            form.save(commit=True)

            # Now call the index() view.
            # The user will be shown the homepage.
            return render(request, 'polls/index.html', {})
        else:
            # The supplied form contained errors - just print them to the terminal.
            print form.errors
    else:
        # If the request was not a POST, display the form to enter details.
        form = PollForm()

    # Bad form (or form details), no form supplied...
    # Render the form with error messages (if any).
    return render(request, 'polls/add_poll.html', {'form': form})

Currently, my view allows me a user to add a poll. I am just not sure how to go about adapting it to pass the entered text as the Poll model's question_text, to the Choice model, and in turn, the ChoiceForm.

user202729
  • 3,358
  • 3
  • 25
  • 36
ploo
  • 667
  • 3
  • 12
  • 26

1 Answers1

39

Formsets are the way to do it in django.

First add default value for Poll.pub_date field:

class Poll(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published', default=timezone.now)

Then make forms a bit simpler:

class PollForm(forms.ModelForm):
    class Meta:
        model = Poll
        fields = ('question_text', )

class ChoiceForm(forms.ModelForm):
    class Meta:
        model = Choice
        fields = ('choice_text',)

Add formset support to your view:

from django.forms.formsets import formset_factory

def add_poll(request):
    ChoiceFormSet = formset_factory(ChoiceForm, extra=3,
                                    min_num=2, validate_min=True)
    if request.method == 'POST':
        form = PollForm(request.POST)
        formset = ChoiceFormSet(request.POST)
        if all([form.is_valid(), formset.is_valid()]):
            poll = form.save()
            for inline_form in formset:
                if inline_form.cleaned_data:
                    choice = inline_form.save(commit=False)
                    choice.question = poll
                    choice.save()
            return render(request, 'polls/index.html', {})
    else:
        form = PollForm()
        formset = ChoiceFormSet()

    return render(request, 'polls/add_poll.html', {'form': form,
                                                   'formset': formset})

And finally your template:

<form method="post">

    {% csrf_token %}

    <table>
        {{ form }}
        {{ formset }}
    </table>

    <button>Add</button>

</form>
Rafael T
  • 15,401
  • 15
  • 83
  • 144
catavaran
  • 44,703
  • 8
  • 98
  • 85
  • 1
    Thank you so much catavaran! That was immensely helpful, I appreciate the time you took there! I did not quite get how formset_factory was used, but I see the value in it, especially with how it handles a lot of logic with attributes like extra and validate_min. I think I need to do a bit more research on inline_form. Thanks again! – ploo Jan 21 '15 at 15:51
  • Is this saving in only one table in database? thanks! – jned29 Dec 05 '16 at 08:57
  • 1
    @jned29 each Form will save to the relevant table of the Model it is associated with. So if the Django app is called "quest", `PollForm` will save its data into `quest_poll` table (1 record updated) while `ChoiceForm` will save its data into `quest_choice` table (multiple records updated due to `FormSet`). – interDist Jan 07 '18 at 19:52
  • I have 1 model, but I divided it in to 2 ModelForms, because of bootstrap issue. How can I fix this in the views.py? – AnonymousUser Nov 02 '21 at 04:03
  • Super valuable answer, even after so many years! – panos Aug 25 '23 at 12:41