Предварительно заполнить встроенный 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 не допускает начальных значений в этом случае.
Любые предложения?
Ответы
Ответ 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