Martin CR
Martin CR

Reputation: 1420

Limit Django ModelForm ForeignKey choices depending on request.user

It is sometimes desirable to restrict the choices presented by the ForeignKey field of a ModelForm so that users don't see each others' content. However this can be tricky because models do not have access to request.user.

Consider an app with two models:

class Folder(models.Model):
    user = models.ForeignKey(get_user_model(), editable=False, on_delete=models.CASCADE)
    name = models.CharField(max_length=30)

class Content(models.Model):
    folder = models.ForeignKey(Folder, on_delete=models.CASCADE)
    text = models.CharField(max_length=50)

The idea is that users can create folders and content, but may only store content in folders that they themselves created.

i.e.:

Content.folder.user == request.user

QUESTION: How can we use for example CreateView, so that when creating new content users are shown the choice of only their own folders?

Upvotes: 1

Views: 1388

Answers (2)

Bruno Rijsman
Bruno Rijsman

Reputation: 3797

The following mixin looks automatically limits the choices for all foreign key fields that point to a model that has a user field:

class LimitForeignKeyChoicesToSameUserMixin:

    def get_form_class(self):
        this_model_has_user_field = False
        choice_limited_fields = []
        for field in self.model._meta.get_fields():
            if isinstance(field, ForeignKey) or isinstance(field, ManyToManyField):
                if field.name == 'user':
                    this_model_has_user_field = True
                elif hasattr(field.related_model, 'user'):
                    choice_limited_fields.append(field.name)
        form_class = super().get_form_class()
        if this_model_has_user_field:
            for field in choice_limited_fields:
                form_class.base_fields[field].limit_choices_to = {'user': self.request.user}
        return form_class  

And then I use the following base class for all 'add' views:

class AddViewBase(LoginRequiredMixin, LimitForeignKeyChoicesToSameUserMixin, 
                  CreateView, FormMixin):
    slug_url_kwarg = 'id'
    slug_field = 'id'

    def form_valid(self, form):
        form.instance.user = self.request.user
        return super().form_valid(form)

And this base class for 'edit' views:


class EditViewBase(LoginRequiredMixin, LimitForeignKeyChoicesToSameUserMixin,
                   UpdateView):
    slug_url_kwarg = 'id'
    slug_field = 'id'

Upvotes: 0

Martin CR
Martin CR

Reputation: 1420

I do this by overriding CreateView.get_form_class() in order to modify the attributes of the relevant field of the form before it is passed to the rest of the view.

The default method, inherited from views.generic.edit.ModelFormMixin, returns a ModelForm that represents all the editable fields of the model in the base_fields dictionary. So it's a good place to make any desired changes and also has access to self.request.user.

So for the above example, in views.py we might say:

class ContentCreateView(LoginRequiredMixin, CreateView):
    model = Content
    fields = '__all__'

    def get_form_class(self):
        modelform = super().get_form_class()
        modelform.base_fields['folder'].limit_choices_to = {'user': self.request.user}
        return modelform

Read more about ForeignKey.limit_choices_to in the docs.

Note that field choices are enforced by form validation so this should be quite robust.

Upvotes: 1

Related Questions