Правильный способ динамического добавления экземпляров Django formset и POST с помощью HTMX?

Я делаю форму с вложенным динамическим набором форм, используя htmx я (хочу избежать использования JS, но если нет выбора.... ), чтобы инстанцировать больше полей набора форм, чтобы сделать динамическую вложенную форму, однако когда я POST, только данные из 1 экземпляра Chlid formset (последний) размещаются, остальная часть формы размещается правильно и Child model получает связь с Parent model

Я прочитал документацию django о том, как POST экземпляры formset и попытался применить это в своем коде, также я понял, как POST одновременно Parent и Child. Для наборов форм я делаю htmx get запрос hx-get к частичному шаблону, который содержит дочерний набор форм, и это работает отлично, единственная проблема в том, что это всегда возвращает form-0 набор форм на сторону клиента, поэтому для POST данные повторяются x раз для каждого поля и принимаются только данные, размещенные в последнем экземпляре, Однако я попытался изменить значение extra=int в моем наборе форм, чтобы получить больше форм, это дало ожидаемый результат, один Child instance на форму в extra=int, так что моя проблема в htmx и в том, как я вызываю new Child formset экземпляры.

вот мой код. (я планирую вложить больше дочерних наборов форм внутри этой формы, поэтому для удобства я называю ее sformset)

****views.py****

def createPlan(request):#Requst for the Parent form 

    form = PlanForm(request.POST or None)
    sformset = StructureFormset(request.POST or None) #Nesting the Child formset

    context = {
        'form':form,
        'sformset':sformset,
        }

    if request.method == 'POST':

        print(request.POST)
        if form.is_valid() and sformset.is_valid():

            plan = form.save(commit=False)
            print(plan)
            plan.save()
             
            sform = sformset.save(commit=False)     
            for structure in sform:

                structure.plan = plan
                structure.save()

    return render(request, 'app/plan_forms.html', context)


def addStructure(request):

    sformset = StructureFormset(queryset=Structure.objects.none())#add a empty formset instance 
    
    context = {"sformset":sformset}

    return render(request, 'app/formsets/structure_form.html', context)
****forms.py****

StructureFormset = modelformset_factory(Structure,
        fields = (
            'material_type',
            'weight',
            'thickness',
            'provider'
        ))
****relevant part for plan_forms.html template****

<form method="POST">
  {% csrf_token %}
  <div class="col-12 px-2">
    <div class="row px-3 py-1">
      <div class="col-3 px-1">{{ form.format }}</div>
      <div class="col-3 px-1">{{ form.pc }}</div>
      <div class="col-3 px-1">{{ form.revission }}</div>
      <div class="col-3 px-1">{{ form.rev_date }}</div>
    </div>
    <div class="row px-3 py-1">
      <div class="col-3 px-1">{{ form.client }}</div>
      <div class="col-3 px-1">{{ form.product }}</div>
      <div class="col-3 px-1">{{ form.gp_code }}</div>
      <div class="col-3 px-1">{{ form.code }}</div>
    </div>
  </div>
  <div>
    <table>
      <tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
        <!--Structure formset goes here-->
      </tbody>
      <tfoot>
        <a href="" hx-get="{% url 'structure-form' %}" hx-swap="beforeend" hx-target="#structureforms">
          Add structure <!--Button to call structure formset-->
        </a>
      </tfoot>
    </table>
  </div>
  <div class="col-12 px-2">
    <div class="row px-4 py-1">{{ form.observation }}</div>
    <div class="row px-4 py-1">{{ form.continuation }}</div>
    <div class="row px-4 py-1">{{ form.dispatch_conditions }}</div>
    <div class="row px-3 py-1">
      <div class="col-6 px-1">{{ form.elaborator }}</div>
      <div class="col-6 px-1">{{ form.reviewer }}</div>
    </div>
  </div>
  <button type="submit">Submit</button>
</form>
****formsets/structure_form.html****

<tr>
  <td class="col-12 px-1">
    {{ sformset }}
  </td>
</tr>
**** relevant urls.py****

urlpatterns = [
    path('create_plan/', views.createPlan, name='create_plan'),
    path('htmx/structure-form/', views.addStructure, name='structure-form')]

Кроме того, форма, которую я построил в admin.py, используя fields и inlines, является именно тем, что я хочу получить в качестве исходного продукта (за исключением количества исходных наборов форм и стилей)

Кратко о проблеме: В настоящее время ваш код успешно добавляет новый набор форм, но каждый новый набор форм поставляется с атрибутом name form-0-title (то же самое для id и других атрибутов). Кроме того, после добавления нового набора форм с помощью hx-get скрытые поля, первоначально созданные ManagementForm, больше не будут отражать количество наборов форм на странице.

Что нужно

После добавления нового набора форм на сайт, вот что, по моему мнению, должно произойти, чтобы Django смог обработать отправку формы.

  1. Обновите атрибут value в элементе input с помощью id="id_form-TOTAL_FORMS", чтобы число соответствовало фактическому количеству наборов форм на странице после того, как hx-get введет новый набор форм.

  2. Обновить name и id нового набора форм из form-0-title, чтобы использовать любое число, отражающее текущее общее количество наборов форм.

  3. Обновите атрибуты меток for таким же образом.

Вы можете сделать это с помощью Javascript на стороне клиента. В качестве альтернативы, вы можете сделать практически то же самое с помощью Django на стороне сервера, и тогда htmx может быть единственным javascript, необходимым для всего остального. Для этого я использовал empty_form для создания html-содержимого набора форм, которое можно изменять по мере необходимости. Эта работа показана в помощнике build_new_formset(), ниже.

Пример

Вот что у меня работает:

forms.py

from django import forms
from django.forms import formset_factory

class BookForm(forms.Form):
    title = forms.CharField()
    author = forms.CharField()

BookFormSet = formset_factory(BookForm)

views.py

from django.utils.safestring import mark_safe
from app2.forms import BookFormSet


def formset_view(request):
    template = 'formset.html'

    if request.POST:
        formset = BookFormSet(request.POST)
        if formset.is_valid():
            print(f">>>> form is valid. Request.post is {request.POST}")
            return HttpResponseRedirect(reverse('app2:formset_view'))
    else:
        formset = BookFormSet()

    return render(request, template, {'formset': formset})


def add_formset(request, current_total_formsets):
    new_formset = build_new_formset(BookFormSet(), current_total_formsets)
    context = {
        'new_formset': new_formset,
        'new_total_formsets': current_total_formsets + 1,
    }
    return render(request, 'formset_partial.html', context)


# Helper to build the needed formset
def build_new_formset(formset, new_total_formsets):
    html = ""

    for form in formset.empty_form:
        html += form.label_tag().replace('__prefix__', str(new_total_formsets))
        html += str(form).replace('__prefix__', str(new_total_formsets))
    
    return mark_safe(html)

Примечание к помощнику build_new_formset(): formset.empty_form будет опускать номера индексов, которые должны идти в атрибутах id, name и label, и вместо этого будет использовать "__prefix__". Вы хотите заменить эту часть "__prefix__" на соответствующее число. Например, если это второй набор форм на странице, его id должен быть id_form-1-title (заменен с id_form-__prefix__-title).

formset.html

<form action="{% url 'app2:formset_view' %}" method="post">
  {% csrf_token %}
  {{ formset.management_form }}

  {% for form in formset %}
  <p>{{ form }}</p>    
  {% endfor %}

  <button type="button" 
          hx-trigger="click"
          hx-get="{% url 'app2:add_formset' formset.total_form_count %}"
          hx-swap="outerHTML">
    Add formset
  </button> 

  <input type="submit" value="Submit">
</form>

formset_partial.html

<input hx-swap-oob="true" 
       type="hidden" 
       name="form-TOTAL_FORMS" 
       value="{{ new_total_formsets }}" 
       id="id_form-TOTAL_FORMS">

<p>{{ new_formset }}</p>

<button type="button" 
        hx-trigger="click"
        hx-get="{% url 'app2:add_formset' new_total_formsets %}"  
        hx-swap="outerHTML">
  Add formset
</button>

Примечание по поводу скрытого input: С каждым новым добавленным набором форм, value элемента input, который имеет id="id_form-TOTAL_FORMS", больше не будет отражать фактическое количество наборов форм на странице. Вы можете отправить новый скрытый input с вашим набором форм и включить в него hx-swap-oob="true". Htmx затем заменит старый на новый

Ссылка на документы: https://docs.djangoproject.com/en/4.1/topics/forms/formsets/

Мой подход к работе с динамическими наборами форм с помощью могущественного HTMX:

Вид, который отображает empty_form экземпляра набора форм (т.е. formset.empty_form). Это вернет форму с __prefix__ в той части, которая обычно содержит номер формы (т. е. вместо form-0-field_name будет form-__prefix__-field_name). Хитрость заключается в том, чтобы просто заменить __prefix__ на соответствующий номер формы.

@В ответе Мэтта это достигается путем итерации по форме и выполнения соответствующей замены. Хотя это работает хорошо, я думаю, что еще более простой способ - просто изменить префикс набора форм после его инстанцирования. Таким образом, вы не будете использовать вспомогательную функцию build_new_formset.

На стороне клиента: Рендеринг sformset.management_form в plan_forms.html. И включите значение form-TOTAL_FORMS [скрытого поля ввода] набора форм в GET-запрос HTMX с помощью атрибута hx-vals (пожалуйста, обратите внимание, что я использую селектор jquery, это просто для удобства. Ванильный JS тоже должен работать).

plan_forms.html

<form method="POST">
   {% csrf_token %}
   {{sformset.management_form}} 
        ...
      <tbody style="user-select: none;" id="structureforms" hx-sync="closest form:queue">
        <!--Structure formset goes here-->
      </tbody>
      <tfoot>
        <button hx-get="{% url 'structure-form' %}" 
           hx-swap="beforeend" 
           hx-target="#structureforms"
           hx-get="{% url 'structure-form' %}"
           hx-vals='js:{totalForms: $("#id_form-TOTAL_FORMS").val()}' \\ New>
          Add structure <!--Button to call structure formset-->
        </button>
      </tfoot>
        ...
</form>

После запроса htmx необходимо обновить общее количество форм в наборе форм (увеличивается при каждом добавлении набора форм). Если вы хотите строго использовать HTMX, вы можете использовать атрибут hx-on, который позволяет слушать события и запускать inline JS. Однако я нахожу hyperscript более удобным для этого атрибут , поскольку он является командой increment.

plan_forms.html

<form method="POST">
        ...
      <tbody style="user-select: none;"
             id="structureforms" 
             hx-sync="closest form:queue"
             _="on htmx:afterSettle increment #id_form-TOTAL_FORMS's value">
        <!--Structure formset goes here-->
      </tbody>
       ...
</form>

Затем вы можете обработать запрос в своем представлении: views.py

      ...
def createPlan(request): #Requst for the Parent form 
    form = PlanForm(request.POST or None)
    sformset = StructureFormset(request.POST or None) #Nesting the Child formset
      ...

def addStructure(request):
    sformset = StructureFormset(queryset=Structure.objects.none()).empty_form # Creates an empty formset
    form_number = int(request.GET.get("totalForms")) - 1 # forms are numbered from 0, so they will always be 1 less than formset's total forms.
    sformset.prefix = sformset.prefix.replace("__prefix__", str(form_number)) # override the formset's prefix, replacing the __prefix__
    context = {"sformset":sformset}
    return render(request, 'app/formsets/structure_form.html', context)

Я думаю, что это практически все.

Notes Вместо hx-vals можно использовать hx-include

...
hx-include='[name="form-TOTAL_FORMS"]'
...

А в представлении addStructure вам придется получить значение с помощью request.GET.get("form0TOTAL_FORMS")

Вернуться на верх