Reputation: 1420
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
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
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