Django - CreateView не зберігає форму з вкладеним набором форматів


14

Я намагаюся адаптувати підхід для збереження вкладених наборів форматів з основною формою за допомогою функції макета Django-Crispy-Forms, але я не можу її зберегти. Я слідую за цим прикладом кодового проекту, але не вдалося отримати перевірку наборів для збереження даних. Я буду дуже вдячний, якщо хтось може вказати на мою помилку. Мені також потрібно додати три рядки в одному поданні для EmployeeForm. Я спробував Django-Extra-Views, але не зміг зробити це. Будемо вдячні, якщо ви радите додати більше одного рядка для того ж перегляду, як приблизно 5. Все, що я хочу досягти, щоб створити одну сторінку для створення Employeeта її вкладки Education, Experience, Others. Нижче наведено код:

моделі:

class Employee(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employees',
                                null=True, blank=True)
    about = models.TextField()
    street = models.CharField(max_length=200)
    city = models.CharField(max_length=200)
    country = models.CharField(max_length=200)
    cell_phone = models.PositiveIntegerField()
    landline = models.PositiveIntegerField()

    def __str__(self):
        return '{} {}'.format(self.id, self.user)

    def get_absolute_url(self):
        return reverse('bars:create', kwargs={'pk':self.pk})

class Education(models.Model):
    employee = models.ForeignKey('Employee', on_delete=models.CASCADE, related_name='education')
    course_title = models.CharField(max_length=100, null=True, blank=True)
    institute_name = models.CharField(max_length=200, null=True, blank=True)
    start_year = models.DateTimeField(null=True, blank=True)
    end_year = models.DateTimeField(null=True, blank=True)

    def __str__(self):
        return '{} {}'.format(self.employee, self.course_title)

Вид:

class EmployeeCreateView(CreateView):
    model = Employee
    template_name = 'bars/crt.html'
    form_class = EmployeeForm
    success_url = None

    def get_context_data(self, **kwargs):
        data = super(EmployeeCreateView, self).get_context_data(**kwargs)
        if self.request.POST:
            data['education'] = EducationFormset(self.request.POST)
        else:
            data['education'] = EducationFormset()
        print('This is context data {}'.format(data))
        return data


    def form_valid(self, form):
        context = self.get_context_data()
        education = context['education']
        print('This is Education {}'.format(education))
        with transaction.atomic():
            form.instance.employee.user = self.request.user
            self.object = form.save()
            if education.is_valid():
                education.save(commit=False)
                education.instance = self.object
                education.save()

        return super(EmployeeCreateView, self).form_valid(form)

    def get_success_url(self):
        return reverse_lazy('bars:detail', kwargs={'pk':self.object.pk})

Форми:

class EducationForm(forms.ModelForm):
    class Meta:
        model = Education
        exclude = ()
EducationFormset =inlineformset_factory(
    Employee, Education, form=EducationForm,
    fields=['course_title', 'institute_name'], extra=1,can_delete=True
    )

class EmployeeForm(forms.ModelForm):

    class Meta:
        model = Employee
        exclude = ('user', 'role')

    def __init__(self, *args, **kwargs):
        super(EmployeeForm, self).__init__(*args, **kwargs)
        self.helper = FormHelper()
        self.helper.form_tag = True
        self.helper.form_class = 'form-horizontal'
        self.helper.label_class = 'col-md-3 create-label'
        self.helper.field_class = 'col-md-9'
        self.helper.layout = Layout(
            Div(
                Field('about'),
                Field('street'),
                Field('city'),
                Field('cell_phone'),
                Field('landline'),
                Fieldset('Add Education',
                    Formset('education')),
                HTML("<br>"),
                ButtonHolder(Submit('submit', 'save')),
                )
            )

Спеціальний макет об'єкта, як на прикладі:

from crispy_forms.layout import LayoutObject, TEMPLATE_PACK
from django.shortcuts import render
from django.template.loader import render_to_string

class Formset(LayoutObject):
    template = "bars/formset.html"

    def __init__(self, formset_name_in_context, template=None):
        self.formset_name_in_context = formset_name_in_context
        self.fields = []
        if template:
            self.template = template

    def render(self, form, form_style, context, template_pack=TEMPLATE_PACK):
        formset = context[self.formset_name_in_context]
        return render_to_string(self.template, {'formset': formset})

