M2M relationship, labels / names of the form fields displayed in HTML

Hi!
First, allow me to describe my models etc:


**models.py**

class Item(models.Model):
    name = models.CharField(max_length=50)
    ...

class Order(models.Model):
    table = models.ForeignKey(
        "Table", on_delete=models.SET_NULL, null=True, blank=True
    )
    items = models.ManyToManyField(
        Item, through="OrderItem", related_name="orders_items"
    ...

    def get_items(self):
        return self.orderitem_set.prefetch_related("item")

class OrderItem(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    item = models.ForeignKey(
        Item, on_delete=models.SET_NULL, null=True, blank=True
    )
    quantity = models.PositiveIntegerField(default=1)
    date_added = models.DateTimeField(auto_now_add=True)
    rating = models.PositiveSmallIntegerField(default=0, null=True, blank=True)

**views.py**

class ReviewOrderView(LoginRequiredMixin, View):
    template_name = "menu/submit_review_form.html"

    OrderItemFormSet = inlineformset_factory(
        Order,
        OrderItem,
        fields=["rating"],
        extra=0,
        can_delete=False,
        labels="item_names",
)

    def get(self, request, *args, **kwargs):
       # Please ignore the Table model reference here, it has nothing to do with the problem        
        order = (
            Table.objects.get(id=request.user.table.id).get_order()  
        )  
        item_names = order.get_items().values("item__name")

        form = self.OrderItemFormSet(
            instance=order,
        )
        return render(request, self.template_name, {"form": form})

    def post(request, *args, **kwargs):
        pass

As you can see, I have two models in an m2m relationship with a through-model (OrderItem) that contains additional data, the rating.

I was trying to make a form that will allow users to submit their reviews on items in the order. Since it’s actually a number of small forms with the field rating that depends on the number of items in the order, I thought the correct way to do so is to implement a formset_factory—or, more specifically, an inlineformset_factory.
I was able to get this:


This correlates with the number of items in order, but I want the actual name of the Item ("item__name") instead of the word Rating:

How can I do that?
Will appreciate any help.

Yes it can be done. I can see a couple different ways to possibly do it, I’m not sure what’s going to be easiest.

You could override the add_fields method on the formset class, to set the form.fields['a_field'].label attribute, or you could create a custom model form that sets the label in its __init__ method (but you might need to override get_form_kwargs to pass the data through that you need).

The model form being created should have the instance available for accessing the specific object for which the form is being created. Otherwise, the formset itself has the queryset of related objects available in self.queryset if you need to get the specific object.

I haven’t had a chance to try either one of these, so this is all just a semi-educated guess, but one of them should work.

Hi Ken!
Thank you for your swift reply and excuse me for my delayed answer.
So far, I was able to make a bit ugly solution, not without outside help, that looks like this:


** forms.py **

def item_field(db_field, **kwargs):
    if db_field.name == "item":
        return db_field.formfield(**kwargs, disabled=True)

    return db_field.formfield(**kwargs)


ReviewFormset = modelformset_factory(
    OrderItem,
    formfield_callback=item_field,
    fields=["rating", "item"],
    extra=0,
)

** views.py **

class ReviewOrderView(LoginRequiredMixin, View):
    template_name = "menu/submit_review_form.html"

    def get_queryset(self): #yes, I know that there is no such method by default in the generic View 
        return (
            Table.objects.get(id=self.request.user.table.id)
            .get_order()
            .get_items()
        )

    def get(self, request, *args, **kwargs):
        queryset = self.get_queryset()
        review_formset = ReviewFormset(queryset=queryset)
        context = {"forms": review_formset}
        return render(request, self.template_name, context)

    def post(self, request, *args, **kwargs):
        review_formset = ReviewFormset(data=request.POST)
        if review_formset.is_valid():
            review_formset.save()
        return_url = reverse_lazy("menu:all")
        return HttpResponseRedirect(return_url)

That results in:
image

And it works :thinking: