Good afternoon! I'm using the verbose_name_plural dynamic field to show some up-to-date information in the admin panel.
Django version: 4.1
The model looks like this:
from django.utils.functional import lazy
from django.utils.translation import gettext_lazy as _
class Post(models.Model):
# ...
class Meta:
verbose_name = 'Post'
verbose_name_plural = lazy(lambda: _('Posts ({})').format(Post.....count()), str)()
I don't remember where I found this option to display some information, but it works great, except that every time the value changes and the command to create migrations is run, I get something like this:
from django.db import migrations
class Migration(migrations.Migration):
operations = [
migrations.AlterModelOptions(
name='post',
options={'verbose_name': 'Post', 'verbose_name_plural': 'Posts (123)'},
),
]
I found this option: https://stackoverflow.com/a/39801321/2166371
But I don’t know how relevant it is and why a class that is not used is imported there
Based on the variant that 0sVoid suggested, I implemented a slightly different one that seems to work without problems.
Initially, it was proposed to change one method in the main class of the admin panel (_build_app_dict): https://stackoverflow.com/a/71740645/2166371
But there is too much code in this method that I didn't want to rewrite
I rewrote another method get_app_list
Parent method:
def get_app_list(self, request, app_label=None):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request, app_label)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x["name"].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app["models"].sort(key=lambda x: x["name"])
return app_list
My version:
def get_app_list(self, request, app_label=None):
"""
Return a sorted list of all the installed apps that have been
registered in this site.
"""
app_dict = self._build_app_dict(request, app_label)
# Sort the apps alphabetically.
app_list = sorted(app_dict.values(), key=lambda x: x["name"].lower())
# Sort the models alphabetically within each app.
for app in app_list:
app["models"].sort(key=lambda x: x["name"])
for model in app['models']:
if hasattr(model['model'], 'get_name_admin_panel'):
model['name'] = model['model'].get_name_admin_panel()
return app_list
I have added 3 new lines, before return (these lines can also be added before sorting if sequence is important to you)
There is a check that if the get_name_admin_panel method is described for the model, then we change its name to the one that the method returns
In the model itself, I added the following lines:
#classmethod
def get_name_admin_panel(cls):
return _('Posts ({})').format(Post.objects.filter(...).count())
Thanks to this, I can now set a static value for the verbose_name_plural field and the migration will not be repeated many times
If you do not specify a #classmethod, then unfortunately the admin panel page will load with an error, but I didn't see any problem with that.
Related
Just started using django-autocomplete-light (autocomplete.ModelSelect2) and while I have managed to get it working, I wondered if it is possible to pass disabled options?
I have a list of customers to choose from but some, for various reasons, shouldn't be selected they shouldn't be able to use them. I know I could filter these non-selectable customers out, but this wouldn't be very usable as the user might think that the customer isn't in the database. If so, could someone point me in the right direction as I'm not sure where to start.
It says in the Select2 documentation that disabling options should be possible. Presumably if I could also send a 'disabled':true within the returned json response that might do it.
OK, so here is what I came up with and it works.
view.py
The Select2ViewMixin is subclassed and then a 'disabled' attribute is added to the customer details. This value provided by the ParentAutocomplete view.
from dal import autocomplete
from dal_select2.views import Select2ViewMixin
from dal.views import BaseQuerySetView
class CustomSelect2ViewMixin(Select2ViewMixin):
def get_results(self, context):
return [
{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
'disabled': self.is_disabled_choice(result), # <-- this gets added
} for result in context['object_list']
]
class CustomSelect2QuerySetView(CustomSelect2ViewMixin, BaseQuerySetView):
"""Adds ability to pass a disabled property to a choice."""
class ParentAutocomplete(CustomSelect2QuerySetView):
def get_queryset(self):
qs = Customer.objects.all()
if self.q:
qs = qs.filter(org_name__icontains=self.q)
return qs.order_by('org_name', 'org_city')
def get_result_label(self, item):
return item.selector_name
def get_selected_result_label(self, item):
return item.selector_name
def is_disabled_choice(self, item): # <-- this is where we determine if the record is selectable or not.
customer_id = self.forwarded.get('customer_id', None)
return not (item.can_have_children and not str(item.pk) == customer_id)
form.py
The form is then used as normal.
from dal import autocomplete
class CustomerBaseForm(forms.ModelForm):
customer_id= forms.IntegerField(required=False, widget=forms.HiddenInput)
class Meta:
model = Customer
widgets = {
'parent':autocomplete.ModelSelect2(
url='customer:parent-autocomplete',
forward=['customer_id'],
)
}
Hopefully this might be useful to someone.
I'm struggling with some Django, where I want to make a custom MultiValueField combined with MultiWidget. I've read misc. tutorials, but it seem I'm missing something - most of them were quite old, which I suspect could be the reason.
I'm using Django 1.10.
Goal: Make a custom field that provides three dropdowns for a form. So far, no requirements to the contents of the dropdowns - first I just want to see them in my form :-)
I have a fields.py file, containing:
from django import forms
from widgets import MyCustomWidget
class MyCustomField(forms.MultiValueField):
widget = MyCustomWidget
def __init__(self, *args, **kwargs):
fields = (
forms.CharField(max_length=31),
forms.CharField(max_length=31),
forms.CharField(max_length=31),
)
super(MyCustomField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
return "-".join(data_list)
And then there's a widgets.py, containing:
import re
from django import forms
class MyCustomWidget(forms.MultiWidget):
def __init__(self, attrs=None):
widgets = (
forms.widgets.Select(attrs=attrs, choices=[("1", "1")]),
forms.widgets.Select(attrs=attrs, choices=[("2", "2")]),
forms.widgets.Select(attrs=attrs, choices=[("3", "3")]),
)
super(MyCustomWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return re.split(r"\-", value)
return [None, None, None]
forms.py:
from django.forms import ModelForm
from django import forms
class MyCustomForm(forms.ModelForm):
class Meta:
model = MyCustomModel
fields = ("name")
name = forms.CharField(widget=MyCustomField)
This works fine when migrating, but I try to view the form, I get this error:
'MyCustomField' object has no attribute 'is_hidden'
I have tried to implement this attribute in MyCustomField, but then I get another error:
'MyCustomField' object has no attribute 'attrs'
These attributes should be provided by forms.MultiValueField, as far as I understand - thus I shouldn't need to write them myself.
In the template, I'm just using "{{ form }}" as I wan't to use Django's default layout.
I'm going nuts here and hope someone is able to help to the right path :-)
Kind regards,
Kasper
I believe your mistake is on the line where you set the name
name = forms.CharField(widget=MyCustomField). I haven't tested the codes thou.
from django.forms import ModelForm
from django import forms
class MyCustomForm(forms.ModelForm):
class Meta:
model = MyCustomModel
fields = ("name")
name = MyCustomField # This I believe is will fix your error
I'd like to define a custom application list to use in django's admin index page because I want the apps displayed in a specific order, rather than the default alphabetical order. Trawling through various SO posts it would appear that it's not yet possible to declare the desired application order in any of the obvious places (e.g. admin.py, models.py).
Now, I can see that the django admin's index.html file contains the following statement:
{% for app in app_list %}
# do stuff with the app object
So I'd like to change this to use a custom list object called, say, my_app_list. In python I'd do this along the following lines:
from django.db.models import get_app
my_app_list = [get_app('myapp1'), get_app('myapp2'), ..., get_app('django.contrib.auth')]
for app in my_app_list
...
My question then is, how do I code the equivalent of the first 2 lines above into my local copy of the index.html file?
Or, alternatively, what python source file should I insert those lines into such that the variable my_app_list is available within index.html.
Thanks in advance.
Phil
Subclass django.contrib.admin.site.AdminSite(). Override the .index() method, and do something like this:
class MyAdminSite(django.contrib.admin.site.AdminSite):
def index(self, request, extra_context=None):
if extra_context is None:
extra_context = {}
extra_context["app_list"] = get_app_list_in_custom_order()
return super(MyAdminSite, self).index(request, extra_context)
Instantiate an instance of this subclass with my_admin_site = MyAdminSite(), attach your models to it (using the usual my_admin_site.register()), and attach it to the URLconf; that should do it.
(I haven't tried this, I'm basing this on my reading of the AdminSite source.)
If you don't mind to use a subclass of django.contrib.admin.site.AdminSite(), as expected in cases when you need to customize your admin site, I think it's a feasible idea rewriting "index" and "app_index" methods in the derived class. You can do custom ordering using two dictionaries that store the app declararion order in settings.py and the registration order of models.
Then rewrite the code of the original AdminSite().index() and app_index(), adding a custom order fields ('order') in app_list and order by this field despite 'name'. This is the code, excluding app_index(), that is similar to index() function:
class MyAdminSite(AdminSite):
def __init__(self, name='admin', app_name='admin'):
super(MyAdminSite, self).__init__(name, app_name)
# Model's registration ordering. It's not necessary to
# categorize by app.
self._registry_ord = {}
# App ordering determined by declaration
self._app_ord = { 'auth' : 0 }
app_position = 1
for app in settings.INSTALLED_APPS:
self._app_ord[app] = app_position
app_position += 1
def register(self, model_or_iterable, admin_class=None, **options):
super(MyAdminSite, self).register(model_or_iterable, admin_class, **options)
if isinstance(model_or_iterable, ModelBase):
model_or_iterable = [model_or_iterable]
for model in model_or_iterable:
if model in self._registry:
if self._registry_ord:
self._registry_ord[model._meta.object_name] = max(self._registry_ord.values()) + 1
else:
self._registry_ord[model._meta.object_name] = 1
#never_cache
def index(self, request, extra_context=None):
"""
Displays the main admin index page, which lists all of the installed
apps that have been registered in this site.
"""
app_dict = {}
user = request.user
for model, model_admin in self._registry.items():
app_label = model._meta.app_label
has_module_perms = user.has_module_perms(app_label)
if has_module_perms:
perms = model_admin.get_model_perms(request)
# Check whether user has any perm for this module.
# If so, add the module to the model_list.
if True in perms.values():
info = (app_label, model._meta.module_name)
model_dict = {
'name': capfirst(model._meta.verbose_name_plural),
'perms': perms,
'order': self._registry_ord[model._meta.object_name]
}
if perms.get('change', False):
try:
model_dict['admin_url'] = reverse('admin:%s_%s_changelist' % info, current_app=self.name)
except NoReverseMatch:
pass
if perms.get('add', False):
try:
model_dict['add_url'] = reverse('admin:%s_%s_add' % info, current_app=self.name)
except NoReverseMatch:
pass
if app_label in app_dict:
app_dict[app_label]['models'].append(model_dict)
else:
app_dict[app_label] = {
'name': app_label.title(),
'app_url': reverse('admin:app_list', kwargs={'app_label': app_label}, current_app=self.name),
'has_module_perms': has_module_perms,
'models': [model_dict],
'order': self._app_ord[app_label],
}
# Sort the apps alphabetically.
app_list = app_dict.values()
app_list.sort(key=lambda x: x['order'])
# Sort the models alphabetically within each app.
for app in app_list:
app['models'].sort(key=lambda x: x['order'])
context = {
'title': _('Site administration'),
'app_list': app_list,
}
context.update(extra_context or {})
return TemplateResponse(request, [
self.index_template or 'admin/index.html',
], context, current_app=self.name)
If you use custom AdminSite and you want to include Auth models you probably need this, somewhere in your code (I made it in a specific app to extend user information :
from django.contrib.auth.models import User, Group
from myproject import admin
admin.site.register(User)
admin.site.register(Group)
After doing what #AdminKG said copy the index.html file to the root of the admin directory that you need to create inside the templates directory you declared on you setting.py.
if you you have a clear sorting logic for app_list you can implement it in the .index() method of your AdminSite's subclass. Otherwise you will need to hard code the app list on index.html.
To access something in your template just have it in your context, something like that:
def index(self, request, extra_context=None):
context = {
'app1':get_app('myappname'),
'app2': get_app('mysecondappname'),
# ...
}
context.update(extra_context or {})
context_instance = template.RequestContext(request, current_app=self.name)
return render_to_response(self.index_template or 'admin/terminal_index.html', context,
context_instance=context_instance
)
Now apps objects are available to use on your index.htm
Since you are concerned about the order, you can find my solution helpful.
Basically, I created a filter, which moves desired elements of app_list to the beginning.
#register.filter
def put_it_first(value, arg):
'''The filter shifts specified items in app_list to the top,
the syntax is: LIST_TO_PROCESS|put_it_first:"1st_item[;2nd_item...]"
'''
def _cust_sort(x):
try:
return arg.index(x['name'].lower())
except ValueError:
return dist
arg = arg.split(';')
arg = map(unicode.lower, arg)
dist = len(arg) + 1
value.sort(key=_cust_sort)
return value
However, if you need to remove some elements you can use:
#register.filter
def remove_some(value, arg):
'''The filter removes specified items from app_list,
the syntax is: LIST_TO_PROCESS|remove_some:"1st_item[;2nd_item...]"
'''
arg = arg.split(';')
arg = map(unicode.lower, arg)
return [v for v in value if v['name'].lower() not in arg]
Filters can be chained, so you can use both at the same time.
Filtering functions are not written the way which would make them speed demons, but this template is not being rendered too often by definition.
app_list = admin.site.get_app_list(context['request'])
apply any sort on app_list
I try customize django comments module (delete field url)
i create empty class VSComments and form
from django import forms
from django.contrib.comments.forms import CommentForm
from vs_comments.models import VSComment
class VSCommentForm(CommentForm):
"""
No url Form
"""
VSCommentForm.base_fields.pop('url')
__init__
from vs_comments.models import VSComment
from vs_comments.forms import VSCommentForm
def get_model():
return VSComment
def get_form():
return VSCommentForm
also url(r'^comments/', include('django.contrib.comments.urls')),
include 'vs_comments' and 'django.contrib.comments' into INSTALLED_APPS and COMMENTS_APP = 'vs_comments'
As result, I have right form, without url field, but posting comments doesn't work
soution add to the form class
def get_comment_create_data(self):
# Use the data of the superclass, and remove extra fields
return dict(
content_type = ContentType.objects.get_for_model(self.target_object),
object_pk = force_unicode(self.target_object._get_pk_val()),
comment = self.cleaned_data["comment"],
name = self.cleaned_data["name"],
submit_date = datetime.datetime.now(),
site_id = settings.SITE_ID,
is_public = True,
is_removed = False,
)
For admin panel
class VSCommentAdmin(CommentsAdmin):
"""
all like native comments
"""
admin.site.register(Comment, CommentsAdmin)
But now didn't work tags render_comment_list and other. No any errors, only empty result
How can I fix it?
Do you have any error trace ?
I think it doesn't work because the validation of your VSCommentForm uses the validator of CommentForm, and the "url" field is missing in your customized form.
You should read more on customized form that inherits from another form and its validation :
https://docs.djangoproject.com/en/dev/topics/forms/#processing-the-data-from-a-form
https://code.djangoproject.com/wiki/CustomFormFields
https://docs.djangoproject.com/en/dev/ref/forms/validation/
I have a standard admin change form for an object, with the usual StackedInline forms for a ForeignKey relationship. I would like to be able to link each inline item to its corresponding full-sized change form, as the inline item has inlined items of its own, and I can't nest them.
I've tried everything from custom widgets to custom templates, and can't make anything work. So far, the "solutions" I've seen in the form of snippets just plain don't seem to work for inlines. I'm getting ready to try some DOM hacking with jQuery just to get it working and move on.
I hope I must be missing something very simple, as this seems like such a simple task!
Using Django 1.2.
There is a property called show_change_link since Django 1.8.
I did something like the following in my admin.py:
from django.utils.html import format_html
from django.core.urlresolvers import reverse
class MyModelInline(admin.TabularInline):
model = MyModel
def admin_link(self, instance):
url = reverse('admin:%s_%s_change' % (instance._meta.app_label,
instance._meta.module_name),
args=(instance.id,))
return format_html(u'Edit', url)
# … or if you want to include other fields:
return format_html(u'Edit: {}', url, instance.title)
readonly_fields = ('admin_link',)
The currently accepted solution here is good work, but it's out of date.
Since Django 1.3, there is a built-in property called show_change_link = True that addresses this issue.
This can be added to any StackedInline or TabularInline object. For example:
class ContactListInline(admin.TabularInline):
model = ContactList
fields = ('name', 'description', 'total_contacts',)
readonly_fields = ('name', 'description', 'total_contacts',)
show_change_link = True
The result will be something line this:
I had similar problem and I came up with custom widget plus some tweaks to model form. Here is the widget:
from django.utils.safestring import mark_safe
class ModelLinkWidget(forms.Widget):
def __init__(self, obj, attrs=None):
self.object = obj
super(ModelLinkWidget, self).__init__(attrs)
def render(self, name, value, attrs=None):
if self.object.pk:
return mark_safe(
u'<a target="_blank" href="../../../%s/%s/%s/">%s</a>' %\
(
self.object._meta.app_label,
self.object._meta.object_name.lower(),
self.object.pk, self.object
)
)
else:
return mark_safe(u'')
Now since widget for each inline need to get different object in constructor you can't just set it in standard way, but in Form's init method:
class TheForm(forms.ModelForm):
...
# required=False is essential cause we don't
# render input tag so there will be no value submitted.
link = forms.CharField(label='link', required=False)
def __init__(self, *args, **kwargs):
super(TheForm, self).__init__(*args, **kwargs)
# instance is always available, it just does or doesn't have pk.
self.fields['link'].widget = ModelLinkWidget(self.instance)
Quentin's answer above works, but you also need to specify fields = ('admin_link',)
There is a module for this purpose. Check out:
django-relatives
I think: args=[instance.id] should be args=[instance.pk]. It worked for me!