2009-01-14 17 views
38

Estoy trabajando en un formulario de inscripción de asistencia para una banda. Mi idea es tener una sección del formulario para ingresar la información del evento para una presentación o ensayo. Aquí está el modelo de la tabla de eventos:¿Rellene previamente un FormSet en línea?

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

Entonces me gustaría tener un FormSet línea que une a los miembros de la banda para el evento y los registros si estaban presentes, ausentes, o justificadas:

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) 

Ahora, lo que me gustaría hacer es rellenar previamente este FormSet en línea con entradas para todos los miembros actuales y establecerlos por defecto para que estén presentes (alrededor de 60 miembros). Desafortunadamente, Django doesn't allow initial values in this case.

¿Alguna sugerencia?

+4

Esta pregunta tiene un par de años. ¿Hay una solución más fácil ahora en django 1.3? – monkut

+1

Sí, está resuelto. Solo pase la instancia relacionada en el parámetro y lo rellenará previamente. –

+12

¿Alguien sabe de una referencia en cualquier lugar sobre cómo lograr lo mencionado anteriormente "simplemente pase la instancia relacionada en el parámetro y lo rellenará previamente"? – i3enhamin

Respuesta

28

Por lo tanto, no le va a gustar la respuesta, en parte porque aún no he terminado de escribir el código y en parte porque es mucho trabajo.

Lo que hay que hacer, como descubrí cuando me encontré con esto mismo, es:

  1. pasar mucho tiempo leyendo a través del código de juego de formularios y modelo de juego de formularios para tener una idea de cómo todo funciona (no ayudado por el hecho de que parte de la funcionalidad vive en las clases de formset, y parte de ella vive en funciones de fábrica que las escupen). Necesitarás este conocimiento en los pasos posteriores.
  2. Escriba su propia clase de formset que subclases desde BaseInlineFormSet y acepta initial. La parte realmente difícil aquí es que necesidad anulación __init__(), y debe asegurarse de que se llama por teléfono para BaseFormSet.__init__() en lugar de utilizar la matriz directa o abuelo __init__() (ya que esos son BaseInlineFormSet y BaseModelFormSet, respectivamente, y ninguno de los dos puede manejar datos iniciales).
  3. Escriba su propia subclase de la clase en línea admin apropiada (en mi caso fue TabularInline) y anule su método get_formset para devolver el resultado de inlineformset_factory() usando su clase de formset personalizada.
  4. En la subclase real ModelAdmin para el modelo con la línea, anula add_view y change_view, y replica la mayor parte del código, pero con un gran cambio: cree los datos iniciales que necesitará su formset y páselos a su formset personalizado (que será devuelto por su ModelAdmin 's método get_formsets()).

He tenido algunos chats productivos con Brian y Joseph para mejorar esto para futuros lanzamientos de Django; por el momento, la forma en que funcionan los formularios modelo solo hace que esto sea más problemático de lo normal, pero con un poco de limpieza de API creo que podría hacerse extremadamente fácil.

+4

Yikes. Creo que evitaré el problema haciendo dos formas diferentes. ¡Gracias! –

+1

Buena respuesta. Esta es una característica que falta realmente muy desordenada, complicada y mal documentada en django. – Rich

+1

¿Cuál es el estado en esto? ¿Hay algún boleto al que me pueda suscribir? ¿Necesitas ayuda? – Thomas

3

Me encontré con el mismo problema.

Puede hacerlo a través de JavaScript, hacer una JS simple que haga una llamada ajax para todos los miembros de la banda, y completa el formulario.

Esta solución carece del principio SECO, porque necesita escribir esto para cada forma en línea que tenga.

+0

Gracias, lo consideraré. Realmente tengo una sola forma para la que necesito esto. No soy un desarrollador web experimentado y no he hecho Ajax, así que tal vez esto me dará una excusa para aprender. 8v) –

0

Así es como he resuelto el problema. Hay un poco de una solución de compromiso en la creación y supresión de los registros, pero el código es limpio ...

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

yo pasamos una buena cantidad de tiempo tratando de llegar a una solución que pude volver usar en todos los sitios. La publicación de James contenía la clave de la sabiduría de extender BaseInlineFormSet pero invocar llamadas estratégicamente contra BaseFormSet.

