Django Admin linking to related objects - django

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.

Related

How can I integrate Django Import Export and Wagtail?

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.

Django, better many 2 many in the admin

is there anyway to improve this in the django admin panel?
As you can see its a simple Many2Many field.
I would like to access that record with a simple mouse click.
You can add custom HTML into the admin as a field if you define the content in a method:
from django.utils.html import format_html
from django.utils.safestring import mark_safe
#admin.register(MyModel)
class MyModelAdmin(ModelAdmin):
fields = ['related_thing_links', ...]
readonly_fields = ['related_thing_links']
def related_thing_links(self, obj):
items = []
for thing in obj.related_things.all():
url = reverse(
'admin:myapp_relatedthing_change',
kwargs={'pk': thing.pk}
)
items.append(format_html(
'<li>{thing}</li>',
url=url,
thing=thing,
))
return mark_safe('<ul>' + ' '.join(items) + '</ul>')
related_thing_links.short_description = 'related things'
I've not managed to test this exactly, but it's based on working code.

Adding a "View on Site" link to the Django admin list_display

I'm trying to add a "View on site" link to my list_display in Django Admin.
This seems like a pretty common use case, is there a shortcut way of doing it?
You could write a reusable mixin like this (untested):
class ViewOnSiteMixin(object):
def view_on_site(self, obj):
return mark_safe(u"<a href='%s'>view on site</a>" % obj.get_absolute_url())
view_on_site.allow_tags = True
view_on_site.short_description = u"View on site"
Use it like this:
class SomeAdmin(ViewOnSiteMixin, admin.ModelAdmin):
list_display = [..., "view_on_site", ...]
(of course needs get_absolute_url defined on your model)
Using view_on_site will muck up the "View on site" link within the admin view - that link shows up when you set get_absolute_url for your model.
Instead you can use just about any other name for that column, just match it with the function name.
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
class ServisAdmin(admin.ModelAdmin):
list_display = (.. ,'view_link')
def view_link(self, obj):
return mark_safe(
'{1}'.format(
obj.get_absolute_url(),
_("View on site")
)
)
view_link.allow_tags = True
view_link.short_description = _("View on site")

Django: change admin FileField output?

I've made a model with file that is uploaded to custom path (not in MEDIA_ROOT). So it's some kind like protected file.
Now I need to change it's representation in admin details. It shows a path relative to MEDIA_URL. I need to change that, to show a URL to an application view which generates a proper URL.
So, what is the best way to display link, and only in objects details in admin?
Here is the way I did it:
models.py
class SecureFile(models.Model):
upload_storage = FileSystemStorage(
location=settings.ABS_DIR('secure_file/files/'))
secure_file = models.FileField(verbose_name=_(u'file'),
upload_to='images', storage=upload_storage)
widgets.py
from django import forms
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe
class AdminFileWidget(forms.FileInput):
"""A FileField Widget that shows secure file link"""
def __init__(self, attrs={}):
super(AdminFileWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
output = []
if value and hasattr(value, "url"):
url = reverse('secure_file:get_secure_file',
args=(value.instance.slug, ))
out = u'{}<br />{} '
output.append(out.format(url, _(u'Download'), _(u'Change:')))
output.append(super(AdminFileWidget, self).render(name, value, attrs))
return mark_safe(u''.join(output))
admin.py
class SecureFileAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(SecureFileAdminForm, self).__init__(*args, **kwargs)
self.fields['secure_file'].widget = AdminFileWidget()
class Meta:
model = SecureFile
class SecureFileAdmin(admin.ModelAdmin):
form = SecureFileAdminForm

How Do I Show Django Admin Change List View of foreign key children?

I'm working on an app with a Model hierarchy of Campaign > Category > Account. Ideally, I'd like users to be able to click on a link in the campaign admin list view and go to a URL like "/admin/myapp/campaign/2/accounts/" which will show a Django admin view with all the handy ChangeList amenities but which is filtered to show just the accounts in categories in the specified campaign (ie. Account.object.filter(category__campaign__id = 2)). (Note, categories themselves I'm happy to just be "filters" on this accounts list view).
I can't seem to find any reference to a way to mimic this item-click-goes-to-list-of-foriegn-key-children approach that is common in many other frameworks.
Is it possible? Is there a "better" approach in the django paradigm?
thanks for any help!
This was an interesting question so I whipped up a sample app to figure it out.
# models.py
from django.db import models
class Campaign(models.Model):
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Category(models.Model):
campaign = models.ForeignKey(Campaign)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
class Account(models.Model):
category = models.ForeignKey(Category)
name = models.CharField(max_length=20)
def __unicode__(self):
return unicode(self.name)
# admin.py
from django.contrib import admin
from models import Campaign, Category, Account
class CampaignAdmin(admin.ModelAdmin):
list_display = ('name', 'related_accounts', )
def related_accounts(self, obj):
from django.core import urlresolvers
url = urlresolvers.reverse("admin:<yourapp>_account_changelist")
lookup = u"category__campaign__exact"
text = u"View Accounts"
return u"<a href='%s?%s=%d'>%s</a>" % (url, lookup, obj.pk, text)
related_accounts.allow_tags = True
admin.site.register(Campaign, CampaignAdmin)
admin.site.register(Category)
class AccountAdmin(admin.ModelAdmin):
list_display = ('category', 'name')
list_filter = ('category__campaign',)
admin.site.register(Account, AccountAdmin)
You'll need to replace with the name of your app where the Account ModelAdmin lives.
Note: the list_filter on the AccountAdmin is required since Django 1.2.4, Django 1.1.3 and Django 1.3 beta 1, which introduced protection from arbitrary filtering via URL parameter in the admin.
If i understand you correctly, you want to add a custom field (a callable in your ModelAdmin's list_display) to your CampaignAdmin change_list view.
Your custom field would be a link that takes the category.id of each category in your change_list and generates a link to the desired, filtered admin view, which seems to be the account-change_list in your case:
admin/yourproject/account/?category__id__exact=<category.id>
Assuming category is a field on your Campaign-Model you could add the follwoing method to your CampaignAdmin:
def account_link(self, obj):
return 'Accounts' % (obj.category.id)
account_link.allow_tags = True
And then you add it to the admin's list_display option:
list_display = ('account_link', ...)
It depends a bit on your data model though.
If you want to create a permanent, filtered change_list view that suits your needs, you may take a look at this article: http://lincolnloop.com/blog/2011/jan/11/custom-filters-django-admin/
The other solutions don't pay attention to the filters you already have applied. They are part of the query string and I wanted to retain them as well.
First you need to get a reference to the request, you can do that by wrapping changelist_view or queryset as I did:
class AccountAdmin(ModelAdmin):
model = Account
list_display = ('pk', 'campaign_changelist')
# ...
def queryset(self, request):
self._get_params = request.GET
return super(AccountAdmin, self).queryset(request)
def campaign_changelist(self, obj):
url = reverse('admin:yourapp_account_changelist')
querystring = self._get_params.copy()
querystring['campaign__id__exact'] = obj.campaign.pk
return u'{2}'.format(
url, querystring.urlencode(), obj.campaign)
campaign_changelist.allow_tags = True
And something like that will give you a filter inside the changelist rows. Really helpful. :-)
These are good solutions. I wasn't aware of the auto-filter by url paradigm. Here's another I've discovered which allows you use a custom url scheme:
from consensio.models import Account
from django.contrib import admin
from django.conf.urls.defaults import patterns, include, url
class AccountAdmin(admin.ModelAdmin):
campaign_id = 0;
def campaign_account_list(self, request, campaign_id, extra_context=None):
'''
First create your changelist_view wrapper which grabs the extra
pattern matches
'''
self.campaign_id = int(campaign_id)
return self.changelist_view(request, extra_context)
def get_urls(self):
'''
Add your url patterns to get the foreign key
'''
urls = super(AccountAdmin, self).get_urls()
my_urls = patterns('',
(r'^bycampaign/(?P<campaign_id>\d+)/$', self.admin_site.admin_view(self.campaign_account_list))
)
return my_urls + urls
def queryset(self, request):
'''
Filter the query set based on the additional param if set
'''
qs = super(AccountAdmin, self).queryset(request)
if (self.campaign_id > 0):
qs = qs.filter(category__campaign__id = self.campaign_id)
return qs
And plus you'd need to incorporate the URL link into CampaignAdmin's list view...