Understanding Python Classes or Customising the Django UserAdmin model - python

I'm attempting to override some of the behaviour of the Django UserAdmin model. Particularly, I'd like to hide the 'superuser' field from non-superusers.
So, my approach is this:
class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model."
# ...
def has_change_permission(self, request, obj=None):
"""
Returns True if the given request has permission to change the given
Django model instance.
If `obj` is None, this should return True if the given request has
permission to change *any* object of the given type.
"""
opts = self.opts
return request.user.has_perm(opts.app_label + '.' + opts.get_change_permission())
#...
Based on what I've found in ModelAdmin
class UserAdmin(UserAdmin):
"""
... my customised UserAdmin
"""
# adding a new method
def is_superuser(self, request):
"Returns True if the given user is a superuser."
return request.user.is_superuser
# then elsewhere 'hopefully' show a slightly different fieldset
# the following, of course, doesn't work.
fieldsets = (
(None, {
'fields': (
("first_name", "last_name"),
("email", "password"),
"is_staff",
"is_active",
"is_superuser" if self.is_superuser() else None
)
}),
('Groups', {
'fields': (
'groups',
)
}),
('Meta', {
'classes': ('collapse',),
'fields': (
'username',
"last_login",
"date_joined"
)
})
)
So, my questions are:
How do I create a def within my new custom UserAdmin class, such as above, and how do I call it? (How do I know when I'm in the right context to do so)
Part 2 (bonus): how can I succinctly include/exclude the 'is_superuser' field in the form, as the psuedo code above is suggesting?
Kind thanks fellows!
~ Daryl
Thank you

If you just want to forbid users to promote themselves to superuser, override YourUserAdmin.get_readonly_fields():
class YourUserAdmin(admin.ModelAdmin):
...
def get_readonly_fields(self, request, obj=None):
if request.user.is_superuser:
return None
try:
return self.readonly_fields + ('is_superuser',)
except:
return ('is_superuser',)
You have to unregister the default User/UserAdmin and then register your own.
admin.site.unregister(User)
admin.site.register(User, YourUserAdmin)
However, the ZEN of admin says:
At it's core, Django's admin is designed for a single activity: trusted users editing structured content.
If the user is not trusted, do not give him edit rights to edit user accounts, period. Even if you hide the superadmin option and the "filter by superadmin status" filter, he can just change your password and log in as you. So, if you need some untrusted users to edit user accounts, forget the admin and write your own dumbed down interface.

No experience with Django but thinking a closure might help you in this situation:
class UserAdmin(UserAdmin):
# adding a new method
def is_superuser(self, request):
"Returns True if the given user is a superuser."
return request.user.is_superuser
def gen_fieldset(self, request):
'''
If request.user.is_supeuser == true then return fieldset generator for superuser.
Else return fieldset generator for normal user.
'''
su = is_superuser(request)
def get_fieldset():
if su:
return super_user_fieldset
else:
return normal_user_fieldset
return get_fieldset
Usage would go like this:
obj = UserAdmin()
request = ... #Generate a normal or super-user request.
fieldset = obj.gen_fieldset(request)
# Sometime later when you want to use the fieldset...
fields = fieldset() # Function will decide which fieldset to give based on gen_fieldset() call earlier.
So the basic idea is that you can configure get_fieldset() ahead of time (via the call to gen_fieldset() where the closure is employed) to return the appropriate fieldset for the user's level. Give the user the function object returned by gen_fieldset() it'll generate the appropriate fieldset at a later time whenever the user calls it.
NOTE: If you've never used closures before I'd suggest googling around for other examples and use scenarios. There may be other solutions that'll fit your situation better (again, not familiar with Django) but that was the first thing that came to my mind.

Related

Django-allauth restrict registration to list of emails

I'm using Django-allauth, I have a list of emails and I want to restrict registration to this list. My idea was to check the signing up user email and, if not in the emails list, stop registration process and redirect.
As suggested by Chetan Ganji I tried editing allauth.account.views.SignupView but it does not subscribe the form_valid method. How can i do that? Thank you for help
from allauth.account.views import SignupView
class AllauthCustomSignupView(SignupView):
def form_valid(self, form):
email = form.cleaned_data['email']
auth_user_list = [ 'email_1',
'email_2',
...
]
if not any(email in s for s in auth_user_list):
return reverse('url')
return super(MySignupView, self).form_valid(form)
You can do it by extending the DefaultAccountAdapter class. You have to figure out a way to store and fetch the restricted list on demand.
You can then use the adapters and raise validation error in the registration from. Extend a DefaultAccountAdapter and override the clean_email method. Create an adapter.py in your project directory and extend the default adapter class.
from allauth.account.adapter import DefaultAccountAdapter
from django.forms import ValidationError
class RestrictEmailAdapter(DefaultAccountAdapter):
def clean_email(self,email):
RestrictedList = ['Your restricted list goes here.']
if email in RestrictedList
raise ValidationError('You are restricted from registering. Please contact admin.')
return email
Finally, point the account adapter in settings.py to your extended class.
ACCOUNT_ADAPTER = 'YourProject.adapter.RestrictEmailAdapter'
Maybe try with this code
class AllauthCustomSignupView(SignupView):
def form_valid(self, form):
email = form.cleaned_data['email']
auth_user_list = [ 'email_1',
'email_2',
...
]
if email in auth_user_list:
return reverse('blocked-email') # whatever url, make sure that the url is defined inside form_valid or in approriate location.
else:
return super(AllauthCustomSignupView, self).form_valid(form)
class BlockedEmailView(TemplateView):
template_name = "blocked-email.html"
Add below line to your urls.py
url(r'^signup/$', views.AllauthCustomSignupView.as_view(), name="signup"),
url(r'^blocked/email$', views.BlockedEmailView.as_view(), name="blocked-email"),
Also, you will need to change the action attribute of the form that SignupView has. So, your will have to override the template of that view, keep everything else the same, just change the action to point to "signup/".

Django admin is_staff based on group

Is it possible to have is_staff selected by choosing a group? Let's say there are two groups: users, admins
When a new user is in the users group he is not staff, but if he is in the admins group he is staff.
There is an easy way to do this define the following in your user model
#property
def is_staff(self):
if self.is_staff == True or self.groups.filter(name="staff").exists()
Thus during admin login or any other time when you call from the user_object.is_staff You will be getting what you want on basis of groups too.
I managed to make it work by extending the UserAdmin class and in the get_form function I placed this with help of mascot6699's answer:
if obj.groups.filter(name="Administrator").exists():
obj.is_staff = True
else:
obj.is_staff = False
So whenever I place a user (with the admin menu) in the Administrator group it will check the is_staff option else it unchecks it.
The is_staff property is primarily used by the admin interface. If you want to have an admin interface that's dependent on group membership, you can override AdminSite.has_permission() instead:
class GroupBasedAdminSite(admin.AdminSite):
def has_permission(self, request):
return request.user.is_active and request.user.groups.filter(name = 'admins').exists()
# override default admin site
admin.site = GroupBasedAdminSite()
You can also use the official override feature, or have a dedicated GroupBasedAdminSite hosted on a different path, in case you want to support different types of "admins".
There are two place one should override to implement this behaviour
# inside any app's admin.py module
import types
from django.contrib import admin
from django.contrib.admin.forms import AdminAuthenticationForm
def has_permission(self, request):
return request.user.is_active and (
request.user.is_staff
or request.user.groups.filter(name="grp").exists()
)
class GrpAdminAuthenticationForm(AdminAuthenticationForm):
def confirm_login_allowed(self, user):
if user.groups.filter(name="grp").exists():
user.is_staff = True
super().confirm_login_allowed(user)
admin.site.login_form = GrpAdminAuthenticationForm
admin.site.has_permission = types.MethodType(has_permission, admin.site)
It will update the default admin.site object so one doesn't need to register to a custom object.

How to make Django admin site accessed by non-staff user?

I would like to implement a 2nd admin site which provides a subset of feature of the primary admin site. That's possible and described in the Django docs
However, I would like to limit access on the primary admin site. Some users can access the 2ndary site but not the primary site.
In order to implement that feature, I would like these users not to be in the staff (is_staff=False) and rewrite the AdminSite.has_permission
class SecondaryAdminSite(AdminSite):
def has_permission(self, request):
if request.user.is_anonymous:
try:
username = request.POST['username']
password = request.POST['password']
except KeyError:
return False
try:
user = User.objects.get(username = username)
if user.check_password(password):
return user.has_perm('app.change_onlythistable')
else:
return False
except User.DoesNotExist:
return False
else:
return request.user.has_perm('app.change_onlythistable')
Unfortunately, this approach doesn't work. The user can login but can't see anything in the secondary admin site.
What's wrong with this approach?
Any idea how to implement this feature?
Thanks in advance
Here's what worked for me with Django >= 3.2.
Create a subclass of AdminSite
Override the has_permission() method to remove the is_staff check.
Override the login_form to use AuthenticationForm.
AdminSite uses AdminAuthenticationForm, which extends AuthenticationForm and adds a check for is_staff.
Code
# PROJECT/APP/admin.py
from django.contrib.admin import AdminSite
from django.contrib.admin.forms import AuthenticationForm
class MyAdminSite(AdminSite):
"""
App-specific admin site implementation
"""
login_form = AuthenticationForm
site_header = 'Todomon'
def has_permission(self, request):
"""
Checks if the current user has access.
"""
return request.user.is_active
site = MyAdminSite(name='myadmin')
I think that your approach should now be possible: http://code.djangoproject.com/ticket/14434 (closed 5 weeks ago)
However, the explicit "is_staff" check is still done in two places (apart from the staff_member_required decorator):
django.contrib.admin.forms.AdminAuthenticationForm.clean()
On top of "has_permission()" you'd need to provide your non-staff AdminSite with a "login_form" that doesn't do the is_staff check, so could just subclass and adjust clean() accordingly.
templates/admin/base.html
would need to be slightly customized.
The div with id "user-tools" is only shown for active staff members. I'm assuming that's done because the login form also uses this template, and someone could be logged in as an active non-staff member but still should'nt see those links.
What's wrong with this approach? Any idea how to implement this feature?
What's wrong with this approach is that permissions and groups can already provide you with what you need. There is no need to subclass AdminSite if all you need is to divide users.
This is probably why this feature is so poorly documented, IMHO

Django-Registration & Django-Profile, using your own custom form

I am making use of django-registration and django-profile to handle registration and profiles. I would like to create a profile for the user at the time of registration. I have created a custom registration form, and added that to the urls.py using the tutorial on:
http://dewful.com/?p=70
The basic idea in the tutorial is to override the default registration form to create the profile at the same time.
forms.py - In my profiles app
from django import forms
from registration.forms import RegistrationForm
from django.utils.translation import ugettext_lazy as _
from profiles.models import UserProfile
from registration.models import RegistrationProfile
attrs_dict = { 'class': 'required' }
class UserRegistrationForm(RegistrationForm):
city = forms.CharField(widget=forms.TextInput(attrs=attrs_dict))
def save(self, profile_callback=None):
new_user = RegistrationProfile.objects.create_inactive_user(username=self.cleaned_data['username'],
password=self.cleaned_data['password1'],
email=self.cleaned_data['email'])
new_profile = UserProfile(user=new_user, city=self.cleaned_data['city'])
new_profile.save()
return new_user
In urls.py
from profiles.forms import UserRegistrationForm
and
url(r'^register/$',
register,
{'backend': 'registration.backends.default.DefaultBackend', 'form_class' : UserRegistrationForm},
name='registration_register'),
The form is displayed, and i can enter in City, however it does not save or create the entry in the DB.
You're halfway there - you've successfully built a custom form that replaces the default form. But you're attempting to do your custom processing with a save() method on your model form. That was possible in older versions of django-registration, but I can see from the fact that you specified a backend in your URL conf that you're using v0.8.
The upgrade guide says:
Previously, the form used to collect
data during registration was expected
to implement a save() method which
would create the new user account.
This is no longer the case; creating
the account is handled by the backend,
and so any custom logic should be
moved into a custom backend, or by
connecting listeners to the signals
sent during the registration process.
In other words, the save() method on the form is being ignored now that you're on version 0.8. You need to do your custom processing either with a custom backend or with a signal. I chose to create a custom back-end (if anyone has gotten this working with signals, please post code - I wasn't able to get it working that way). You should be able to modify this to save to your custom profile.
Create a regbackend.py in your app.
Copy the register() method from DefaultBackend into it.
At the end of the method, do a query to get the corresponding User instance.
Save the additional form fields into that instance.
Modify the URL conf so that it points to BOTH the custom form AND the custom back-end
So the URL conf is:
url(r'^accounts/register/$',
register,
{'backend': 'accounts.regbackend.RegBackend','form_class':MM_RegistrationForm},
name='registration_register'
),
regbackend.py has the necessary imports and is basically a copy of DefaultBackend with just the register() method, and the addition of:
u = User.objects.get(username=new_user.username)
u.first_name = kwargs['first_name']
u.last_name = kwargs['last_name']
u.save()
As described in my comment on Django Trac ticket I made a metaclass and mixin to allow multiple inheritance for ModelForm Django forms. With this you can simply make a form which allows registration with fields from user and profile models at the same time without hard-coding fields or repeating yourself. By using my metaclass and mixin (and also fieldset mixin) you can do:
class UserRegistrationForm(metaforms.FieldsetFormMixin, metaforms.ParentsIncludedModelFormMixin, UserCreationForm, UserProfileChangeForm):
error_css_class = 'error'
required_css_class = 'required'
fieldset = UserCreationForm.fieldset + [(
utils_text.capfirst(UserProfileChangeForm.Meta.model._meta.verbose_name), {
'fields': UserProfileChangeForm.base_fields.keys(),
})]
def save(self, commit=True):
# We disable save method as registration backend module should take care of user and user
# profile objects creation and we do not use this form for changing data
assert False
return None
__metaclass__ = metaforms.ParentsIncludedModelFormMetaclass
Where UserCreationForm can be for example django.contrib.auth.forms.UserCreationForm form and UserProfileChangeForm a simple ModelForm for your profile model. (Do not forget to set editable to False in your foreign key to User model.)
With django-registration backend having such register method:
def register(self, request, **kwargs):
user = super(ProfileBackend, self).register(request, **kwargs)
profile, created = utils.get_profile_model().objects.get_or_create(user=user)
# lambda-object to the rescue
form = lambda: None
form.cleaned_data = kwargs
# First name, last name and e-mail address are stored in user object
forms_models.construct_instance(form, user)
user.save()
# Other fields are stored in user profile object
forms_models.construct_instance(form, profile)
profile.save()
return user
Be careful that registration signal is send at the beginning of this method (in method in superclass) and not at the end.
In the same manner you can make a change form for both user and profile information. Example for this you can find in my comment on Django Trac ticket mentioned above.
With registration 0.8 and later:
Create a subclass of registration.backends.default.views.RegistrationView in your views.py or equivalent:
from registration.backends.default.views import RegistrationView
class MyRegistrationView(RegistrationView):
form_class= MyCustomRegistrationForm
def register(self, request, **cleaned_data):
new_user= super(MyRegistrationView, self).register(request, **cleaned_data)
# here create your new UserProfile object
return new_user

