Search code examples
djangomodelform

How to create a subform for Django ModelForm


I'm having a problem to add a ManyToMany field with ModelForm, the problem is that I don't know how to create a subform to add this data to my primary form. I want to create a subform to be saved when I click a button, and I want the data that was saved to be selected to use in the primary form.

my model

class Teste(models.Model):

   name = models.CharField(max_length=255)
   parent_institution_name = models.CharField(max_length=255)
   laboratory_departament = models.CharField(max_length=255, null=True, 
   blank=True, verbose_name="Laboratório")
   cep = models.CharField(max_length=255, verbose_name="Cep")
   cnpj = models.CharField(max_length=255, verbose_name="CNPJ")
   lat = models.FloatField(blank=True, null=True)
   lng = models.FloatField(blank=True, null=True)
   institution_name = models.CharField(max_length=255, null=False, 
   verbose_name="Nome da instituição")
   parent_institution_name = models.CharField(max_length=255, blank=True, 
   null=True, verbose_name="Nome da instituição vinculada")
   coordinator = models.CharField(max_length=255, verbose_name="Pessoa 
   Responsavel")
   email = models.CharField(max_length=255, verbose_name="E-mail")
   website = models.CharField(max_length=255, blank=True, null=True, 
   verbose_name="Website")
   rad_operating_time = models.IntegerField(verbose_name="Tempo de atuação 
   ")
   research_line = models.ManyToManyField('researcher.ResearchLines')

my ModelForm class TestForm(forms.ModelForm): class Meta:

        model = Test
        exclude = ('employee_number', ' revenues', 'filter_grade', 'grade', ' 
        knowledge_grade',
    'application_grade', 'ie_grade', ' ia_grade', 'final_grade', 'inactive', 'last_coordinator_access', ' hr_count',
    'hr_returned', ' match_hr_count', 'general_grade', 'lat', 'lng'
    'tokens', 'iso_certification', 'other_certification', 'researchers', 'thematic_network',
    )


    widgets ={
        'name':forms.TextInput(attrs={
            'class':'form-control',
            'placeholder': 'Nome da UBC'
        }),
        'parent_institution_name': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Nome da Instituição à qual a UBC é vinculada'
        }),
        'laboratory_departament': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Laboratório/Departamento'
        }),
        'cep': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'CEP'
        }),
        'cnpj': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'CNPJ'
        }),
        'institution_name': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Nome da instituição'
        }),
        'coordinator': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Pessoa Responsável'

        }),
        'email': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'E-mail'
        }),
        'website': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Website'
        }),
        'rad_operating_time': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder':'Tempo de atuação em projetos de P&D+I'
        }),
        'phone': forms.TextInput(attrs={
            'class':'form-control',
            'placeholder': 'Telefone'
        }),
        'partner_category': forms.RadioSelect(),

        'main_product':forms.CheckboxSelectMultiple( attrs={
            'type':'checkbox'
        }),        
    }

How my form looks, the last input shows where the subform goes to add data
enter image description here


