I have two admin models, one is called Animal and another one is called Person. Each one has its own has_delete_permission on the ModelAdmin class.
The code I am using is listed below.
class Animal(models.Model):
sound = models.CharField(max_length=25, blank=True, null=True)
class Person(models.Model):
sound = models.CharField(max_length=25, blank=True, null=True)
class AnimalAdmin(admin.ModelAdmin):
model = Animal
def has_delete_permission(self, request, obj=None):
if request.POST and request.POST.get('action') == 'delete_selected':
animals = Animal.objects.filter( id__in = request.POST.getlist('_selected_action') )
print (animals)
return True
class PersonAdmin(admin.ModelAdmin):
model = Person
def has_delete_permission(self, request, obj=None):
return True
admin.site.register(Animal, AnimalAdmin)
admin.site.register(Person, PersonAdmin)
When I try to delete a Person instance that have the same ID of some Animals, the instances of Animals are printed. This could be a serious problem if I was doing some logic like changing the database or showing a message to the user.
The point is why has_delete_permission methods of different classes are also executed ?
This happens because of one method of the class AdminSite : each_context.
def each_context(self, request):
"""
Return a dictionary of variables to put in the template context for
*every* page in the admin site.
For sites running on a subpath, use the SCRIPT_NAME value if site_url
hasn't been customized.
"""
This method is called in every pages rendered in the admin backend, and it calls successively get_app_list(request) then _build_app_dict(request), which is looping over all admin models to check get_model_perms(request) :
def get_model_perms(self, request):
"""
Return a dict of all perms for this model. This dict has the keys
``add``, ``change``, ``delete``, and ``view`` mapping to the True/False
for each of those actions.
"""
return {
'add': self.has_add_permission(request),
'change': self.has_change_permission(request),
'delete': self.has_delete_permission(request),
'view': self.has_view_permission(request),
}
So if you override has_delete_permission in others ModelAdmin, it will be called too, each time you display a page.
You are seeing the instances of Animals matching the same indexes of the deleted Person because you filter the queryset with a simple list of indexes coming from request (request.POST.getlist('_selected_action')).
Note : nothing is printed when you are not in a POST request.
That said, you should avoid mixing different things in one function : has_delete_permission is about checking a delete permission on a model. I'm not sure what you want to check/prevent here, but you may find a better place.
Related
I am working on a project which is administered by a super admin who puts in data for different companies.
Lets say, I have these models:
class Company(models.Model):
name = models.CharField(max_length=100)
class ContactPerson(models.Model):
name = models.CharField(max_length=100)
company = models.ForeignKey(Company)
class Item(models.Model):
company = models.ForeignKey(Company)
contact_person = models.ForeignKey(ContactPerson)
I need to ensure that I (in django admin) in the edit mode I only see contact persons which belong to the selected company.
Being not in the year 2005 anymore I want to avoid writing loads of super ugly jQuery code.
I guess I could overwrite the admin form for Item. But still I had to make the contact_person optional, so when I create a new Item, the list of contact persons need to be empty. Then I'd select a company, save it and go back to edit. Now the contact_person list would be filled and I could add somebody. But if I now change the comany, I'd have to remove all selected contact persons. Sure, I could to this in the form... but it looks SO hacky and not like a nice django solution.
Anybody got some fancy ideas?
Actually, django provided me with a neat solution.
When you look at the UserAdmin class within the django code, you'll find a built-in way to handle a two-step creation process.
#admin.register(User)
class UserAdmin(admin.ModelAdmin):
...
add_form = UserCreationForm
...
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during user creation
"""
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
When the attribute add_form is set and the object has no id yet (= we are creating it), it takes a different form than usual.
I wrapped this idea in an admin mixin like this:
class AdminCreateFormMixin:
"""
Mixin to easily use a different form for the create case (in comparison to "edit") in the django admin
Logic copied from `django.contrib.auth.admin.UserAdmin`
"""
add_form = None
def get_form(self, request, obj=None, **kwargs):
defaults = {}
if obj is None:
defaults['form'] = self.add_form
defaults.update(kwargs)
return super().get_form(request, obj, **defaults)
Now, when I have dependent fields, I create a small form, containing all values independent of - in my case - company and a regular form containing everything.
#admin.register(Item)
class ItemAdmin(AdminCreateFormMixin, admin.ModelAdmin):
form = ItemEditForm
add_form = ItemAddForm
...
Now I can customise the querysets of the dependent field in my edit form:
class ItemEditForm(forms.ModelForm):
class Meta:
model = Item
exclude = ()
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['contact_person'].queryset = ContactPerson.objects.filter(company=self.instance.company)
The only drawback is, that all dependent fields need to be nullable for the database. Otherwise you wouldn't be able to save it in the creation process.
Luckily, you can tell django that a field is required in the form but not on database level with blank=False, null=True in the model declaration.
Hope this helps somebody else as well!
I'm fairly new to Django and Django Rest Framework and I can't figure out why my code isn't working.
I have a Biz model that has a few fields:
class Biz(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
title = models.CharField(max_length=200)
description = models.TextField()
address = models.CharField(max_length=255, blank=True)
city = models.CharField(max_length=100)
phone = PhoneNumberField()
which I serializer using ModelSerializer:
class BizSerializer(serializers.ModelSerializer):
class Meta:
model = Biz
fields = "__all__"
And I use ModelViewSet to have an endpoint for it:
class BizViewSet(viewsets.ModelViewSet):
queryset = Biz.objects.all()
authentication_classes = (authentication.TokenAuthentication,)
permission_classes = [HasGroupPermission]
required_groups = {
"GET": ["__all__"],
"POST": ["member", "biz_post"],
"PUT": ["member", "biz_edit"],
"PATCH": ["member", "biz_edit"],
}
serializer_class = BizSerializer
You probably noticed HasGroupPermission. It is a custom permission I made to confirm the requesting user is in required group(s) the code is:
def is_in_group(user, group_name):
"""
Takes a user and a group name, and returns `True` if the user is in that group.
"""
try:
return Group.objects.get(name=group_name).user_set.filter(id=user.id).exists()
except Group.DoesNotExist:
return None
class HasGroupPermission(permissions.BasePermission):
"""
Ensure user is in required groups.
"""
def has_permission(self, request, view):
# Get a mapping of methods -> required group.
required_groups_mapping = getattr(view, "required_groups", {})
# Determine the required groups for this particular request method.
required_groups = required_groups_mapping.get(request.method, [])
# Return True if the user has all the required groups or is staff.
return all(
[
is_in_group(request.user, group_name)
if group_name != "__all__"
else True
for group_name in required_groups
]
) or (request.user and request.user.is_staff)
However, when I make a GET request, the permission function works like it's supposed to and allows everyone to make the request, and when i make a POST request, the permission function also works perfectly (if user isn't in both "member" and "biz_post" groups the request is denied).
The problem arises when I try other methods such as PUT, PATCH, and DELETE. Why is this issue happening? Half the methods work and the other half (sorta) don't. My knowledge in DRF is limited at the moment, and I can't seem to solve the issue.
I realized my problem which I found very silly. My BizViewSet is actually a ViewSet and I didn't realize that I have to make PATCH, PUT, and DELETE requests to the object link (as in localhost:8000/api/biz/$id). Since my User serializer isn't a ViewSet I thought the patch method works the same way which was I pass a primary key in JSON along with the data I wanted to patch but ViewSets are different and I didn't know that. Silly.
Hi you can use DjangoModelPermissions instead of HasGroupPermission
(at the first you must import it)
from rest_framework.permissions import DjangoModelPermissions
This permission check that user have permission for PUT, POST and DELETE
All user have GET permission
You must set permission for user in admin or set permission for group of user
I hope it helps you
has_permission method not provide object-level permission and PUT and PATCH need object-level permission.
You must create object-level permissions, that are only run against operations that affect a particular object instance by using has_object_permission method of permissions.BasePermission class.
See this link.
Hope it helped.
I changed my .all method so it would select only instances with published=True:
class EventManager(models.Manager):
def all(self, *args, **kwargs):
return super().get_queryset().filter(published=True, *args, **kwargs)
This is related to the problem model fields:
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
As a result ManyToManyField ends up selecting all the Event instances.
What would you suggest me to do in order to save the published functionality and be able to manually add related events? Thank you.
As far as I know, Django does not use Model.objects as manager, but the Model._basemanager, which normally should return all objects.
You can use limit_choices_to [Django-doc] here to limit the choices of the many-to-many field, like:
from django.db.models import Q
class Event(models.Model):
related_events = models.ManyToManyField(
'self',
limit_choices_to=Q(published=True)
blank=False,
related_name='related'
)
published = models.BooleanField(default=False)
objects = EventManager()
You probably also want to remove blank=True, since that means that by default, you make the field not to show op in the forms. So if you want to manually edit the related events, then.blank=False.
Furthermore a ManyToManyField to 'self' is by default symmatrical. This thus means that if event1 is in the related_events of event2, then event2 is in related_events of event1 as well. If you do not want that, you might want to add symmetrical=False [Django-doc].
Note that there are still some scenario's where non-published events might end up in the related events. For example by adding a published event to the related events, and then "unpublish" it.
As for the manager, I think you better patch the get_queryset method:
class EventManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(published=True)
Otherwise there are lot of ways to "circumvent" the filtering. For example: Event.objects.filter(id__gt=-1) would still give me all Events, but since I did not call .all(), this would thus not filter on published.
In the ModelAdmin, you could aim to specify the queryset for this ManyToManyField with:
class EventAdmin(admin.ModelAdmin):
def get_field_queryset(self, db, db_field, request):
if db_field.name == 'event_dates':
return db_field.remote_field.model.base_manager.all()
else:
super(EventAdmin, self).get_field_queryset(db, db_field, request)
That's what I ended up doing in order to show only published events in my html and show all the events (published and unpublished) in admin dashboard.
class EventManager(models.Manager):
"""
changing get_queryset() to show only published events.
if all is set to True it will show both published and unpublished
if False, which is default it will show only published ones
"""
def get_queryset(self, all=False):
if not all:
return super().get_queryset().filter(published=True)
else:
return super().get_queryset()
class Event(models.Model):
related_events = models.ManyToManyField('self', blank=True, related_name='related')
published = models.BooleanField(default=False)
objects = EventManager()
And in ModelAdmin I call get_queryset with all set to True, otherwise I won't be able to see unpublished ones.
class EventAdmin(admin.ModelAdmin):
def get_queryset(self, request):
return Event.objects.get_queryset(all=True)
I could not simply change my model's all method because it would mess with my ManyToManyField by adding all the model instances to to it. So I did all this.
I have created a many-to-many relationship between my User model and my Projects model, and I want to list all the projects a specific user is part of on the user's profile page.
My models are:
class User(auth.models.User,auth.models.PermissionsMixin):
def __str__(self):
return "#{}".format(self.username)
class Prosjekter(models.Model):
id = models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')
...
users = models.ManyToManyField(settings.AUTH_USER_MODEL, blank=True, verbose_name="Project participants")
...
The Django documentation states that:
Reverse m2m queries are supported (i.e., starting at the table that doesn’t have a ManyToManyField):
>>> Publication.objects.filter(article__id=1)
<QuerySet [<Publication: The Python Journal>]>
I am able to print the QuerySet of a specific user by typing (here illustrated with the user whose primary key is 2):
class UserDetailView(generic.DetailView):
model = User
template_name = 'accounts/user_detail.html'
results = Prosjekter.objects.filter(users__pk=2),
print(results)
This will return:
(<QuerySet [<Prosjekter: Project 1>, <Prosjekter: Project two>, <Prosjekter: Project third>]>,)
However, as you can see, I am only calling the user whose primary key is 2. Instead, I want to create a view that can be used in the general sense. Specifically, that I can see:
all user 1's projects on http://.../users/1/
all user 2's projects on http://.../users/2/
and so on
I am new to Python and Django and have trouble creating this view. Among many other attempts, I tried following this suggestion without success. Therefore, can you please help me with the following?
Can you update my UserDetailView code so that it works for all users?
Can you show me the code that I must implement on the /user_detail.html page (if I have understood it correctly, it should be a for loop that iterates through all the projects in the Prosjekter model and lists only those in which the specific user is a part of).
You are using DetailView wrong:
class UserDetailView(generic.DetailView):
model = User
template_name = 'accounts/user_detail.html'
def get_context_data(self, **kwargs):
ctx = super(UserDetailView, self).get_context_data(**kwargs)
ctx['results'] = Prosjekter.objects.filter(users=self.get_object())
return ctx
If you want more context variables, add them this way:
def get_context_data(self, **kwargs):
ctx = super(UserDetailView, self).get_context_data(**kwargs)
ctx.update({
'results': Prosjekter.objects.filter(users=self.get_object()),
'foo': 'bar',
'spam': 'ham',
})
return ctx
If user 1 creat this ticket :
mywebsite/manager/tickets/ticket-from-user-1/
And user 2 create that :
mywebsite/manager/tickets/ticket-from-user-2/
How can I prevent user 1 to access the ticket from user 2 or other users by typing it in the url?
views.py
class TicketDisplay(LoginRequiredMixin, DetailView):
model = Ticket
template_name = 'ticket_detail.html'
context_object_name = 'ticket'
slug_field = 'slug'
def get_context_data(self, **kwargs):
context = super(TicketDisplay, self).get_context_data(**kwargs)
context['form_add_comment'] = CommentForm()
return context
url.py
url(r'^manager/tickets/(?P<slug>[-\w]+)/$',views.TicketDetail.as_view(), name='ticket_detail')
I recently implemented this functionality in a project. It can be done by using automatically generated uuid's. Django has a built-in model field for this, or you can use a slug field and give it a default value. Here is a quick example.
In your models.py file, import the uuid library and then set the default value of your slug field to be uuid.uuid4.
models.py:
import uuid
class Ticket(models.Model):
uuid = models.SlugField(default=uuid.uuid4, editable=False)
...
In urls.py, just use the uuid field as if it were a pk. Something like this:
url(r'^manager/tickets/(?P<uuid>[0-9a-z-]+)/?$', TicketDetail.as_view(), name='ticket-detail'),
In your detail, update, and delete views, you will need to make sure and set these two attributes so that Django knows which field to use as the slug:
slug_field = 'uuid'
slug_url_kwarg = 'uuid'
Then in your templates and whenever you need to retrieve an object for the kwargs, just use the uuid instead of the pk.
Note that in addition to this, you should also do all you can with permissions to block users from seeing other pages. You may be able to block certain accounts from viewing other peoples details. For instance, you could probably write a permissions mixin to check whether request.user matches up with the object that the view is handling.
tldr This is assuming that you have some kind of relation to a user on your Ticket model:
class SameUserOnlyMixin(object):
def has_permissions(self):
# Assumes that your Ticket model has a foreign key called user.
return self.get_object().user == self.request.user
def dispatch(self, request, *args, **kwargs):
if not self.has_permissions():
raise Http404('You do not have permission.')
return super(SameUserOnlyMixin, self).dispatch(
request, *args, **kwargs)
Finally, stick it on to your view like this:
class TicketDisplay(LoginRequiredMixin, SameUserOnlyMixin, DetailView):
...
You will need to make user 1 have something that user 2 can not imitate.
Preferred way would be to use your existing auth methods and check if user accessing the page is allowed to do so.
If you don't have registration on your site then you could generate some random string - secret - and store it with the question. If 'user' has that secret then he's allowed.
This secret string can be stored in cookie or made a part of URL.
Storing it in a cookie has a drawback: if cookie is lost then nobody can access the page. Also user cannot access it from other browser.
Making it a part of url has another drawback: if someone else sees the link, he can access that page too. This could be bad if user's software automatically reports likns he visits somewhere.
Combining these approaches has both drawbacks.