import copy
from django import forms
from django.forms.forms import pretty_name
from django.core.exceptions import ValidationError, ImproperlyConfigured
from django.db.models import Q
try:
from collections import OrderedDict
except ImportError:
# Support for Python < 2.6
from django.utils.datastructures import SortedDict as OrderedDict
from django.utils import six
from django.utils.six.moves import reduce
from django.utils.http import urlencode
from .forms import BetterForm
def construct_querystring(data, **kwargs):
params = copy.copy(data)
params.update(kwargs)
return urlencode(params)
class IterDict(OrderedDict):
"""
Extension of djangos built in sorted dictionary class which iterates
through the values rather than keys.
"""
def __iter__(self):
for key in super(IterDict, self).__iter__():
yield self[key]
class BaseChangeListForm(BetterForm):
"""
Base class for all ``ChangeListForms``.
"""
def __init__(self, *args, **kwargs):
"""
Takes an option named argument ``queryset`` as the base queryset used in
the ``get_queryset`` method.
"""
try:
self.base_queryset = kwargs.pop('queryset', None)
if self.base_queryset is None:
self.base_queryset = self.model.objects.all()
except AttributeError:
raise AttributeError('`ChangeListForm`s must be instantiated with a\
queryset, or have a `model` attribute set on\
them')
super(BaseChangeListForm, self).__init__(*args, **kwargs)
def get_queryset(self):
"""
If the form was initialized with a queryset, this method returns that
queryset. Otherwise it returns ``Model.objects.all()`` for whatever
model was defined for the form.
"""
return self.base_queryset
class BoundHeader(object):
def __init__(self, form, header):
self.form = form
self.header = header
self.sorts = getattr(form, 'cleaned_data', {}).get('sorts', [])
self.param = "{0}-sorts".format(form.prefix or '').strip('-')
@property
def name(self):
return self.header.name
@property
def label(self):
return self.header.label
@property
def column_name(self):
return self.header.column_name
@property
def is_sortable(self):
return self.header.is_sortable
@property
def _index(self):
return self.form.HEADERS.index(self.header)
@property
def _sort_index(self):
"""
1-indexed value for what number represents this header in the sorts
querystring parameter.
"""
return self._index + 1
@property
def is_active(self):
"""
Returns whether this header is currently being used for sorting.
"""
return self._sort_index in map(abs, self.sorts)
@property
def is_ascending(self):
"""
Returns whether this header is currently being used for sorting in
ascending order.
"""
return self.is_active and self._sort_index in self.sorts
@property
def is_descending(self):
"""
Returns whether this header is currently being used for sorting in
descending order.
"""
return self.is_active and self._sort_index not in self.sorts
@property
def css_classes(self):
"""
String suitable to be used for the `class` attribute for an HTML
element. Denotes whether this header is active in the sorts, and the
order in which it is being used.
"""
classes = []
if self.is_active:
classes.append('active')
if self.is_ascending:
classes.append('ascending')
elif self.is_descending:
classes.append('descending')
return ' '.join(classes)
def add_to_sorts(self):
"""
Compute the sorts that should be used when we're clicked on. If we're
currently in the sorts, we'll be set as the first sort [ascending].
Unless we're already at the front then we'll be inverted.
"""
if self.sorts and abs(self.sorts[0]) == self._sort_index:
return [-1 * self.sorts[0]] + self.sorts[1:]
else:
return [self._sort_index] + list(filter(lambda x: abs(x) != self._sort_index, self.sorts))
@property
def priority(self):
if self.is_active:
return list(map(abs, self.sorts)).index(self._sort_index) + 1
@property
def querystring(self):
return construct_querystring(self.form.data, **{self.param: '.'.join(map(str, self.add_to_sorts()))})
@property
def singular_querystring(self):
if self.is_active and abs(self.sorts[0]) == self._sort_index:
value = -1 * self._sort_index
else:
value = self._sort_index
return construct_querystring(self.form.data, **{self.param: str(value)})
@property
def remove_querystring(self):
return construct_querystring(self.form.data, **{self.param: '.'.join(map(str, self.add_to_sorts()[1:]))})
def is_header_kwargs(header):
try:
if not len(header) == 2:
return False
except AttributeError:
return False
try:
return all((
isinstance(header[0], six.string_types),
isinstance(header[1], dict),
))
except (IndexError, KeyError):
return False
class HeaderSet(object):
HeaderClass = Header
def __init__(self, form, headers):
self.form = form
self.headers = OrderedDict()
if headers is None:
return
for header in headers:
if isinstance(header, Header):
self.headers[header.name] = header
elif isinstance(header, six.string_types):
self.headers[header] = self.HeaderClass(header)
elif is_header_kwargs(header):
header_name, header_kwargs = header
self.headers[header_name] = self.HeaderClass(header_name, **header_kwargs)
elif len(header):
try:
header_name = header[0]
header_args = header[1:]
self.headers[header_name] = self.HeaderClass(header_name, *header_args)
except KeyError:
raise ImproperlyConfigured('Unknown format in header declaration: `{0}`'.format(repr(header)))
else:
raise ImproperlyConfigured('Unknown format in header declaration: `{0}`'.format(repr(header)))
if not len(self) == len(headers):
raise ImproperlyConfigured('Header names must be unique')
def __len__(self):
return len(self.headers)
def __iter__(self):
for header in self.headers.values():
yield self.HeaderClass.BoundClass(self.form, header)
def __getitem__(self, key):
if isinstance(key, int):
return self.HeaderClass.BoundClass(self.form, list(self.headers.values())[key])
else:
return self.HeaderClass.BoundClass(self.form, self.headers[key])