Django Class based Form only updating the final post value

Viewed 557

As the title states, I have a table that I'm attempting to update that only updates the final value in the post, from what I understand if I want to update multiple records I must iterate over my request object and update a form instance with the specified ID in my db.

Before submission all records have a price_checked of 0.

enter image description here

and then after - you can see the final value from the post request updates all the records!

enter image description here

postgres table

enter image description here

The code in question that updates my model form instance.

def post(self,request):
    if request.method=='POST':
    
        for key in request.POST.getlist('product_id'):
                product_update = ProductTable.objects.get(id=key)
                form = ProductUpdateForm(request.POST,instance=product_update)
                print(form)
                if form.is_valid():
                    form.save()
                messages.success(request,message='price checked...') 
        return redirect('product')

is anyone able to assist? I've been at this point for over 2 weeks.


models/form/view for reference.

models.py

from django.db import models

class ProductTable(models.Model):

    id = models.AutoField(
        primary_key=True,
        editable=False
    )

    product_name = models.TextField(max_length=255,null=False)
    price = models.DecimalField(max_digits=6,decimal_places=2,null=False)
    from_date = models.DateTimeField()
    to_date = models.DateTimeField(null=True)
    price_checked = models.IntegerField(default=0,null=False)

    def __str__(self : str) -> str:
        return f"{self.product_name} - {self.id} with a price check of {self.price_checked}"

forms.py

from django.forms import ModelForm
from .models import ProductTable


class ProductUpdateForm(ModelForm):
    class Meta:
        model = ProductTable
        fields = ('price_checked',)

views.py

from typing import Any, Dict

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.views.generic import ListView
from django.db import transaction
from .forms import ProductUpdateForm
from .models import ProductTable


class TableDataView(LoginRequiredMixin,ListView):
    model = ProductTable
    context_object_name = 'product'
    template_name = 'tables/product.html'


    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
        context = super().get_context_data(**kwargs)

        context['product'] = ProductTable.objects.filter(pk__in= [4607,4642, 4645])

        return context


    def post(self,request):
        if request.method=='POST':
        
            for key in request.POST.getlist('product_id'):
                    product_update = ProductTable.objects.get(id=key)
                    form = ProductUpdateForm(request.POST,instance=product_update)
                    print(form)
                    if form.is_valid():
                        form.save()
                    messages.success(request,message='price checked...') 
            return redirect('product')

product.html

{% extends '_base.html' %} {% block content %} {% load crispy_forms_tags %} 
<form method="POST" action="">
    {% csrf_token %} 
    {{ form|crispy }}
  <div class="container mx-flex pt-5 mb-5">
    <table class="table" id="table">
      <thead>
        <tr>
          <th>Product</th>
          <th>Price</th>
          <th>Date</th>
          <th>Input</th>
        </tr>
      </thead>
      <tbody>
        {% for data in product %}
        <tr>
          <th>{{ data.product_name }}</th>
          <th>{{ data.price }}</th>
          <th>{{ data.from_date }}</th>
          <th>
            <input type="hidden" name="product_id" value="{{data.id}}" />
            <input class="form" name="price_checked" id="priced_checked" type="number" placeholder="{{ data.price_checked }}"/>
            <input type="submit" value="OK">
          </th>
        </tr>
        {% endfor %}
      </tbody>
    </table>
  </div>
</form>
{% endblock content %}

request.POST

 <QueryDict: {'csrfmiddlewaretoken': ['p0j3e0UbrY1VuFEAnJWopaCICCxOPj8v2OkLRZZiGlUa4YtxGwduD2bAIrm91VKe'], 'product_id': ['4607', '4642', '4645'], 'price_checked': ['1', '2', '3']}>
2 Answers

after read formset documents,I think it should looks like this.
the view python return a formset to template,and in the post function create formset by request.POST

def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
    context = super().get_context_data(**kwargs)

    ProductFormSet = modelformset_factory(ProductTable)
    context['product_formset'] = ProductFormSet(queryset=ProductTable.objects.filter(pk__in= [4607,4642, 4645]))

    return context


