57

I have a model:

class Article(models.Model):
    text = models.CharField()
    author = models.ForeignKey(User)

How do I write class-based view that creates a new model instance and sets author foreign key to request.user?

Update:

Solution moved to separate answer below.

Vlad T.
  • 2,568
  • 3
  • 26
  • 40

5 Answers5

47

I solved this by overriding form_valid method. Here is verbose style to clarify things:

class CreateArticle(CreateView):
    model = Article

    def form_valid(self, form):
        article = form.save(commit=False)
        article.author = self.request.user
        #article.save()  # This is redundant, see comments.
        return super(CreateArticle, self).form_valid(form)

Yet we can make it short (thanks dowjones123), this case is mentioned in docs.:

class CreateArticle(CreateView):
    model = Article

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(CreateArticle, self).form_valid(form)
Vlad T.
  • 2,568
  • 3
  • 26
  • 40
  • 1
    +1 for doing it properly. All other methods of using HiddenInput or setting initials would be a potential security risk. – Irfan Jul 28 '15 at 10:19
  • 5
    This is not ideal, as it results in the form's `save` method being called twice. This is one of those cases where your best choice is to re-implement the rest of the `super` method, vs delegating. In other words, set `self.object` to the new instance, then redirect to `self.get_success_url()`. – orokusaki Aug 01 '15 at 17:59
  • @orokusaki You are absolutely right. I looked through the source code and found that `form_valid` does nothing except saving (already validated) form instance. So we can safely remove either `super` or `save` line. The latter is better as we don't need to think about the `success_url` (otherwise we need to set it explicitly). Corrected the answer. – Vlad T. Aug 03 '15 at 07:15
  • 1
    @VladT. it's still technically incorrect, because the save method of the form is still being called twice (the first with `commit=False`), and a form's save method could have logic in it (or even just logging) that you might not want to run twice. If you add back the `article.save()` and replace the `super(...)` line with `return redirect(self.get_success_url())`, that will be the most correct solution. – orokusaki Aug 03 '15 at 12:21
  • @orokusaki This is not so straightforward, because `form_valid` also sets `self.object`. Without that you cannot access view's object in other methods in case you want to override them. Also, by the same reason you are not able to run `get_success_url` and have to set it explicitly, as I mentioned above. As for `form.save` method, if you need to integrate some logic in it you can always check the `commit` flag. – Vlad T. Aug 03 '15 at 19:22
  • 2
    a slightly more efficient way (viewpoints may differ) would be to have replace first two lines of `form_valid` by `form.instance.author = self.request.user` – dowjones123 May 25 '16 at 22:44
  • @dowjones123 You may be right, but my case is what [official documentation](https://docs.djangoproject.com/en/dev/topics/forms/modelforms/#the-save-method) recommends (see `commit=False` part). – Vlad T. May 27 '16 at 15:21
  • @VladT. I am following this doc only here we map an author to requested user that's good. what if i want to map a city to a hotel (HotelModel contain city with ForeignKey to CityModel). How do i do that?. i am getting error `No City matches the given query.` – Rahul Verma Apr 21 '20 at 06:28
15

I just stumbled into this problem and this thread led me in the right direction (thank you!). Based on this Django documentation page, we can avoid calling the form's save() method at all:

class CreateArticle(LoginRequiredMixin, CreateView):
    model = Article

    def form_valid(self, form):
        form.instance.author = self.request.user
        return super(CreateArticle, self).form_valid(form)
Community
  • 1
  • 1
Carlos Mermingas
  • 3,822
  • 2
  • 21
  • 40
2

You should set up a CreateView using a ModelForm for that model. In the form definition, you set the ForeignKey to have the HiddenInput widget, and then use the get_form method on the view to set the value of your user:

forms.py:

from django import forms

class ArticleForm(forms.ModelForm):
    class Meta:
        model = Article
        widgets = {"user": forms.HiddenInput()}

views.py:

from django.views.generic import *
from myapp.forms import ArticleForm
from myapp.models import Article

class NewArticleView(CreateView):
    model = Article
    form_class = ArticleForm
    def get_form(self, form_class):
        initials = {
            "user": self.request.user
        }
        form = form_class(initial=initials)
        return form
Berislav Lopac
  • 16,656
  • 6
  • 71
  • 80
  • I tried this code, but it does not pass user to view, and I cannot save new article (since `user` fiedl is requied). Instead I used form_valid method in CBV (see UPDATE in question. Anyway, thanks for having time for help! – Vlad T. May 01 '12 at 09:52
  • I'm not sure what you mean by "does not pass user to view"? It passed the user's ID and puts in in the hidden field "user". If you need the user elsewhere in the view (i.e. template), it's already in the `request` variable. – Berislav Lopac May 07 '12 at 05:52
  • I tried your code again: I can't find any hidden input in the page source (except csrf token). And why do you think this field will be the user's ID? Why not username? – Vlad T. May 07 '12 at 09:46
  • Or maybe this just CBV problem? See http://stackoverflow.com/questions/907858/how-to-let-djangos-generic-view-use-a-form-with-initial-values#comment716057_908038 – Vlad T. May 07 '12 at 10:01
  • Damn, my code was wrong. Fixed it now. Note the error in the method name. – Berislav Lopac May 07 '12 at 11:58
  • I confirm that, as **jantoniomartin** said, this code passes request.user.id with hidden field to template, but I can't save the form. – Vlad T. May 12 '12 at 16:58
2

Berislav's code in views.py doesn't work for me. The form is rendered as expected, with the user value in a hidden input, but the form is not saved (I don't know why). I have tried a slightly different approach, that works for me:

views.py

from django.views.generic import *
from myapp.forms import ArticleForm
from myapp.models import Article

class NewArticleView(CreateView):
    model = Article
    form_class = ArticleForm
    def get_initial(self):
        return {
            "user": self.request.user
        }
orokusaki
  • 55,146
  • 59
  • 179
  • 257
jantoniomartin
  • 215
  • 3
  • 9
1

There are answers that are mainly related to the User model foreign key. However, let's suppose a simple scenario in which there is a model Comment containing a foreign key of the Article model, and you need to have a CreateView for Comment where each comment will have a foreign key of the Article model. In that case, the Article id would probably be in the URL, for example, /article/<article-id>/comment/create/. Here is how you can deal with such a scenario

class CommentCreateView(CreateView):
    model = Comment
    # template_name, etc 


    def dispatch(self, request, *args, **kwargs):
        self.article = get_object_or_404(Article, pk=self.kwargs['article_id'])
        return super(CommentCreateView, self).dispatch(request, *args, **kwargs)


    def form_valid(self, form):
        form.instance.article= self.article    # if the article is not a required field, otherwise you can use the commit=False way
        return super(CommentCreateView, self).form_valid(form)
Muhammad Zubair
  • 466
  • 5
  • 17