La solución a continuación se divide en dos partes: una AdminInline y una BaseInlineFormSet.

  1. El InlineAdmin genera dinámicamente un valor inicial basado en el objeto de solicitud expuesto.
  2. Utiliza el currying para exponer los valores iniciales a un BaseInlineFormSet personalizado a través de argumentos de palabra clave pasados ​​al constructor.
  3. El constructor BaseInlineFormSet muestra los valores iniciales de la lista de argumentos y construcciones de palabra clave normalmente.
  4. La última pieza es reemplazar el proceso de construcción forma cambiando el número máximo total de formas y usando los BaseFormSet._construct_form y BaseFormSet._construct_forms métodos

Éstos son algunos fragmentos de hormigón utilizando las clases de la OP. He probado esto contra Django 1.2.3. Recomiendo mucho tener a mano la documentación formset y admin durante el desarrollo.

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

¿Es posible usar el atributo obj en el método get_formset de AttendanceInline? Tengo que admitir que no entiendo completamente lo que está detrás de la escena. Por el momento, estoy tratando de lograr alguna lógica de aplicación para generar valores iniciales, pero experimentando que get_formset se llame dos veces, una con obj configurado correctamente y la segunda vez establecida en None. Al proporcionar vals intial solo cuando está configurado, resulta que formset no se guarda al enviarlo :( – xaralis

+0

No he usado el argumento obj en mis sitios. Podría intentar rastrear la variable para ver dónde se inicializa, pero Aconsejaría tratar de descubrir por qué el método save está fallando. Supongo que una variable oculta de la clave principal no se está aplicando. –

14

Django 1.4 y superiores son compatibles providing initial values.

En cuanto a la pregunta original, lo siguiente funcionaría:

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) 

Si usted encuentra que las formas están siendo pobladas pero no está guardando a continuación, es posible que necesite para personalizar su modelo de formulario. Una manera fácil es pasar un tag en los datos iniciales y buscar en forma 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 
+1

anulando el __init__ en el formset funcionó para mí. Gracias. – safoo

+0

Creo que te estás perdiendo un -¡bucle allí, pero la solución elegante de todos modos! –

+0

Tuve el problema de los datos que se muestran, pero no se guardan. El método '_changed_data' no funcionó para mí. En su lugar, puse 'self.has_changed = returnTrue' en la rama' if initial' en el ModelForm, donde 'returnTrue' es una pequeña función que siempre devuelve True. No sé si esto es súper pitónico, pero funcionó en Django 1.10. – yerforkferchips

-1

Sólo reemplazar el método "save_new", que trabajó para mí en 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 
3

Al usar django 1.7 nos topamos con algunos problemas al crear una forma en línea con un contexto adicional incorporado en el modelo (no solo una instancia del modelo a pasar).

Se me ocurrió una solución diferente para inyectar datos en el ModelForm que se pasa al conjunto de formularios. Debido a que en Python puede crear clases dinámicamente, en lugar de intentar pasar los datos directamente a través del constructor del formulario, la clase puede crearse con un método con los parámetros que desee que pasen. Luego, cuando se crea una instancia de la clase, tiene acceso al método parámetros.

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 

A continuación, la llamada a la fábrica en línea juego de formularios podría tener este aspecto:

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

me encontré con esta pregunta -6 años más tarde-, y estamos en Django 1.8 ahora.

Todavía no está perfectamente limpio, respuesta breve a la pregunta.

El problema está en el ModelAdmin._create_formsets() github; Mi solución es anularlo e inyectar los datos iniciales que quiero en algún lugar alrededor de las líneas resaltadas en el enlace github.

También tuve que anular InlineModelAdmin.get_extra() para "tener espacio" para los datos iniciales proporcionados. por defecto será dejado sólo 3 de los datos iniciales

mostrar Creo que debe haber una respuesta más limpia en las próximas versiones

+6

Acepto, pero sería más útil si publicaste tu código aquí, para aquellos de nosotros que estamos teniendo el mismo problema con 1.8. – szeitlin

1

Puede anular captador empty_form en un juego de formularios. Aquí hay un ejemplo de cómo puedo lidiar con esto en conjunción con administración de 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 
0

estoy teniendo el mismo problema. Estoy usando Django 1.9, y probé la solución propuesta por Simanas, anulando la propiedad "empty_form", agregando algunos valores predeterminados en la inicial de dict. Eso funcionó, pero en mi caso tenía 4 formularios adicionales en línea, 5 en total, y solo uno de los cinco formularios estaba lleno con los datos iniciales.

he modificado el código como el siguiente (ver dict inicial):

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 

Si encontramos una manera de hacer que funcione al tener formas adicionales, esta solución sería una buena solución.

Cuestiones relacionadas