The Django Admin's User management gives us easy access to setting a User's group allegiances via the list for picking the Groups. This is the ManyToMany relationship default widget for assignment. And for the most part that's nice for User creation from the get go.
However, say I have a huge number of users and groups, I'd like the chance to view and manage group membership by clicking on the Group in the admin and then seeing all the members in a similar picker (or via the horizontal widget).
Is there a built-in Django method/trick to show ManyToMany relations in "reverse?"
Setting group assignment is fine from the User adminform on User creation. But this can be a big pain to find and manage once users and groups are well established.
I guess the workaround is to make my own view to show group members, I just wanted to see if there was some trick I could do with inlines or something in the admin.
Try to use this snippet. It's customisation example of contrib.auth app admin part.
# utils/admin_auth.py
# -*- coding: utf-8 -*-
from django.utils.safestring import mark_safe
from django.contrib.auth.models import User, Group
from django.contrib.auth.admin import UserAdmin, GroupAdmin
from django.contrib import admin
def roles(self):
#short_name = unicode # function to get group name
short_name = lambda x:unicode(x)[:1].upper() # first letter of a group
p = sorted([u"<a title='%s'>%s</a>" % (x, short_name(x)) for x in self.groups.all()])
if self.user_permissions.count(): p += ['+']
value = ', '.join(p)
return mark_safe("<nobr>%s</nobr>" % value)
roles.allow_tags = True
roles.short_description = u'Groups'
def last(self):
fmt = "%b %d, %H:%M"
#fmt = "%Y %b %d, %H:%M:%S"
value = self.last_login.strftime(fmt)
return mark_safe("<nobr>%s</nobr>" % value)
last.allow_tags = True
last.admin_order_field = 'last_login'
def adm(self):
return self.is_superuser
adm.boolean = True
adm.admin_order_field = 'is_superuser'
def staff(self):
return self.is_staff
staff.boolean = True
staff.admin_order_field = 'is_staff'
from django.core.urlresolvers import reverse
def persons(self):
return ', '.join(['%s' % (reverse('admin:auth_user_change', args=(x.id,)), x.username) for x in self.user_set.all().order_by('username')])
persons.allow_tags = True
class UserAdmin(UserAdmin):
list_display = ['username', 'email', 'first_name', 'last_name', 'is_active', staff, adm, roles, last]
list_filter = ['groups', 'is_staff', 'is_superuser', 'is_active']
class GroupAdmin(GroupAdmin):
list_display = ['name', persons]
list_display_links = ['name']
admin.site.unregister(User)
admin.site.unregister(Group)
admin.site.register(User, UserAdmin)
admin.site.register(Group, GroupAdmin)
Related
I'm trying to add a model resource from django-import-export into the admin for Wagtail. The only documentation I can find says that you would do it through hooks. The problem is, I keep getting the error:
missing 2 required positional arguments: 'model' and 'admin_site'
The whole resource and ModelAdmin are:
class AccountResource(resources.ModelResource):
class Meta:
model = Account
fields = ('first_name', 'last_name', 'email', 'created', 'archived')
class AccountsAdmin(ImportExportModelAdmin, ModelAdmin):
resource_class = AccountResource
model = Account
menu_label = 'Accounts' # ditch this to use verbose_name_plural from model
menu_icon = 'group' # change as required
menu_order = 200 # will put in 3rd place (000 being 1st, 100 2nd)
add_to_settings_menu = False # or True to add your model to the Settings sub-menu
exclude_from_explorer = False # or True to exclude pages of this type from Wagtail's explorer view
list_display = ('first_name', 'last_name', 'email', 'created', 'archived')
search_fields = ('first_name', 'last_name', 'email', 'created')
# Now you just need to register your customised ModelAdmin class with Wagtail
modeladmin_register(AccountsAdmin)
Any suggestions?
The Wagtail ModelAdmin does not share the API of the Django ModelAdmin.
The mixins from django-import-export expect to be used with a Django ModelAdmin and won't work with Wagtail ModelAdmin as you have experienced yourself.
For the export functionality, I have solved the problem by hooking the export_action of a Django ModelAdmin with the ExportMixin into the urls of a Wagtail ModelAdmin.
This might not be too pretty, but allows reusing the view and the logic that is part of the ExportMixin.
I have published an example Project on GitHub that makes use of this design.
The actual glue code that can be found here is not all that much:
from django.conf.urls import url
from django.contrib.admin import ModelAdmin as DjangoModelAdmin
from django.core.exceptions import ImproperlyConfigured
from django.utils.http import urlencode
from django.utils.translation import gettext_lazy as _
from import_export.admin import ExportMixin
from wagtail.contrib.modeladmin.helpers import ButtonHelper
class ExporterDummySite:
name = None
def each_context(self, request):
return {}
class WMAExporter(ExportMixin, DjangoModelAdmin):
export_template_name = 'wma_export/export.html'
def __init__(self, wagtail_model_admin):
self.wagtail_model_admin = wagtail_model_admin
super().__init__(wagtail_model_admin.model, ExporterDummySite())
def get_export_queryset(self, request):
index_view = self.wagtail_model_admin.index_view_class(
model_admin=self.wagtail_model_admin
)
index_view.dispatch(request)
return index_view.get_queryset(request)
class ExportButtonHelper(ButtonHelper):
export_button_classnames = ['bicolor', 'icon', 'icon-download']
def export_button(self, classnames_add=None, classnames_exclude=None):
if classnames_add is None:
classnames_add = []
if classnames_exclude is None:
classnames_exclude = []
classnames = self.export_button_classnames + classnames_add
cn = self.finalise_classname(classnames, classnames_exclude)
return {
'url': self.url_helper.get_action_url("export") + '?' + urlencode(self.view.params),
'label': _('Export'),
'classname': cn,
'title': _('Export these %s') % self.verbose_name_plural,
}
class WMAExportMixin:
button_helper_class = ExportButtonHelper
exporter_class = None
def get_admin_urls_for_registration(self):
return super().get_admin_urls_for_registration() + (
url(self.url_helper._get_action_url_pattern("export"),
self.export_view,
name=self.url_helper.get_action_url_name("export")),
)
def export_view(self, request):
if self.exporter_class is None:
raise ImproperlyConfigured(f"{self.__class__.__name__}.exporter_class not set!")
exporter = self.exporter_class(self)
return exporter.export_action(request)
I would assume that something similar can be done to implement the import part.
I would like to obtain the name of the logged user inside a form of a Django App and use the results as input of a form field.
How can I do that? What should I do in my code?
Here is my code
Views.py
form = ProductForm()
Admin.py
class AccessLevelInline(admin.StackedInline):
model = AccessLevel
can_delete = False
verbose_name_plural = 'accesslevel'
# Define a new User admin
class UserAdmin(BaseUserAdmin):
inlines = (AccessLevelInline, )
# Re-register UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
Form.py
user_name = HOW DO I ACCESS THE USER NAME HERE?
u = User.objects.get(username=user_name).accesslevel.prod.split(',')
v=(('---------','---------'),)
for l in u:
v=v+((l.lstrip(),l.lstrip()),)
class ProductForm(forms.Form):
model = Manufact
Prod = forms.ChoiceField(choices=v)
Expanding off of Stepan's answer.
In views.py:
form=ProductFrom(request.user)
In forms.py:
# Declare __init__ function
def __init__(self, user_name):
self.u = User.objects.get(username=user_name).accesslevel.prod.split(',')
# DO the rest that you need here.
In your view you have access to the request object which has the the current user on it. You can get it like this: request.user. So you can pass that to your form when you initialize it so you have access to when you need it.
form=ProductForm(request.user)
Within my Users page, I would like to add more column fields to display for each user.
By default, the User page shows username, first name, last name, email, and staff status, but I would also like to add the column "Chosen Groups" to be displayed as well.
Each user can be a part of zero or more groups, so I would like to show a list of all groups (or None) that each user is a part of.
I put something together but I am not sure how to actually get the group data and put it into a "string" form for the "Chosen Groups" column.
Here was my failed attempt...
class UserAdmin(admin.ModelAdmin):
list_display = ('username', 'email', 'first_name', 'last_name', 'date_joined', 'get_groups')
def get_groups(self, object):
#code...
#User is registered by default, need to unregister than register to apply custom updates to UserAdmin
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
Here it is:
def get_groups(self, object):
return u', '.join(g.name for g in object.groups.all()) or 'None'
get_groups.short_description = 'Chosen Groups'
To reduce the number of sql queries you can also override the get_queryset() method:
def get_queryset(self, request):
qs = super(UserAdmin, self).get_queryset(request)
return qs.prefetch_related('groups')
My app has users who create pages. In the Page screen of the admin, I'd like to list the User who created the page, and in that list, I'd like the username to have a link that goes to the user page in admin (not the Page).
class PageAdmin(admin.ModelAdmin):
list_display = ('name', 'user', )
list_display_links = ('name','user',)
admin.site.register(Page, PageAdmin)
I was hoping that by making it a link in the list_display it would default to link to the actual user object, but it still goes to Page.
I'm sure I'm missing something simple here.
Modifying your model isn't necessary, and it's actually a bad practice (adding admin-specific view-logic into your models? Yuck!) It may not even be possible in some scenarios.
Luckily, it can all be achieved from the ModelAdmin class:
from django.urls import reverse
from django.utils.safestring import mark_safe
class PageAdmin(admin.ModelAdmin):
# Add it to the list view:
list_display = ('name', 'user_link', )
# Add it to the details view:
readonly_fields = ('user_link',)
def user_link(self, obj):
return mark_safe('{}'.format(
reverse("admin:auth_user_change", args=(obj.user.pk,)),
obj.user.email
))
user_link.short_description = 'user'
admin.site.register(Page, PageAdmin)
Edit 2016-01-17:
Updated answer to use make_safe, since allow_tags is now deprecated.
Edit 2019-06-14:
Updated answer to use django.urls, since as of Django 1.10 django.core.urls has been deprecated.
Add this to your model:
def user_link(self):
return '%s' % (reverse("admin:auth_user_change", args=(self.user.id,)) , escape(self.user))
user_link.allow_tags = True
user_link.short_description = "User"
You might also need to add the following to the top of models.py:
from django.template.defaultfilters import escape
from django.core.urls import reverse
In admin.py, in list_display, add user_link:
list_display = ('name', 'user_link', )
No need for list_display_links.
You need to use format_html for modern versions of django
#admin.register(models.Foo)
class FooAdmin(admin.ModelAdmin):
list_display = ('ts', 'bar_link',)
def bar_link(self, item):
from django.shortcuts import resolve_url
from django.contrib.admin.templatetags.admin_urls import admin_urlname
url = resolve_url(admin_urlname(models.Bar._meta, 'change'), item.bar.id)
return format_html(
'{name}'.format(url=url, name=str(item.bar))
)
I ended up with a simple helper:
from django.shortcuts import resolve_url
from django.utils.safestring import SafeText
from django.contrib.admin.templatetags.admin_urls import admin_urlname
from django.utils.html import format_html
def model_admin_url(obj: Model, name: str = None) -> str:
url = resolve_url(admin_urlname(obj._meta, SafeText("change")), obj.pk)
return format_html('{}', url, name or str(obj))
Then you can use the helper in your model-admin:
class MyAdmin(admin.ModelAdmin):
readonly_field = ["my_link"]
def my_link(self, obj):
return model_admin_url(obj.my_foreign_key)
I needed this for a lot of my admin pages, so I created a mixin for it that handles different use cases:
pip install django-admin-relation-links
Then:
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
#admin.register(Group)
class MyModelAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
# ...
change_links = ['field_name']
See the GitHub page for more info. Try it out and let me know how it works out!
https://github.com/gitaarik/django-admin-relation-links
I decided to make a simple admin mixin that looks like this (see docstring for usage):
from django.contrib.contenttypes.models import ContentType
from django.utils.html import format_html
from rest_framework.reverse import reverse
class RelatedObjectLinkMixin(object):
"""
Generate links to related links. Add this mixin to a Django admin model. Add a 'link_fields' attribute to the admin
containing a list of related model fields and then add the attribute name with a '_link' suffix to the
list_display attribute. For Example a Student model with a 'teacher' attribute would have an Admin class like this:
class StudentAdmin(RelatedObjectLinkMixin, ...):
link_fields = ['teacher']
list_display = [
...
'teacher_link'
...
]
"""
link_fields = []
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.link_fields:
for field_name in self.link_fields:
func_name = field_name + '_link'
setattr(self, func_name, self._generate_link_func(field_name))
def _generate_link_func(self, field_name):
def _func(obj, *args, **kwargs):
related_obj = getattr(obj, field_name)
if related_obj:
content_type = ContentType.objects.get_for_model(related_obj.__class__)
url_name = 'admin:%s_%s_change' % (content_type.app_label, content_type.model)
url = reverse(url_name, args=[related_obj.pk])
return format_html('{}', url, str(related_obj))
else:
return None
return _func
If anyone is trying to do this with inline admin, consider a property called show_change_link since Django 1.8.
Your code could then look like this:
class QuestionInline(admin.TabularInline):
model = Question
extra = 1
show_change_link = True
class TestAdmin(admin.ModelAdmin):
inlines = (QuestionInline,)
admin.site.register(Test, TestAdmin)
This will add a change/update link for each foreign key relationship in the admin's inline section.
It gives you filter by staff status and superuser status, but what about groups?
Since version 1.3 it can be done using this:
list_filter = ('groups__name')
Of course as #S.Lott explains you must register your customized class in the admin.py file:
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class MyUserAdmin(UserAdmin):
list_filter = UserAdmin.list_filter + ('groups__name',)
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
See Customizing an Admin form in Django while also using autodiscover
Essentially, you define a customized Admin class with the features you want.
Then unregister and register your revised Admin class.
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
Here is a complete example, that inherits from SimpleListFilter, which is available in Django 1.4 and up.
https://docs.djangoproject.com/en/1.4/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_filter
It support setting all available labels and parameters to create the completely custom filter.
It shows up as "By group" in the filter panel, with a list of all available groups.
from django.contrib.admin import SimpleListFilter
from django.contrib.auth.models import Group
from django.utils.translation import ugettext as _
class GroupListFilter(SimpleListFilter):
title = _('group')
parameter_name = 'group'
def lookups(self, request, model_admin):
items = ()
for group in Group.objects.all():
items += ((str(group.id), str(group.name),),)
return items
def queryset(self, request, queryset):
group_id = request.GET.get(self.parameter_name, None)
if group_id:
return queryset.filter(groups=group_id)
return queryset
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.models import User
class MyUserAdmin(UserAdmin):
list_filter = UserAdmin.list_filter + (GroupListFilter,)
admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
In later versions of Django, it works exactly as you'd expect:
list_filter = ('groups', )
No need to unregister/register the admin class.