Jeremy Gillick
Jeremy Gillick

Reputation: 2700

Modifying a inline formset before it is saved

tldr; I need to add the required 'author' field before the forms are validated and saved.

There are two models: Documents and Revisions. Each Document can have many Revisions and each revisions as an author (User object). Of course I don't want the user to be able to set the author ID themselves, so I need to set it myself.

On the edit page a user can modify the document title and create a new revision. There are two forms: DocumentForm and RevisionInlineFormset.

I cannot seem to assign an author to RevisionInlineFormset in the post method before we validate and save everything.

forms.py

class DocumentForm(forms.ModelForm):
    class Meta:
        fields = ['title', 'path']
        model = Document

RevisionInlineFormset = inlineformset_factory(
    Document,
    Revision,
    extra=1, max_num=1,
    fields=('content',),
    can_delete=False)

views.py

class DocUpdate(mixins.LoginRequiredMixin, generic.edit.UpdateView):

    """ Edit a document. """

    form_class = DocumentForm
    template_name = 'spaces/document/edit.html'

    def get_object(self):
        try:
            return Document.objects.get_by_path(self.kwargs["path"])
        except ObjectDoesNotExist:
            raise Http404

    def get(self, request, *args, **kwargs):
        """ Handles GET requests. """
        self.object = self.get_object()

        # Get latest revision
        rev_qs = self.object.revision_set.order_by('-created_on')
        if rev_qs.count():
            rev_qs = rev_qs.filter(pk=rev_qs[0].pk)

        form_class = self.get_form_class()
        form = self.get_form(form_class)
        revision_form = RevisionInlineFormset(
            instance=self.object,
            queryset=rev_qs)

        return self.render_to_response(
            self.get_context_data(form=form,
                                  revision_form=revision_form))

    def post(self, request, *args, **kwargs):
        """ Handles POST requests. """
        self.object = self.get_object()

        form_class = self.get_form_class()
        form = self.get_form(form_class)
        revision_form = RevisionInlineFormset(
            self.request.POST, instance=self.object)

        if (form.is_valid() and revision_form.is_valid()):
            return self.form_valid(form, revision_form)
        return self.form_invalid(form, revision_form)

    def form_valid(self, form, revision_form):
        """ All good. Finish up and save. """
        self.object = form.save()

        revision_form.instance = self.object
        revision_form.save()

        return HttpResponseRedirect(self.get_success_url())

Upvotes: 3

Views: 2212

Answers (1)

Alex Polekha
Alex Polekha

Reputation: 1690

I would suggest approach like this:

forms.py

from .models import Comment, Post
from django.forms.models import inlineformset_factory, ModelForm, BaseInlineFormSet


class PostCommentBaseFormset(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user', None)
        super(PostCommentBaseFormset, self).__init__(*args, **kwargs)
        if user:
            for form in self.forms:
                if form.instance.pk is None:
                    form.fields['user'].initial = user #or form.initial['user'] = user

CommentFormset = inlineformset_factory(Post, Comment, extra=3, exclude=[], formset=PostCommentBaseFormset)

views.py

from .forms import CommentFormset

def post_create(request, *args, **kwargs):
    ...
    context['formset'] = CommentFormset(instance=post, user=request.user)
    ...

If you want to avoid overriding 'user' field with POST data, exclude 'user' field from formset:

CommentFormset = inlineformset_factory(Post, Comment, extra=3, exclude=['user'], formset=PostCommentBaseFormset)

and change PostCommentBaseFormset like this:

class PostCommentBaseFormset(BaseInlineFormSet):
    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user', None)
        super(PostCommentBaseFormset, self).__init__(*args, **kwargs)
        for form in self.forms:
            if form.instance.pk is None:
                form.instance.user = user 

There are other ways to achieve your goal(like iterating through each new instance in view function after formset is saved or overriding the "clean" method in formset's form and so on...) but they are more complicated in my opinion.

Upvotes: 5

Related Questions