Pol Frances
Pol Frances

Reputation: 402

Django - Multiple custom models on the same form

I'm using Django 2.1 and PostgreSQL. My problem is that I'm trying to create a form to edit two different models at the same time. This models are related with a FK, and every example that I see is with the user and profile models, but with that I can't replicate what I really need.

My models simplified to show the related information about them are:

# base model for Campaigns.
class CampaignBase(models.Model):
    ....
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    creation_date = models.DateTimeField(auto_now_add=True)
    start_date = models.DateTimeField(null=True, blank=True)
    end_date = models.DateTimeField(null=True, blank=True)
    ....

# define investment campaign made on a project.
class InvestmentCampaign(models.Model):
    ....
    campaign = models.ForeignKey(CampaignBase, on_delete=models.CASCADE, null=True, blank=True)

    description = models.CharField(
        blank=True,
        max_length=25000,
    )
    ....

And the form that I want to create is one that includes the end_date of the FK CampaignBase, and the Description from the InvestmentCampaign.

Now I have this UpdateView to edit the InvestmentCampaign, and I need to adapt to my actual needs, that are also update the CampaignBase model:

class ProjectEditInvestmentCampaignView(LoginRequiredMixin, SuccessMessageMixin, generic.UpdateView):
    template_name = 'webplatform/project_edit_investment_campaign.html'
    model = InvestmentCampaign
    form_class = CreateInvestmentCampaignForm
    success_message = 'Investment campaign updated!'

    def get_success_url(self):
        return reverse_lazy('project-update-investment-campaign', args=(self.kwargs['project'], self.kwargs['pk']))

    # Make the view only available for the users with current fields
    def dispatch(self, request, *args, **kwargs):
        self.object = self.get_object()
        # here you can make your custom validation for any particular user
        if request.user != self.object.campaign.project.user:
            raise PermissionDenied()
        return super().dispatch(request, *args, **kwargs)

    # Set field as current user
    def form_valid(self, form):
        campaign = InvestmentCampaign.objects.get(pk=self.kwargs['campaign'])
        form.instance.campaign = campaign
        form.instance.history_change_reason = 'Investment campaign updated'
        return super(ProjectEditInvestmentCampaignView, self).form_valid(form)

    def get_context_data(self, **kwargs):
        project = Project.objects.get(pk=self.kwargs['project'])
        context = super(ProjectEditInvestmentCampaignView, self).get_context_data(**kwargs)
        context['project'] = project
        return context

My forms are:

class CreateCampaignBaseForm(forms.ModelForm):
    class Meta:
        model = CampaignBase
        fields = ('end_date',)
        widgets = {
            'end_date': DateTimePickerInput(),
        }

    def __init__(self, *args, **kwargs):
        # first call parent's constructor
        super(CreateCampaignBaseForm, self).__init__(*args, **kwargs)
        # evade all labels and help text to appear when using "as_crispy_tag"
        self.helper = FormHelper(self)
        self.helper.form_show_labels = False
        self.helper._help_text_inline = True


class CreateInvestmentCampaignForm(forms.ModelForm):
    class Meta:
        model = InvestmentCampaign
        fields = ('description')
        widgets = {
            'description': SummernoteWidget(attrs={'summernote': {
                'placeholder': 'Add some details of the Investment Campaign here...'}}),
        }

    def __init__(self, *args, **kwargs):
        # first call parent's constructor
        super(CreateInvestmentCampaignForm, self).__init__(*args, **kwargs)
        # evade all labels and help text to appear when using "as_crispy_tag"
        self.helper = FormHelper(self)
        self.helper.form_show_labels = False
        self.helper._help_text_inline = True

I've read everywhere that the best way of doing this is using function based views, and call each of the forms that I have and then do the validation. the thing is that I don't know how can I populate the fields with the right object in both forms, and also, I don't know how to do the equivalent of the get_context_data nor getting the self arguments to do the equivalent of the get_success_url (because with function based views I only have the request attr so I can't access the kwargs).

I've seen some people using the django-betterforms, but again, the only examples are with the auth and profile models and I don't see the way to replicate that with my own models.

Thank you very much.

Upvotes: 2

Views: 173

Answers (2)

Pol Frances
Pol Frances

Reputation: 402

Based on the conversation on the answer of @dirkgroten, I've developed what worked for me and what I'm actually using, but I market his answer as correct because his code is also functional.

So, meanwhile he is initiating the values on the form, I'm using the view to do that by adding a def get_initial(self): and also adding the validation on the def form_valid(self, form)::

On the view:

...

    def get_initial(self):
        """
        Returns the initial data to use for forms on this view.
        """
        initial = super(ProjectEditInvestmentCampaignView, self).get_initial()
        initial['end_date'] = self.object.campaign.end_date
        return initial
...
    # Set field as current user
    def form_valid(self, form):
        form.instance.history_change_reason = 'Investment campaign updated'
        is_valid = super(ProjectEditInvestmentCampaignView, self).form_valid(form)
        if is_valid:
            # the base campaign fields
            campaign = form.instance.campaign
            campaign.end_date = form.cleaned_data.get("end_date")
            campaign.save()
        return is_valid

And on the form I just added the end_date field:

class CreateInvestmentCampaignForm(forms.ModelForm):
    end_date = forms.DateTimeField()

    class Meta:
        model = InvestmentCampaign
        fields = ('description',)
        widgets = {
            'description': SummernoteWidget(attrs={'summernote': {
                'placeholder': 'Add some details of the Investment Campaign here...'}}),
            'end_date': DateTimePickerInput(),  # format='%d/%m/%Y %H:%M')

        }

    def __init__(self, *args, **kwargs):
        # first call parent's constructor
        super(CreateInvestmentCampaignForm, self).__init__(*args, **kwargs)
        # evade all labels and help text to appear when using "as_crispy_tag"
        self.helper = FormHelper(self)
        self.helper.form_show_labels = False
        self.helper._help_text_inline = True

Upvotes: 0

dirkgroten
dirkgroten

Reputation: 20702

If the only thing you want to change is one field end_date on BaseCampaign, then you should use just one form. Just add end_date as an additional field (e.g. forms.DateTimeField()) on your CreateInvestmentCampaignForm and in your form.valid() method, after saving the form, set the value on the associated campaign:

def form_valid(self, form):
    inv_campaign = form.save(commit=False)
    inv_campaign.campaign.end_date = form.cleaned_data['end_date']
    inv_campaign.campaign.save()
    inv_campaign.history_change_reason = ...
    return super().form_valid(form)

Here's how to add end_date to your form and initialize it correctly:

class CreateInvestmentCampaignForm(ModelForm):
    end_date = forms.DateTimeField(blank=True)

    class Meta:
        model = InvestmentCampaign
        fields = ('description')

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if self.instance.campaign:
            self.fields['end_date'].initial = self.instance.campaign.end_date

Upvotes: 2

Related Questions