-->

Nested and Segmented Crispy Layouts

2019-05-29 04:11发布

问题:

TLDR Question: How do you make one crispy form with a ¿segmented?(not sure if this is considered inline) layout with multiple models(some related, some not).

I am trying to understand several things in Django: forms, formsets, nested forms, and crispy, and I've been at it for a while, and feel I am close, just need someone to help connect the dots. I'm not sure how to accomplish it without crispy, so I started down this path thinking crispy was the solution. Please correct if I am wrong, thanks :)

I would like one form (as in HTML form, not necessarily Django Form), that has a primary model with lots of fields, but with secondary/tertiary models in the middle of the primary fields. I am rather close to the layout, but can't seem to get the secondary/tertiary models to render in the middle of the layout, let alone compile without crispy/django erroring.

Here is a color-coded visual of what I am trying to attain

I assume I am wrong with at least one of the following:

  • I am not calling the correct formfactory
  • I am not properly using formsets
  • I am not referencing the form fields to the correct model fields correctly in the layout of the form helper
  • The layout is not possible, or I am applying the wrong code structure to get the result.
  • I don't think I can call two forms directly as directly below, as they would not be nested/integrated

code for above list item (cant put a code block directly below

#I don't think this will achieve the integration/nested look I am aiming for
#views.py:
parent_form = ParentForm()
child_form = ChildForm()
render(template.html, {
  "pform": parent_form,
  "cform": child_form
})

#template.html:
<form>
  {{ pform }}
  {{ cform }}
</form>

Files For reference

models.py

#Black in the picture
class Truck(models.Model):
  name = models.CharField(…)
  …

#Blue in the picture
class QuickInspection(models.Model):
  odometer = models.IntegerField(…)
  … (created_at, user_cookie#who did it, …)
  truck = models.ForeignKey(Truck)

-----
#These two are unrelated to the Truck in the DB, and I would prefer to keep it that way, if for at least to understand how to accomplish this 
-----
#Red
class Tires(models.Model):
  front_tire = models.CharField(…)
  … (created_at, …)
  truck = models.ForeignKey(Truck)
  full_inspection = models.ForeignKey(FullInspection, blank=True, null=True) #optional, and if it has this foreign key, then I know the Tires were looked at in a full inspection.  If not, then they were looked at in the quick inspection, without having a foreign key to the QuickInspection

#Green
class Brakes(models.Model):
  front_axle = models.CharField(…)
  …
  createdAt = models.DateTimeField(auto_now_add=True)
  truck = models.ForeignKey(Truck)
  pm = models.ForeignKey(PM, blank=True, null=True)
  full_inspection = models.ForeignKey(FullInspection, blank=True, null=True) #optional, same as full_inspection in Tires

views.py

def weeklyView(request, truckID):
  # POST
  if request.method == 'POST':
    # Check forms for valid data and save or provide error
    #return response
  # GET
  else:
    #make each form individually?
    quickForm = OHReadingForm(…)
    tireForm = TireForm()
    brakeForm = BrakeForm()

    #Or import a formset and helper?
    formset = ExampleFormSet()
    helper = ExampleFormSetHelper()

    response = render(request, 'trucks/weeklyInspection.html', {
      'ohrForm': ohrForm,
      'formset': formset,
      'helper': helper,
      'tireForm': tireForm,
      'truck': truck,
    })

forms.py

class QuickInspectionForm(forms.ModelForm):
  def __init__(self, *args, **kwargs):
    super(QuickInspectionForm, self).__init__(*args, **kwargs)
    self.helper = FormHelper()
    self.helper.form_tag = False
    self.helper.form_method = 'post'
    self.helper.form_action = 'quickInspectionURL'
    self.helper.layout = Layout(
        Div(
          Div(
            Fieldset(
              '',        # 'first arg is the legend of the fieldset',
              'quickInspectionMetric1', #From QuickInspection.metric1
              'quickInspectionMetric2', #From QuickInspection.metric2
              'quickInspectionMetric3', #From QuickInspection.metric3
            ),            
            css_class="blue"
          ),
          Div(
            Fieldset(
              'tireMetric1',  #from Tire.metric1
              'tireMetric2',  #from Tire.metric2
            css_class="red"
          ),
          Div(
            Fieldset(
              'brakeMetric1',  #from Brake.metric1
              'brakeMetric2',  #from Brake.metric2
            css_class="green"
          ),
          Div(
            Fieldset(
              'quickInspectionMetric4',  #from QuickInspection.metric4
              'quickInspectionMetric5',  #from QuickInspection.metric5
            css_class="blue"
          ),
        ),
        Div(
          FormActions(
            Reset('reset', 'Reset'),
            Submit('submit', 'Submit') #submit for all
          )
        ),
    )

  class Meta:
    model = QuickInspection
    fields = [
      'metric1','metric2','metric3','metric4','metric5',
      'truck',
      …,
    ]

ExampleFormSet = formset_factory(QuickInspectionForm, extra=1)
# Other failed attempts
# ExampleFormSet = inlineformset_factory(QuickInspectionForm, extra=1)
# ExampleFormSet = inlineformset_factory(QuickInspectionForm, TireForm, extra=1)
# ExampleFormSet = inlineformset_factory(QuickInspectionForm, TireForm, BrakeForm, extra=1)

class ExampleFormSetHelper(FormHelper):
  def __init__(self, *args, **kwargs):
    super(ExampleFormSetHelper, self).__init__(*args, **kwargs)
    self.form_method = 'post'
    self.form_tag = False
    self.layout = Layout(…)


#Same as Brake Form
class TireForm(forms.ModelForm):
  def __init__(self, *args, **kwargs):
    super(TCForm, self).__init__(*args, **kwargs)
    self.helper = FormHelper()
    self.helper.form_method = 'post'
    self.helper.form_action = 'tireURL'
    self.helper.layout = Layout(…)
  class Meta:
    model = TireCondition
    fields = [
      'metric1', 'metric2', …
      'truck',
    ]

JS fiddle for code repo. I don't know of a DJango-like Fiddle environment...

回答1:

Crispy is independent of this problem. Forms can be included int the template with either:

{{form1}}
{{form2}}
...

or

{% crispy form1 form1.helper %} #although the helper name here is default and not needed
{% crispy form2 %} # form2.helper is implied
...

For the assumptions:

  • I am not calling the correct formfactory
    • form factory is not needed, as there arent multiple versions of any single form
  • I am not properly using formsets
    • also not needed, as there arent multiple versions of any single form
  • I am not referencing the form fields to the correct model fields correctly in the layout of the form helper
    • somewhat true
  • The layout is not possible, or I am applying the wrong code structure to get the result.
    • see below for answer
  • I don't think I can call two forms directly as directly below, as they would not be nested/integrated
    • one can, see below

Code to make an integrated form with related objects/foreign keys:

views.py:

if request.method == 'POST':
  formData = request.POST.dict()
  form1 = form1Form(formData, instance=currentUser, prefix='form1')
  form2 = form2Form(formData, truck=truck, user_cookie=currentUser, prefix='form2')
  form3 = form3Form(formData, truck=truck, instance=truck, user_cookie=currentUser, prefix='form3')
  form4 = form4Form(formData, truck=truck, user_cookie=currentUser, prefix='form4')
  userForm = userFormForm(formData, truck=truck, user_cookie=currentUser, prefix='userForm')
  ... Other forms as needed
if all([form1.is_valid(), form2.is_valid(), form3.is_valid(), form4.is_valid(), userForm.is_valid()]):
  currentUser.save()
  form1.save()
  form2.save()
  ...
# The Get
else:
  form1 = form1Form(instance=truck, prefix='form1')
  form2 = form2Form(instance=truck, prefix='form2')
  form3 = form3Form(instance=truck, prefix='form3')
  form4 = form4Form(instance=truck, prefix='form4')
  userForm = userForm(instance=currentUser, prefix='userForm')

return render(request, 'trucks/weeklyInspection.html', {
  'truck': truck,
  'form1': form1,
  'form2': form2,
  'form3': form3,
  'form4': form4,
  'userForm': userForm,
})

template.html:

<div class="container">
  <form action="{% url 'app:formAllView' truck=truck %}" class="form" method="post">
    {{ form1 }}
    {{ form2 }}
    {{ form3 }}
    {{ form4 }}
    # Either put the submit in the form here manually or in the form4 template
  </form>

forms.py

# create a shared 
class BaseSharedClass(forms.ModelForm):
  def save(self, commit=True):
  """Save the instance, but not to the DB jsut yet"""
  obj = super(WIBaseClass, self).save(commit=False)
  if commit:
    obj.currentUser = self.currentUser
    obj.truck = self.truck
    obj.save()
  return obj
def __init__(self, *args, **kwargs):
  self.currentUser = kwargs.pop('currentUser', None)
  self.truck = kwargs.pop('truck', None)
  super(WIBaseClass, self).__init__(*args, **kwargs)

#note inherting the base shared class
class form1Form(BaseSharedClass):
  def __init__(self, *args, **kwargs):
    super(form1Form, self).__init__(*args, **kwargs)

THE IMPORTANT PARTS

When Architecting the forms in the forms.py

  • Overall
    • Make a parent class (BaseSharedClass) that all forms that need shared information will inherit from
    • Extend all the forms that need the shared info from the parent class (class form1Form(BaseSharedClass) )
  • With regard to init-ing
    • remove the shared objects from the form, to avoid duplications of the shared fields across all of the forms that will be in the same page (self.currentUser = kwargs.pop('currentUser', None) && self.truck = kwargs.pop('truck', None) )
  • With regard to Saving
    • override the save function to do the magic
    • make commit=false to prevent it from saving just yet
    • add the related fields from the passed in context ( obj.currentUser = self.currentUser && obj.truck = self.truck ), and then save (obj.save() )

When creating the form in the view:

  • pass it the instance of the shared object instance=truck. You can also pass other objects if you need access to them as well, such as relatedObject=queriredObject
  • Pass in the prefix prefix=formIdentifierN, as this helps Django keep track of what unique information, fields, entries are associated with which forms. No special naming is needed, and form1, form2, etc... works well, so long as you know which are each.

When saving the form in the view:

  • everything here is the same (saving, error handling, etc...) as a single form, but you can check it in one line with all( [a,b,c,d] )