Formset.html:

{% load static %}
{% load crispy_forms_tags %}
{% load staticfiles %}

<table>
{{ formset.management_form|crispy }}

    {% for form in formset.forms %}
            <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
                {% for field in form.visible_fields %}
                <td>
                    {# Include the hidden fields in the form #}
                    {% if forloop.first %}
                        {% for hidden in form.hidden_fields %}
                            {{ hidden }}
                        {% endfor %}
                    {% endif %}
                    {{ field.errors.as_ul }}
                    {{ field|as_crispy_field }}
                </td>
                {% endfor %}
            </tr>
    {% endfor %}

</table>
<br>
<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="{% static 'js/jquery.formset.js' %}">
</script>
<script type="text/javascript">
    $('.formset_row-{{ formset.prefix }}').formset({
        addText: 'add another',
        deleteText: 'remove',
        prefix: '{{ formset.prefix }}',
    });
</script>

Немає помилок у терміналі та чи іншим чином. Допомога дуже цінується.


Альтернативним рішенням є також форма для обробки набору форматів: я роблю це за допомогою кешованої власності для відповідного набору форм у schinckel.net/2019/05/23/form-and-formset
Matthew Schinckel

Відповіді:


0

Наразі ви не обробляєте набір форматів належним чином у вашому CreateView. form_validу цьому представленні буде оброблятися лише батьківська форма, а не набори форм. Що ви повинні зробити, це перекрити postметод, і там вам потрібно перевірити як форму, так і будь-які набори форм, що додаються до неї:

def post(self, request, *args, **kwargs):
    form = self.get_form()
    # Add as many formsets here as you want
    education_formset = EducationFormset(request.POST)
    # Now validate both the form and any formsets
    if form.is_valid() and education_formset.is_valid():
        # Note - we are passing the education_formset to form_valid. If you had more formsets
        # you would pass these as well.
        return self.form_valid(form, education_formset)
    else:
        return self.form_invalid(form)

Потім ви модифікуєте form_validтак:

def form_valid(self, form, education_formset):
    with transaction.atomic():
        form.instance.employee.user = self.request.user
        self.object = form.save()
        # Now we process the education formset
        educations = education_formset.save(commit=False)
        for education in educations:
            education.instance = self.object
            education.save()
        # If you had more formsets, you would accept additional arguments and
        # process them as with the one above.
    # Don't call the super() method here - you will end up saving the form twice. Instead handle the redirect yourself.
    return HttpResponseRedirect(self.get_success_url())

Це get_context_data()означає, що ви зараз використовуєте невірно - видаліть цей метод повністю. Його слід використовувати лише для отримання контекстних даних для візуалізації шаблону. Ви не повинні називати це зі свого form_valid()методу. Замість цього вам потрібно передати набір форматів цього методу з post()методу, як описано вище.

У прикладі коду я залишив кілька додаткових коментарів, які, сподіваємось, допоможуть вам розібратися в цьому.


Будь ласка, створіть приклад локально, перш ніж відповісти. Я спробував твій твір, але не працював.
Шазія Нусрат

1
@ShaziaNusrat Вибачте, у мене немає часу спробувати розробити те, що не працює для вас, особливо якщо ви не скажете, що ви намагалися, а що не вийшло ("Це не працює" - це не адекватний опис того, що не вийшло). Я вважаю, що в моїй відповіді достатньо, щоб допомогти вам визначити, що потрібно змінити під час поточної реалізації. Якщо ні, то сподіваємось, що хтось інший зможе дати вам більш вичерпну відповідь.
solarissmoke

Я спробував це в коді для тестування, і він зіткнувся з проблемами. Ось чому я смиренно прошу вас спробувати це на вашому боці локально, щоб ви могли мене краще направляти. Я вдячний за те, що ви зайняли час, щоб допомогти мені. Але не працює.
Шазія Нусрат

0

Можливо, ви хотіли б побачити пакет django-extra-views, надає вигляд CreateWithInlinesView, відьма дозволяє створити форму з вкладеними рядками, такими як Django-admin inlines.

У вашому випадку це було б щось подібне:

views.py

class EducationInline(InlineFormSetFactory):
    model = Education
    fields = ['course_title', 'institute_name']


class EmployeeCreateView(CreateWithInlinesView):
    model = Employee
    inlines = [EducationInline,]
    fields = ['about', 'street', 'city', 'cell_phone', 'landline']
    template_name = 'bars/crt.html'

crt.html

<form method="post">
  ...
  {{ form }}
  <table>
  {% for formset in inlines %}
    {{ formset.management_form }}
      {% for inline_form in formset %}
        <tr class="{% cycle 'row1' 'row2' %} formset_row-{{ formset.prefix }}">
          {{ inline_form }}
        </tr>
      {% endfor %}
  {% endfor %}
  </table>
  ...
  <input type="submit" value="Submit" />
</form>

<script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script src="{% static 'js/jquery.formset.js' %}">
</script>
<script type="text/javascript">
    {% for formset in inlines %}
      $('.formset_row-{{ formset.prefix }}').formset({
          addText: 'add another',
          deleteText: 'remove',
          prefix: '{{ formset.prefix }}',
      });
    {% endfor %}
</script>

Перегляд EmployeeCreateViewобробляє форми для вас, як у Django-admin. З цього моменту ви можете застосувати потрібний стиль до форм.

Рекомендую відвідати документацію для отримання додаткової інформації

ЗМІНЕНО: Я додав management_form і js кнопки для додавання / видалення.


Я вже спробував це, але це не дозволить мені додати / видалити кнопки для кількох рядків. Він підтримує лише одну вбудовану за допомогою кнопок JS. Я вже пробував це.
Шазія Нусрат

1
Це підтримує, ви повинні додати management_formдля кожногоformset
Іван

0

Ви сказали, що є помилка, але ви її не показуєте у своєму запитанні. Помилка (і весь трекбек) важливіша за все, що ви написали (за винятком випадків, що це від form.py та views.py)

Ваш випадок трохи складніше через набір форм та використання декількох форм на одному CreateView. В Інтернеті не так багато (чи не багато хороших) прикладів. Доки ви не копаєте код django про те, як працюють набори форматів, у вас виникнуть проблеми.

Гаразд прямо до точки. Ваша проблема полягає в тому, що набори форм не ініціалізуються тим самим екземпляром, що і ваша основна форма. І коли ваша форма amin зберігає дані до бази даних, екземпляр у наборі форматів не змінюється, і в кінці у вас немає ідентифікатора основного об'єкта для того, щоб бути поміщеним як зовнішній ключ. Зміна атрибута екземпляра атрибуту форми після init - це не дуже гарна ідея.

У звичайних формах, якщо ви зміните це після is_valid, у вас будуть непередбачувані результати. Для наборів форм, що змінюють атрибут екземпляра, навіть безпосередньо після того, як init не збігатиметься, бо форми у наборі форм вже ініціалізовані з деяким екземпляром, а зміна його після не допоможе. Хороша новина полягає в тому, що ви можете змінювати атрибути екземпляра після ініціалізації Formset, оскільки всі атрибути екземпляра форм будуть вказувати на один і той же об'єкт після ініціалізації набору форм.

У вас є два варіанти:

Замість встановлення атрибута екземпляра, якщо набір форм, встановіть лише instance.pk. (Це лише здогадка, я ніколи цього не робив, але я думаю, що це має спрацювати. Проблема в тому, що це буде виглядати як хак). Створіть форму, яка буде ініціалізувати всі форми / набори форм одразу. Коли метод is_valid () називається, всі фоми повинні бути перевірені. Коли метод save () викликається, всі форми мають бути збережені. Тоді вам потрібно встановити атрибут form_class вашого CreateView для цього класу форм. Єдина складна частина полягає в тому, що після ініціалізації вашої основної форми вам потрібно ініціалізувати інші (формати) з екземпляром першої форми. Також вам потрібно встановити форми / набори форм як атрибути вашої форми, щоб мати доступ до них у шаблоні. Я використовую другий підхід, коли мені потрібно створити об’єкт з усіма пов'язаними з ним об’єктами.

ініціалізована з деякими даними (у цьому випадку дані POST), перевіреними на дійсність, з is_valid (), може бути збережена за допомогою save (), коли вона дійсна. Ви зберігаєте інтерфейс форми, і якщо ви правильно зробили форму, ви навіть можете використовувати її не тільки для створення, але й для оновлення об'єктів разом із пов’язаними з ними об’єктами, і представлення буде дуже простим.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.