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

File 8054-list-column.3.diff, 21.8 KB (added by alekam, 5 years ago)
  • django/contrib/admin/validation.py

     
    55from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
    66from django.contrib.admin.options import HORIZONTAL, VERTICAL
    77from django.contrib.admin.util import lookup_field
     8from django.contrib.admin.options import ListColumn
    89
    910
    1011__all__ = ['validate']
     
    2728        check_isseq(cls, 'list_display', cls.list_display)
    2829        for idx, field in enumerate(cls.list_display):
    2930            if not callable(field):
    30                 if not hasattr(cls, field):
    31                     if not hasattr(model, field):
     31                if isinstance(field, ListColumn) or not hasattr(cls, field):
     32                    if not isinstance(field, ListColumn) and not hasattr(model, field):
    3233                        try:
    3334                            opts.get_field(field)
    3435                        except models.FieldDoesNotExist:
     
    3637                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
    3738                    else:
    3839                        # getattr(model, field) could be an X_RelatedObjectsDescriptor
    39                         f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field)
     40                        if isinstance(field, ListColumn):
     41                            f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field.field_name)
     42                        else:
     43                            f = fetch_attr(cls, model, opts, "list_display[%d]" % idx, field)
    4044                        if isinstance(f, models.ManyToManyField):
    4145                            raise ImproperlyConfigured("'%s.list_display[%d]', '%s' is a ManyToManyField which is not supported."
    4246                                % (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, lookup_field
     13from django.contrib.admin import views
     14from django.core.exceptions import ObjectDoesNotExist
    1215from django.db import models, transaction
    1316from django.db.models.fields import BLANK_CHOICE_DASH
    1417from django.http import Http404, HttpResponse, HttpResponseRedirect
     
    1619from django.utils.decorators import method_decorator
    1720from django.utils.datastructures import SortedDict
    1821from django.utils.functional import update_wrapper
     22from django.utils.encoding import smart_unicode, smart_str
    1923from django.utils.html import escape
    2024from django.utils.safestring import mark_safe
    2125from django.utils.functional import curry
     
    2428from django.utils.translation import ungettext, ugettext_lazy
    2529from django.utils.encoding import force_unicode
    2630
     31
     32class ListColumn(object):
     33    def __init__(self, field_name, header=None, filter='', load_filters=[], order_field=None, value_map=None):
     34        self.field_name = field_name
     35        self.header = header
     36        self.filter = filter
     37        self.load_filters = load_filters
     38        self.order_field = order_field
     39        self.value_map = value_map
     40        self._nowrap = False
     41
     42    def for_model(self, model, cl=None):
     43        """Create a new ListColumn instance where unset properties in this
     44        ListColumn have been filled in by inspecting the Model provided."""
     45        header, attr = label_for_field(self.field_name, model,
     46                model_admin = cl.model_admin,
     47                return_attr = True
     48            )
     49        try:
     50            f = model._meta.get_field(self.field_name)
     51        except models.FieldDoesNotExist:
     52            return AttributeListColumn(model, field_name=self.field_name,
     53                header=self.header if self.header else header,
     54                filter=self.filter,
     55                load_filters=self.load_filters,
     56                order_field=self.order_field,
     57                attr=attr
     58            )
     59        else:
     60            return FieldListColumn(f, self.field_name,
     61                header=self.header if self.header else header,
     62                filter=self.filter,
     63                load_filters=self.load_filters,
     64                order_field=self.order_field
     65                #attr=attr
     66            )
     67
     68    def get_value_display(self, result, cl=None):
     69        from django.template import Context, Parser, get_library
     70        #field_value = self.get_value(result)
     71        # TODO: i don't know how 2 lines below is working
     72        #if self.value_map:
     73        #    field_value = self.value_map.get(field_value, None)
     74        try:   
     75            f, attr, field_value = lookup_field(self.field_name, result, cl.model_admin)
     76        except (AttributeError, ObjectDoesNotExist):
     77            field_value = views.main.EMPTY_CHANGELIST_VALUE
     78        else:
     79            #if field_value is None:
     80            #    return EMPTY_CHANGELIST_VALUE
     81       
     82            # apply filters by faking a template token
     83            if self.filter:
     84                p = Parser([])
     85                for lib in ['admin_list', ] + self.load_filters:
     86                    p.add_library(get_library(lib))
     87                fe = p.compile_filter('val|' + self.filter)
     88                return fe.resolve(Context({'val': field_value}))
     89
     90        if force_unicode(field_value) == '':
     91            return mark_safe(' ')
     92        return smart_unicode(field_value)
     93
     94
     95class FieldListColumn(ListColumn):
     96    def __init__(self, field, *args, **kwargs):
     97        super(FieldListColumn, self).__init__(*args, **kwargs)
     98        self.field = field
     99       
     100        if not self.header:
     101            self.header = field.verbose_name
     102
     103        #if isinstance(field.rel, models.ManyToOneRel):
     104        #    if field.null:
     105        #        self.order_field = None
     106        if not self.order_field:
     107            self.order_field = self.field_name
     108
     109        if not self.filter:
     110            if isinstance(field, models.DateTimeField):
     111                self.filter = 'admin_datetime_format'
     112            elif isinstance(field, models.DateField):
     113                self.filter = 'admin_date_format'
     114            elif isinstance(field, models.TimeField):
     115                self.filter = 'admin_time_format'
     116            elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
     117                self.filter = 'boolean_icon'
     118            elif isinstance(field, models.DecimalField):
     119                self.filter = 'floatformat:%d' % field.decimal_places
     120
     121        if isinstance(field, models.DateField) or isinstance(field, models.TimeField):
     122            self._nowrap = True   
     123
     124        if not self.value_map and field.choices:
     125            self.value_map = dict(field.choices)
     126
     127    def get_value(self, result):
     128        return getattr(result, self.field_name, None)
     129
     130
     131class AttributeListColumn(ListColumn):
     132    def __init__(self, model, attr=None, *args, **kwargs):
     133        super(AttributeListColumn, self).__init__(*args, **kwargs)
     134       
     135        if attr is not None and not self.filter:
     136            if getattr(attr, 'boolean', False):
     137                self.filter = 'boolean_icon'
     138            elif getattr(attr, 'allow_tags', False):
     139                self.filter = 'safe'
     140
     141        if self.header is None:
     142            if self.field_name == '__unicode__':
     143                self.header = force_unicode(model._meta.verbose_name)
     144            elif self.field_name == '__str__':
     145                self.header = smart_str(model._meta.verbose_name)
     146            elif attr is not None:
     147                try:
     148                    self.header = attr.short_description
     149                except AttributeError:
     150                    pass
     151               
     152            if not self.header:   
     153                self.header = self.field_name.replace('_', ' ')
     154
     155        if attr is not None and not self.order_field:
     156            self.order_field = getattr(attr, "admin_order_field", None)
     157
     158    def get_value(self, result):
     159        attr = getattr(result, self.field_name, None)
     160        if callable(attr):
     161            return attr()
     162        return attr
     163   
     164
    27165HORIZONTAL, VERTICAL = 1, 2
    28166# returns the <ul> class for a given radio_admin field
    29167get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '')
     
    233371        if not self.list_display_links:
    234372            for name in self.list_display:
    235373                if name != 'action_checkbox':
    236                     self.list_display_links = [name]
     374                    if isinstance(name, ListColumn):
     375                        self.list_display_links = [name.field_name]
     376                    else:
     377                        self.list_display_links = [name]
    237378                    break
    238379        super(ModelAdmin, self).__init__()
    239380
  • django/contrib/admin/util.py

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

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

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

     
    1 import datetime
    2 
    31from django.conf import settings
    42from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
    53from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
    64from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
     5from django.contrib.admin.options import ListColumn
    76from django.core.exceptions import ObjectDoesNotExist
    87from django.db import models
    98from django.forms.forms import pretty_name
    10 from django.utils import formats
     9from django.utils import formats, dateformat
    1110from django.utils.html import escape, conditional_escape
    1211from django.utils.safestring import mark_safe
    1312from django.utils.text import capfirst
    14 from django.utils.translation import ugettext as _
     13from django.utils.translation import ugettext as _, get_date_formats
    1514from django.utils.encoding import smart_unicode, force_unicode
    1615from django.template import Library
     16import datetime
    1717
    1818
    1919register = Library()
     
    3232        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))
    3333paginator_number = register.simple_tag(paginator_number)
    3434
     35
    3536def pagination(cl):
    3637    """
    3738    Generates the series of links to the pages in a paginated list.
     
    7879    }
    7980pagination = register.inclusion_tag('admin/pagination.html')(pagination)
    8081
    81 def result_headers(cl):
     82def result_headers(cl, columns):
    8283    """
    8384    Generates the list column headers.
    8485    """
    85     lookup_opts = cl.lookup_opts
    86 
    87     for i, field_name in enumerate(cl.list_display):
    88         header, attr = label_for_field(field_name, cl.model,
    89             model_admin = cl.model_admin,
    90             return_attr = True
    91         )
    92         if attr:
    93             # if the field is the action checkbox: no sorting and special class
    94             if field_name == 'action_checkbox':
    95                 yield {
     86    for i, col in enumerate(columns):
     87        # if the field is the action checkbox: no sorting and special class
     88        if col.field_name == 'action_checkbox':
     89            header, attr = label_for_field(col.field_name, cl.model,
     90                model_admin = cl.model_admin,
     91                return_attr = True
     92            )
     93            yield {
    9694                    "text": header,
    9795                    "class_attrib": mark_safe(' class="action-checkbox-column"')
    9896                }
    99                 continue
     97            continue
    10098
    101             # It is a non-field, but perhaps one that is sortable
    102             admin_order_field = getattr(attr, "admin_order_field", None)
    103             if not admin_order_field:
    104                 yield {"text": header}
    105                 continue
     99        if col.order_field is None:
     100            yield {'text': col.header}
     101            continue
    106102
    107             # So this _is_ a sortable non-field.  Go to the yield
    108             # after the else clause.
    109         else:
    110             admin_order_field = None
    111 
    112103        th_classes = []
    113104        new_order_type = 'asc'
    114         if field_name == cl.order_field or admin_order_field == cl.order_field:
     105        if col.order_field == cl.order_field:
    115106            th_classes.append('sorted %sending' % cl.order_type.lower())
    116107            new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
    117108
    118109        yield {
    119             "text": header,
     110            "text": col.header,
    120111            "sortable": True,
    121112            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
    122113            "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
    123114        }
    124115
    125 def _boolean_icon(field_val):
    126     BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
    127     return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, BOOLEAN_MAPPING[field_val], field_val))
     116def boolean_icon(field_val):
     117    if field_val is None:
     118        v = 'unknown'
     119    else:
     120        v = field_val and 'yes' or 'no'
     121    return mark_safe(u'<img src="%simg/admin/icon-%s.gif" alt="%s" />' % (settings.ADMIN_MEDIA_PREFIX, v, field_val))
     122boolean_icon = register.filter(boolean_icon)
    128123
    129 def items_for_result(cl, result, form):
     124
     125def admin_datetime_format(field_val):
     126    (date_format, datetime_format, time_format) = get_date_formats()
     127    return capfirst(dateformat.format(field_val, datetime_format))
     128admin_datetime_format = register.filter(admin_datetime_format)
     129
     130
     131def admin_date_format(field_val):
     132    (date_format, datetime_format, time_format) = get_date_formats()
     133    return capfirst(dateformat.format(field_val, date_format))
     134admin_date_format = register.filter(admin_date_format)
     135
     136
     137def admin_time_format(field_val):
     138    (date_format, datetime_format, time_format) = get_date_formats()
     139    return capfirst(dateformat.format(field_val, time_format))
     140admin_time_format = register.filter(admin_time_format)
     141
     142
     143def items_for_result(cl, result, form, columns):
    130144    """
    131145    Generates the actual list of data.
    132146    """
    133     first = True
    134147    pk = cl.lookup_opts.pk.attname
    135     for field_name in cl.list_display:
    136         row_class = ''
    137         try:
    138             f, attr, value = lookup_field(field_name, result, cl.model_admin)
    139         except (AttributeError, ObjectDoesNotExist):
    140             result_repr = EMPTY_CHANGELIST_VALUE
    141         else:
    142             if f is None:
    143                 allow_tags = getattr(attr, 'allow_tags', False)
    144                 boolean = getattr(attr, 'boolean', False)
    145                 if boolean:
    146                     allow_tags = True
    147                     result_repr = _boolean_icon(value)
    148                 else:
    149                     result_repr = smart_unicode(value)
    150                 # Strip HTML tags in the resulting text, except if the
    151                 # function has an "allow_tags" attribute set to True.
    152                 if not allow_tags:
    153                     result_repr = escape(result_repr)
    154                 else:
    155                     result_repr = mark_safe(result_repr)
    156             else:
    157                 if value is None:
    158                     result_repr = EMPTY_CHANGELIST_VALUE
    159                 if isinstance(f.rel, models.ManyToOneRel):
    160                     result_repr = escape(getattr(result, f.name))
    161                 else:
    162                     result_repr = display_for_field(value, f)
    163                 if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
    164                     row_class = ' class="nowrap"'
    165         if force_unicode(result_repr) == '':
    166             result_repr = mark_safe('&nbsp;')
     148    for i, col in enumerate(columns):
     149        result_repr = col.get_value_display(result, cl)
     150        row_class = col._nowrap and ' class="nowrap"' or ''
     151        first = (i == 0)
    167152        # If list_display_links not defined, add the link tag to the first field
    168         if (first and not cl.list_display_links) or field_name in cl.list_display_links:
     153        if (first and not cl.list_display_links) or col.field_name in cl.list_display_links:
    169154            table_tag = {True:'th', False:'td'}[first]
    170             first = False
    171155            url = cl.url_for_result(result)
    172156            # Convert the pk to something that can be used in Javascript.
    173157            # Problem cases are long ints (23L) and non-ASCII strings.
     
    183167            # By default the fields come from ModelAdmin.list_editable, but if we pull
    184168            # the fields out of the form instead of list_editable custom admins
    185169            # can provide fields on a per request basis
    186             if form and field_name in form.fields:
    187                 bf = form[field_name]
     170            if form and col.field_name in form.fields:
     171                bf = form[col.field_name]
    188172                result_repr = mark_safe(force_unicode(bf.errors) + force_unicode(bf))
    189173            else:
    190174                result_repr = conditional_escape(result_repr)
     
    192176    if form and not form[cl.model._meta.pk.name].is_hidden:
    193177        yield mark_safe(u'<td>%s</td>' % force_unicode(form[cl.model._meta.pk.name]))
    194178
    195 def results(cl):
     179def results(cl, columns):
    196180    if cl.formset:
    197181        for res, form in zip(cl.result_list, cl.formset.forms):
    198             yield list(items_for_result(cl, res, form))
     182            yield list(items_for_result(cl, res, form, columns))
    199183    else:
    200184        for res in cl.result_list:
    201             yield list(items_for_result(cl, res, None))
     185            yield list(items_for_result(cl, res, None, columns))
    202186
    203187def result_hidden_fields(cl):
    204188    if cl.formset:
     
    210194    """
    211195    Displays the headers and data list together
    212196    """
     197    cols = []
     198    for col in cl.list_display:
     199        if isinstance(col, ListColumn):
     200            cols += [col.for_model(cl.model, cl)]
     201        else:
     202            cols += [ListColumn(col).for_model(cl.model, cl)]
    213203    return {'cl': cl,
    214204            'result_hidden_fields': list(result_hidden_fields(cl)),
    215             'result_headers': list(result_headers(cl)),
    216             'results': list(results(cl))}
     205            'result_headers': list(result_headers(cl, cols)),
     206            'results': list(results(cl, cols))}
    217207result_list = register.inclusion_tag("admin/change_list_results.html")(result_list)
    218208
    219209def 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
     
    140140        if ORDER_VAR in params:
    141141            try:
    142142                field_name = self.list_display[int(params[ORDER_VAR])]
     143                field = self.list_display[int(params[ORDER_VAR])]
     144                if isinstance(field, ListColumn):
     145                    field_name = field.field_name
     146                else:
     147                    field_name = field
    143148                try:
    144149                    f = lookup_opts.get_field(field_name)
    145150                except models.FieldDoesNotExist:
Back to Top