Mac Chaffee
Mac Chaffee

Reputation: 110

How do I construct a Django form that has a ModelChoiceField?

I'm trying to write a test for my form which uses a custom ModelChoiceField:

from django.forms import ModelChoiceField
class CycleModelChoiceField(ModelChoiceField):
    def label_from_instance(self, cycle):
        return str(cycle.begin_date)

Whatever the user selects in this field needs to be passed into 2 other fields (DateField and radio ChoiceField) by overriding the clean() method. That's complicated logic that I need to test.

So here's what I've tried in my test so far:

self.client.login(username='user', password='1234')
response = self.client.get(reverse('my_form'), follow=True)
cyc = list(CycleDate.objects.all())[0]
form_data = {'type_of_input': '0', 'cycle': cyc,'usage_type': 'E',}
form = EnergyUsageForm(data=form_data, user=response.context['user'])

But the form.is_valid() returns false and form.errors says:

{'cycle': [u'Select a valid choice. That choice is not one of the available choices.']}

There must be something wrong with my form construction. 'cycle': cyc clearly isn't working as intended. I've also tried 'cycle': '0' and 'cycle': '1'.

What is the correct way to construct a form like this?

EDIT: I should explain what the available choices are. There's only one CycleDate in the database and only one choice. After running the lines of my test in the shell, I typed form.fields['cycle'].choices.choice(cyc) which returns (1, '2015-05-01'). The strange thing is that form.fields['cycle'].queryset returns []. Maybe the issue is related to that?

EDIT2: Here's my form with that complicated (read: messy and terrible and shameful) clean method:

class EnergyUsageForm(forms.Form):
    # Override init so that we can pass the user as a parameter.
    # Then put the cycle form inside init so that it can access the current user variable
    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user', None)
        super(EnergyUsageForm, self).__init__(*args, **kwargs)

        # Get the last 12 cycle dates for the current user
        td = datetime.date.today
        cycle_dates = CycleDate.objects.filter(cycle_ref=Account.objects.get(holder__user=user).cycle,
                                               begin_date__lte=td).order_by('begin_date')
        self.fields['cycle'] = CycleModelChoiceField(queryset = cycle_dates,
                                                      required = False,
                                                      widget = forms.Select(attrs={"onChange":'changeCalendar()'}),
                                                      label = "Choose a billing cycle")


    type_of_input = forms.ChoiceField(required=False,
                                    widget=forms.Select(attrs={"onChange": "switchInput()"}),
                                    choices=INPUT,
                                    initial='0',
                                    label="Choose a way to display usage", )

    end_date = forms.DateField(widget=forms.TextInput(attrs=
                                {
                                    'class':'datepicker'
                                }), 
                                label="Choose start date",
                                help_text='Choose a beginning date for displaying usage',
                                required=True,
                                initial=datetime.date.today,)

    period = forms.ChoiceField(required=True,
                                widget=forms.RadioSelect, 
                                choices=DISPLAY_PERIOD, 
                                initial='01',
                                label="Choose period to display",)

    usage_type = forms.ChoiceField(required=True,
                                widget=forms.RadioSelect,
                                choices=USAGE_TYPE,
                                initial='E',
                                label="Choose type of usage to display",)

    def clean_end_date(self):
        data = self.cleaned_data['end_date']

        if datetime.date.today() < data:
            raise forms.ValidationError("Don't choose a future date")
        # Always return the cleaned data, whether you have changed it or
        # not.
        return data

    def clean(self):
        cleaned_data = super(EnergyUsageForm, self).clean()
        selection = cleaned_data['type_of_input']
        # Check if the user wants to use cycle_dates instead
        if selection == '0':
            # Set the end_date and period
            cleaned_data['end_date'] = cleaned_data['cycle'].begin_date #MUST BE CHANGED TO END_DATE LATER
            cleaned_data['period'] = cleaned_data['cycle'].duration
        return cleaned_data

EDIT3 Fixed a typo in my test, and also here's my test's setUp method:

client = Client()
    def setUp(self):
        user = User.objects.create_user(username = 'user', password = '1234')
        user.save()
        profile = UserProfile.objects.create(user = user)
        profile.save()
        account = Account(number=1, first_name='test',
                          last_name='User',
                          active=True,
                          holder=profile,
                          holder_verification_key=1)

        account.save()

        the_cycle = Cycle.objects.create(name = 'test cycle')
        the_cycle.save()
        cd = CycleDate.objects.create(begin_date = datetime.date(2015, 5, 1),
                                 end_date = datetime.date.today(),
                                 cycle_ref = the_cycle)
        cd.save()

EDIT4: In addition to all this mess, I'm now getting KeyError: 'cycle' whenever I call form.is_valid(). Probably due to the clean() method trying to access cleaned_data['cycle'] when the cycle field has an invalid selection.

Upvotes: 3

Views: 8838

Answers (2)

Jerzyk
Jerzyk

Reputation: 3742

self.client.login(username='user', password='1234')
response = self.client.get(reverse('my_form'), follow=True)
cyc = list(CycleDate.objects.all())[0]
form_data = {'type_of_input': '0', 'cycle': cyc,'usage_type': 'E',}
form = EnergyUsageForm(data=form_data, user=response.context['user'])

especially line

cyc = list(CycleDate.objects.all())[0]
  • first you get iterator of all CycleDates (ok, more or less ok...)
  • then you read everything to memory (why!)
  • and lastly.. you select first list element (of course, you will get exception if list is empty...)

why not:

cyc = CycleDate.objects.first()
if cyc:
    # cycle is a ModelChoice - in html it stores primary key!
    form_data = {'type_of_input': '0', 'cycle': cyc.pk, 'usage_type': 'E'}
    form = EnergyUsageForm(data=form_data, user=response.context['user'])

EnergyUsageForm's init:

def __init__(self, *args, **kwargs):
    user = kwargs.pop('user', None)
    super(EnergyUsageForm, self).__init__(*args, **kwargs)

    # today is a function!
    td = datetime.date.today()
    account = Account.objects.get(holder__user=user)
    cycle_dates = CycleDate.objects.filter(cycle_ref=account.cycle,
                                           begin_date__lte=td)
                                   .order_by('begin_date')

    self.fields['cycle'] = CycleModelChoiceField(queryset=cycle_dates,
                                                 required=False,
                                                 widget=forms.Select(attrs={"onChange":'changeCalendar()'}),
                                                 label = "Choose a billing cycle")

Upvotes: 1

Mac Chaffee
Mac Chaffee

Reputation: 110

As pointed out by DanielRoseman, the answer is to use the id of the instance.

So the correct way to construct a form with a ModelChoiceField is as follows:

my_instance = MyModelName.objects.get(whatever instance you need)
form_data = {'my_regular_choice_field': '0', 'my_model_choice_field': my_instance.id}
form = MyFormName(data=form_data)

Upvotes: 0

Related Questions