Предварительно заполнить встроенный FormSet? - PullRequest
42 голосов
/ 14 января 2009

Я работаю над формой входа для группы. Моя идея состоит в том, чтобы иметь раздел формы для ввода информации о событии для выступления или репетиции. Вот модель таблицы событий:

class Event(models.Model):
    event_id = models.AutoField(primary_key=True)
    date = models.DateField()
    event_type = models.ForeignKey(EventType)
    description = models.TextField()

Тогда я бы хотел иметь встроенный FormSet, который связывает участников группы с событием и записывает, присутствовали ли они, отсутствовали или извинялись:

class Attendance(models.Model):
    attendance_id = models.AutoField(primary_key=True)
    event_id = models.ForeignKey(Event)
    member_id = models.ForeignKey(Member)
    attendance_type = models.ForeignKey(AttendanceType)
    comment = models.TextField(blank=True)

Теперь я хотел бы предварительно заполнить этот встроенный FormSet записями для всех текущих членов и по умолчанию их присутствие (около 60 членов). К сожалению, Django не допускает начальные значения в этом случае.

Есть предложения?

Ответы [ 10 ]

31 голосов
/ 14 января 2009

Итак, вам не понравится ответ, отчасти потому, что я еще не закончил писать код, а отчасти потому, что много работы.

Что вам нужно сделать, как я обнаружил, когда столкнулся с этим сам, это:

  1. Потратьте много времени на чтение кода набора форм и модели-набора форм, чтобы понять, как все это работает (не помогает тот факт, что некоторые функции работают на классах набора форм, а некоторые - на фабрике). функции, которые выплевывают их). Эти знания понадобятся вам на следующих этапах.
  2. Напишите свой собственный класс formset, который подклассы из BaseInlineFormSet и принимает initial. Действительно хитрый момент в том, что вы должны переопределить __init__(), и вы должны убедиться, что он вызывает до BaseFormSet.__init__() вместо использования прямого родителя или деда __init__() (поскольку это BaseInlineFormSet и BaseModelFormSet, соответственно, и ни один из них не может обрабатывать исходные данные).
  3. Напишите свой собственный подкласс соответствующего встроенного класса администратора (в моем случае это был TabularInline) и переопределите его get_formset метод, чтобы вернуть результат inlineformset_factory(), используя ваш пользовательский класс formset.
  4. На фактическом подклассе ModelAdmin для модели со встроенными элементами переопределите add_view и change_view и скопируйте большую часть кода, но с одним большим изменением: соберите исходные данные, которые понадобятся вашему набору форм, и передайте в ваш пользовательский набор форм (который будет возвращен вашим ModelAdmin get_formsets() методом).

У меня было несколько продуктивных чатов с Брайаном и Джозефом об улучшении этого для будущих выпусков Django; на данный момент способ работы модельных наборов просто создает больше проблем, чем обычно, но с небольшой очисткой API, я думаю, это можно сделать чрезвычайно просто.

20 голосов
/ 22 сентября 2010

Я потратил немало времени, пытаясь найти решение, которое я мог бы повторно использовать на разных сайтах. Пост Джеймса содержал ключевую мудрость в расширении BaseInlineFormSet, но в стратегическом плане призывы против BaseFormSet.

Решение, приведенное ниже, разбито на две части: a AdminInline и a BaseInlineFormSet.

  1. InlineAdmin динамически генерирует начальное значение на основе выставленного объекта запроса.
  2. Он использует каррирование для предоставления начальных значений пользовательскому BaseInlineFormSet через аргументы ключевых слов, передаваемые в конструктор.
  3. Конструктор BaseInlineFormSet извлекает начальные значения из списка аргументов ключевого слова и обычно конструирует.
  4. Последняя часть переопределяет процесс создания формы, изменяя максимальное общее количество форм и используя методы BaseFormSet._construct_form и BaseFormSet._construct_forms

Вот некоторые конкретные фрагменты, использующие классы ОП. Я проверил это против Django 1.2.3. Я настоятельно рекомендую при разработке разрабатывать документацию formset и admin .

admin.py