def post(self,request):
    if request.method=='POST':
        ProductFormSet = modelformset_factory(ProductTable)
        formset = ProductFormSet(request.POST)
        if formset.is_valid():
            formset.save()
            messages.success(request,message='price checked...') 
        return redirect('product')

in the template I have mark the changes,the important thing is {{ product_formset.management_form }} in the begin form.and {{ form.id }} of each loop,the each cell data should be {{ form.product_name.value() }} or {{ form.product_name.data }},you can try that

{% extends '_base.html' %} {% block content %} {% load crispy_forms_tags %} 
<form method="POST" action="">
    {% csrf_token %} 
    {{ form|crispy }}
    <!-- changed -->
    {{ product_formset.management_form }}
  <div class="container mx-flex pt-5 mb-5">
    <table class="table" id="table">
      <thead>
        <tr>
          <th>Product</th>
          <th>Price</th>
          <th>Date</th>
          <th>Input</th>
        </tr>
      </thead>
      <tbody>
        <!-- changed -->
        {% for form in product_formset %}
        {{ form.id }}
        <tr>
          <td>{{ form.product_name.value() }}</td>
          <td>{{ form.price.value() }}</td>
          <td>{{ form.from_date.value() }}</td>
          <td>
            {{ form.price_checked }}
            
          </td>
        </tr>
        {% endfor %}
      </tbody>
    </table>
  </div>
  <input type="submit" value="submit">
</form>
{% endblock content %}

maybe this will not help you,because I never used the django form things.
I guess you are a precise man who want to follow every django's principle.
But as I know,traditional form has much limitation,some ajax or reactjs has more flexible.

A couple changes to support formsets:

  1. Use the same formset for updating and displaying the objects, but disable the fields you don't want to be updated (make them read-only):
class ProductUpdateForm(ModelForm):
    disabled_fields = ['product', 'price', 'date_added', 'date_removed']
    class Meta:
        model = ProductTable
        fields = ('product', 'price', 'date_added', 'date_removed', 'price_checked',)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.disabled_fields:
            self.fields[field].disabled = True
  1. Then update the view to use the form for displaying and for updating:
from django.forms import modelformset_factory


class TableDataView(LoginRequiredMixin,ListView):
    model = ProductTable
    context_object_name = 'product'
    template_name = 'tables/product.html'

    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
        context = super().get_context_data(**kwargs)
        ProductTableFormset = modelformset_factory(ProductTable, form=ProductUpdateForm,)
        context['formset'] = ProductTableFormset(queryset=ProductTable.objects.filter(pk__in=[4607,4642, 4645]))
        return context

    def post(self,request):
        ProductTableFormset = modelformset_factory(ProductTable, form=ProductUpdateForm)
        if request.method == 'POST':
            formset = ProductTableFormset(request.POST, queryset=ProductTable.objects.filter(pk__in=[4607,4642, 4645]))
            if formset.is_valid():
                formset.save()
                messages.success(request,message='price checked...') 
        return redirect('product')
  1. And in your template, render the whole formset:
{% extends '_base.html' %} 
{% block content %} 
{% load crispy_forms_tags %} 
<form method="POST" action="">
    {% csrf_token %} 
    {{ form|crispy }}
    <div class="container mx-flex pt-5 mb-5">
    <table class="table" id="table">
        {{ formset }}
    </table>
    </div>
</form>
{% endblock content %}

EDIT: If you want the simplest change you can do to fix this (so you don't have to work on the styles for the formset), you can do away with the form for the post and update the products as is:

   def post(self,request):
        if request.method == 'POST':
            products_to_update = []
            for id, price_checked in zip(request.POST.getlist('product_id'), request.POST.getlist('price_checked')):
                product = ProductTable.objects.get(id=id)
                product.price_checked = price_checked
                products_to_update.append(product)

            ProductTable.objects.bulk_update(products_to_update, ['price_checked'])

            messages.success(request,message='price checked...') 
            return redirect('product')
Related