Ticket #8054: 8054-list-column.4.diff

File 8054-list-column.4.diff, 22.3 KB (added by alekam, 5 years ago)

Update patch. Now it passes all existing regression tests.

  • django/contrib/admin/validation.py

     
    44    _get_foreign_key)
    55from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
    66from django.contrib.admin.options import HORIZONTAL, VERTICAL
     7from django.contrib.admin.options import ListColumn
    78
    89
    910__all__ = ['validate']
     
    2627        check_isseq(cls, 'list_display', cls.list_display)
    2728        for idx, field in enumerate(cls.list_display):
    2829            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):
    3132                        try:
    3233                            opts.get_field(field)
    3334                        except models.FieldDoesNotExist:
     
    3536                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
    3637                    else:
    3738                        # 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)
    3943                        if isinstance(f, models.ManyToManyField):
    4044                            raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported."
    4145                                % (cls.__name__, idx, field))
  • django/contrib/admin/options.py

     
    99from django.contrib import messages
    1010from django.views.decorators.csrf import csrf_protect
    1111from django.core.exceptions import PermissionDenied, ValidationError
     12from django.contrib.admin.util import label_for_field
     13from django.contrib.admin import views
    1214from django.db import models, transaction
    1315from django.db.models.fields import BLANK_CHOICE_DASH
    1416from django.http import Http404, HttpResponse, HttpResponseRedirect
     
    1618from django.utils.decorators import method_decorator
    1719from django.utils.datastructures import SortedDict
    1820from django.utils.functional import update_wrapper
     21from django.utils.encoding import smart_unicode, smart_str
    1922from django.utils.html import escape
    2023from django.utils.safestring import mark_safe
    2124from django.utils.functional import curry
     
    2326from django.utils.translation import ugettext as _
    2427from django.utils.translation import ungettext
    2528from django.utils.encoding import force_unicode
     29import types
    2630
     31
     32class 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
     92class 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
     129class 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
     170class 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
    27177HORIZONTAL, VERTICAL = 1, 2
    28178# returns the <ul> class for a given radio_admin field
    29179get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
     
    233383        if not self.list_display_links:
    234384            for name in self.list_display:
    235385                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]
    237390                    break
    238391        super(ModelAdmin, self).__init__()
    239392
  • django/contrib/admin/util.py

     
    313313
    314314
    315315def display_for_field(value, field):
    316     from django.contrib.admin.templatetags.admin_list import _boolean_icon
     316    from django.contrib.admin.templatetags.admin_list import boolean_icon
    317317    from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    318318
    319319    if field.flatchoices:
     
    321321    # NullBooleanField needs special-case null-handling, so it comes
    322322    # before the general null test.
    323323    elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
    324         return _boolean_icon(value)
     324        return boolean_icon(value)
    325325    elif value is None:
    326326        return EMPTY_CHANGELIST_VALUE
    327327    elif isinstance(field, models.DateField) or isinstance(field, models.TimeField):
  • django/contrib/admin/__init__.py

     
    11from django.contrib.admin.options import ModelAdmin, HORIZONTAL, VERTICAL
    22from django.contrib.admin.options import StackedInline, TabularInline
    33from django.contrib.admin.sites import AdminSite, site
     4from django.contrib.admin.options import ListColumn
    45
    56
    67def autodiscover():
  • django/contrib/admin/helpers.py

     
    160160        })
    161161
    162162    def contents(self):
    163         from django.contrib.admin.templatetags.admin_list import _boolean_icon
     163        from django.contrib.admin.templatetags.admin_list import boolean_icon
    164164        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
    165165        field, obj, model_admin = self.field['field'], self.form.instance, self.model_admin
    166166        try:
     
    171171            if f is None:
    172172                boolean = getattr(attr, "boolean", False)
    173173                if boolean:
    174                     result_repr = _boolean_icon(value)
     174                    result_repr = boolean_icon(value)
    175175                else:
    176176                    result_repr = smart_unicode(value)
    177177                    if getattr(attr, "allow_tags", False):
  • django/contrib/admin/templatetags/admin_list.py

     
    1 import datetime
    2 
    31from django.conf import settings
    4 from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
    5 from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
     2from django.contrib.admin.util import label_for_field
     3from django.contrib.admin.views.main import ALL_VAR
    64from 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
     5from django.contrib.admin.options import ListColumn
    96from django.utils import formats
    107from django.utils.html import escape, conditional_escape
    118from django.utils.safestring import mark_safe
    129from django.utils.text import capfirst
    13 from django.utils.translation import ugettext as _
    14 from django.utils.encoding import smart_unicode, force_unicode
     10from django.utils.encoding import force_unicode
    1511from django.template import Library
     12import datetime
    1613
    1714
    1815register = Library()
     
    3128        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))
    3229paginator_number = register.simple_tag(paginator_number)
    3330
     31
    3432def pagination(cl):
    3533    """
    3634    Generates the series of links to the pages in a paginated list.
     
    7775    }
    7876pagination = register.inclusion_tag('admin/pagination.html')(pagination)
    7977
    80 def result_headers(cl):
     78def result_headers(cl, columns):
    8179    """
    8280    Generates the list column headers.
    8381    """
    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 {
    9590                    "text": header,
    9691                    "class_attrib": mark_safe(' class="action-checkbox-column"')
    9792                }
    98                 continue
     93            continue
    9994
    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
    10598
    106             # So this _is_ a sortable non-field.  Go to the yield
    107             # after the else clause.
    108         else:
    109             admin_order_field = None
    110 
    11199        th_classes = []
    112100        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:
    114102            th_classes.append('sorted %sending' % cl.order_type.lower())
    115103            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
    116104
    117105        yield {
    118             "text": header,
     106            "text": col.header,
    119107            "sortable": True,
    120108            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
    121109            "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
    122110        }
    123111
    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))
     112def 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))
     118boolean_icon = register.filter(boolean_icon)
    127119
    128 def items_for_result(cl, result, form):
     120
     121def items_for_result(cl, result, form, columns):
    129122    """
    130123    Generates the actual list of data.
    131124    """
    132     first = True
    133125    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('&nbsp;')
     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
    166130        # 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:
    168132            table_tag = {True:'th', False:'td'}[first]
    169             first = False
    170133            url = cl.url_for_result(result)
    171134            # Convert the pk to something that can be used in Javascript.
    172135            # Problem cases are long ints (23L) and non-ASCII strings.
     
    176139                attr = pk
    177140            value = result.serializable_value(attr)
    178141            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                })
    181149        else:
    182150            # By default the fields come from ModelAdmin.list_editable, but if we pull
    183151            # the fields out of the form instead of list_editable custom admins
    184152            # 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]
    187155                result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf))
    188156            else:
    189157                result_repr = conditional_escape(result_repr)
     
    191159    if form and not form[cl.model._meta.pk.name].is_hidden:
    192160        yield mark_safe(u'<td>%s</td>' % force_unicode(form[cl.model._meta.pk.name]))
    193161
    194 def results(cl):
     162def results(cl, columns):
    195163    if cl.formset:
    196164        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))
    198166    else:
    199167        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))
    201169
    202170def result_hidden_fields(cl):
    203171    if cl.formset:
     
    209177    """
    210178    Displays the headers and data list together
    211179    """
     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)]
    212186    return {'cl': cl,
    213187            '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))}
    216190result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
    217191
    218192def date_hierarchy(cl):
  • django/contrib/admin/views/main.py

     
    11from django.contrib.admin.filterspecs import FilterSpec
    2 from django.contrib.admin.options import IncorrectLookupParameters
     2from django.contrib.admin.options import IncorrectLookupParameters, ListColumn
    33from django.contrib.admin.util import quote
    44from django.core.paginator import Paginator, InvalidPage
    55from django.db import models
     
    139139        if ORDER_VAR in params:
    140140            try:
    141141                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
    142147                try:
    143148                    f = lookup_opts.get_field(field_name)
    144149                except models.FieldDoesNotExist:
Back to Top