# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals
#from django.utils import six

import datetime
import json
from django.views.generic import View
from django.http.response import HttpResponse, HttpResponseBadRequest
from django.core.paginator import Paginator
from django.core.serializers.json import DjangoJSONEncoder
from django.db import models
from django.db.models import Q
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt
from django.utils.decorators import method_decorator
from django.template.loader import render_to_string
from django.http import JsonResponse
from django.utils.safestring import mark_safe
from django.template.loader import render_to_string
from django.template import TemplateDoesNotExist
from django.template import loader, Context
from django.utils.translation import ugettext_lazy as _

from .columns import Column
from .columns import ForeignColumn
from .columns import ColumnLink
from .columns import PlaceholderColumnLink
from .columns import Order
from .exceptions import ColumnOrderError
from .utils import prettyprint_queryset
from .utils import trace
from .utils import format_datetime
from .filters import build_column_filter
from .app_settings import MAX_COLUMNS
from .app_settings import ENABLE_QUERYSET_TRACING
from .app_settings import ENABLE_QUERYDICT_TRACING
from .app_settings import TEST_FILTERS
from .app_settings import DISABLE_QUERYSET_OPTIMIZATION


class DatatablesView(View):

    # Either override in derived class, or override self.get_column_defs()
    column_defs = []

    model = None
    template_name = 'datatables_view/datatable.html'
    initial_order = [[1, "asc"]]
    length_menu = [[10, 20, 50, 100], [10, 20, 50, 100]]
    table_row_id_prefix = 'row-'
    table_row_id_fieldname = 'id'

    # Set with self.initialize()
    column_specs = []  # used to keep column ording as required
    column_index = {}  # used to speedup lookups
    #column_objs_lut = {}

    #model_columns = {}
    latest_by = None
    show_date_filters = None
    show_column_filters = None

    disable_queryset_optimization = False

    def initialize(self, request):

        # Retrieve and normalize latest_by fieldname
        latest_by = self.get_latest_by(request)
        if latest_by is None:
            latest_by = getattr(self.model._meta, 'get_latest_by', None)
        if isinstance(latest_by, (list, tuple)):
            latest_by = latest_by[0] if len(latest_by) > 0 else ''
        if latest_by:
            if latest_by.startswith('-'):
                latest_by = latest_by[1:]
        self.latest_by = latest_by

        # Grab column defs and initialize self.column_specs
        column_defs_ex = self.get_column_defs(request)
        self.column_specs = []
        for c in column_defs_ex:

            column = {
                'name': '',
                'data': None,
                'title': '',
                'searchable': False,
                'orderable': False,
                'visible': True,
                'foreign_field': None,
                'placeholder': False,
                'className': None,
                'defaultContent': None,
                'width': None,
                'choices': None,
                'initialSearchValue': None,
                'autofilter': False,
                'boolean': False,
                'max_length': 0,
            }

            #valid_keys = [key for key in column.keys()][:]
            #valid_keys = column.keys().copy()
            valid_keys = list(column.keys())

            column.update(c)

            # We now accept a collable as "initialSearchValue"
            if callable(column['initialSearchValue']):
                column['initialSearchValue'] = column['initialSearchValue']()

            # TODO: do we really want to accept an empty column name ?
            # Investigate !
            if c['name']:

                # Detect unexpected keys
                for key in c.keys():
                    if not key in valid_keys:
                        raise Exception('Unexpected key "%s" for column "%s"' % (key, c['name']))

                if 'title' in c:
                    title = c['title']
                else:
                    try:
                        title = self.model._meta.get_field(c['name']).verbose_name.title()
                    except:
                        title = c['name']

                column['name'] = c['name']
                column['data'] = c['name']
                #column['title'] = c.get('title') if 'title' in c else self.model._meta.get_field(c['name']).verbose_name.title()
                column['title'] = title
                column['searchable'] = c.get('searchable', column['visible'])
                column['orderable'] = c.get('orderable', column['visible'])

            self.column_specs.append(column)

        # # build LUT for column objects
        # #
        # #    self.column_objs_lut = {
        # #        'id': <datatables_view.columns.Column object at 0x109d82850>,
        # #        'code': <datatables_view.columns.Column object at 0x109d82f10>,
        # #        ...
        # #
        # self.column_objs_lut = Column.collect_model_columns(
        #     self.model,
        #     self.column_specs
        # )

        # For each table column, we build either a Columns or ForeignColumns as required;
        # both "column spec" dictionary and the column object are saved in "column_index"
        # to speed up later lookups;
        # Finally, we elaborate "choices" list

        self.column_index = {}
        for cs in self.column_specs:

            key = cs['name']
            column = Column.column_factory(self.model, cs)
            choices = []

            #
            # Adjust choices
            # we do this here since the model field itself is finally available
            #

            # (1) None (default) or False: no choices (use text input box)
            if cs['choices'] == False:
                # Do not use choices
                cs['choices'] = None
            # (2) True: use Model's field choices;
            #     - failing that, we might use "autofilter"; that is: collect the list of distinct values from db table
            #     - BooleanFields deserve a special treatement
            elif cs['choices'] == True:

                # For boolean fields, provide (None)/Yes/No choice sequence
                if isinstance(column.model_field, models.BooleanField):
                    if column.model_field.null:
                        # UNTESTED !
                        choices = [(None, ''), ]
                    else:
                        choices = []
                    choices += [(True, _('Yes')), (False, _('No'))]
                elif cs['boolean']:
                    choices += [(True, _('Yes')), (False, _('No'))]
                else:
                    # Otherwise, retrieve field's choices, if any ...
                    choices = getattr(column.model_field, 'choices', None)
                    if choices is None:
                        choices = []
                    else:
                        #
                        # Here, we could abbreviate select's options as well;
                        # however, the caller can easily apply a 'width' css attribute to the select tag, instead
                        #
                        # max_length = cs['max_length']
                        # if max_length <= 0:
                        #     choices = [(c[0], c[1]) for c in choices]
                        # else:
                        #     choices = [(c[0], self.clip_value(c[1], max_length, False)) for c in choices]
                        #
                        choices = choices[:]

                # ... or collect distict values if 'autofilter' has been enabled
                if len(choices) <= 0 and cs['autofilter']:
                    choices = self.list_autofilter_choices(request, cs, column.model_field, cs['initialSearchValue'])
                cs['choices'] = choices if len(choices) > 0 else None
            # (3) Otherwise, just use the sequence of choices that has been supplied.


            self.column_index[key] = {
                'spec': cs,
                'column': column,
            }

        # Initialize "show_date_filters"
        show_date_filters = self.get_show_date_filters(request)
        if show_date_filters is None:
            show_date_filters = bool(self.latest_by)
        self.show_date_filters = show_date_filters

        # If global date filter is visible,
        # add class 'get_latest_by' to the column used for global date filtering
        if self.show_date_filters and self.latest_by:
            column_def = self.column_spec_by_name(self.latest_by)
            if column_def:
                if column_def['className']:
                    column_def['className'] += 'latest_by'
                else:
                    column_def['className'] = 'latest_by'

        # Initialize "show_column_filters"
        show_column_filters = self.get_show_column_filters(request)
        if show_column_filters is None:
            # By default we show the column filters if there is at least
            # one searchable and visible column
            num_searchable_columns = len([c for c in self.column_specs if c.get('searchable') and c.get('visible')])
            show_column_filters = (num_searchable_columns > 0)
        self.show_column_filters = show_column_filters

        if ENABLE_QUERYDICT_TRACING:
            trace(self.column_specs, prompt='column_specs')

    def get_column_defs(self, request):
        """
        Override to customize based of request
        """
        return self.column_defs

    def get_initial_order(self, request):
        """
        Override to customize based of request
        """
        return self.initial_order

    def get_length_menu(self, request):
        """
        Override to customize based of request
        """
        return self.length_menu

    def get_template_name(self, request):
        """
        Override to customize based of request
        """
        return self.template_name

    def get_latest_by(self, request):
        """
        Override to customize based of request.

        Provides the name of the column to be used for global date range filtering.
        Return either '', a fieldname or None.

        When None is returned, in model's Meta 'get_latest_by' attributed will be used.
        """
        return self.latest_by

    def get_show_date_filters(self, request):
        """
        Override to customize based of request.

        Defines whether to use the global date range filter.
        Return either True, False or None.

        When None is returned, will'll check whether 'latest_by' is defined
        """
        return self.show_date_filters

    def get_show_column_filters(self, request):
        """
        Override to customize based of request.

        Defines whether to use the column filters.
        Return either True, False or None.

        When None is returned, check if at least one visible column in searchable.
        """
        return self.show_column_filters

    def column_obj(self, name):
        """
        Lookup columnObj for the column_spec identified by 'name'
        """
        # assert name in self.column_objs_lut
        # return self.column_objs_lut[name]
        assert name in self.column_index
        return self.column_index[name]['column']

    def column_spec_by_name(self, name):
        """
        Lookup the column_spec identified by 'name'
        """
        if name in self.column_index:
            return self.column_index[name]['spec']
        return None

    def fix_initial_order(self, initial_order):
        """
        "initial_order" is a list of (position, direction) tuples; for example:
            [[1, 'asc'], [5, 'desc']]

        Here, we also accept positions expressed as column names,
        and convert the to the corresponding numeric position.
        """
        values = []
        keys = list(self.column_index.keys())
        for position, direction in initial_order:
            if type(position) == str:
                position = keys.index(position)
            values.append([position, direction])
        return values

    #@method_decorator(csrf_exempt)
    def dispatch(self, request, *args, **kwargs):

        if not getattr(request, 'REQUEST', None):
            request.REQUEST = request.GET if request.method=='GET' else request.POST

        self.initialize(request)
        if request.is_ajax():
            action = request.REQUEST.get('action', '')
            if action == 'initialize':

                # Sanity check for initial order
                initial_order = self.get_initial_order(request)
                initial_order = self.fix_initial_order(initial_order)

                initial_order_columns = [row[0] for row in initial_order]
                for col in initial_order_columns:
                    if col >= len(self.column_specs):
                        raise Exception('Initial order column %d does not exists' % col)
                    elif not self.column_specs[col]['orderable']:
                        raise Exception('Column %d is not orderable' % col)

                # Initial values for column filters, when supplied
                # See: https://datatables.net/reference/option/searchCols
                searchCols = [
                    {'search': cs['initialSearchValue'], }
                    for cs in self.column_specs
                ]

                return JsonResponse({
                    'columns': self.column_specs,
                    'searchCols': searchCols,
                    'order': initial_order,
                    'length_menu': self.get_length_menu(request),
                    'show_date_filters': self.show_date_filters,
                    'show_column_filters': self.show_column_filters,
                })
            elif action == 'details':
                #row_id = request.REQUEST.get('id')
                row_id = request.REQUEST.get(self.table_row_id_fieldname)
                return JsonResponse({
                    'html': self.render_row_details(row_id, request),
                    'parent-row-id': row_id,
                })

            response = super(DatatablesView, self).dispatch(request, *args, **kwargs)
        else:
            assert False
            #response = HttpResponse(self.render_table(request))
        return response

    def get_model_admin(self):
        from django.contrib import admin
        if self.model in admin.site._registry:
            return admin.site._registry[self.model]
        return None

    def render_row_details(self, id, request=None):

        obj = self.model.objects.get(id=id)

        # Search a custom template for rendering, if available
        try:
            template = loader.select_template([
                'datatables_view/%s/%s/render_row_details.html' % (self.model._meta.app_label, self.model._meta.model_name),
                'datatables_view/%s/render_row_details.html' % (self.model._meta.app_label, ),
                'datatables_view/render_row_details.html',
            ])
            html = template.render({
                'model': self.model,
                'model_admin': self.get_model_admin(),
                'object': obj,
            }, request)

        # Failing that, display a simple table with field values
        except TemplateDoesNotExist:
            fields = [f.name for f in self.model._meta.get_fields() if f.concrete]
            html = '<table class="row-details">'
            for field in fields:
                try:
                    value = getattr(obj, field)
                    html += '<tr><td>%s</td><td>%s</td></tr>' % (field, value)
                except:
                    pass
            html += '</table>'
        return html

    @staticmethod
    def render_row_tools_column_def():
        column_def = {
            'name': '',
            'visible': True,
            # https://datatables.net/blog/2017-03-31
            'defaultContent': render_to_string('datatables_view/row_tools.html', {'foo': 'bar'}),
            "className": 'dataTables_row-tools',
            'width': 30,
        }
        return column_def

    # def render_table(self, request):

    #     template_name = self.get_template_name(request)

    #     # # When called via Ajax, use the "smaller" template "<template_name>_inner.html"
    #     # if request.is_ajax():
    #     #     template_name = getattr(self, 'ajax_template_name', '')
    #     #     if not template_name:
    #     #         split = self.template_name.split('.html')
    #     #         split[-1] = '_inner'
    #     #         split.append('.html')
    #     #         template_name = ''.join(split)

    #     html = render_to_string(
    #         template_name, {
    #             'title': self.title,
    #             'columns': self.list_columns(request),
    #             'column_details': mark_safe(json.dumps(self.list_columns(request))),
    #             'initial_order': mark_safe(json.dumps(self.get_initial_order(request))),
    #             'length_menu': mark_safe(json.dumps(self.get_length_menu(request))),
    #             'view': self,
    #             'show_date_filter': self.model._meta.get_latest_by is not None,
    #         },
    #         request=request
    #     )

    #     return html

    def post(self, request, *args, **kwargs):
        """
        Treat POST and GET the like
        """
        return self.get(request, *args, **kwargs)

    def get(self, request, *args, **kwargs):

        # if not getattr(request, 'REQUEST', None):
        #     request.REQUEST = request.GET if request.method=='GET' else request.POST

        t0 = datetime.datetime.now()

        if not request.is_ajax():
            return HttpResponseBadRequest()

        try:
            query_dict = request.REQUEST
            params = self.read_parameters(query_dict)
        except ValueError:
            return HttpResponseBadRequest()

        if ENABLE_QUERYDICT_TRACING:
            trace(query_dict, prompt='query_dict')
            trace(params, prompt='params')

        # Prepare the queryset and apply the search and order filters
        qs = self.get_initial_queryset(request)
        if not DISABLE_QUERYSET_OPTIMIZATION and not self.disable_queryset_optimization:
            qs = self.optimize_queryset(qs)
        qs = self.prepare_queryset(params, qs)
        if ENABLE_QUERYSET_TRACING:
            prettyprint_queryset(qs)

        # Slice result
        paginator = Paginator(qs, params['length'] if params['length'] != -1 else qs.count())
        response_dict = self.get_response_dict(request, paginator, params['draw'], params['start'])
        response_dict['footer_message'] = self.footer_message(qs, params)

        # Prepare response
        response = HttpResponse(
            json.dumps(
                response_dict,
                cls=DjangoJSONEncoder
            ),
            content_type="application/json")

        # Trace elapsed time
        if ENABLE_QUERYSET_TRACING:
            td = datetime.datetime.now() - t0
            ms = (td.seconds * 1000) + (td.microseconds / 1000.0)
            trace('%d [ms]' % ms, prompt="Table rendering time")

        return response

    def read_parameters(self, query_dict):
        """
        Converts and cleans up the GET parameters.
        """
        params = {field: int(query_dict[field]) for field in ['draw', 'start', 'length']}
        params['date_from'] = query_dict.get('date_from', None)
        params['date_to'] = query_dict.get('date_to', None)

        column_index = 0
        has_finished = False
        column_links = []

        while column_index < MAX_COLUMNS and not has_finished:
            column_base = 'columns[%d]' % column_index
            try:
                column_name = query_dict[column_base + '[name]']
                if column_name == '':
                    column_name = query_dict[column_base + '[data]']

                if column_name != '':
                    column_links.append(
                        ColumnLink(
                            column_name,
                            #self.model_columns[column_name],
                            self.column_obj(column_name),
                            query_dict.get(column_base + '[orderable]'),
                            query_dict.get(column_base + '[searchable]'),
                            query_dict.get(column_base + '[search][value]'),
                        )
                    )
                else:
                    column_links.append(PlaceholderColumnLink())
            except KeyError:
                has_finished = True

            column_index += 1

        orders = []
        order_index = 0
        has_finished = False
        columns = [c['name'] for c in self.column_specs]
        while order_index < len(columns) and not has_finished:
            try:
                order_base = 'order[%d]' % order_index
                order_column = query_dict[order_base + '[column]']
                orders.append(Order(
                    order_column,
                    query_dict[order_base + '[dir]'],
                    column_links))
            except ColumnOrderError:
                pass
            except KeyError:
                has_finished = True

            order_index += 1

        search_value = query_dict.get('search[value]')
        if search_value:
            params['search_value'] = search_value

        params.update({'column_links': column_links, 'orders': orders})

        return params

    def get_initial_queryset(self, request=None):
        return self.model.objects.all()

    def get_foreign_queryset(self, request, field):
        queryset = field.model.objects.all()
        return queryset

    def render_column(self, row, column):
        #return self.model_columns[column].render_column(row)
        value = self.column_obj(column).render_column(row)
        return value

    def render_clip_value_as_html(self, long_text, short_text, is_clipped):
        """
        Given long and shor version of text, the following html representation:
            <span title="long_text">short_text[ellipsis]</span>

        To be overridden for further customisations.
        """
        return '<span title="{long_text}">{short_text}{ellipsis}</span>'.format(
            long_text=long_text,
            short_text=short_text,
            ellipsis='&hellip;' if is_clipped else ''
        )

    def clip_value(self, text, max_length, as_html):
        """
        Given `text`, returns:
            - original `text` if it's length is less then or equal `max_length`
            - the clipped left part + ellipses otherwise
        as either plain text or html (depending on `as_html`)
        """
        is_clipped = False
        long_text = text
        if len(text) <= max_length:
            short_text = text
        else:
            short_text = text[:max_length]
            is_clipped = True
        if as_html:
            result = self.render_clip_value_as_html(long_text, short_text, is_clipped)
        else:
            result = short_text
            if is_clipped:
                result += '…'
        return result

    def clip_results(self, retdict):
        rows = [(cs['name'], cs['max_length']) for cs in  self.column_specs if cs['max_length'] > 0]
        for name, max_length in rows:
            retdict[name] = self.clip_value(str(retdict[name]), max_length, True)

    def prepare_results(self, request, qs):
        json_data = []
        columns = [c['name'] for c in self.column_specs]
        for cur_object in qs:
            retdict = {
                #fieldname: '<div class="field-%s">%s</div>' % (fieldname, self.render_column(cur_object, fieldname))
                fieldname: self.render_column(cur_object, fieldname)
                for fieldname in columns
                if fieldname
            }

            self.customize_row(retdict, cur_object)
            self.clip_results(retdict)

            row_id = self.get_table_row_id(request, cur_object)
            if row_id:
                # "Automatic addition of row ID attributes"
                # https://datatables.net/examples/server_side/ids.html
                retdict['DT_RowId'] = row_id

            json_data.append(retdict)
        return json_data

    def get_response_dict(self, request, paginator, draw_idx, start_pos):
        page_id = (start_pos // paginator.per_page) + 1
        if page_id > paginator.num_pages:
            page_id = paginator.num_pages
        elif page_id < 1:
            page_id = 1

        objects = self.prepare_results(request, paginator.page(page_id))

        return {"draw": draw_idx,
                "recordsTotal": paginator.count,
                "recordsFiltered": paginator.count,
                "data": objects,
                }

    def get_table_row_id(self, request, obj):
        """
        Provides a specific ID for the table row; default: "row-ID"
        Override to customize as required.

        Do to a limitation of datatables.net, we can only supply to table rows
        a id="row-ID" attribute, and not a data-row-id="ID" attribute
        """
        result = ''
        if self.table_row_id_fieldname:
            try:
                result = self.table_row_id_prefix + str(getattr(obj, self.table_row_id_fieldname))
            except:
                result = ''
        return result

    def customize_row(self, row, obj):
        # 'row' is a dictionnary representing the current row, and 'obj' is the current object.
        #row['age_is_even'] = obj.age%2==0
        pass

    def optimize_queryset(self, qs):

        # use sets to remove duplicates
        only = set()
        select_related = set()

        # collect values for qs optimizations
        fields = [f.name for f in self.model._meta.get_fields()]
        for column in self.column_specs:
            foreign_field = column.get('foreign_field')
            if foreign_field:

                # Examples:
                #
                #   +-----------------------------+-------------------------------+-----------------------------------+
                #   | if foreign_key is:          | add this to only[]:           | and add this to select_related[]: |
                #   +-----------------------------+-------------------------------+-----------------------------------+
                #   | 'lotto__codice'             | 'lotto__codice'               | 'lotto'                           |
                #   | 'lotto__articolo__codice'   | 'lotto__articolo__codice'     | 'lotto__articolo'                 |
                #   +-----------------------------+-------------------------------+-----------------------------------+
                #

                only.add(foreign_field)
                #select_related.add(column.get('name'))
                #select_related.add(foreign_field.split('__')[0])
                select_related.add('__'.join(foreign_field.split('__')[0:-1]))
            else:
                [f.name for f in self.model._meta.get_fields()]
                field = column.get('name')
                if field in fields:
                    only.add(field)

        # convert to lists
        only = [item for item in list(only) if item]
        select_related = list(select_related)

        # apply optimizations:

        # (1) use select_related() to reduce the number of queries
        if select_related:
            qs = qs.select_related(*select_related)

        # (2) use only() to reduce the number of columns in the resultset
        if only:
            qs = qs.only(*only)

        return qs

    def prepare_queryset(self, params, qs):
        qs = self.filter_queryset(params, qs)
        qs = self.sort_queryset(params, qs)
        return qs

    def filter_queryset(self, params, qs):

        qs = self.filter_queryset_by_date_range(params.get('date_from', None), params.get('date_to', None), qs)

        if 'search_value' in params:
            qs = self.filter_queryset_all_columns(params['search_value'], qs)

        for column_link in params['column_links']:
            if column_link.searchable and column_link.search_value:
                qs = self.filter_queryset_by_column(column_link.name, column_link.search_value, qs)

        return qs

    def sort_queryset(self, params, qs):
        if len(params['orders']):
            qs = qs.order_by(
                *[order.get_order_mode() for order in params['orders']])
        return qs

    # TODO: currently unused; in the orginal project was probably related to the
    # management of fields with choices;
    # check and refactor this
    def choice_field_search(self, column, search_value):
        values_dict = self.choice_fields_completion[column]
        # matching_choices = [
        #     val for key, val in six.iteritems(values_dict)
        #     if key.startswith(search_value)
        # ]
        matching_choices = [
            val for key, val in values_dict.items()
            if key.startswith(search_value)
        ]
        return Q(**{column + '__in': matching_choices})

    def _filter_queryset(self, column_names, search_value, qs):

        if TEST_FILTERS:
            trace(', '.join(column_names), 'Filtering "%s" over fields' % search_value)

        search_filters = Q()
        for column_name in column_names:

            column_obj = self.column_obj(column_name)
            column_spec = self.column_spec_by_name(column_name)

            column_filter = build_column_filter(column_name, column_obj, column_spec, search_value)
            if column_filter:
                search_filters |= column_filter
                if TEST_FILTERS:
                    trace(column_name, "Test filter")
                    qstest = qs.filter(column_filter)
                    trace('%d/%d records filtered' % (qstest.count(), qs.count()))

        if TEST_FILTERS:
            trace(search_filters, prompt='Search filters')

        return qs.filter(search_filters)

    def filter_queryset_all_columns(self, search_value, qs):
        searchable_columns = [c['name'] for c in self.column_specs if c['searchable']]
        return self._filter_queryset(searchable_columns, search_value, qs)

    def filter_queryset_by_column(self, column_name, search_value, qs):
        return self._filter_queryset([column_name, ], search_value, qs)

    def filter_queryset_by_date_range(self, date_from, date_to, qs):

        if self.latest_by and (date_from or date_to):

            daterange_filter = Q()
            is_datetime = isinstance(self.latest_by, models.DateTimeField)

            if date_from:
                dt = datetime.datetime.strptime(date_from, '%Y-%m-%d').date()
                if is_datetime:
                    daterange_filter &= Q(**{self.latest_by+'__date__gte': dt})
                else:
                    daterange_filter &= Q(**{self.latest_by+'__gte': dt})

            if date_to:
                dt = datetime.datetime.strptime(date_to, '%Y-%m-%d').date()
                if is_datetime:
                    daterange_filter &= Q(**{self.latest_by+'__date__lte': dt})
                else:
                    daterange_filter &= Q(**{self.latest_by+'__lte': dt})

            if TEST_FILTERS:
                n0 = qs.count()

            qs = qs.filter(daterange_filter)

            if TEST_FILTERS:
                n1 = qs.count()
                trace(daterange_filter, prompt='Daterange filter')
                trace('%d/%d records filtered' % (n1, n0))

        return qs

    def footer_message(self, qs, params):
        """
        Overriden to append a message to the bottom of the table
        """
        #return 'Selected rows: %d' % qs.count()
        return None


    def list_autofilter_choices(self, request, column_spec, field, initial_search_value):
        """
        Collects distinct values from specified field,
        and prepares a list of choices for "autofilter" selection.
        Sample result:
            [
                ('Alicia', 'Alicia'), ('Amanda', 'Amanda'), ('Amy', 'Amy'),
                ...
                ('William', 'William'), ('Yolanda', 'Yolanda'), ('Yvette', 'Yvette'),
            ]
        """
        try:
            if field.model == self.model:
                queryset = self.get_initial_queryset(request)
            else:
                queryset = self.get_foreign_queryset(request, field)
            values = list(queryset
                .values_list(field.name, flat=True)
                .distinct()
                .order_by(field.name)
            )

            # Make sure initial_search_value is available
            if initial_search_value is not None:
                if initial_search_value not in values:
                    values.append(initial_search_value)

            if isinstance(field, models.DateField):
                choices = [(item, format_datetime(item)) for item in values]
            else:
                max_length = column_spec['max_length']
                if max_length <= 0:
                    choices = [(item, item) for item in values]
                else:
                    choices = [
                        (item, self.clip_value(str(item), max_length, False))
                        for item in values
                    ]

        except Exception as e:
            # TODO: investigate what happens here with FKs
            print('ERROR: ' + str(e))
            choices = []
        return choices