Model and form with foreign key

I think I have a fundamental misunderstanding of using the ORM in Django with ModelForms and, although I’ve been pouring through search engines since mid-day Friday, I feel I am no further on in figuring this out.

I have two models as shown here…

class Modules(models.Model):
    module_code = models.CharField(max_length=5)
    module_name = models.CharField(max_length=100)
    datetimestamp = models.DateTimeField(auto_now=True)

    class Meta:
        # Defines the name to appear in the Admin section
        verbose_name_plural = "Modules"


class ChangeLogEntries(models.Model):
    change_date = models.DateField()
    change_note = models.TextField()
    form_name = models.CharField(max_length=100)
    datetimestamp = models.DateTimeField(auto_now=True)
    module_code = models.CharField(max_length=5, null=True)
    binary_only = models.BooleanField(default=False)
    installed = models.BooleanField(default=False)
    dateinstalled = models.DateField(blank=True, null=True)
    module_category = models.ForeignKey(Modules, default=1, verbose_name="Module",
                                        on_delete=models.SET_DEFAULT, null=True)

    class Meta:
        # Defines the name to appear in the Admin section
        verbose_name_plural = "Log Entries"

    def __str__(self):
        return self.module_code

I have a form which will be used to create a new ChangeLogEntries row as shown here…

class CreateLogEntryForm(forms.ModelForm):
    class Meta:
        model = ChangeLogEntries
        fields = ['change_date', 'change_note', 'module_code', 'binary_only', 'form_name']

    def __init__(self, *args, **kwargs):
        super(ModelForm, self).__init__(*args, **kwargs)
        today = date.today()
        self.fields['change_date'] = forms.DateField(initial=today, required=True,
                                                     widget=DateInput(attrs={'type': 'date', 'style': 'width:200px'}))
        self.fields['change_note'].widget = Textarea(attrs={'rows': '6', 'cols': '80'})
        module_categories = Modules.objects.all().order_by('module_name').values_list('id', 'module_name')
        self.fields['module_category'] = forms.CharField(label="Module Name", required=True,
                                                     widget=forms.Select(choices=module_categories,
                                                                         attrs={'style': 'width: 300px;'}))
        self.fields['form_name'] = forms.CharField(label="Form/Class Name", required=False, initial="", max_length=50)

    def clean(self):
        super(CreateLogEntryForm, self).clean()

        # get the change note from the data
        change_note = self.cleaned_data.get('change_note')
        if len(change_note) < 5:
            self.errors['change_note'] = self.error_class(['The change note requires a minimum of 5 characters.'])

        # the form name must have at least 5 characters in order to be valid
        form_name = self.cleaned_data.get('form_name')
        if len(form_name) > 0:
            if len(form_name) < 5:
                self.errors['form_name'] = self.error_class(
                    ['The form name if specified, must be at least 5 characters.'])
            # if the binary-only field is checked, then we should not have a form name
            binary_only = self.cleaned_data.get('binary_only')
            if binary_only:
                self.errors['binary_only'] = self.error_class(
                    ['If Binary-Only is checked, you should not specify a form name.'])

        return self.cleaned_data

Here is the code views.py…

def createlogentry(request):
    if request.method == 'POST':
        form = CreateLogEntryForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('home')
        else:
            return render(request, 'main/createlogentry.html', {'form': form})
    else:
        form = CreateLogEntryForm()
    return render(request,
                  'main/createlogentry.html',
                  {'form': form
                   })

The template correctly shows the module list for selection, but when I attempt to save, I get a validation error that the field is required, which I believe is referring to the module_category.

I’m thinking that the dropdown list for Select Module is not correctly defined in the form and that I should be, somehow displaying objects directly from the Modules model. However, I’m not sure how to do that. I haven’t even gotten to the point in views.py where I try to save this, but maybe, just getting the form definition correct will solve the issue.

At this point, I’m trying to find some example that is similar to what I am trying to do here, namely, save a log entry with a valid, related entry from the Modules model. If you can point me in that direction, or provide some feedback here, I would greatly appreciate it. Also, let me know if there is any other information that would be helpful to post.

Here is an update…

After reviewing my post, I thought I needed to add module_category to my field list in forms.py for ChangeLogEntries, so I added that.

Now I am getting this error message:

This is sort of what I expected to see. I’m showing a dropdown list of modules from a result/query set, but these should actually (and somehow) be objects from the Modules table. I think if I can figure that part out, it will help a lot!

Thanks,
BH

Two separate topics here.

Your first topic, regarding the required field, you might want to look at the html in the form to see what field that error message is related to. If it’s not clear in the html, then you may want to just add a print statement in your view to print the form errors before re-rendering it.

Right off-hand, I’m going to say the issue is related to the boolean fields in the form. See the green Note box. You’ll want to define those fields on the form as required=False.

Second topic, not at all related to the first, you probably want to define module_category in the form as a ModelChoiceField, and not a CharField

Okay, thanks. Printing the errors helped. I realized there was a required field that was missing, even though the model indicates that null=True in the model definition. For now, I just added that to the form so I’m no longer getting that.

However, I did change the form to use a ModelChoiceField. Here’s the definition.

self.fields['module_category'] = forms.ModelChoiceField(queryset=module_categories, empty_label='(select)')

It renders with the ID and description, which I can probably customize through the widgets setting later, but when I try to save it I get the following error message.

I’m not sure why it is suggesting that this is not a valid choice. Any ideas?

<conjecture>
Right offhand, I’d say it’s because of the value_list clause in your query.
</conjecture>

The queryset docs for a ModelChoice field says:

A QuerySet of model objects from which the choices for the field are derived

I read this as saying that it’s looking for the instances of the objects, and not just a two-tuple of values.

Okay, got it now.

Here is the new definition in the form.

        self.fields['module_category'] = ModuleSelectorModelChoiceField(queryset=Modules.objects.all(),
                                                                        empty_label='(select module)',
                                                                        widget=forms.Select(
                                                                            attrs={'style': 'width:300px'}, ))

Here is the inherited ModelChoiceField…

class ModuleSelectorModelChoiceField(forms.ModelChoiceField):
    def label_from_instance(self, obj):
        self.widget_attrs({'style': 'width:200px;'}, )
        return "%s" % obj.module_name

The option is working now and properly saving the entries.

Once again, thanks for your help Ken!!

BH