from django.utils.functional import curry
from django.contrib import admin
from example_app.forms import *
from example_app.models import *

class AttendanceInline(admin.TabularInline):
    model           = Attendance
    formset         = AttendanceFormSet
    extra           = 5

    def get_formset(self, request, obj=None, **kwargs):
        """
        Pre-populating formset using GET params
        """
        initial = []
        if request.method == "GET":
            #
            # Populate initial based on request
            #
            initial.append({
                'foo': 'bar',
            })
        formset = super(AttendanceInline, self).get_formset(request, obj, **kwargs)
        formset.__init__ = curry(formset.__init__, initial=initial)
        return formset

forms.py

from django.forms import formsets
from django.forms.models import BaseInlineFormSet

class BaseAttendanceFormSet(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        """
        Grabs the curried initial values and stores them into a 'private'
        variable. Note: the use of self.__initial is important, using
        self.initial or self._initial will be erased by a parent class
        """
        self.__initial = kwargs.pop('initial', [])
        super(BaseAttendanceFormSet, self).__init__(*args, **kwargs)

    def total_form_count(self):
        return len(self.__initial) + self.extra

    def _construct_forms(self):
        return formsets.BaseFormSet._construct_forms(self)

    def _construct_form(self, i, **kwargs):
        if self.__initial:
            try:
                kwargs['initial'] = self.__initial[i]
            except IndexError:
                pass
        return formsets.BaseFormSet._construct_form(self, i, **kwargs)

AttendanceFormSet = formsets.formset_factory(AttendanceForm, formset=BaseAttendanceFormSet)
14 голосов
/ 07 мая 2013

Django 1.4 и выше поддерживает , обеспечивая начальные значения .

С точки зрения исходного вопроса, будет работать следующее:

class AttendanceFormSet(models.BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        super(AttendanceFormSet, self).__init__(*args, **kwargs)
        # Check that the data doesn't already exist
        if not kwargs['instance'].member_id_set.filter(# some criteria):
            initial = []
            initial.append({}) # Fill in with some data
            self.initial = initial
            # Make enough extra formsets to hold initial forms
            self.extra += len(initial)

Если вы обнаружите, что формы заполняются, но не сохраняются, вам может потребоваться настроить форму модели. Самый простой способ - передать тег в исходных данных и найти его в форме init:

class AttendanceForm(forms.ModelForm):
    def __init__(self, *args, **kwargs):
        super(AttendanceForm, self).__init__(*args, **kwargs)
        # If the form was prepopulated from default data (and has the
        # appropriate tag set), then manually set the changed data
        # so later model saving code is activated when calling
        # has_changed().
        initial = kwargs.get('initial')
        if initial:
            self._changed_data = initial.copy()

    class Meta:
        model = Attendance
3 голосов
/ 07 февраля 2015

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

Я предложил другое решение для ввода данных в ModelForm, передаваемых в набор форм. Поскольку в python вы можете динамически создавать классы, вместо того, чтобы пытаться передавать данные непосредственно через конструктор формы, класс может быть создан методом с любыми параметрами, которые вы хотите передать. Затем, когда создается экземпляр класса, он получает доступ к методу. параметры.

def build_my_model_form(extra_data):
    return class MyModelForm(forms.ModelForm):
        def __init__(self, *args, **kwargs):
            super(MyModelForm, self).__init__(args, kwargs)
            # perform any setup requiring extra_data here

        class Meta:
            model = MyModel
            # define widgets here

Тогда вызов фабрики встроенных форм будет выглядеть так:

inlineformset_factory(ParentModel, 
                      MyModel, 
                      form=build_my_model_form(extra_data))
3 голосов
/ 06 ноября 2009

Я столкнулся с той же проблемой.

Вы можете сделать это через JavaScript, создать простой JS, который выполняет вызов ajax для всех членов группы и заполняет форму.

В этом решении отсутствует принцип СУХОГО, потому что вы должны писать это для каждой имеющейся у вас встроенной формы.

2 голосов
/ 28 апреля 2016

Вы можете переопределить метод empty_form для набора форм. Вот пример того, как мне справиться с этим в сочетании с администратором django:

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset
2 голосов
/ 06 июня 2015

Я столкнулся с этим вопросом через 6 лет, и сейчас мы находимся на Django 1.8.

До сих пор не совсем чистый, короткий ответ на вопрос.

Проблема заключается в ModelAdmin._create_formsets () github ; Мое решение состоит в том, чтобы переопределить его и вставить нужные мне исходные данные где-нибудь вокруг выделенных строк в ссылке github.

Мне также пришлось переопределить InlineModelAdmin.get_extra (), чтобы "иметь место" для предоставленных исходных данных. По умолчанию слева отображаются только 3 исходные данные

Я полагаю, что в следующих версиях должен быть более четкий ответ

0 голосов
/ 20 мая 2016

У меня такая же проблема. Я использую Django 1.9 и попробовал решение, предложенное Simanas, переопределив свойство "empty_form", добавив некоторые значения по умолчанию в de dict initial. Это сработало, но в моем случае у меня было 4 дополнительных встроенных формы, всего 5, и только одна из пяти форм была заполнена исходными данными.

Я изменил код следующим образом (см. Исходный текст):

class MyFormSet(forms.models.BaseInlineFormSet):
    model = MyModel

    @property
    def empty_form(self):
        initial = {'model_attr_name':'population_value'}
        if self.parent_obj:
            initial['name'] = self.parent_obj.default_child_name
        form = self.form(
            auto_id=self.auto_id,
            prefix=self.add_prefix('__prefix__'),
            empty_permitted=True, initial=initial
        )
        self.add_fields(form, None)
        return form    

class MyModelInline(admin.StackedInline):
    model = MyModel
    formset = MyFormSet

    def get_formset(self, request, obj=None, **kwargs):    
        formset = super(HostsSpaceInline, self).get_formset(request, obj, **kwargs)
        formset.parent_obj = obj
        return formset

Если мы найдем способ заставить его работать при наличии дополнительных форм, это решение будет хорошим обходным путем.

0 голосов
/ 19 декабря 2013

Просто переопределите метод «save_new», он работал для меня в Django 1.5.5:

class ModelAAdminFormset(forms.models.BaseInlineFormSet):
    def save_new(self, form, commit=True):
        result = super(ModelAAdminFormset, self).save_new(form, commit=False)
        # modify "result" here
        if commit:
            result.save()
        return result
0 голосов
/ 08 марта 2010

Вот как я решил проблему. Существует немного компромисса в создании и удалении записей, но код чистый ...

def manage_event(request, event_id):
    """
    Add a boolean field 'record_saved' (default to False) to the Event model
    Edit an existing Event record or, if the record does not exist:
    - create and save a new Event record
    - create and save Attendance records for each Member
    Clean up any unsaved records each time you're using this view
    """
    # delete any "unsaved" Event records (cascading into Attendance records)
    Event.objects.filter(record_saved=False).delete()
    try:
        my_event = Event.objects.get(pk=int(event_id))
    except Event.DoesNotExist:
        # create a new Event record
        my_event = Event.objects.create()
        # create an Attendance object for each Member with the currect Event id
        for m in Members.objects.get.all():
            Attendance.objects.create(event_id=my_event.id, member_id=m.id)
    AttendanceFormSet = inlineformset_factory(Event, Attendance, 
                                        can_delete=False, 
                                        extra=0, 
                                        form=AttendanceForm)
    if request.method == "POST":
        form = EventForm(request.POST, request.FILES, instance=my_event)
        formset = AttendanceFormSet(request.POST, request.FILES, 
                                        instance=my_event)
        if formset.is_valid() and form.is_valid():
            # set record_saved to True before saving
            e = form.save(commit=False)
            e.record_saved=True
            e.save()
            formset.save()
            return HttpResponseRedirect('/')
    else:
        form = EventForm(instance=my_event)
        formset = OptieFormSet(instance=my_event)
    return render_to_response("edit_event.html", {
                            "form":form, 
                            "formset": formset,
                            }, 
                            context_instance=RequestContext(request))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...