django - iterate through model in create view & return inline formset

Viewed 331

I'm trying to create a formset that will allow the user to set scores for each Priority when creating a new project with the CreateView. The Priority model already has data associated (3 values) foreach Priority that has been created I'm trying to return a char-field in the Project CreateView where the user can enter the score for the Priority.

Currently I have the three char-fields showing up in the Project CreateView but the priority isn't being saved. I have done some testing & it looks like the ProjectPriority is never having the Project or Priority values set only the score value.

I have been struggling for days trying to get this to work. I appreciate all the help.

Views.py

class ProjectCreateview(LoginRequiredMixin, CreateView):
    model = Project
    form_class = ProjectCreationForm
    success_url = 'home/project/project_details.html'

    def get_context_data(self, **kwargs):
        ChildFormset = inlineformset_factory(
            Project, ProjectPriority, fields=('priority', 'score'), can_delete=False, extra=Priority.objects.count(),
        )
        data = super().get_context_data(**kwargs)
        if self.request.POST:
            data['priorities'] = ChildFormset(self.request.POST, instance=self.object)
        else:
            data['priorities'] = ChildFormset(instance=self.object)
        return data

    def form_valid(self, form):
        context = self.get_context_data()
        priorities = context["priorities"]
        self.object = form.save()
        if priorities.is_valid():
            priorities.instance = self.object
            priorities.save()
        return super().form_valid(form)

Models.py

class Priority(models.Model):
     title = models.CharField(verbose_name="Title", max_length=250)

     def __str__(self):
         return self.title

class Project(models.Model):
    name = models.CharField(verbose_name="Project Title", max_length=100)
    details = models.TextField(verbose_name="Project Details/Description", blank=False)
    priority = models.ManyToManyField(
        Priority,
        through='ProjectPriority',
        related_name='priority'
    )
    creator = models.ForeignKey(User, on_delete=models.CASCADE)

class ProjectPriority(models.Model):
    project = models.ForeignKey(Project, on_delete=models.CASCADE)
    priority = models.ForeignKey(Priority, on_delete=models.CASCADE)
    score = models.CharField(max_length=1000, choices=priority_choices)

    class Meta:
        verbose_name = "Priority"
        verbose_name_plural = "Priorities"

    def __str__(self):
        return f"Priority: {self.priority.title}, Score: {self.score}, Project: {self.project.name}"

Template

{% block content %}
<div class="container-fluid" style="margin-top: 25px;">
<div class="row">
<div class="col-xl-8">
<form method="POST">
    {% csrf_token %}
    <fieldset class="form-group">
        <legend class="border-bottom mb-4">New Project</legend>
        {{ form|crispy }}
    </fieldset>

    <h2>Priority Criteria</h2>
    {{ priorities|crispy }}
    <div class="form-group">
        <button class="btn btn-outline-info" type="submit">Create 
Project</button>
    </div>
</form>
</div>
</div>
{% endblock content %}
1 Answers

In your form_valid method you have code like so:

def form_valid(self, form):
    context = self.get_context_data()
    priorities = context["priorities"]
    self.object = form.save()
    if priorities.is_valid():
        priorities.instance = self.object
        priorities.save()
    return super().form_valid(form)

But trying to set the instance at this point (after calling is_valid) will not work because the forms have already been constructed when you call priorities.is_valid() which loops over the cached property forms of the formset which causes the forms to be constructed with the instance then being set to an empty one (because you didn't provide it yet). The solution would be simply to set the instance before calling is_valid. Also note the super().form_valid(form) will also save the object, you should instead return the response yourself there:

from django.http import HttpResponseRedirect


class ProjectCreateview(LoginRequiredMixin, CreateView):
    # Other attributes and get_context_data here
    
    def form_valid(self, form):
        context = self.get_context_data()
        priorities = context["priorities"]
        self.object = form.save()
        priorities.instance = self.object # Set the instance here
        if priorities.is_valid():
            priorities.save()
        return HttpResponseRedirect(self.get_success_url())
Related