Solution

  • Here is an example where I use inline formsets, which is in essence combining two forms into one displayed form. The formset is the service form, and the punches form where my intention is that the punches are the "form within the form".

    enter image description here

    VIEWS.PY (the most influential part)

    def reportcreateview(request):
        if request.method == 'POST':
            reportform = ServiceReportCreateForm(request.POST)
            if reportform.is_valid():
                report = reportform.save(commit=False)
                report.reported_by = request.user
                punchesform = PunchesFormSet(request.POST, request.FILES, instance=report)
                if punchesform.is_valid():
                    report.save()
                    punchesform.save()
                return redirect('service:update-report', pk=report.pk)
        else:
            punchesform = PunchesFormSet()
            reportform = ServiceReportCreateForm()
            reportform.reported_by = request.user
        context = {
            'report': reportform,
            'punches': punchesform,
        }
        return render(request, 'service/report-create.html', context)
    

    FORMS.PY

    from django import forms
    from django.forms.models import inlineformset_factory, BaseInlineFormSet
    from .models import ServiceReportModel, ReportPunchesModel, ServiceRequestModel
    
    
    class ServiceReportCreateForm(forms.ModelForm):
    
        class Meta:
            model = ServiceReportModel
            fields = [
                'site',
                'invoiced',
                'paid',
                'request_number',
                'equipment',
                'report_reason',
                'actions_taken',
                'recommendations',
            ]
            widgets = {
                'site': forms.Select(attrs={
                    'class': 'form-control',
                    'id': 'inputSite',
                }),
                'invoiced': forms.CheckboxInput(attrs={
                    'class': 'form-control',
                }),
                'paid': forms.CheckboxInput(attrs={
                    'class': 'form-control',
                }),
                'request_number': forms.Select(attrs={
                    'class': 'form-control',
                    'id': 'inputRequest',
                }),
                'equipment': forms.Select(attrs={
                    'class': 'form-control',
                    'id': 'inputEquipment',
                }),
                'report_reason': forms.TextInput(attrs={
                    'class': 'form-control',
                    'placeholder': 'Enter Reason for Service Report'}),
                'actions_taken': forms.Textarea(attrs={
                    'class': 'form-control',
                    'placeholder': 'To the best of your abilities, list all actions taken during service.  Please include'
                    'dates, times, and equipment names'}),
                'recommendations': forms.Textarea(attrs={
                    'class': 'form-control',
                    'placeholder': 'If any recommendations were made to the customer that'
                                   'require follow-up itemize them here...'}),
            }
    
            def __init__(self, *args, **kwargs):
                super(ServiceReportCreateForm, self).__init__(*args, **kwargs)
                self.fields['request_number'].required = False
    
    
    class ServiceReportUpdateForm(forms.ModelForm):
    
        def __init__(self, *args, **kwargs):
            super(ServiceReportUpdateForm, self).__init__(*args, **kwargs)
            instance = getattr(self, 'instance', None)
            if instance and instance.pk:
                self.fields['request_number'].required = False
                self.fields['report_reason'].widget.attrs['readonly'] = True
    
        class Meta:
            model = ServiceReportModel
            fields = [
                'invoiced',
                'paid',
                'request_number',
                'updated_by',
                'report_reason',
                'actions_taken',
                'recommendations',
            ]
            widgets = {
                'invoiced': forms.CheckboxInput(attrs={
                    'class': 'form-control',
                }),
                'paid': forms.CheckboxInput(attrs={
                    'class': 'form-control',
                }),
                'request_number': forms.Select(attrs={
                    'class': 'form-control',
                    'id': 'inputRequest',
                }),
                'updated_by': forms.Select(attrs={
                    'class': 'form-control',
                    'id': 'inputReporter',
                }),
                'report_reason': forms.TextInput(attrs={
                    'class': 'form-control',
                    'placeholder': 'Enter Reason for Service Report'}),
                'actions_taken': forms.Textarea(attrs={
                    'class': 'form-control',
                    'placeholder': 'To the best of your abilities, list all actions taken during service.  Please include' +
                    'dates, times, and equipment names'}),
                'recommendations': forms.Textarea(attrs={
                    'class': 'form-control',
                    'placeholder': 'If any recommendations were made to the customer that'
                                   'require follow-up itemize them here...'}),
            }
    
    
    PunchesFormSet = inlineformset_factory(
        ServiceReportModel,
        ReportPunchesModel,
        fields=('date',
                'time_in',
                'time_out',
                ),
        widgets={
                'date': forms.DateInput(attrs={
                    'type': 'date'
                }),
                'time_in': forms.TimeInput(attrs={
                    'type': 'time'
                }),
                'time_out': forms.TimeInput(attrs={
                    'type': 'time'
                })},
        extra=1,
        can_order=True
    )
    

    MODELS.PY

    class ServiceReportModel(ServiceParent):
        report_number = models.UUIDField(
            primary_key=True, default=uuid.uuid4, editable=False)
        invoiced = models.BooleanField(default=False, null=False)
        paid = models.BooleanField(default=False, null=False)
        request_number = models.ForeignKey(ServiceRequestModel,
                                           on_delete=models.PROTECT,
                                           null=True,
                                           blank=True,
                                           related_name='s_report_number'
                                           )
        reported_by = models.ForeignKey(
            main_models.MyUser, related_name='reporter', on_delete=models.PROTECT)
        reported_date = models.DateTimeField(auto_now_add=True)
        report_reason = models.CharField(max_length=255, null=True)
        actions_taken = models.TextField(null=False, blank=False)
        recommendations = models.TextField(null=True, blank=True)
    
        def get_absolute_url(self):
            return reverse('service-report', kwargs={'pk': self.pk})
    
        def __str__(self):
            return '%s - %s, %s' % (self.site.company,
                                    self.reported_date.strftime('%d %B %Y'),
                                    self.equipment.name
                                    )
    
        class Meta:
            ordering = ['reported_date', 'updated_date']
            verbose_name = 'Service Report'
            verbose_name_plural = 'Service Reports'
    
    
    class ReportPunchesModel(models.Model):
        punch_id = models.UUIDField(
            primary_key=True, default=uuid.uuid4, editable=False)
        added = models.DateTimeField(auto_now_add=True)
        report = models.ForeignKey(ServiceReportModel, on_delete=models.CASCADE)
        date = models.DateField(blank=True, null=True)
        time_in = models.TimeField(blank=True, null=True)
        time_out = models.TimeField(blank=True, null=True)
    
        def __str__(self):
            return '%s - %s' % (self.time_in,
                                self.time_out
                                )
    
        class Meta:
            ordering = ['added', 'date', 'time_in']
            verbose_name = 'Report Punches'
            verbose_name_plural = verbose_name