Ticket #8054: 8054-list-column.diff

File 8054-list-column.diff, 17.0 KB (added by dan@…, 7 years ago)

Initial patch

  • django/contrib/admin/validation.py

     
    77from django.db import models
    88from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model
    99from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
     10from django.contrib.admin.options import ListColumn
    1011from django.contrib.admin.options import HORIZONTAL, VERTICAL
    1112
    1213def validate(cls, model):
     
    3536    if hasattr(cls, 'list_display'):
    3637        _check_istuplew('list_display', cls.list_display)
    3738        for idx, field in enumerate(cls.list_display):
    38             f = _check_attr_existsw("list_display[%d]" % idx, field)
     39            if isinstance(field, ListColumn):
     40                f = _check_attr_existsw("list_display[%d]" % idx, field.field_name)
     41            else:
     42                f = _check_attr_existsw("list_display[%d]" % idx, field)
    3943            if isinstance(f, models.ManyToManyField):
    4044                raise ImproperlyConfigured("`%s.list_display[%d]`, `%s` is a "
    4145                        "ManyToManyField which is not supported."
  • django/contrib/admin/options.py

     
    99from django.db import models, transaction
    1010from django.http import Http404, HttpResponse, HttpResponseRedirect
    1111from django.shortcuts import get_object_or_404, render_to_response
    12 from django.utils.html import escape
     12from django.utils.html import escape, conditional_escape
    1313from django.utils.safestring import mark_safe
     14from django.utils.encoding import smart_unicode, smart_str, force_unicode
    1415from django.utils.text import capfirst, get_text_list
    1516from django.utils.translation import ugettext as _
    1617from django.utils.encoding import force_unicode
     
    905906                self.extend(inline_formset.non_form_errors())
    906907                for errors_in_inline_form in inline_formset.errors:
    907908                    self.extend(errors_in_inline_form.values())
     909
     910
     911class ListColumn(object):
     912        def __init__(self, field_name, header=None, filter='', load_filters=[], order_field=None, value_map=None):
     913                self.field_name = field_name
     914                self.header = header
     915                self.filter = filter
     916                self.load_filters = load_filters
     917                self.order_field = order_field
     918                self.value_map = value_map
     919                self._nowrap = False
     920
     921        def for_model(self, model):
     922                """Create a new ListColumn instance where unset properties in this
     923                ListColumn have been filled in by inspecting the Model provided."""
     924               
     925                try:
     926                        f = model._meta.get_field(self.field_name)
     927                except models.FieldDoesNotExist:
     928                        return AttributeListColumn(model, self.field_name,
     929                                header=self.header,
     930                                filter=self.filter,
     931                                load_filters=self.load_filters,
     932                                order_field=self.order_field
     933                        )
     934                else:
     935                        return FieldListColumn(f, self.field_name,
     936                                header=self.header,
     937                                filter=self.filter,
     938                                load_filters=self.load_filters,
     939                                order_field=self.order_field
     940                        )
     941
     942        def get_value_display(self, result):
     943                from django.template import Context, Parser, get_library
     944                field_value = self.get_value(result)
     945                if self.value_map:
     946                        field_value = self.value_map.get(field_value, None)
     947
     948                if field_value is None:
     949                        return EMPTY_CHANGELIST_VALUE
     950                if field_value == '':
     951                        return mark_safe(' ')
     952
     953                # apply filters by faking a template token
     954                if self.filter:
     955                        p = Parser([])
     956                        for lib in ['django.contrib.admin.templatetags.admin_list'] + self.load_filters:
     957                                p.add_library(get_library(lib))
     958                        fe = p.compile_filter('val|' + self.filter)
     959                        return fe.resolve(Context({'val': field_value}))
     960
     961                return smart_unicode(field_value)
     962
     963
     964class FieldListColumn(ListColumn):
     965        def __init__(self, field, *args, **kwargs):
     966                super(FieldListColumn, self).__init__(*args, **kwargs)
     967                self.field = field
     968
     969                if not self.header:
     970                        self.header = field.verbose_name
     971
     972                if isinstance(field.rel, models.ManyToOneRel):
     973                        if field.null:
     974                                self.order_field = None
     975                elif not self.order_field:
     976                        self.order_field = self.field_name
     977
     978                if not self.filter:
     979                        if isinstance(field, models.DateTimeField):
     980                                self.filter = 'admin_datetime_format'
     981                        elif isinstance(field, models.DateField):
     982                                self.filter = 'admin_date_format'
     983                        elif isinstance(field, models.TimeField):
     984                                self.filter = 'admin_time_format'
     985                        elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
     986                                self.filter = 'boolean_icon'
     987                        elif isinstance(field, models.DecimalField):
     988                                self.filter = 'floatformat:%d' % field.decimal_places
     989
     990                if isinstance(field, models.DateField) or isinstance(field, models.TimeField):
     991                        self._nowrap = True     
     992
     993                if not self.value_map and field.choices:
     994                        self.value_map = dict(field.choices)
     995
     996        def get_value(self, result):
     997                return getattr(result, self.field_name, None)
     998
     999
     1000class AttributeListColumn(ListColumn):
     1001        def __init__(self, model, *args, **kwargs):
     1002                super(AttributeListColumn, self).__init__(*args, **kwargs)
     1003                attr = getattr(model, self.field_name, None)
     1004               
     1005                if attr is not None and not self.filter:
     1006                        if getattr(attr, 'boolean', False):
     1007                                self.filter = 'boolean_icon'
     1008                        elif getattr(attr, 'allow_tags', False):
     1009                                self.filter = 'safe'
     1010
     1011                if self.header is None:
     1012                        if self.field_name == '__unicode__':
     1013                                self.header = force_unicode(model._meta.verbose_name)
     1014                        elif self.field_name == '__str__':
     1015                                self.header = smart_str(model._meta.verbose_name)
     1016                        elif attr is not None:
     1017                                try:
     1018                                        self.header = attr.short_description
     1019                                except AttributeError:
     1020                                        pass
     1021                               
     1022                        if not self.header:     
     1023                                self.header = self.field_name.replace('_', ' ')
     1024
     1025                if attr is not None and not self.order_field:
     1026                        self.order_field = getattr(attr, "admin_order_field", None)
     1027
     1028        def get_value(self, result):
     1029                attr = getattr(result, self.field_name, None)
     1030                if callable(attr):
     1031                        return attr()
     1032                return attr
  • django/contrib/admin/__init__.py

     
    11from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    22from django.contrib.admin.options import StackedInline, TabularInline
     3from django.contrib.admin.options import ListColumn
    34from django.contrib.admin.sites import AdminSite, site
    45
    56def autodiscover():
  • django/contrib/admin/templatetags/admin_list.py

     
    11from django.conf import settings
     2from django.contrib.admin.options import ListColumn
    23from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
    34from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
    45from django.core.exceptions import ObjectDoesNotExist
     
    6869    }
    6970pagination = register.inclusion_tag('admin/pagination.html')(pagination)
    7071
    71 def result_headers(cl):
    72     lookup_opts = cl.lookup_opts
    7372
    74     for i, field_name in enumerate(cl.list_display):
    75         try:
    76             f = lookup_opts.get_field(field_name)
    77             admin_order_field = None
    78         except models.FieldDoesNotExist:
    79             # For non-field list_display values, check for the function
    80             # attribute "short_description". If that doesn't exist, fall back
    81             # to the method name. And __str__ and __unicode__ are special-cases.
    82             if field_name == '__unicode__':
    83                 header = force_unicode(lookup_opts.verbose_name)
    84             elif field_name == '__str__':
    85                 header = smart_str(lookup_opts.verbose_name)
    86             else:
    87                 attr = getattr(cl.model, field_name) # Let AttributeErrors propagate.
    88                 try:
    89                     header = attr.short_description
    90                 except AttributeError:
    91                     header = field_name.replace('_', ' ')
     73def boolean_icon(field_val):
     74    if field_val is None:
     75        v = 'unknown'
     76    else:
     77        v = field_val and 'yes' or 'no'
     78    return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, v, field_val))
     79boolean_icon = register.filter(boolean_icon)
    9280
    93             # It is a non-field, but perhaps one that is sortable
    94             admin_order_field = getattr(getattr(cl.model, field_name), "admin_order_field", None)
    95             if not admin_order_field:
    96                 yield {"text": header}
    97                 continue
    9881
    99             # So this _is_ a sortable non-field.  Go to the yield
    100             # after the else clause.
    101         else:
    102             if isinstance(f.rel, models.ManyToOneRel) and f.null:
    103                 yield {"text": f.verbose_name}
    104                 continue
    105             else:
    106                 header = f.verbose_name
     82def admin_datetime_format(field_val):
     83    (date_format, datetime_format, time_format) = get_date_formats()
     84    return capfirst(dateformat.format(field_val, datetime_format))
     85admin_datetime_format = register.filter(admin_datetime_format)
    10786
    108         th_classes = []
    109         new_order_type = 'asc'
    110         if field_name == cl.order_field or admin_order_field == cl.order_field:
    111             th_classes.append('sorted %sending' % cl.order_type.lower())
    112             new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
    11387
    114         yield {"text": header,
    115                "sortable": True,
    116                "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
    117                "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
     88def admin_date_format(field_val):
     89    (date_format, datetime_format, time_format) = get_date_formats()
     90    return capfirst(dateformat.format(field_val, date_format))
     91admin_date_format = register.filter(admin_date_format)
    11892
    119 def _boolean_icon(field_val):
    120     BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
    121     return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))
    12293
    123 def items_for_result(cl, result):
    124     first = True
    125     pk = cl.lookup_opts.pk.attname
    126     for field_name in cl.list_display:
    127         row_class = ''
    128         try:
    129             f = cl.lookup_opts.get_field(field_name)
    130         except models.FieldDoesNotExist:
    131             # For non-field list_display values, the value is either a method
    132             # or a property.
    133             try:
    134                 attr = getattr(result, field_name)
    135                 allow_tags = getattr(attr, 'allow_tags', False)
    136                 boolean = getattr(attr, 'boolean', False)
    137                 if callable(attr):
    138                     attr = attr()
    139                 if boolean:
    140                     allow_tags = True
    141                     result_repr = _boolean_icon(attr)
    142                 else:
    143                     result_repr = smart_unicode(attr)
    144             except (AttributeError, ObjectDoesNotExist):
    145                 result_repr = EMPTY_CHANGELIST_VALUE
    146             else:
    147                 # Strip HTML tags in the resulting text, except if the
    148                 # function has an "allow_tags" attribute set to True.
    149                 if not allow_tags:
    150                     result_repr = escape(result_repr)
    151                 else:
    152                     result_repr = mark_safe(result_repr)
    153         else:
    154             field_val = getattr(result, f.attname)
     94def admin_time_format(field_val):
     95    (date_format, datetime_format, time_format) = get_date_formats()
     96    return capfirst(dateformat.format(field_val, time_format))
     97admin_time_format = register.filter(admin_time_format)
    15598
    156             if isinstance(f.rel, models.ManyToOneRel):
    157                 if field_val is not None:
    158                     result_repr = escape(getattr(result, f.name))
    159                 else:
    160                     result_repr = EMPTY_CHANGELIST_VALUE
    161             # Dates and times are special: They're formatted in a certain way.
    162             elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
    163                 if field_val:
    164                     (date_format, datetime_format, time_format) = get_date_formats()
    165                     if isinstance(f, models.DateTimeField):
    166                         result_repr = capfirst(dateformat.format(field_val, datetime_format))
    167                     elif isinstance(f, models.TimeField):
    168                         result_repr = capfirst(dateformat.time_format(field_val, time_format))
    169                     else:
    170                         result_repr = capfirst(dateformat.format(field_val, date_format))
    171                 else:
    172                     result_repr = EMPTY_CHANGELIST_VALUE
    173                 row_class = ' class="nowrap"'
    174             # Booleans are special: We use images.
    175             elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
    176                 result_repr = _boolean_icon(field_val)
    177             # DecimalFields are special: Zero-pad the decimals.
    178             elif isinstance(f, models.DecimalField):
    179                 if field_val is not None:
    180                     result_repr = ('%%.%sf' % f.decimal_places) % field_val
    181                 else:
    182                     result_repr = EMPTY_CHANGELIST_VALUE
    183             # Fields with choices are special: Use the representation
    184             # of the choice.
    185             elif f.choices:
    186                 result_repr = dict(f.choices).get(field_val, EMPTY_CHANGELIST_VALUE)
    187             else:
    188                 result_repr = escape(field_val)
    189         if force_unicode(result_repr) == '':
    190             result_repr = mark_safe('&nbsp;')
    191         # If list_display_links not defined, add the link tag to the first field
    192         if (first and not cl.list_display_links) or field_name in cl.list_display_links:
    193             table_tag = {True:'th', False:'td'}[first]
    194             first = False
    195             url = cl.url_for_result(result)
    196             # Convert the pk to something that can be used in Javascript.
    197             # Problem cases are long ints (23L) and non-ASCII strings.
    198             result_id = repr(force_unicode(getattr(result, pk)))[1:]
    199             yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
    200                 (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))
    201         else:
    202             yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
    20399
    204 def results(cl):
    205     for res in cl.result_list:
    206         yield list(items_for_result(cl,res))
     100def result_headers(cl, columns):
     101        for i, col in enumerate(columns):
     102                if col.order_field is None:
     103                        yield {'text': col.header}
     104                        continue
    207105
     106                th_classes = []
     107                new_order_type = 'asc'
     108                if col.order_field == cl.order_field:
     109                        th_classes.append('sorted %sending' % cl.order_type.lower())
     110                        new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
     111
     112                yield {"text": col.header,
     113                           "sortable": True,
     114                           "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
     115                           "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
     116
     117
     118def items_for_result(cl, columns, result):
     119        pk = cl.lookup_opts.pk.attname
     120        for i, col in enumerate(columns):
     121                result_repr = col.get_value_display(result)
     122                row_class = col._nowrap and ' class="nowrap"' or ''
     123                first = i == 0
     124                # If list_display_links not defined, add the link tag to the first field
     125                if (first and not cl.list_display_links) or col.field_name in cl.list_display_links:
     126                        table_tag = {True:'th', False:'td'}[first]
     127                        url = cl.url_for_result(result)
     128                        # Convert the pk to something that can be used in Javascript.
     129                        # Problem cases are long ints (23L) and non-ASCII strings.
     130                        result_id = repr(force_unicode(getattr(result, pk)))[1:]
     131                        yield mark_safe(u'<%s%s><a href="%s"%s>%s</a></%s>' % \
     132                                (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))
     133                else:
     134                        yield mark_safe(u'<td%s>%s</td>' % (row_class, conditional_escape(result_repr)))
     135
     136
    208137def result_list(cl):
     138    cols = []
     139    for col in cl.list_display:
     140        if isinstance(col, ListColumn):
     141            cols += [col.for_model(cl.model)]
     142        else:
     143            cols += [ListColumn(col).for_model(cl.model)]
    209144    return {'cl': cl,
    210             'result_headers': list(result_headers(cl)),
    211             'results': list(results(cl))}
     145            'result_headers': list(result_headers(cl, cols)),
     146            'results': [items_for_result(cl, cols, r) for r in cl.result_list]}
    212147result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
    213148
     149
    214150def date_hierarchy(cl):
    215151    if cl.date_hierarchy:
    216152        field_name = cl.date_hierarchy
Back to Top