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.
Related
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")
I would like to add some extra fields to pages in django-cms (in django admin panel). How do this in the simplest way?
Create a new app (called extended_cms or something) and in models.py create the following:
from django.db import models
from django.utils.translation import ugettext_lazy as _
from cms.models.pagemodel import Page
class ExtendedPage(models.Model):
page = models.ForeignKey(Page, unique=True, verbose_name=_("Page"), editable=False, related_name='extended_fields')
my_extra_field = models.CharField(...)
then create an admin.py:
from models import ExtendedPage
from cms.admin.pageadmin import PageAdmin
from cms.models.pagemodel import Page
from django.contrib import admin
class ExtendedPageAdmin(admin.StackedInline):
model = ExtendedPage
can_delete = False
PageAdmin.inlines.append(ExtendedPageAdmin)
try:
admin.site.unregister(Page)
except:
pass
admin.site.register(Page, PageAdmin)
which will add your extended model to as an inline to any page you create. The easiest way to access the extended model setttings, is to create a context processor:
from django.core.cache import cache
from django.contrib.sites.models import Site
from models import ExtendedPage
def extended_page_options(request):
cls = ExtendedPage
extended_page_options = None
try:
extended_page_options = request.current_page.extended_fields.all()[0]
except:
pass
return {
'extended_page_options' : extended_page_options,
}
and now you have access to your extra options for the current page using {{ extended_page_options.my_extra_field }} in your templates
Essentially what you are doing is creating a separate model with extra settings that is used as an inline for every CMS Page. I got this from a blog post previously so if I can find that I'll post it.
EDIT
Here is the blog post: http://ilian.i-n-i.org/extending-django-cms-page-model/
There is an official way to extend the page & title models, I highly recommend this official documentation:
Extending the page & title models from docs.django-cms.org
I also highly recommend using a placeholder if you can, since writing this answer, I now prefer creating a placeholder for the use case of cover images. (You can even get just the image URL in your template if you want to).
Summary of the link:
Create a subclass of PageExtension in your models.py file and register it:
class IconExtension(PageExtension):
image = models.ImageField(upload_to='icons')
extension_pool.register(IconExtension)
Create also a subclass of PageExtensionAdmin in your admin.py file and register it:
class IconExtensionAdmin(PageExtensionAdmin):
pass
admin.site.register(IconExtension, IconExtensionAdmin)
Finally, to make it accessible from the toolbar, create a subclass of ExtensionToolbar in cms_toolbars.py and register it:
#toolbar_pool.register
class IconExtensionToolbar(ExtensionToolbar):
model = IconExtension
def populate(self):
current_page_menu = self._setup_extension_toolbar()
if current_page_menu:
page_extension, url = self.get_page_extension_admin()
if url:
current_page_menu.add_modal_item(_('Page Icon'), url=url,
disabled=not self.toolbar.edit_mode)
The official documentation goes into more detail and explanation.
There is an open GitHub issue on adding support for adding elements to the normal and advanced "page settings" dialogues.
There's also a way to do this without using an inline, and having the fields anywhere on the Page form. For example, I have a custom setting for "color scheme" that I wanted to be under the "Basic Settings" fieldset. This can be done by overriding the ModelForm and the ModelAdmin's fieldsets. Also, I opted for a OneToOne field instead of a ForeignKey, for simplicity's sake.
models.py:
from django.db import models
from cms.models.pagemodel import Page
from django.conf import settings
class PageCustomSettings(models.Model):
page = models.OneToOneField(Page, editable=False,
related_name='custom_settings')
color_scheme = models.CharField(blank=True, choices=settings.COLOR_SCHEMES,
max_length=20)
admin.py:
from django import forms
from django.conf import settings
from django.contrib import admin
from cms.admin.pageadmin import PageAdmin, PageForm
from cms.models.pagemodel import Page
from web.models import PageCustomSettings
color_scheme_choices = (('', '---------'),) + settings.COLOR_SCHEMES
class CustomPageForm(PageForm):
color_scheme = forms.ChoiceField(choices=color_scheme_choices,
required=False)
def __init__(self, *args, **kwargs):
# make sure that when we're changing a current instance, to set the
# initial values for our custom fields
obj = kwargs.get('instance')
if obj:
try:
opts = obj.custom_settings
kwargs['initial'] = {
'color_scheme': opts.color_scheme
}
except PageCustomSettings.DoesNotExist:
pass
super(CustomPageForm, self).__init__(*args, **kwargs)
def save(self, commit=True):
# set the custom field values when saving the form
obj = super(CustomPageForm, self).save(commit)
try:
opts = PageCustomSettings.objects.get(page=obj)
except PageCustomSettings.DoesNotExist:
opts = PageCustomSettings(page=obj)
opts.color_scheme = self.cleaned_data['color_scheme']
opts.save()
return obj
PageAdmin.form = CustomPageForm
PageAdmin.fieldsets[1][1]['fields'] += ['color_scheme']
admin.site.unregister(Page)
admin.site.register(Page, PageAdmin)
I've got here via Google and the answers got me on the right track for Django CMS 3 Beta. To extend the page model and hook your extension into the toolbar, you can follow along the official documentation:
http://django-cms.readthedocs.org/en/latest/how_to/extending_page_title.html
Access value in template
{{ request.current_page.<your_model_class_name_in_lowercase>.<field_name> }}
For example, I extended the page model with this model:
from django.db import models
from cms.extensions import PageExtension
from cms.extensions.extension_pool import extension_pool
class ShowDefaultHeaderExtension(PageExtension):
show_header = models.BooleanField(default=True)
extension_pool.register(ShowDefaultHeaderExtension)
To access its values in the template:
{{ request.current_page.showdefaultheaderextension.show_header }}
Since I dont have enough reputation I cannot comment on Timmy O'Mahony's Post directly. However I want to note that the proposed solution of adding a StackedInline Object to the PageAdmin.inlines list does not work any more as supposed.
I'm working with Djangocms 3.3 and somewhere between Timmy O'Mahony's version any mine the authors changed the semantic of the inline List. It's content is now shown in the Permissions Menu for that specific page (including possibly added futher StackedInline or TabularInline items).
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.
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...
I have a django admin interface and in the model listing I want a custom column that will be a hyperlink using one of the fields values. Basically one of the models' fields is a url and i'd like the column to have that URL in a clickable hyperlink. This link will need to have additional URL prepended to it as its a relative path in the model field.
Define a method in your ModelAdmin-class and set its allow_tags attribute to True. This will allow the method to return unescaped HTML for display in the column.
Then list it as an entry in the ModelAdmin.list_display attribute.
Example:
class YourModelAdmin(admin.ModelAdmin):
list_display = ('my_url_field',)
def my_url_field(self, obj):
return '%s' % ('http://url-to-prepend.com/', obj.url_field, obj.url_field)
my_url_field.allow_tags = True
my_url_field.short_description = 'Column description'
See the documentation for ModelAdmin.list_display for more details.
Use the format_html utility. This will escape any html from parameters and mark the string as safe to use in templates. The allow_tags method attribute has been deprecated in Django 1.9.
from django.utils.html import format_html
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
class MyModelAdmin(admin.ModelAdmin):
list_display = ['show_url', ...]
# ...
#admin.display(description=_("Column title"))
def show_url(self, obj):
return format_html("<a href='http://pre.com{0}'>{0}</a>", obj.url)
Now your admin users are safe even in the case of:
url == '<script>eval(...);</script>'
See the documentation for more info.
I am using 'instance' instead of 'obj'. Seppo Erviälä's answer helped me the most as I'm using Django 3.0.1.
def get_facebook(self, instance):
return format_html("<a target='_blank' href='{0}'>{0}</a>", instance.profile.facebook)
get_facebook.short_description = 'Facebook'