Prefetch m2m field values in django-admin - django

I have own Auth User model, which inherits from PermissionsMixin. While I'm visiting django-admin page for any instance of that model, I get a lot of db queries (as I have a lot of permissions). The problem lies here, django/contrib/auth/models.py:
class Permission(models.Model):
[...]
def __str__(self):
return "%s | %s | %s" % (
six.text_type(self.content_type.app_label),
six.text_type(self.content_type),
six.text_type(self.name))
Every time a permission is displayed on admin page, it makes a query for its content_type.
Question is: can I ensure that for every query involving my Auth User model, especially for that which doesn't come from my code (like django admin) will be run prefetch_related for permissions and their content_types?

If you need this only for django-admin, then you can create your own admin page using techniques described in official documentation.
For instance:
from django.contrib import admin
from django.contrib.auth.models import Permission
from foo.models import User
class UserAdmin(admin.ModelAdmin):
def formfield_for_manytomany(self, db_field, request=None, **kwargs):
if db_field.name == 'user_permissions':
kwargs['queryset'] = Permission.objects.all().select_related('content_type')
return super(UserAdmin, self).formfield_for_manytomany(db_field, request, **kwargs)
admin.site.register(User, UserAdmin)

Related

Django permission's specific to templateview

How to create multiple permission classes that would restrict user from accessing a template view ( that doesnt use any model).
For example: I have 4 user categories - ( admin, management, principal, teacher). I have a Admin dashboard template view which should be restricted to user type=Admin.
I would like to be able to write multiple permission classes, which I can then use in combination in any view.
Following code generates 403 error:
class AdministratorPermission(AccessMixin):
def has_permission(self):
return True
class GeneralPerm1(AccessMixin):
def has_permission(self):
return True
class DashboardView(PermissionRequiredMixin,
LoginRequiredMixin, TemplateView):
template_name = 'administrator/dashboard.html'
permission_required = (AdministratorPermission,GeneralPerm1)
Is there a way to do something like DRF permissions.
Thanks
Permission1Mixin.py:
from django.contrib.auth.mixins import AccessMixin
from django.core.exceptions import PermissionDenied
class IsAdministratorMixin(AccessMixin):
""" if user is not administrator, decline permission """
def dispatch(self, request, *args, **kwargs):
"""
if user is authenticated and administrator
we are good. otherwise permission denied
"""
if request.user.is_authenticated and \
request.user.category.category == \
UserCategoryEnum.ADMINISTRATOR.value:
return super().dispatch(request, *args, **kwargs)
raise PermissionDenied('Permission denied') # decline permission
view.py
class DashboardView(IsAdministratorMixin, TemplateView):
template_name = 'administrator/dashboard.html'
This way we can create multiple independent permission mixins and use them in combination.

Autocomplete in django and allow creating user if not found

