django формы: редактирование нескольких наборов связанных объектов в одной форме - PullRequest
6 голосов
/ 01 декабря 2011

Я пытаюсь сделать что-то, что должно быть очень распространено: добавить / отредактировать кучу связанных моделей в одной форме.Например:

Visitor Details:
Select destinations and activities:
    Miami  []   -  swimming [], clubbing [], sunbathing[]
    Cancun []   -  swimming [], clubbing [], sunbathing[]

Моими моделями являются Visitor, Destination и Activity, при этом Visitor имеет поле ManyToMany в Destination через посредническую модель VisitorDestination, в которой содержатся подробные сведения о действиях, которые необходимо выполнить в пункте назначения (само по себе поле ManyToMany в Activity).

Visitor ---->(M2M though VisitorDestination) -------------> Destination
                                            |
                       activities            ---->(M2M)---> Activity  

Обратите внимание, что я не хочу вводить новые значения назначения / активности, просто выберите из доступныхв БД (но это совершенно законное использование полей M2M, верно?)

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

Я провел несколько дней в поисках Django docs / SO / googling, но не смог работатькак с этим бороться.Я попробовал несколько подходов:

  1. Форма настраиваемой модели для посетителя, где я добавляю поля с несколькими вариантами выбора для пункта назначения и действия.Это работает нормально, если пункт назначения и действие можно выбрать независимо, но здесь они связаны , т.е. я хочу выбрать одно или несколько действий для каждое место назначения

  2. Использование inlineformset_factory для генерации набора форм назначения / действий, с inlineformset_factory(Destination, Visitor).Это нарушается, поскольку у посетителя есть отношение M2M к назначению, а не FK.

  3. Настройка простого набора форм с использованием formset_factory, например DestinationActivityFormSet = formset_factory(DestinationActivityForm, extra=2).А как оформить DestinationActivityForm?Я недостаточно изучил это, но это не выглядит многообещающе: я не хочу вводить пункт назначения и список действий, мне нужен список флажков с метками, установленными для пункта назначения / действий, которые я хочучтобы выбрать, но formset_factory вернет список форм с одинаковыми метками.

Я полный новичок с django, так что, возможно, решение очевидно, но я считаю, чтодокументация в этой области очень слабая - если у кого-то есть ссылки на примеры использования форм / наборов форм, которые также были бы полезны

спасибо!

Ответы [ 3 ]

7 голосов
/ 03 декабря 2011

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

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

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

Первые пользовательские формы:

class VisitorForm(ModelForm):
    class Meta:
      model = Visitor
      exclude = ['destinations']

class VisitorDestinationForm(Form):
    visited = forms.BooleanField(required=False)
    activities = forms.MultipleChoiceField(choices = [(obj.pk, obj.name) for obj in Activity.objects.all()], required=False, 
                                                      widget = CheckboxSelectMultipleInline(attrs={'style' : 'display:inline'}))

    def __init__(self, visitor, destination, visited,  *args, **kwargs):
        super(VisitorDestinationForm, self).__init__(*args, **kwargs)
        self.destination = destination
        self.fields['visited'].initial = visited
        self.fields['visited'].label= destination.destination

        # load initial choices for activities
        activities_initial = []
        try:
            visitorDestination_entry = VisitorDestination.objects.get(visitor=visitor, destination=destination)
            activities = visitorDestination_entry.activities.all()
            for activity in Activity.objects.all():
                if activity in activities: 
                    activities_initial.append(activity.pk)
        except VisitorDestination.DoesNotExist:
            pass
        self.fields['activities'].initial = activities_initial

Я настраиваю каждую форму, передавая объекты Visitor и Destination (и флаг "посещен", который для удобства вычисляется снаружи)

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

Действия обрабатываются обычным MultipleChoiceField (я использовал настроенный виджет, чтобы флажки отображались на столе, довольно просто, но могу опубликовать его, если это кому-то нужно)

Тогда код просмотра:

