| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661 |
- from __future__ import absolute_import
- from collections import OrderedDict
- from django.core.exceptions import PermissionDenied, ObjectDoesNotExist
- from django.core.paginator import InvalidPage, Paginator
- from django.urls.base import NoReverseMatch
- from django.db import models
- from django.http import HttpResponseRedirect
- from django.template.response import SimpleTemplateResponse, TemplateResponse
- from django.utils import six
- from django.utils.encoding import force_text, smart_text
- from django.utils.html import escape, conditional_escape
- from django.utils.safestring import mark_safe
- from django.utils.text import capfirst
- from django.utils.translation import ugettext as _
- from xadmin.util import lookup_field, display_for_field, label_for_field, boolean_icon
- from .base import ModelAdminView, filter_hook, inclusion_tag, csrf_protect_m
- # List settings
- ALL_VAR = 'all'
- ORDER_VAR = 'o'
- PAGE_VAR = 'p'
- TO_FIELD_VAR = 't'
- COL_LIST_VAR = '_cols'
- ERROR_FLAG = 'e'
- DOT = '.'
- # Text to display within change-list table cells if the value is blank.
- EMPTY_CHANGELIST_VALUE = _('Null')
- class FakeMethodField(object):
- """
- This class used when a column is an model function, wrap function as a fake field to display in select columns.
- """
- def __init__(self, name, verbose_name):
- # Initial comm field attrs
- self.name = name
- self.verbose_name = verbose_name
- self.primary_key = False
- class ResultRow(dict):
- pass
- class ResultItem(object):
- def __init__(self, field_name, row):
- self.classes = []
- self.text = ' '
- self.wraps = []
- self.tag = 'td'
- self.tag_attrs = []
- self.allow_tags = False
- self.btns = []
- self.menus = []
- self.is_display_link = False
- self.row = row
- self.field_name = field_name
- self.field = None
- self.attr = None
- self.value = None
- @property
- def label(self):
- text = mark_safe(
- self.text) if self.allow_tags else conditional_escape(self.text)
- if force_text(text) == '':
- text = mark_safe(' ')
- for wrap in self.wraps:
- text = mark_safe(wrap % text)
- return text
- @property
- def tagattrs(self):
- return mark_safe(
- '%s%s' % ((self.tag_attrs and ' '.join(self.tag_attrs) or ''),
- (self.classes and (' class="%s"' % ' '.join(self.classes)) or '')))
- class ResultHeader(ResultItem):
- def __init__(self, field_name, row):
- super(ResultHeader, self).__init__(field_name, row)
- self.tag = 'th'
- self.tag_attrs = ['scope="col"']
- self.sortable = False
- self.allow_tags = True
- self.sorted = False
- self.ascending = None
- self.sort_priority = None
- self.url_primary = None
- self.url_remove = None
- self.url_toggle = None
- class ListAdminView(ModelAdminView):
- """
- Display models objects view. this class has ordering and simple filter features.
- """
- list_display = ('__str__',)
- list_display_links = ()
- list_display_links_details = False
- list_select_related = None
- list_per_page = 50
- list_max_show_all = 200
- list_exclude = ()
- search_fields = ()
- paginator_class = Paginator
- ordering = None
- # Change list templates
- object_list_template = None
- def init_request(self, *args, **kwargs):
- if not self.has_view_permission():
- raise PermissionDenied
- request = self.request
- request.session['LIST_QUERY'] = (self.model_info, self.request.META['QUERY_STRING'])
- self.pk_attname = self.opts.pk.attname
- self.lookup_opts = self.opts
- self.list_display = self.get_list_display()
- self.list_display_links = self.get_list_display_links()
- # Get page number parameters from the query string.
- try:
- self.page_num = int(request.GET.get(PAGE_VAR, 0))
- except ValueError:
- self.page_num = 0
- # Get params from request
- self.show_all = ALL_VAR in request.GET
- self.to_field = request.GET.get(TO_FIELD_VAR)
- self.params = dict(request.GET.items())
- if PAGE_VAR in self.params:
- del self.params[PAGE_VAR]
- if ERROR_FLAG in self.params:
- del self.params[ERROR_FLAG]
- @filter_hook
- def get_list_display(self):
- """
- Return a sequence containing the fields to be displayed on the list.
- """
- self.base_list_display = (COL_LIST_VAR in self.request.GET and self.request.GET[COL_LIST_VAR] != "" and
- self.request.GET[COL_LIST_VAR].split('.')) or self.list_display
- return list(self.base_list_display)
- @filter_hook
- def get_list_display_links(self):
- """
- Return a sequence containing the fields to be displayed as links
- on the changelist. The list_display parameter is the list of fields
- returned by get_list_display().
- """
- if self.list_display_links or not self.list_display:
- return self.list_display_links
- else:
- # Use only the first item in list_display as link
- return list(self.list_display)[:1]
- def make_result_list(self):
- # Get search parameters from the query string.
- self.list_queryset = self.get_list_queryset()
- self.ordering_field_columns = self.get_ordering_field_columns()
- self.paginator = self.get_paginator()
- # Get the number of objects, with admin filters applied.
- self.result_count = self.paginator.count
- self.can_show_all = self.result_count <= self.list_max_show_all
- self.multi_page = self.result_count > self.list_per_page
- # Get the list of objects to display on this page.
- if (self.show_all and self.can_show_all) or not self.multi_page:
- self.result_list = self.list_queryset._clone()
- else:
- try:
- self.result_list = self.paginator.page(
- self.page_num + 1).object_list
- except InvalidPage:
- if ERROR_FLAG in self.request.GET.keys():
- return SimpleTemplateResponse('xadmin/views/invalid_setup.html', {
- 'title': _('Database error'),
- })
- return HttpResponseRedirect(self.request.path + '?' + ERROR_FLAG + '=1')
- self.has_more = self.result_count > (
- self.list_per_page * self.page_num + len(self.result_list))
- @filter_hook
- def get_result_list(self):
- return self.make_result_list()
- @filter_hook
- def post_result_list(self):
- return self.make_result_list()
- @filter_hook
- def get_list_queryset(self):
- """
- Get model queryset. The query has been filted and ordered.
- """
- # First, get queryset from base class.
- queryset = self.queryset()
- # Use select_related() if one of the list_display options is a field
- # with a relationship and the provided queryset doesn't already have
- # select_related defined.
- if not queryset.query.select_related:
- if self.list_select_related:
- queryset = queryset.select_related()
- elif self.list_select_related is None:
- related_fields = []
- for field_name in self.list_display:
- try:
- field = self.opts.get_field(field_name)
- except models.FieldDoesNotExist:
- pass
- else:
- if isinstance(field.remote_field, models.ManyToOneRel):
- related_fields.append(field_name)
- if related_fields:
- queryset = queryset.select_related(*related_fields)
- else:
- pass
- # Then, set queryset ordering.
- queryset = queryset.order_by(*self.get_ordering())
- # Return the queryset.
- return queryset
- # List ordering
- def _get_default_ordering(self):
- ordering = []
- if self.ordering:
- ordering = self.ordering
- elif self.opts.ordering:
- ordering = self.opts.ordering
- return ordering
- @filter_hook
- def get_ordering_field(self, field_name):
- """
- Returns the proper model field name corresponding to the given
- field_name to use for ordering. field_name may either be the name of a
- proper model field or the name of a method (on the admin or model) or a
- callable with the 'admin_order_field' attribute. Returns None if no
- proper model field name can be matched.
- """
- try:
- field = self.opts.get_field(field_name)
- return field.name
- except models.FieldDoesNotExist:
- # See whether field_name is a name of a non-field
- # that allows sorting.
- if callable(field_name):
- attr = field_name
- elif hasattr(self, field_name):
- attr = getattr(self, field_name)
- else:
- attr = getattr(self.model, field_name)
- return getattr(attr, 'admin_order_field', None)
- @filter_hook
- def get_ordering(self):
- """
- Returns the list of ordering fields for the change list.
- First we check the get_ordering() method in model admin, then we check
- the object's default ordering. Then, any manually-specified ordering
- from the query string overrides anything. Finally, a deterministic
- order is guaranteed by ensuring the primary key is used as the last
- ordering field.
- """
- ordering = list(super(ListAdminView, self).get_ordering()
- or self._get_default_ordering())
- if ORDER_VAR in self.params and self.params[ORDER_VAR]:
- # Clear ordering and used params
- ordering = [
- pfx + self.get_ordering_field(field_name)
- for n, pfx, field_name in map(
- lambda p: p.rpartition('-'),
- self.params[ORDER_VAR].split('.')
- )
- if self.get_ordering_field(field_name)
- ]
- # Ensure that the primary key is systematically present in the list of
- # ordering fields so we can guarantee a deterministic order across all
- # database backends.
- pk_name = self.opts.pk.name
- if not (set(ordering) & set(['pk', '-pk', pk_name, '-' + pk_name])):
- # The two sets do not intersect, meaning the pk isn't present. So
- # we add it.
- ordering.append('-pk')
- return ordering
- @filter_hook
- def get_ordering_field_columns(self):
- """
- Returns a OrderedDict of ordering field column numbers and asc/desc
- """
- # We must cope with more than one column having the same underlying sort
- # field, so we base things on column numbers.
- ordering = self._get_default_ordering()
- ordering_fields = OrderedDict()
- if ORDER_VAR not in self.params or not self.params[ORDER_VAR]:
- # for ordering specified on ModelAdmin or model Meta, we don't know
- # the right column numbers absolutely, because there might be more
- # than one column associated with that ordering, so we guess.
- for field in ordering:
- if field.startswith('-'):
- field = field[1:]
- order_type = 'desc'
- else:
- order_type = 'asc'
- for attr in self.list_display:
- if self.get_ordering_field(attr) == field:
- ordering_fields[field] = order_type
- break
- else:
- for p in self.params[ORDER_VAR].split('.'):
- none, pfx, field_name = p.rpartition('-')
- ordering_fields[field_name] = 'desc' if pfx == '-' else 'asc'
- return ordering_fields
- def get_check_field_url(self, f):
- """
- Return the select column menu items link.
- We must use base_list_display, because list_display maybe changed by plugins.
- """
- fields = [fd for fd in self.base_list_display if fd != f.name]
- if len(self.base_list_display) == len(fields):
- if f.primary_key:
- fields.insert(0, f.name)
- else:
- fields.append(f.name)
- return self.get_query_string({COL_LIST_VAR: '.'.join(fields)})
- def get_model_method_fields(self):
- """
- Return the fields info defined in model. use FakeMethodField class wrap method as a db field.
- """
- methods = []
- for name in dir(self):
- try:
- if getattr(getattr(self, name), 'is_column', False):
- methods.append((name, getattr(self, name)))
- except:
- pass
- return [FakeMethodField(name, getattr(method, 'short_description', capfirst(name.replace('_', ' '))))
- for name, method in methods]
- @filter_hook
- def get_context(self):
- """
- Prepare the context for templates.
- """
- self.title = _('%s List') % force_text(self.opts.verbose_name)
- model_fields = [(f, f.name in self.list_display, self.get_check_field_url(f))
- for f in (list(self.opts.fields) + self.get_model_method_fields()) if f.name not in self.list_exclude]
- new_context = {
- 'model_name': force_text(self.opts.verbose_name_plural),
- 'title': self.title,
- 'cl': self,
- 'model_fields': model_fields,
- 'clean_select_field_url': self.get_query_string(remove=[COL_LIST_VAR]),
- 'has_add_permission': self.has_add_permission(),
- 'app_label': self.app_label,
- 'brand_name': self.opts.verbose_name_plural,
- 'brand_icon': self.get_model_icon(self.model),
- 'add_url': self.model_admin_url('add'),
- 'result_headers': self.result_headers(),
- 'results': self.results()
- }
- context = super(ListAdminView, self).get_context()
- context.update(new_context)
- return context
- @filter_hook
- def get_response(self, context, *args, **kwargs):
- pass
- @csrf_protect_m
- @filter_hook
- def get(self, request, *args, **kwargs):
- """
- The 'change list' admin view for this model.
- """
- response = self.get_result_list()
- if response:
- return response
- context = self.get_context()
- context.update(kwargs or {})
- response = self.get_response(context, *args, **kwargs)
- return response or TemplateResponse(request, self.object_list_template or
- self.get_template_list('views/model_list.html'), context)
- @filter_hook
- def post_response(self, *args, **kwargs):
- pass
- @csrf_protect_m
- @filter_hook
- def post(self, request, *args, **kwargs):
- return self.post_result_list() or self.post_response(*args, **kwargs) or self.get(request, *args, **kwargs)
- @filter_hook
- def get_paginator(self):
- return self.paginator_class(self.list_queryset, self.list_per_page, 0, True)
- @filter_hook
- def get_page_number(self, i):
- if i == DOT:
- return mark_safe(u'<span class="dot-page">...</span> ')
- elif i == self.page_num:
- return mark_safe(u'<span class="this-page">%d</span> ' % (i + 1))
- else:
- return mark_safe(u'<a href="%s"%s>%d</a> ' % (escape(self.get_query_string({PAGE_VAR: i})), (i == self.paginator.num_pages - 1 and ' class="end"' or ''), i + 1))
- # Result List methods
- @filter_hook
- def result_header(self, field_name, row):
- ordering_field_columns = self.ordering_field_columns
- item = ResultHeader(field_name, row)
- text, attr = label_for_field(field_name, self.model,
- model_admin=self,
- return_attr=True
- )
- item.text = text
- item.attr = attr
- if attr and not getattr(attr, "admin_order_field", None):
- return item
- # OK, it is sortable if we got this far
- th_classes = ['sortable']
- order_type = ''
- new_order_type = 'desc'
- sort_priority = 0
- sorted = False
- # Is it currently being sorted on?
- if field_name in ordering_field_columns:
- sorted = True
- order_type = ordering_field_columns.get(field_name).lower()
- arr = ordering_field_columns.keys()
- if six.PY3:
- arr = list(arr)
- sort_priority = arr.index(field_name) + 1
- th_classes.append('sorted %sending' % order_type)
- new_order_type = {'asc': 'desc', 'desc': 'asc'}[order_type]
- # build new ordering param
- o_list_asc = [] # URL for making this field the primary sort
- o_list_desc = [] # URL for making this field the primary sort
- o_list_remove = [] # URL for removing this field from sort
- o_list_toggle = [] # URL for toggling order type for this field
- make_qs_param = lambda t, n: ('-' if t == 'desc' else '') + str(n)
- for j, ot in ordering_field_columns.items():
- if j == field_name: # Same column
- param = make_qs_param(new_order_type, j)
- # We want clicking on this header to bring the ordering to the
- # front
- o_list_asc.insert(0, j)
- o_list_desc.insert(0, '-' + j)
- o_list_toggle.append(param)
- # o_list_remove - omit
- else:
- param = make_qs_param(ot, j)
- o_list_asc.append(param)
- o_list_desc.append(param)
- o_list_toggle.append(param)
- o_list_remove.append(param)
- if field_name not in ordering_field_columns:
- o_list_asc.insert(0, field_name)
- o_list_desc.insert(0, '-' + field_name)
- item.sorted = sorted
- item.sortable = True
- item.ascending = (order_type == "asc")
- item.sort_priority = sort_priority
- menus = [
- ('asc', o_list_asc, 'caret-up', _(u'Sort ASC')),
- ('desc', o_list_desc, 'caret-down', _(u'Sort DESC')),
- ]
- if sorted:
- row['num_sorted_fields'] = row['num_sorted_fields'] + 1
- menus.append((None, o_list_remove, 'times', _(u'Cancel Sort')))
- item.btns.append('<a class="toggle" href="%s"><i class="fa fa-%s"></i></a>' % (
- self.get_query_string({ORDER_VAR: '.'.join(o_list_toggle)}), 'sort-up' if order_type == "asc" else 'sort-down'))
- item.menus.extend(['<li%s><a href="%s" class="active"><i class="fa fa-%s"></i> %s</a></li>' %
- (
- (' class="active"' if sorted and order_type == i[
- 0] else ''),
- self.get_query_string({ORDER_VAR: '.'.join(i[1])}), i[2], i[3]) for i in menus])
- item.classes.extend(th_classes)
- return item
- @filter_hook
- def result_headers(self):
- """
- Generates the list column headers.
- """
- row = ResultRow()
- row['num_sorted_fields'] = 0
- row.cells = [self.result_header(
- field_name, row) for field_name in self.list_display]
- return row
- @filter_hook
- def result_item(self, obj, field_name, row):
- """
- Generates the actual list of data.
- """
- item = ResultItem(field_name, row)
- try:
- f, attr, value = lookup_field(field_name, obj, self)
- except (AttributeError, ObjectDoesNotExist, NoReverseMatch):
- item.text = mark_safe("<span class='text-muted'>%s</span>" % EMPTY_CHANGELIST_VALUE)
- else:
- if f is None:
- item.allow_tags = getattr(attr, 'allow_tags', False)
- boolean = getattr(attr, 'boolean', False)
- if boolean:
- item.allow_tags = True
- item.text = boolean_icon(value)
- else:
- item.text = smart_text(value)
- else:
- if isinstance(f.remote_field, models.ManyToOneRel):
- field_val = getattr(obj, f.name)
- if field_val is None:
- item.text = mark_safe("<span class='text-muted'>%s</span>" % EMPTY_CHANGELIST_VALUE)
- else:
- item.text = field_val
- else:
- item.text = display_for_field(value, f)
- if isinstance(f, models.DateField)\
- or isinstance(f, models.TimeField)\
- or isinstance(f, models.ForeignKey):
- item.classes.append('nowrap')
- item.field = f
- item.attr = attr
- item.value = value
- # If list_display_links not defined, add the link tag to the first field
- if (item.row['is_display_first'] and not self.list_display_links) \
- or field_name in self.list_display_links:
- item.row['is_display_first'] = False
- item.is_display_link = True
- if self.list_display_links_details:
- item_res_uri = self.model_admin_url("detail", getattr(obj, self.pk_attname))
- if item_res_uri:
- if self.has_change_permission(obj):
- edit_url = self.model_admin_url("change", getattr(obj, self.pk_attname))
- else:
- edit_url = ""
- item.wraps.append('<a data-res-uri="%s" data-edit-uri="%s" class="details-handler" rel="tooltip" title="%s">%%s</a>'
- % (item_res_uri, edit_url, _(u'Details of %s') % str(obj)))
- else:
- url = self.url_for_result(obj)
- item.wraps.append(u'<a href="%s">%%s</a>' % url)
- return item
- @filter_hook
- def result_row(self, obj):
- row = ResultRow()
- row['is_display_first'] = True
- row['object'] = obj
- row.cells = [self.result_item(
- obj, field_name, row) for field_name in self.list_display]
- return row
- @filter_hook
- def results(self):
- results = []
- for obj in self.result_list:
- results.append(self.result_row(obj))
- return results
- @filter_hook
- def url_for_result(self, result):
- return self.get_object_url(result)
- # Media
- @filter_hook
- def get_media(self):
- media = super(ListAdminView, self).get_media() + self.vendor('xadmin.page.list.js', 'xadmin.page.form.js')
- if self.list_display_links_details:
- media += self.vendor('xadmin.plugin.details.js', 'xadmin.form.css')
- return media
- # Blocks
- @inclusion_tag('xadmin/includes/pagination.html')
- def block_pagination(self, context, nodes, page_type='normal'):
- """
- Generates the series of links to the pages in a paginated list.
- """
- paginator, page_num = self.paginator, self.page_num
- pagination_required = (
- not self.show_all or not self.can_show_all) and self.multi_page
- if not pagination_required:
- page_range = []
- else:
- ON_EACH_SIDE = {'normal': 5, 'small': 3}.get(page_type, 3)
- ON_ENDS = 2
- # If there are 10 or fewer pages, display links to every page.
- # Otherwise, do some fancy
- if paginator.num_pages <= 10:
- page_range = range(paginator.num_pages)
- else:
- # Insert "smart" pagination links, so that there are always ON_ENDS
- # links at either end of the list of pages, and there are always
- # ON_EACH_SIDE links at either end of the "current page" link.
- page_range = []
- if page_num > (ON_EACH_SIDE + ON_ENDS):
- page_range.extend(range(0, ON_EACH_SIDE - 1))
- page_range.append(DOT)
- page_range.extend(
- range(page_num - ON_EACH_SIDE, page_num + 1))
- else:
- page_range.extend(range(0, page_num + 1))
- if page_num < (paginator.num_pages - ON_EACH_SIDE - ON_ENDS - 1):
- page_range.extend(
- range(page_num + 1, page_num + ON_EACH_SIDE + 1))
- page_range.append(DOT)
- page_range.extend(range(
- paginator.num_pages - ON_ENDS, paginator.num_pages))
- else:
- page_range.extend(range(page_num + 1, paginator.num_pages))
- need_show_all_link = self.can_show_all and not self.show_all and self.multi_page
- return {
- 'cl': self,
- 'pagination_required': pagination_required,
- 'show_all_url': need_show_all_link and self.get_query_string({ALL_VAR: ''}),
- 'page_range': map(self.get_page_number, page_range),
- 'ALL_VAR': ALL_VAR,
- '1': 1,
- }
|