Trying to use ModelFormSets to create an HTML table

I’m trying to create a table in a database where one field has empty values that a user needs to input. The entire table with the missing field should display. I built the following template:

table.html

<form method = "post">
      <input type="submit" value="Submit">
      {% csrf_token %} 
      <table>
        <tr>
          <th>Header 1</th>
          <th>Input Header</th>
          <th>Header 2</th>
          <th>Header 3</th>
        </tr>
        {{ formset.management_form }}
        {% for form in formset %}
          {% for field in form %}
          <tr>
            <td>{{ field.field1 }}</td>
            <td>{{ form }}</td> // this is my input field as a dropdown
            <td>{{ field.field2 }}</td>
            <td>{{ field.field3 }}</td>
          </tr>
          {% endfor %}
        {% endfor %}
     </table>
</form>

forms.py

from django.forms.widgets import Select
from django import forms
from .models import DbTable
from .models import Choices

class DbTableForm(forms.ModelForm):
    class Meta:
        CHOICES = Choices.objects.all().order_by("the_choice")   
        model = DbTable
        fields = ["input_field"]
        widgets = {
            'input_field': Select(choices=([['', '']] + [[x.pkid, x.analyst] for x in CHOICES])),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.queryset = DbTable.objects.filter(input_field__exact='')

views.py

def table_unassigned(request):
    DbTableFormSet = modelformset_factory(DbTable, form=DbTableForm)
    if request.method == 'POST':    
        formset = DbTableFormSet(request.POST)
        if formset.is_valid():
            formset.save(commit=True)
    else:
        formset = DbTableFormSet()
    return render(request, 'table.html', {'formset': formset})

So I can see the dropdown for every record so the user can choose one and post it back but I can’t see the other fields and I can’t figure out how to show the other fields from the model.

Before I could see the data but only if I created a query object like:

data = DbTable.objects.filter(input_field__exact='')

But I’m not sure if I should do that. I was then sending the form and the data back with:

return render(request, 'table.html', {'form': form, 'data':'data'})

I could see the data for the model and have an input field but it seemed it was de-coupled so I moved everything to modelformset and now I can’t get at all the other fields in the model.

I’m just trying to create something like:

Header Field 1 Input Field Header Field 2 Header Field 3
Data 1 Input Widget Data 2 Data 3
Data 4 Input Widget Data 5 Data 6

How do you access the data of the model and display the values?

UPDATE
models.py

class DbTable(models.Model):
    field1 = models.CharField(db_column='Field1', max_length=40, blank=True, null=True)      
    field2 = models.CharField(db_column='Field2', max_length=11, blank=True, null=True)  
    input_field = models.CharField(db_column='InputField', max_length=9, blank=True, null=True)
    field3 = models.AutoField(db_column='Field3', primary_key=True)

    class Meta:
        managed = False
        db_table = 'My_Table'

This is basically what the model looks like. Field3 is the primary key.

Can you post your Model, too? I’m not quite seeing the relationships among the data yet.

I have added the model as well. Sorry for obscuring everything but I can’t put the actual field names into everything.

The most obvious problem that I see so far is the inner loop iterating over each field in the form in addition to rendering the entire form.

You want to eliminate that inner loop (for field in form), and make your references form.field1, etc.

Since you don’t want field1, 2, and 3 to be editable, you’ve got a couple of different options, depending upon exactly how you want this to appear:

  • You can reference the field through the ‘instance’ attribute of the form and render it.
  • You can set those fields as being “disabled” - they’ll render as a widget but will be disabled and unable to be edited.
  • You can create a “ReadOnly” widget that renders the field as text.

There are probably other options, but these are the three that came to mind first.

I’ve been trying to get to the form fields instance but it’s not there. I’ve tried form.fields[‘Field1’] but I get an error. I’ve tried form.field1 and get nothing too. I’ve been trying to find out how to set them as read-only but I’m back to they don’t seem to exist.

I’ll keep digging through the form api. Any other place I should try and look?

It’s the standard instance attribute of the form, so it would be form.instance.field1

To access the fields within the form, you’d need to add those fields to the fields attribute of the form.

(Note: These are two different implementations of two different solutions - you don’t need to do both of these.)

That worked! You wrote instance and I was testing it out (badly) in the interpreter and it was saying instance didn’t exist but when I went to the template and entered that it worked.

Thank you so much @KenWhitesell! You’ve helped me a lot again!

Question, when you submit on a formset the entire formset is submitted back. Is there a way to just submit those forms that have a changed record instead?

I did place this bit of code to check to see if an individual form has changed:

 if formset.is_valid():
        for form in formset:
             if form.has_changed():
                 form.save()

No, nor is there a desire or need to - that’s the purpose of performing the save on the formset rather than on the individual forms.

There are other functions / features that formsets manage that don’t involve directly modifying the form data - marking forms as being deleted and managing the order of the forms are two of them.

So, for a large table I need to increase the setting that allows the entire formset to be modified, or limit how many I show at a time. I’m wondering why everyone told me to go this route when all I wanted was to show table grid but have only individual records that have changed updated back to the database?

Is there another route to take to show the same grid but only save those records with changed data?

If you’re using formsets, then only updated rows will be updated in the database.

I’ve updated my code but this is the error I’m getting:

TooManyFieldsSent at /cma/

The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.

Request Method: POST
Request URL: http://localhost:8000/table/
Django Version: 3.1
Exception Type: TooManyFieldsSent
Exception Value: The number of GET/POST parameters exceeded settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.

My code change, because of the article, is as such:

if request.method == 'POST':    
    formset = DbTableFormSet(request.POST)
    if formset.is_valid():
        instance = formset.save(commit=False)
        for form in instance:
            if form.has_changed():
                form.save(commit=True)

I did add the commits to the form. Still not sure, I’m going to keep reading.

You’re still doing more work than needed. All that’s necessary to save the formset is formset.save(). There is no need or value to iterating over the individual forms.

See https://docs.djangoproject.com/en/3.1/ref/settings/#data-upload-max-number-fields

But I will say, overall performance of 1000+ fields probably isn’t going to be great. You’d be a lot better off overall with paginating this in, say, units of 100.

Yeah, I know but I get that error. And looking at the actual data in the model I can say that it’s showing me data that I shouldn’t even see. IE Input Field should be blank when the data is pulled and I just found that it’s not. There is stuff in there but the same data.

Looks like I created the queryset for the model all wrong(!?). I just know that a user is only going to select like 5 records assign the data in the drop down and leave the rest for another user to do. But if I’m pulling a bunch of data that are already filled then there is larger problem I need to solve.

Ugh, frustration. Never going to finish this it seems.

Thanks.