Подтвердить что ты не робот

Предварительно заполнить встроенный FormSet?

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

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 не допускает начальных значений в этом случае.

Любые предложения?

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

Вот некоторые конкретные фрагменты, использующие классы OP. Я протестировал это против 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)

Ответ 3

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

Ответ 4

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

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

В этом решении отсутствует принцип DRY, потому что вам нужно написать это для каждой встроенной формы.

Ответ 5

Используя 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

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

inlineformset_factory(ParentModel, 
                      MyModel, 
                      form=build_my_model_form(extra_data))

Ответ 6

Я столкнулся с этим вопросом -6 лет спустя, и теперь мы на Django 1.8.

По-прежнему нет абсолютно чистого, короткого ответа на вопрос.

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

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

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

Ответ 7

Вы можете переопределить get_form getter на formet. Вот пример того, как я могу справиться с этим в сочетании с администратором 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

Ответ 8

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

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))

Ответ 9

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

Я изменил код, подобный этому (см. первоначальный dict):

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

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

Ответ 10

Просто переопределить метод "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