Permissions for a site only

I have a multilingual Django project. Every language is a different subdomain.
So we've decided to use the "sites" application and to create one different site for every language.
On that project, I also have a "pages" application, which is quite similar to a CMS. The user can create pages with content and they'll be displayed in the appropriate language site.
Now I'm looking to be able to manage advanced permissions. What I need to do is to allow, in the admin application a user only to create and update pages for one (or many) specific language/site.
What'd be the cleaner way to do something like that ?
Edit : Here is the solution I've adapted, given by Chris
I create a decorator that's checking if the user is appropriately in the group that has access to the lang.
See Chris' accepted answer for an example of this.
In a "normal" view, I do the following :
def view(self):
# Whatever you wanna do
return render_to_response('page.html', {}, RequestContext(request))
view = group_required(view)
If the user is in the group, it'll return the method. Otherwise, it'll return an "Access Denied" error.
And in my admin, I do the following :
class PageAdmin(admin.ModelAdmin):
list_display = ('title', 'published')
fieldsets = [
(None, {'fields': ['title', 'slug', 'whatever_field_you_have']}),
]
def has_add_permission(self, request):
return in_group_required(request)
admin.site.register(Page, PageAdmin)
Where the in_group_required is a similar method to group_required mentionned above. But will return only true or false depending of if we have access or not.
And because we use them quite much in the previous examples, you'll find above here what I have in my in_group and group_required methods.
def group_required(func):
def _decorator(request, *args, **kwargs):
if not in_group(request):
return HttpResponse("Access denied")
return func(*args, **kwargs)
return _decorator
def in_group(request):
language = Language.objects.get(site__domain__exact=request.get_host())
for group in language.group.all():
if request.user in group.user_set.all():
return True
return False
You could create a Group (http://docs.djangoproject.com/en/dev/topics/auth/)
per site / language and add the users to the groups accordingly.
Then, you can check if the request.user.groups belongs to the group.
(You can do this with a decorator:
def group_required(func):
def _decorator(request, *args, **kwargs):
hostname = request.META.get('HTTP_HOST')
lang = hostname.split(".")[0]
if not lang in request.user.groups:
return HttpResponse("Access denied")
return func(*args, **kwargs)
return _decorator
(Correct / modify the code to match your requirements...)
You can override has_add_permission (and related methods) in your ModelAdmin class.
(With similar code like shown above)
If you want to filter the Page objects on the admin index of your page-application,
you can override the method queryset() in ModelAdmin.
This QuerySet returns only those Page objects, that belong to a Site (and therefore Group)
of which the request.user is a member.
Pages.objects.filter(site__name__in=request.user.groups)

Categories