Ticket #8054: 8054-list-column.4.diff
File 8054-list-column.4.diff, 22.3 KB (added by , 14 years ago) |
---|
-
django/contrib/admin/validation.py
4 4 _get_foreign_key) 5 5 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin 6 6 from django.contrib.admin.options import HORIZONTAL, VERTICAL 7 from django.contrib.admin.options import ListColumn 7 8 8 9 9 10 __all__ = ['validate'] … … 26 27 check_isseq(cls, 'list_display', cls.list_display) 27 28 for idx, field in enumerate(cls.list_display): 28 29 if not callable(field): 29 if not hasattr(cls, field):30 if not hasattr(model, field):30 if isinstance(field, ListColumn) or not hasattr(cls, field): 31 if not isinstance(field, ListColumn) and not hasattr(model, field): 31 32 try: 32 33 opts.get_field(field) 33 34 except models.FieldDoesNotExist: … … 35 36 % (cls.__name__, idx, field, cls.__name__, model._meta.object_name)) 36 37 else: 37 38 # getattr(model, field) could be an X_RelatedObjectsDescriptor 38 f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) 39 if isinstance(field, ListColumn): 40 f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field.field_name) 41 else: 42 f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field) 39 43 if isinstance(f, models.ManyToManyField): 40 44 raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported." 41 45 % (cls.__name__, idx, field)) -
django/contrib/admin/options.py
9 9 from django.contrib import messages 10 10 from django.views.decorators.csrf import csrf_protect 11 11 from django.core.exceptions import PermissionDenied, ValidationError 12 from django.contrib.admin.util import label_for_field 13 from django.contrib.admin import views 12 14 from django.db import models, transaction 13 15 from django.db.models.fields import BLANK_CHOICE_DASH 14 16 from django.http import Http404, HttpResponse, HttpResponseRedirect … … 16 18 from django.utils.decorators import method_decorator 17 19 from django.utils.datastructures import SortedDict 18 20 from django.utils.functional import update_wrapper 21 from django.utils.encoding import smart_unicode, smart_str 19 22 from django.utils.html import escape 20 23 from django.utils.safestring import mark_safe 21 24 from django.utils.functional import curry … … 23 26 from django.utils.translation import ugettext as _ 24 27 from django.utils.translation import ungettext 25 28 from django.utils.encoding import force_unicode 29 import types 26 30 31 32 class ListColumn(object): 33 def __init__(self, field_name, header=None, filter='', load_filters=[], \ 34 order_field=None, value_map=None): 35 self.field_name = field_name 36 self.header = header 37 self.filter = filter 38 self.load_filters = load_filters 39 self.order_field = order_field 40 self.value_map = value_map 41 self._nowrap = False 42 43 def for_model(self, model, cl=None): 44 """Create a new ListColumn instance where unset properties in this 45 ListColumn have been filled in by inspecting the Model provided.""" 46 header, attr = label_for_field(self.field_name, model, 47 model_admin = cl.model_admin, 48 return_attr = True 49 ) 50 try: 51 f = model._meta.get_field(self.field_name) 52 except models.FieldDoesNotExist: 53 if isinstance(self.field_name, types.FunctionType): 54 columnlist_class = FunctionListColumn 55 else: 56 columnlist_class = AttributeListColumn 57 return columnlist_class(model, field_name=self.field_name, 58 header=self.header if self.header else header, 59 filter=self.filter, 60 load_filters=self.load_filters, 61 order_field=self.order_field, 62 attr=attr 63 ) 64 else: 65 return FieldListColumn(f, self.field_name, 66 header=self.header if self.header else header, 67 filter=self.filter, 68 load_filters=self.load_filters, 69 order_field=self.order_field 70 ) 71 72 def get_value_display(self, result, cl=None): 73 field_value = self.get_value(result) 74 if self.value_map: 75 field_value = self.value_map.get(field_value, views.main.EMPTY_CHANGELIST_VALUE) 76 77 # apply filters by faking a template token 78 if self.filter: 79 p = template.Parser([]) 80 for lib in ['admin_list', ] + self.load_filters: 81 p.add_library(template.get_library(lib)) 82 fe = p.compile_filter('val|' + self.filter) 83 return fe.resolve(template.Context({'val': field_value})) 84 85 if field_value is None: 86 return views.main.EMPTY_CHANGELIST_VALUE 87 if force_unicode(field_value) == '': 88 return mark_safe(' ') 89 return smart_unicode(field_value) 90 91 92 class FieldListColumn(ListColumn): 93 """Model field column""" 94 def __init__(self, field, *args, **kwargs): 95 super(FieldListColumn, self).__init__(*args, **kwargs) 96 self.field = field 97 98 if not self.header: 99 self.header = field.verbose_name 100 101 #if isinstance(field.rel, models.ManyToOneRel): 102 # if field.null: 103 # self.order_field = None 104 if not self.order_field: 105 self.order_field = self.field_name 106 107 if not self.filter: 108 if isinstance(field, models.DateTimeField): 109 self.filter = 'date:"DATETIME_FORMAT"' 110 elif isinstance(field, models.DateField): 111 self.filter = 'date:"DATE_FORMAT"' 112 elif isinstance(field, models.TimeField): 113 self.filter = 'time:"TIME_FORMAT"' 114 elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField): 115 self.filter = 'boolean_icon' 116 elif isinstance(field, models.DecimalField): 117 self.filter = 'floatformat:%d' % field.decimal_places 118 119 if isinstance(field, models.DateField) or isinstance(field, models.TimeField): 120 self._nowrap = True 121 122 if not self.value_map and field.choices: 123 self.value_map = dict(field.flatchoices) 124 125 def get_value(self, result): 126 return getattr(result, self.field_name, None) 127 128 129 class AttributeListColumn(ListColumn): 130 """Custom column - Model or ModelAdmin method""" 131 def __init__(self, model, attr=None, *args, **kwargs): 132 super(AttributeListColumn, self).__init__(*args, **kwargs) 133 134 self.callable = attr 135 136 if attr is not None and not self.filter: 137 if getattr(attr, 'boolean', False): 138 self.filter = 'boolean_icon' 139 elif getattr(attr, 'allow_tags', False): 140 self.filter = 'safe' 141 142 if self.header is None: 143 if self.field_name == '__unicode__': 144 self.header = force_unicode(model._meta.verbose_name) 145 elif self.field_name == '__str__': 146 self.header = smart_str(model._meta.verbose_name) 147 elif attr is not None: 148 try: 149 self.header = attr.short_description 150 except AttributeError: 151 pass 152 153 if not self.header: 154 self.header = self.field_name.replace('_', ' ') 155 156 if attr is not None and not self.order_field: 157 self.order_field = getattr(attr, "admin_order_field", None) 158 159 def get_value(self, result): 160 # if label_for_field resolve callable for this field call it 161 if callable(self.callable): 162 return self.callable(result) 163 # check Model method 164 attr = getattr(result, self.field_name, None) 165 if callable(attr): 166 return attr() 167 return attr 168 169 170 class FunctionListColumn(AttributeListColumn): 171 """Custom column based on function""" 172 173 def get_value(self, result): 174 return self.field_name(getattr(result, self.order_field, result)) 175 176 27 177 HORIZONTAL, VERTICAL = 1, 2 28 178 # returns the <ul> class for a given radio_admin field 29 179 get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') … … 233 383 if not self.list_display_links: 234 384 for name in self.list_display: 235 385 if name != 'action_checkbox': 236 self.list_display_links = [name] 386 if isinstance(name, ListColumn): 387 self.list_display_links = [name.field_name] 388 else: 389 self.list_display_links = [name] 237 390 break 238 391 super(ModelAdmin, self).__init__() 239 392 -
django/contrib/admin/util.py
313 313 314 314 315 315 def display_for_field(value, field): 316 from django.contrib.admin.templatetags.admin_list import _boolean_icon316 from django.contrib.admin.templatetags.admin_list import boolean_icon 317 317 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 318 318 319 319 if field.flatchoices: … … 321 321 # NullBooleanField needs special-case null-handling, so it comes 322 322 # before the general null test. 323 323 elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField): 324 return _boolean_icon(value)324 return boolean_icon(value) 325 325 elif value is None: 326 326 return EMPTY_CHANGELIST_VALUE 327 327 elif isinstance(field, models.DateField) or isinstance(field, models.TimeField): -
django/contrib/admin/__init__.py
1 1 from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL 2 2 from django.contrib.admin.options import StackedInline, TabularInline 3 3 from django.contrib.admin.sites import AdminSite, site 4 from django.contrib.admin.options import ListColumn 4 5 5 6 6 7 def autodiscover(): -
django/contrib/admin/helpers.py
160 160 }) 161 161 162 162 def contents(self): 163 from django.contrib.admin.templatetags.admin_list import _boolean_icon163 from django.contrib.admin.templatetags.admin_list import boolean_icon 164 164 from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE 165 165 field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin 166 166 try: … … 171 171 if f is None: 172 172 boolean = getattr(attr, "boolean", False) 173 173 if boolean: 174 result_repr = _boolean_icon(value)174 result_repr = boolean_icon(value) 175 175 else: 176 176 result_repr = smart_unicode(value) 177 177 if getattr(attr, "allow_tags", False): -
django/contrib/admin/templatetags/admin_list.py
1 import datetime2 3 1 from django.conf import settings 4 from django.contrib.admin.util import l ookup_field, display_for_field, label_for_field5 from django.contrib.admin.views.main import ALL_VAR , EMPTY_CHANGELIST_VALUE2 from django.contrib.admin.util import label_for_field 3 from django.contrib.admin.views.main import ALL_VAR 6 4 from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR 7 from django.core.exceptions import ObjectDoesNotExist 8 from django.db import models 5 from django.contrib.admin.options import ListColumn 9 6 from django.utils import formats 10 7 from django.utils.html import escape, conditional_escape 11 8 from django.utils.safestring import mark_safe 12 9 from django.utils.text import capfirst 13 from django.utils.translation import ugettext as _ 14 from django.utils.encoding import smart_unicode, force_unicode 10 from django.utils.encoding import force_unicode 15 11 from django.template import Library 12 import datetime 16 13 17 14 18 15 register = Library() … … 31 28 return mark_safe(u'<a href="%s"%s>%d</a> ' % (escape(cl.get_query_string({PAGE_VAR: i})), (i == cl.paginator.num_pages-1 and ' class="end"' or ''), i+1)) 32 29 paginator_number = register.simple_tag(paginator_number) 33 30 31 34 32 def pagination(cl): 35 33 """ 36 34 Generates the series of links to the pages in a paginated list. … … 77 75 } 78 76 pagination = register.inclusion_tag('admin/pagination.html')(pagination) 79 77 80 def result_headers(cl ):78 def result_headers(cl, columns): 81 79 """ 82 80 Generates the list column headers. 83 81 """ 84 lookup_opts = cl.lookup_opts 85 86 for i, field_name in enumerate(cl.list_display): 87 header, attr = label_for_field(field_name, cl.model, 88 model_admin = cl.model_admin, 89 return_attr = True 90 ) 91 if attr: 92 # if the field is the action checkbox: no sorting and special class 93 if field_name == 'action_checkbox': 94 yield { 82 for i, col in enumerate(columns): 83 # if the field is the action checkbox: no sorting and special class 84 if col.field_name == 'action_checkbox': 85 header, attr = label_for_field(col.field_name, cl.model, 86 model_admin = cl.model_admin, 87 return_attr = True 88 ) 89 yield { 95 90 "text": header, 96 91 "class_attrib": mark_safe(' class="action-checkbox-column"') 97 92 } 98 93 continue 99 94 100 # It is a non-field, but perhaps one that is sortable 101 admin_order_field = getattr(attr, "admin_order_field", None) 102 if not admin_order_field: 103 yield {"text": header} 104 continue 95 if col.order_field is None: 96 yield {'text': col.header} 97 continue 105 98 106 # So this _is_ a sortable non-field. Go to the yield107 # after the else clause.108 else:109 admin_order_field = None110 111 99 th_classes = [] 112 100 new_order_type = 'asc' 113 if field_name == cl.order_field or admin_order_field == cl.order_field:101 if col.order_field == cl.order_field: 114 102 th_classes.append('sorted %sending' % cl.order_type.lower()) 115 103 new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()] 116 104 117 105 yield { 118 "text": header,106 "text": col.header, 119 107 "sortable": True, 120 108 "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}), 121 109 "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '') 122 110 } 123 111 124 def _boolean_icon(field_val): 125 BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'} 126 return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val)) 112 def boolean_icon(field_val): 113 if field_val is None: 114 v = 'unknown' 115 else: 116 v = field_val and 'yes' or 'no' 117 return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, v, field_val)) 118 boolean_icon = register.filter(boolean_icon) 127 119 128 def items_for_result(cl, result, form): 120 121 def items_for_result(cl, result, form, columns): 129 122 """ 130 123 Generates the actual list of data. 131 124 """ 132 first = True133 125 pk = cl.lookup_opts.pk.attname 134 for field_name in cl.list_display: 135 row_class = '' 136 try: 137 f, attr, value = lookup_field(field_name, result, cl.model_admin) 138 except (AttributeError, ObjectDoesNotExist): 139 result_repr = EMPTY_CHANGELIST_VALUE 140 else: 141 if f is None: 142 allow_tags = getattr(attr, 'allow_tags', False) 143 boolean = getattr(attr, 'boolean', False) 144 if boolean: 145 allow_tags = True 146 result_repr = _boolean_icon(value) 147 else: 148 result_repr = smart_unicode(value) 149 # Strip HTML tags in the resulting text, except if the 150 # function has an "allow_tags" attribute set to True. 151 if not allow_tags: 152 result_repr = escape(result_repr) 153 else: 154 result_repr = mark_safe(result_repr) 155 else: 156 if value is None: 157 result_repr = EMPTY_CHANGELIST_VALUE 158 if isinstance(f.rel, models.ManyToOneRel): 159 result_repr = escape(getattr(result, f.name)) 160 else: 161 result_repr = display_for_field(value, f) 162 if isinstance(f, models.DateField) or isinstance(f, models.TimeField): 163 row_class = ' class="nowrap"' 164 if force_unicode(result_repr) == '': 165 result_repr = mark_safe(' ') 126 for i, col in enumerate(columns): 127 result_repr = col.get_value_display(result, cl) 128 row_class = col._nowrap and ' class="nowrap"' or '' 129 first = (i == 1) # first (with i == 0) column is checkbox 166 130 # If list_display_links not defined, add the link tag to the first field 167 if (first and not cl.list_display_links) or field_name in cl.list_display_links:131 if (first and not cl.list_display_links) or col.field_name in cl.list_display_links: 168 132 table_tag = {True:'th', False:'td'}[first] 169 first = False170 133 url = cl.url_for_result(result) 171 134 # Convert the pk to something that can be used in Javascript. 172 135 # Problem cases are long ints (23L) and non-ASCII strings. … … 176 139 attr = pk 177 140 value = result.serializable_value(attr) 178 141 result_id = repr(force_unicode(value))[1:] 179 yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \ 180 (table_tag, row_class, url, (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), conditional_escape(result_repr), table_tag)) 142 yield mark_safe(u'<%(tag)s%(row_class)s><a href="%(url)s"%(onclick)s>%(value)s</a></%(tag)s>' % { 143 'tag': table_tag, 144 'row_class': row_class, 145 'url': url, 146 'onclick': (cl.is_popup and ' onclick="opener.dismissRelatedLookupPopup(window, %s); return false;"' % result_id or ''), 147 'value': conditional_escape(result_repr) 148 }) 181 149 else: 182 150 # By default the fields come from ModelAdmin.list_editable, but if we pull 183 151 # the fields out of the form instead of list_editable custom admins 184 152 # can provide fields on a per request basis 185 if form and field_name in form.fields:186 bf = form[ field_name]153 if form and col.field_name in form.fields: 154 bf = form[col.field_name] 187 155 result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf)) 188 156 else: 189 157 result_repr = conditional_escape(result_repr) … … 191 159 if form and not form[cl.model._meta.pk.name].is_hidden: 192 160 yield mark_safe(u'<td>%s</td>' % force_unicode(form[cl.model._meta.pk.name])) 193 161 194 def results(cl ):162 def results(cl, columns): 195 163 if cl.formset: 196 164 for res, form in zip(cl.result_list, cl.formset.forms): 197 yield list(items_for_result(cl, res, form ))165 yield list(items_for_result(cl, res, form, columns)) 198 166 else: 199 167 for res in cl.result_list: 200 yield list(items_for_result(cl, res, None ))168 yield list(items_for_result(cl, res, None, columns)) 201 169 202 170 def result_hidden_fields(cl): 203 171 if cl.formset: … … 209 177 """ 210 178 Displays the headers and data list together 211 179 """ 180 cols = [] 181 for col in cl.list_display: 182 if isinstance(col, ListColumn): 183 cols += [col.for_model(cl.model, cl)] 184 else: 185 cols += [ListColumn(col).for_model(cl.model, cl)] 212 186 return {'cl': cl, 213 187 'result_hidden_fields': list(result_hidden_fields(cl)), 214 'result_headers': list(result_headers(cl )),215 'results': list(results(cl ))}188 'result_headers': list(result_headers(cl, cols)), 189 'results': list(results(cl, cols))} 216 190 result_list = register.inclusion_tag("admin/change_list_results.html")(result_list) 217 191 218 192 def date_hierarchy(cl): -
django/contrib/admin/views/main.py
1 1 from django.contrib.admin.filterspecs import FilterSpec 2 from django.contrib.admin.options import IncorrectLookupParameters 2 from django.contrib.admin.options import IncorrectLookupParameters, ListColumn 3 3 from django.contrib.admin.util import quote 4 4 from django.core.paginator import Paginator, InvalidPage 5 5 from django.db import models … … 139 139 if ORDER_VAR in params: 140 140 try: 141 141 field_name = self.list_display[int(params[ORDER_VAR])] 142 field = self.list_display[int(params[ORDER_VAR])] 143 if isinstance(field, ListColumn): 144 field_name = field.field_name 145 else: 146 field_name = field 142 147 try: 143 148 f = lookup_opts.get_field(field_name) 144 149 except models.FieldDoesNotExist: