2

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.

1
  • If the only thing you want to change is one field end_date on BaseCampaign, just add that as an additional field on your CreateInvestmentCampaignForm and in your form.valid() method, after saving the form, set the value on the associated campaign: inv_campaign = form.save(); inv_campaign.campaign.end_date = form.cleaned_data['end_date']; inv_campaign.campaign.save(). Commented Oct 22, 2019 at 10:01

2 Answers 2

2

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
Sign up to request clarification or add additional context in comments.

13 Comments

But with this when I will not see the actual value of the end_date field populated on the form, you know? I will have the field always empty and the user will need to put the end_date every time. I need to have all the fields populated on the template so the user won't need to fill it every time it needs to modify something from the description.
Then you add the value to initial data. That’s what get_inital_data does.
get_initial_data on the form or on the view? on the form it appears me that I can add a def get_initial_for_field(self, field, field_name):
sorry, was a bit too fast. As I said, it should be form field and what you're using is a form widget as you can see from the output. You can look it up here
@PolFrances If you have an alternative answer, add it as an answer (you are allowed to answer your own question), don't add it in the question itself.
|
0

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.