I'm trying to add a search box for users on the webpage to see his profile, and if the user doesn't exist, then I have the option to create it.
In flask, I used a solution that used jquery for the autocomplete, and when no one was found, it would simply put "Create_user" as the text submitted in the form, and then redirect to the url for user creation. I was not able to port this to django(javascript is not my forté and I'm starting django.)
So I tried django-autocomplete-light, but while the autocomplete worked, I found no way to replicate the behavior that would redirect me to the user creation page in the case no one was found. (the create exemple in the docs only allow to create a simple entry, while I need to create a user based on a model)
Any leads on how to accomplish this with django?
That's what i was looking few days ago, i found this
Example Admin code for autocomplete
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
from django import forms
from selectable.forms import AutoCompleteSelectField, AutoCompleteSelectMultipleWidget
from .models import Fruit, Farm
from .lookups import FruitLookup, OwnerLookup
class FarmAdminForm(forms.ModelForm):
owner = AutoCompleteSelectField(lookup_class=OwnerLookup, allow_new=True)
class Meta(object):
model = Farm
widgets = {
'fruit': AutoCompleteSelectMultipleWidget(lookup_class=FruitLookup),
}
exclude = ('owner', )
def __init__(self, *args, **kwargs):
super(FarmAdminForm, self).__init__(*args, **kwargs)
if self.instance and self.instance.pk and self.instance.owner:
self.initial['owner'] = self.instance.owner.pk
def save(self, *args, **kwargs):
owner = self.cleaned_data['owner']
if owner and not owner.pk:
owner = User.objects.create_user(username=owner.username, email='')
self.instance.owner = owner
return super(FarmAdminForm, self).save(*args, **kwargs)
class FarmAdmin(admin.ModelAdmin):
form = FarmAdminForm
admin.site.register(Farm, FarmAdmin)
Source code
https://github.com/mlavin/django-selectable
and
Documentation
http://django-selectable.readthedocs.org/en/latest/
Hope this will help you too

How do I present data relevant to the logged-in user based on Group?

I'm a total newbie to django so this may well have an obvious answer but so far google hasn't worked out for me.
I have this skeleton application using Django 1.8.
I have a simple model that has an owner field which is a ForeignKey to Group.
When a user is logged in I would like to show only the items that he/she has access to. Access being determined by the fact that the user belongs to the same group.
model.py
class Device(models.Model):
name = models.CharField(max_length=100,db_index=True)
owner = models.ForeignKey(Group)
def __str__(self):
return self.name
views.py
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.views import generic
from .models import Device
from django.contrib.auth.models import Group, User
class IndexView(generic.ListView):
"""
This renders the index page listing the devices a user can view
"""
template_name = 'devices/index.html'
context_object_name = 'devices_list'
#method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(IndexView, self).dispatch(*args, **kwargs)
def get_queryset(self):
"""
Return the devices visible to the logged-in user
"""
return devices=Device.objects.all()
What I don't seem to be able to figure out is what to put in the .filter() instead of .all() call in my get_queryset method.
Updated based on Jean-Michel's feedback.
I don't have a Django environment in front of me at the moment, but this might be a good start:
return devices=Device.objects.filter(owner=self.request.user.groups.all())
Alternatively, Django's ORM uses double underscore (__) to access field lookups. These can be used to get values greater than (__gt), or in a list (__in) amongst other lookups (see the docs).
return devices=Device.objects.filter(owner__in=self.request.user.groups.all())
This kind of depends on where the user object is located. I'm assuming the logged in user is kept as a class attribute, i.e., self.user. Per, Jean-Michel's comments, the user object is attached to the request. So we can access it from self.request.user.groups.
Finally, you can access specific fields on models using the double underscore notation as well (__), this example is from the docs:
# Find all Articles for any Reporter whose first name is "John".
>>> Article.objects.filter(reporter__first_name='John')
[<Article: John's second story>, <Article: This is a test>]

Django admin - change permissions list

Is there any possibility to change permissions list in user edit page? I don't wan't to show all of permissions for example admin log entry or auth group etc.
How can I modify a main queryset to exclude some of it?
I got the idea from this topic, which also answer your question, but it's not that clear.
You have to overwrite the queryset of user permissions in the UserAdmin form used for visualization.
To do this, the easiest way is to create a subclass of UserAdmin and overwrite the get_form method:
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin
class MyUserAdmin(UserAdmin):
def get_form(self, request, obj=None, **kwargs):
# Get form from original UserAdmin.
form = super(MyUserAdmin, self).get_form(request, obj, **kwargs)
if 'user_permissions' in form.base_fields:
permissions = form.base_fields['user_permissions']
permissions.queryset = permissions.queryset.filter(content_type__name='log entry')
return form
You can change the filter of your queryset for whatever you want:
Examples:
# Exclude admin and auth.
permissions.queryset = permissions.queryset.exclude(content_type__app_label__in=['admin', 'auth'])
# Only view permissions of desired models (Can be your models or Django's)
permissions.queryset = permissions.queryset.filter(content_type__model__in=['blog', 'post', 'user', 'group'])
After you create your class, you have to register your User model with your newly created Admin:
admin.site.unregister(User) # You must unregister first
admin.site.register(User, MyUserAdmin)
Edit:
I added comment from Maik Hoepfel, because this code made django crashed when creating new user.
You can do the same with the permission list in your Group edit page, but you have to create another Admin that extends from GroupAdmin, and change form.base_fields['user_permissions'] with form.base_fields['permissions']
Renato's answer is almost perfect. The Django Admin makes adding a user a two-step process with the same form, and his code fails with a KeyError for 'user_permissions' in the first step.
The fix is easy enough, just use the code below instead:
def get_form(self, request, obj=None, **kwargs):
form = super(MyUserAdmin, self).get_form(request, obj, **kwargs)
# adding a User via the Admin doesn't include the permissions at first
if 'user_permissions' in form.base_fields:
permissions = form.base_fields['user_permissions']
permissions.queryset = permissions.queryset.filter(content_type__name='log entry')
return form

Showing custom model validation exceptions in the Django admin site

I have a booking model that needs to check if the item being booked out is available. I would like to have the logic behind figuring out if the item is available centralised so that no matter where I save the instance this code validates that it can be saved.
At the moment I have this code in a custom save function of my model class:
def save(self):
if self.is_available(): # my custom check availability function
super(MyObj, self).save()
else:
# this is the bit I'm stuck with..
raise forms.ValidationError('Item already booked for those dates')
This works fine - the error is raised if the item is unavailable, and my item is not saved. I can capture the exception from my front end form code, but what about the Django admin site? How can I get my exception to be displayed like any other validation error in the admin site?
In django 1.2, model validation has been added.
You can now add a "clean" method to your models which raise ValidationError exceptions, and it will be called automatically when using the django admin.
The clean() method is called when using the django admin, but NOT called on save().
If you need to use the clean() method outside of the admin, you will need to explicitly call clean() yourself.
http://docs.djangoproject.com/en/dev/ref/models/instances/?from=olddocs#validating-objects
So your clean method could be something like this:
from django.core.exceptions import ValidationError
class MyModel(models.Model):
def is_available(self):
#do check here
return result
def clean(self):
if not self.is_available():
raise ValidationError('Item already booked for those dates')
I haven't made use of it extensively, but seems like much less code than having to create a ModelForm, and then link that form in the admin.py file for use in django admin.
Pretty old post, but I think "use custom cleaning" is still the accepted answer. But it is not satisfactory. You can do as much pre checking as you want you still may get an exception in Model.save(), and you may want to show a message to the user in a fashion consistent with a form validation error.
The solution I found was to override ModelAdmin.changeform_view(). In this case I'm catching an integrity error generated somewhere down in the SQL driver:
from django.contrib import messages
from django.http import HttpResponseRedirect
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
try:
return super(MyModelAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except IntegrityError as e:
self.message_user(request, e, level=messages.ERROR)
return HttpResponseRedirect(form_url)
The best way is put the validation one field is use the ModelForm... [ forms.py]
class FormProduct(forms.ModelForm):
class Meta:
model = Product
def clean_photo(self):
if self.cleaned_data["photo"] is None:
raise forms.ValidationError(u"You need set some imagem.")
And set the FORM that you create in respective model admin [ admin.py ]
class ProductAdmin(admin.ModelAdmin):
form = FormProduct
I've also tried to solve this and there is my solution- in my case i needed to deny any changes in related_objects if the main_object is locked for editing.
1) custom Exception
class Error(Exception):
"""Base class for errors in this module."""
pass
class EditNotAllowedError(Error):
def __init__(self, msg):
Exception.__init__(self, msg)
2) metaclass with custom save method- all my related_data models will be based on this:
class RelatedModel(models.Model):
main_object = models.ForeignKey("Main")
class Meta:
abstract = True
def save(self, *args, **kwargs):
if self.main_object.is_editable():
super(RelatedModel, self).save(*args, **kwargs)
else:
raise EditNotAllowedError, "Closed for editing"
3) metaform - all my related_data admin forms will be based on this (it will ensure that admin interface will inform user without admin interface error):
from django.forms import ModelForm, ValidationError
...
class RelatedModelForm(ModelForm):
def clean(self):
cleaned_data = self.cleaned_data
if not cleaned_data.get("main_object")
raise ValidationError("Closed for editing")
super(RelatedModelForm, self).clean() # important- let admin do its work on data!
return cleaned_data
To my mind it is not so much overhead and still pretty straightforward and maintainable.
from django.db import models
from django.core.exceptions import ValidationError
class Post(models.Model):
is_cleaned = False
title = models.CharField(max_length=255)
def clean(self):
self.is_cleaned = True
if something():
raise ValidationError("my error message")
super(Post, self).clean()
def save(self, *args, **kwargs):
if not self.is_cleaned:
self.full_clean()
super(Post, self).save(*args, **kwargs)