def edit_visitor(request, pk):
    visitor_obj = Visitor.objects.get(pk=pk)
    visitorDestinations = visitor_obj.destinations.all()
    if request.method == 'POST':
        visitorForm = VisitorForm(request.POST, instance=visitor_obj)

        # set up the visitor destination forms
        destinationForms = []
        for destination in Destination.objects.all():
            visited = destination in visitorDestinations
            destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited, request.POST, prefix=destination.destination))

        if visitorForm.is_valid() and all([form.is_valid() for form in destinationForms]):
            visitor_obj = visitorForm.save()
            # clear any existing entries,
            visitor_obj.destinations.clear()
            for form in destinationForms:
                if form.cleaned_data['visited']: 
                    visitorDestination_entry = VisitorDestination(visitor = visitor_obj, destination=form.destination)
                    visitorDestination_entry.save()
                    for activity_pk in form.cleaned_data['activities']: 
                        activity = Activity.objects.get(pk=activity_pk)
                        visitorDestination_entry.activities.add(activity)
                    print 'activities: %s' % visitorDestination_entry.activities.all()
                    visitorDestination_entry.save()

            success_url = reverse('visitor_detail', kwargs={'pk' : visitor_obj.pk})
            return HttpResponseRedirect(success_url)
    else:
        visitorForm = VisitorForm(instance=visitor_obj)
        # set up the visitor destination forms
        destinationForms = []
        for destination in Destination.objects.all():
            visited = destination in visitorDestinations
            destinationForms.append(VisitorDestinationForm(visitor_obj, destination, visited,  prefix=destination.destination))

    return render_to_response('testapp/edit_visitor.html', {'form': visitorForm, 'destinationForms' : destinationForms, 'visitor' : visitor_obj}, context_instance= RequestContext(request))

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

Я оставлю вопрос открытым на несколько дней, если у кого-нибудь есть более чистый метод.

Спасибо!

1 голос
/ 01 декабря 2011

Итак, как вы видели, одна из особенностей inlineformset_factory состоит в том, что она ожидает две модели - родительскую и дочернюю, которые имеют отношение внешнего ключа к родительскому. Как передать дополнительные данные на лету в форму для дополнительных данных в промежуточной модели?

Как мне это сделать, используя карри:

from django.utils.functional import curry

from my_app.models import ParentModel, ChildModel, SomeOtherModel

def some_view(request, child_id, extra_object_id):
    instance = ChildModel.objects.get(pk=child_id)
    some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id)

    MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm)

    #This is where the object "some_extra_model" gets passed to each form via the
    #static method
    MyFormset.form = staticmethod(curry(ChildModelForm,
        some_extra_model=some_extra_model))

    formset = MyFormset(request.POST or None, request.FILES or None,
        queryset=SomeObject.objects.filter(something=something), instance=instance)

Класс формы "ChildModelForm" должен иметь переопределение инициализации, которое добавляет объект "some_extra_model" из аргументов:

def ChildModelForm(forms.ModelForm):
    class Meta:
        model = ChildModel

    def __init__(self, some_extra_model, *args, **kwargs):
        super(ChildModelForm, self).__init__(*args, **kwargs)
        #do something with "some_extra_model" here

Надеюсь, это поможет вам встать на правильный путь.

0 голосов
/ 18 мая 2017

В django 1.9 есть поддержка передачи пользовательских параметров в формы набора форм: https://docs.djangoproject.com/en/1.9/topics/forms/formsets/#passing-custom-parameters-to-formset-forms

Просто добавьте form_kwargs к вашей инициализации FormSet следующим образом:

from my_app.models import ParentModel, ChildModel, SomeOtherModel

def some_view(request, child_id, extra_object_id):
    instance = ChildModel.objects.get(pk=child_id)
    some_extra_model = SomeOtherModel.objects.get(pk=extra_object_id)

    MyFormset = inlineformset_factory(ParentModel, ChildModel, form=ChildModelForm)
    formset = MyFormset(request.POST or None, request.FILES or None,
        queryset=SomeObject.objects.filter(something=something), instance=instance,
        form_kwargs={"some_extra_model": some_extra_model})
...