Source code for betterforms.changelist

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)

    # We can't call update here because QueryDict extends rather than replaces.
    for key, value in kwargs.items():
        params[key] = value

    if hasattr(params, 'urlencode'):
        return params.urlencode()
    else:
        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


[docs]class SearchForm(BaseChangeListForm): SEARCH_FIELDS = None CASE_SENSITIVE = False q = forms.CharField(label="Search", required=False) def __init__(self, *args, **kwargs): self.SEARCH_FIELDS = kwargs.pop('search_fields', self.SEARCH_FIELDS) super(SearchForm, self).__init__(*args, **kwargs) if self.SEARCH_FIELDS is None: raise ImproperlyConfigured('`SearchForm`s must be instantiated with an\ iterable of fields to search over, or have \ a `SEARCH_FIELDS` attribute set on them.') def get_queryset(self): """ Constructs an '__contains' or '__icontains' filter across all of the fields listed in ``SEARCH_FIELDS``. """ qs = super(SearchForm, self).get_queryset() # Do Searching q = self.cleaned_data.get('q', '').strip() if q: args = [] for field in self.SEARCH_FIELDS: if self.CASE_SENSITIVE: kwarg = {field + '__contains': q} else: kwarg = {field + '__icontains': q} args.append(Q(**kwarg)) if len(args) > 1: qs = qs.filter(reduce(lambda x, y: x | y, args)) elif len(args) == 1: qs = qs.filter(args[0]) return qs
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]) class SortFormBase(BetterForm): """ A base class for writing your own SortForm. This form handles everything except applying the sorts to the queryset, which is convenient if you aren't working within the ChangeListForm paradigm. Usage:: class MyForm(SortFormBase): HEADERS = ( Header('name', label='Name'), ) # fields ... def get_results(self): queryset = # ... queryset = self.apply_sorting(queryset) return queryset """ HeaderSetClass = HeaderSet error_messages = { 'unknown_header': 'Invalid sort parameter', 'unsortable_header': 'Invalid sort parameter', } HEADERS = None sorts = forms.CharField(required=False, widget=forms.HiddenInput()) def __init__(self, *args, **kwargs): super(SortFormBase, self).__init__(*args, **kwargs) self.headers = self.HeaderSetClass(self, self.HEADERS) def clean_sorts(self): cleaned_data = self.cleaned_data sorts = list(filter(bool, cleaned_data.get('sorts', '').split('.'))) if not sorts: return [] # Ensure that the sort parameter does not contain non-numeric sort indexes if not all([sort.strip('-').isdigit() for sort in sorts]): raise ValidationError(self.error_messages['unknown_header']) sorts = [int(sort) for sort in sorts] # Ensure that all of our sort parameters are in range of our header values if any([abs(sort) > len(self.HEADERS) for sort in sorts]): raise ValidationError(self.error_messages['unknown_header']) # Ensure not un-sortable fields are being sorted by if not all(self.HEADERS[abs(i) - 1].is_sortable for i in sorts): raise ValidationError(self.error_messages['unsortable_header']) return sorts def get_order_by(self): # Do Sorting sorts = self.cleaned_data.get('sorts', []) order_by = [] for sort in sorts: param = self.headers[abs(sort) - 1].column_name if sort < 0: param = '-' + param order_by.append(param) return order_by def apply_sorting(self, qs): order_by = self.get_order_by() if order_by: qs = qs.order_by(*order_by) return qs
[docs]class SortForm(BaseChangeListForm, SortFormBase): def get_queryset(self): """ Returns an ordered queryset, sorted based on the values submitted in the sort parameter. """ qs = super(SortForm, self).get_queryset() return self.apply_sorting(qs)