I am developing a multi-tenant app in Django. In the Django admin, some querysets are filtered based on the user, using get_queryset().
Up till now, when a user updated an object from the Django change form, I would validate the data by creating a ModelAdmin form using a factory function to capture the HttpRequest object, then ensure that the Guest object's user was the current user:
EXAMPLE
models.py
class Guest(models.Model):
guest_name = models.CharField(max_length=64)
user = models.ForeignKey(User, on_delete=models.CASCADE)
admin.py
#admin.register(Guest)
class GuestAdmin(admin.ModelAdmin):
def get_queryset(self, request)
qs = super().get_queryset(request)
return qs.filter(user=request.user)
def get_form(self, request, obj=None, **kwargs):
self.form = _guest_admin_form_factory(request)
return super().get_form(request, obj, **kwargs)
forms.py
def _guest_admin_form_factory(request):
class GuestAdminForm(forms.ModelForm):
class Meta:
model = Guest
exclude = []
def clean_user(self):
user = self.cleaned_data.get('user', None)
if not user:
return user
if user != request.user:
raise forms.ValidationError('Invalid request.')
return user
return GuestAdminForm
It occurred to me that Django might use the get_queryset() method to validate this for me, since some simple logging showed that the method is called twice when an object gets updated from the change form.
Is this the case, or do I need to stick to validating through a ModelAdmin form?
The documented way to do this is to define has_change_permission():
#admin.register(Guest)
class GuestAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return super().get_queryset(request).filter(user=request.user)
def has_change_permission(self, request, obj=None):
return (obj is None or obj.user == request.user)
No need to muck about with the form.
Related
I have my Comment model:
class Comment(models.Model):
"""A comment is a content shared by a user in some post."""
user = models.ForeignKey('users.User', on_delete=models.CASCADE, null=False)
post = models.ForeignKey('posts.Post', on_delete=models.CASCADE, null=False)
content = models.TextField(max_length=1000, null=False, blank=False)
def __str__(self):
"""Return the comment str."""
return "'{}'".format(self.content)
Its serializer:
class CommentSerializer(serializers.ModelSerializer):
"""Comment model serializer."""
user = serializers.PrimaryKeyRelatedField(read_only=True)
class Meta:
model = Comment
fields = '__all__'
def create(self, validated_data):
"""Create a new comment in some post, by request.user."""
validated_data['user'] = self.context['request'].user
return super().create(validated_data)
def list(self, request):
"""List all the comments from some post."""
if 'post' not in request.query_params:
raise ValidationError('Post id must be provided.')
q = self.queryset.filter(post=request.query_params['post'])
serializer = CommentSerializer(q, many=True)
return Response(serializer.data)
The viewset:
class CommentViewSet(viewsets.ModelViewSet):
serializer_class = CommentSerializer
queryset = Comment.objects.all()
def get_permissions(self):
permissions = []
if self.action == 'create' or self.action == 'destroy':
permissions.append(IsAuthenticated)
return [p() for p in permissions]
def get_object(self):
"""Return comment by primary key."""
return get_object_or_404(Comment, id=self.kwargs['pk']) # this is the drf's get_object_or_404 function
def destroy(self, request, *args, **kwargs):
"""Delete a comment created by request.user from a post."""
pdb.set_trace()
instance = self.get_object()
if instance.user != request.user:
raise ValidationError('Comment does not belong to the authenticated user.')
self.perform_destroy(instance)
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
So far so good when it comes to list, create and retrieve. But when it comes to delete/destroy (Idk the difference) I don't know how to get the URL for the DELETE request.
I'm using Postman to do so.
Urls.py:
router = routers.SimpleRouter()
router.register(r'comments', CommentViewSet, basename='comments')
pdb.set_trace() # Put a pdb here to see what does router.urls have.
urlpatterns = [
# Non api/simple django views
path('create_comment/', create_comment, name='create_comment'),
path('delete_comment/', delete_comment, name='delete_comment'),
# rest api views
path('rest/', include(router.urls))
]
When I debug the router.urls, the terminal shows this:
[<URLPattern '^comments/$' [name='comments-list']>, <URLPattern '^comments/(?P<pk>[^/.]+)/$' [name='comments-detail']>]
Why there is no url for create and destroy?
I could create some Comments from the api, and I didn't even know how did I get the POST request url for the create function, I just guessed lol, but that's not what you want when you're programming right?
I have checked the Routers documentation but I don't get it. Please give some help! Many thanks.
The answer is simple, Django uses the comments-list list for create and list operation and comments-detail for update and delete operations.
That is, there are only two URL end-points, but it supports several actions, which can be performed by changing the HTTP methods
You can use HTTP GET foo-bar/comments/ to retrieve all comments where as HTTP POST foo-bar/comments/ can be used to create a new comment.
When user requests object, when user wants to enter detail view's page, I want to check weather user have subscription and redirect him. But I don't know how to request user and validate him in DetailView. This is what at least I could did.
class PropertyDetailView(LoginRequiredMixin, DetailView):
login_url = '/login/'
model = Property
template_name = 'project/property/property_detail.html'
def post(self, *args, **kwargs):
if self.request.user.sale_tariff is None:
return redirect('/')
Are there any ways how to validate DetailView?
Overriding the get request something like this
def get(self, *args, **kwargs):
if self.request.user.sale_tariff is None:
return redirect('/')
else:
return render(template_name)
views.py
class EditPost(UserPassesTestMixin, LoginRequiredMixin, UpdateView):
model = Posts
form_class = PostForm
template_name="posts/add_post.html"
def test_func(self):
x = self.request.user.pk
print (x)
y = Posts.objects.get(user='user')
print (y)
if x == y:
return True
else:
if self.request.user.is_authenticated():
raise Http404("You are not allowed to edit this Post")
models.py
class Posts(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, default=1)
post = models.CharField(max_length=1200, blank=False)
How do i match the loggedin user and the user object of the Post
i could not find any solution since i am using class based views.
try this:
add dispatch to EditPost class
def dispatch(self, request, *args, **kwargs):
obj = self.get_object()
if obj.user != self.request.user:
raise Http404("You are not allowed to edit this Post")
return super(EditPost, self).dispatch(request, *args, **kwargs)
Doing the check in test_func is tricky. You need to fetch the object once in test_func to check whether the user is allowed to use it, and then the object is fetched again by the same fiew.
An easier approach is to override the get_queryset method. If the user is not the author of the post, they will get a 404.
class EditPost(LoginRequiredMixin, UpdateView):
def get_queryset(self):
return super(EditPost, self).filter(user=self.request.user)
To add to the previous posts, try this:
Add to your class EditPost:
login_url = 'your url name or path to login page'
def test_func(self):
obj = self.get_object()
return obj.author == self.request.user
The test_func method is used by UserPassesTestMixin for this particular logic. To override it we set variable obj to the current object returned by the view using get.object().
After that, if the author on the current object (obj.author) matches the current user (self.request.user) we allow editing. If not (i.e. false) we throw an error.
login_url is from LoginRequiredMixin and the default location is /accounts/login. To override it, set your own path or name of the login template. This will take those people who are not logged in to the login page.
Hello Everybody excuse my english....
I am facing a problem with django.
I need to restrict object so only their owners can print it.
Model.py
class Post(models.Model):
title = models.CharField(max_length=50, blank=False)
prenom = models.CharField(max_length=255, blank=False)
user = models.ForeignKey(User, null=False)
View.py
class detailpost(DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
url.py
url(r'detail-post/(?P<pk>[-\d]+)$', views.detailpost.as_view(), name='detailpost'),
This works properly but the problem is that every users can access to the post of another user (http://localhost:8000/detail-post/1). So my question is how can i do some stuff befor rendering the page and see if the post belongs to the current user if yes we print it else we redirect the user to another page.
You can use the LoginRequiredMixin (new in Django 1.9) to make sure that only logged in users can access the view.
Then override the get_queryset method, and filter the queryset so that it only includes posts by the logged-in user.
from django.contrib.auth.mixins import LoginRequiredMixin
class DetailPost(LoginRequiredMixin, DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
def get_queryset(self):
queryset = super(DetailPost, self).get_queryset()
return queryset.filter(owner=self.request.user)
If the user views a post that does not belong to them, they will see a 404 page. If you must redirect the user instead of showing a 404, then you'll have to take a different approach.
Note that I have renamed your class DetailPost (CamelCase is recommended for classes in Django. You'll have to update your urls.py as well.
You can override get() or post() method in your view class.
from django.shortcuts import redirect
class detailpost(DetailView):
model = Post
template_name = 'detail-post.html'
context_object_name = 'post'
def get(self, request, *args, **kwargs):
self.post = Post.objects.get(pk=self.kwargs['pk'])
if self.post.user != request.user or not request.user.is_superuser:
return redirect('login')
else:
return super(detailpost, self).get(request, *args, **kwargs)
You should override 'get()' method in your 'detailpost' class, so that it would be something like below:
def get(self, request, *args, **kwargs):
queryset = self.model._default_manager.filter(user=request.user)
self.object = self.get_object(queryset)
context = self.get_context_data(object=self.object)
return self.render_to_response(context)
It seems like it is the only way to pass User from Request instance to filter queryset.
I did not find that DetailView uses self.request
I have the following model:
class UserProfile(models.Model):
user = models.OneToOneField(User, related_name="user")
people_interested = models.ManyToManyField(User, related_name="interested")
Now I want a form where I want to offer users a form where they can choose people_interested, so I add the following forms.py
class ChooseForm(forms.Form):
q_set = User.objects.all()
peers = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset = q_set)
and then in views:
form = ChooseForm(data = request.POST or None)
if request.POST and form.is_valid():
uprofile, created = UserProfile.objects.get_or_create(user=request.user)
uprofile.people_interested = form.cleaned_data['peers']
uprofile.save()
return HttpResponseRedirect("/")
else:
return render(request, "form_polls.html", {'form':form})
But the trouble with this is, the current user instance also gets displayed. So I tried the following in views.py:
form = ChooseForm(request.user.id, data = request.POST or None)
and then in forms.py
class ChooseForm(forms.Form):
def __init__(self, uid, *args, **kwargs):
super(ChooseForm, self).__init__(*args, **kwargs)
self.fields['peers'].queryset = User.objects.exclude(id=uid)
q_set = User.objects.all()
peers = forms.ModelMultipleChoiceField(widget=forms.CheckboxSelectMultiple, queryset = q_set)
But the above is not a clean implementation, is there a better method of doing it??
What makes you say this is not a clean implementation? Overwriting queryset on __init__ is perfectly acceptable.
The only things I'd do to improve your code is using a post_save signal on User to create it's UserProfile, then just do user.get_profile() on your view. See this question
You could also use a ModelForm for UserProfile instead of a regular form, and limit the fields to people_interested.