r/django•
mxzf

Looking for help showing/editing a many-to-many relationship

I'm working on making a page that allows saving a few boolean values for the intersection of two different models. I think I need a many-to-many relation through a related model. Here's roughly what I have in that regard.

def A(models.Model):
    name = models.CharField(max_length=200)
    
def B(models.Model):
    rows = models.ManyToManyField('A', through='Row', through_fields=('my_B', 'my_A'))
    
def Row(models.Model)
    my_B = models.ForeignKey(B, on_delete=models.CASCADE)
    my_A = models.ForeignKey(A, on_delete=models.CASCADE)
    col1 = models.BooleanField()
    col2 = models.BooleanField()
    col3 = models.BooleanField()

I believe that's roughly correct for what I'm trying to do (though I'm open to suggestions). However, I'm not sure how to get data into the database in a nice way. I want to make a page laid out something like this (with one such page for each B):

B1Col 1Col 2Col3
A1☐☒☐
A2☒☐☐
A3☐☐☒

I'm looking for one page per element of B with all of the A elements as rows with checkboxes and a Submit button to save changes. The Columns are fixed, but the number of rows of A will vary. Not all As will need relations to all Bs, it's a somewhat sparse dataset; probably 60-80% of As has one or more True columns for any given B.

I'm looking for some help writing a view and template to both display the boolean values as checkboxes and then save modifications back to the database (adding Row elements when a previously all-False A gets at least one True value). Or, if someone knows of a better way to tackle this problem, I'm not at all tied to this approach.

Posted Jan 14, 2020, 3:18 PM UTC

24
belgaros•

Since you have an intermediary model to your m2m relationship, you will need to handle its saving manually.

With a formset_factory this should be fairly straightforward. Each line of your table will be a formset.

class AForm(forms.ModelForm):
    class Meta:
        model = A
        fields = ["col1", "col2", "col3"]
    
    col1 = forms.BooleanField(required=False)
    col2 = forms.BooleanField(required=False)

AFormSet = formset_factory(AForm, extra=0)

Then in your view :

def b_edit(request, id):
    b = B.objects.get(id=id)
    formset = AFormSet(request.POST or None)

    if request.method == "POST" and formset.is_valid():
        for form in formset:
            a = form.save() # If you need to save it, or
            a = form.instance # if not
            Row.objects.create(
                my_B=b,
                my_A=a,
                col1=form.cleaned_data.get("col1"),
                col2=form.cleaned_data.get("col2")
            )

Didn't test this code but I would go with this direction.

3
mxzfOP•

Thanks, that does appear to be along the lines of what I'm looking for. I think I'm still missing something though, since passing the formset to the template and displaying it just leaves me with a blank page. Somehow I need to figure out how to get it to show rows for every A (or a queried subset) on the page.

1
belgaros•

The documentation will be very useful at this point, as this part is more basic Django stuff but usually its just a matter of iterating through every form in the formset like so :

<form>
  {% csrf_token %}
  {% for form in formset %}
    {{ form.as_p }} // or
    <td>{{ form.col1.field }}</td> // or
    {% for field in form %}
      <td>{{ field }}</td>
    {% endfor }
  {% endfor %}
  <input type="submit">
</form>
1
mxzfOP•

Yeah, I'd gotten similar to that for the template from their examples (and tried copy-pasting that code from you), but still no contents.

I suspect it has something to do with how I'm making the formset, I suspect that I need to be telling it somehow what elements of A type I want it to iterate over to make that set.

1