596 lines
19 KiB
Python
596 lines
19 KiB
Python
# coding: utf-8
|
|
from __future__ import absolute_import, unicode_literals
|
|
from django.core.handlers.wsgi import WSGIRequest
|
|
from django.utils.html import escape
|
|
from django.utils.safestring import mark_safe
|
|
from django.test.client import FakePayload
|
|
from itertools import chain
|
|
import inspect
|
|
import six
|
|
import warnings
|
|
|
|
|
|
def python_2_unicode_compatible(klass):
|
|
"""
|
|
A decorator that defines __unicode__ and __str__ methods under Python 2.
|
|
Under Python 3 it does nothing.
|
|
|
|
To support Python 2 and 3 with a single code base, define a __str__ method
|
|
returning text and apply this decorator to the class.
|
|
|
|
Taken directly from Django.
|
|
"""
|
|
if not six.PY3:
|
|
klass.__unicode__ = klass.__str__
|
|
klass.__str__ = lambda self: self.__unicode__().encode('utf-8')
|
|
return klass
|
|
|
|
|
|
class Sequence(list):
|
|
"""
|
|
Represents a column sequence, e.g. ``("first_name", "...", "last_name")``
|
|
|
|
This is used to represent `.Table.Meta.sequence` or the `.Table`
|
|
constructors's *sequence* keyword argument.
|
|
|
|
The sequence must be a list of column names and is used to specify the
|
|
order of the columns on a table. Optionally a "..." item can be inserted,
|
|
which is treated as a *catch-all* for column names that aren't explicitly
|
|
specified.
|
|
"""
|
|
def expand(self, columns):
|
|
"""
|
|
Expands the ``"..."`` item in the sequence into the appropriate column
|
|
names that should be placed there.
|
|
|
|
:raises: `ValueError` if the sequence is invalid for the columns.
|
|
"""
|
|
ellipses = self.count("...")
|
|
if ellipses > 1:
|
|
raise ValueError("'...' must be used at most once in a sequence.")
|
|
elif ellipses == 0:
|
|
self.append("...")
|
|
|
|
# everything looks good, let's expand the "..." item
|
|
columns = list(columns) # take a copy and exhaust the generator
|
|
head = []
|
|
tail = []
|
|
target = head # start by adding things to the head
|
|
for name in self:
|
|
if name == "...":
|
|
# now we'll start adding elements to the tail
|
|
target = tail
|
|
continue
|
|
target.append(name)
|
|
if name in columns:
|
|
columns.pop(columns.index(name))
|
|
self[:] = chain(head, columns, tail)
|
|
|
|
|
|
class OrderBy(six.text_type):
|
|
"""
|
|
A single item in an `.OrderByTuple` object. This class is
|
|
essentially just a `str` with some extra properties.
|
|
"""
|
|
@property
|
|
def bare(self):
|
|
"""
|
|
Return the bare form.
|
|
|
|
The *bare form* is the non-prefixed form. Typically the bare form is
|
|
just the ascending form.
|
|
|
|
Example: ``age`` is the bare form of ``-age``
|
|
|
|
:rtype: `.OrderBy` object
|
|
"""
|
|
return OrderBy(self[1:]) if self[:1] == '-' else self
|
|
|
|
@property
|
|
def opposite(self):
|
|
"""
|
|
Return an `.OrderBy` object with an opposite sort influence.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> order_by = OrderBy('name')
|
|
>>> order_by.opposite
|
|
'-name'
|
|
|
|
:rtype: `.OrderBy` object
|
|
"""
|
|
return OrderBy(self[1:]) if self.is_descending else OrderBy('-' + self)
|
|
|
|
@property
|
|
def is_descending(self):
|
|
"""
|
|
Return `True` if this object induces *descending* ordering
|
|
|
|
:rtype: `bool`
|
|
"""
|
|
return self.startswith('-')
|
|
|
|
@property
|
|
def is_ascending(self):
|
|
"""
|
|
Return `True` if this object induces *ascending* ordering.
|
|
|
|
:returns: `bool`
|
|
"""
|
|
return not self.is_descending
|
|
|
|
|
|
@python_2_unicode_compatible
|
|
class OrderByTuple(tuple):
|
|
"""Stores ordering as (as `.OrderBy` objects). The
|
|
`~django_tables2.tables.Table.order_by` property is always converted
|
|
to an `.OrderByTuple` object.
|
|
|
|
This class is essentially just a `tuple` with some useful extras.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> x = OrderByTuple(('name', '-age'))
|
|
>>> x['age']
|
|
'-age'
|
|
>>> x['age'].is_descending
|
|
True
|
|
>>> x['age'].opposite
|
|
'age'
|
|
|
|
"""
|
|
def __new__(cls, iterable):
|
|
transformed = []
|
|
for item in iterable:
|
|
if not isinstance(item, OrderBy):
|
|
item = OrderBy(item)
|
|
transformed.append(item)
|
|
return super(OrderByTuple, cls).__new__(cls, transformed)
|
|
|
|
def __str__(self):
|
|
return ','.join(self)
|
|
|
|
def __contains__(self, name):
|
|
"""
|
|
Determine if a column has an influence on ordering.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> ordering =
|
|
>>> x = OrderByTuple(('name', ))
|
|
>>> 'name' in x
|
|
True
|
|
>>> '-name' in x
|
|
True
|
|
|
|
:param name: The name of a column. (optionally prefixed)
|
|
:returns: `bool`
|
|
"""
|
|
name = OrderBy(name).bare
|
|
for order_by in self:
|
|
if order_by.bare == name:
|
|
return True
|
|
return False
|
|
|
|
def __getitem__(self, index):
|
|
"""
|
|
Allows an `.OrderBy` object to be extracted via named or integer
|
|
based indexing.
|
|
|
|
When using named based indexing, it's fine to used a prefixed named.
|
|
|
|
.. code-block:: python
|
|
|
|
>>> x = OrderByTuple(('name', '-age'))
|
|
>>> x[0]
|
|
'name'
|
|
>>> x['age']
|
|
'-age'
|
|
>>> x['-age']
|
|
'-age'
|
|
|
|
:rtype: `.OrderBy` object
|
|
"""
|
|
if isinstance(index, six.string_types):
|
|
for order_by in self:
|
|
if order_by == index or order_by.bare == index:
|
|
return order_by
|
|
raise KeyError
|
|
return super(OrderByTuple, self).__getitem__(index)
|
|
|
|
@property
|
|
def key(self):
|
|
accessors = []
|
|
reversing = []
|
|
for order_by in self:
|
|
accessors.append(Accessor(order_by.bare))
|
|
reversing.append(order_by.is_descending)
|
|
|
|
@total_ordering
|
|
class Comparator(object):
|
|
def __init__(self, obj):
|
|
self.obj = obj
|
|
|
|
def __eq__(self, other):
|
|
for accessor in accessors:
|
|
a = accessor.resolve(self.obj, quiet=True)
|
|
b = accessor.resolve(other.obj, quiet=True)
|
|
if not a == b:
|
|
return False
|
|
return True
|
|
|
|
def __lt__(self, other):
|
|
for accessor, reverse in six.moves.zip(accessors, reversing):
|
|
a = accessor.resolve(self.obj, quiet=True)
|
|
b = accessor.resolve(other.obj, quiet=True)
|
|
if a == b:
|
|
continue
|
|
if reverse:
|
|
a, b = b, a
|
|
# The rest of this should be refactored out into a util
|
|
# function 'compare' that handles different types.
|
|
try:
|
|
return a < b
|
|
except TypeError:
|
|
# If the truth values differ, it's a good way to
|
|
# determine ordering.
|
|
if bool(a) is not bool(b):
|
|
return bool(a) < bool(b)
|
|
# Handle comparing different types, by falling back to
|
|
# the string and id of the type. This at least groups
|
|
# different types together.
|
|
a_type = type(a)
|
|
b_type = type(b)
|
|
return (repr(a_type), id(a_type)) < (repr(b_type), id(b_type))
|
|
return False
|
|
return Comparator
|
|
|
|
@property
|
|
def cmp(self):
|
|
"""
|
|
Return a function for use with `list.sort` that implements this
|
|
object's ordering. This is used to sort non-`.QuerySet` based
|
|
:term:`table data`.
|
|
|
|
:rtype: function
|
|
"""
|
|
warnings.warn('`cmp` is deprecated, use `key` instead.',
|
|
DeprecationWarning)
|
|
|
|
# pylint: disable=C0103
|
|
def _cmp(a, b):
|
|
for accessor, reverse in instructions:
|
|
x = accessor.resolve(a)
|
|
y = accessor.resolve(b)
|
|
try:
|
|
res = cmp(x, y)
|
|
except TypeError:
|
|
res = cmp((repr(type(x)), id(type(x)), x),
|
|
(repr(type(y)), id(type(y)), y))
|
|
if res != 0:
|
|
return -res if reverse else res
|
|
return 0
|
|
instructions = []
|
|
for order_by in self:
|
|
if order_by.startswith('-'):
|
|
instructions.append((Accessor(order_by[1:]), True))
|
|
else:
|
|
instructions.append((Accessor(order_by), False))
|
|
return _cmp
|
|
|
|
def get(self, key, fallback):
|
|
"""
|
|
Identical to __getitem__, but supports fallback value.
|
|
"""
|
|
try:
|
|
return self[key]
|
|
except (KeyError, IndexError):
|
|
return fallback
|
|
|
|
@property
|
|
def opposite(self):
|
|
"""
|
|
Return version with each `.OrderBy` prefix toggled.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> order_by = OrderByTuple(('name', '-age'))
|
|
>>> order_by.opposite
|
|
('-name', 'age')
|
|
"""
|
|
return type(self)((o.opposite for o in self))
|
|
|
|
|
|
class Accessor(str):
|
|
"""
|
|
A string describing a path from one object to another via attribute/index
|
|
accesses. For convenience, the class has an alias `.A` to allow for more concise code.
|
|
|
|
Relations are separated by a ``.`` character.
|
|
"""
|
|
SEPARATOR = '.'
|
|
|
|
def resolve(self, context, safe=True, quiet=False):
|
|
"""
|
|
Return an object described by the accessor by traversing the attributes
|
|
of *context*.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> x = Accessor('__len__')
|
|
>>> x.resolve('brad')
|
|
4
|
|
>>> x = Accessor('0.upper')
|
|
>>> x.resolve('brad')
|
|
'B'
|
|
|
|
:type context: `object`
|
|
:param context: The root/first object to traverse.
|
|
:type safe: `bool`
|
|
:param safe: Don't call anything with ``alters_data = True``
|
|
:type quiet: bool
|
|
:param quiet: Smother all exceptions and instead return `None`
|
|
:returns: target object
|
|
:raises: anything ``getattr(a, "b")`` raises, e.g. `TypeError`,
|
|
`AttributeError`, `KeyError`, `ValueError` (unless *quiet* ==
|
|
`True`)
|
|
|
|
`~.Accessor.resolve` attempts lookups in the following order:
|
|
|
|
- dictionary (e.g. ``obj[related]``)
|
|
- attribute (e.g. ``obj.related``)
|
|
- list-index lookup (e.g. ``obj[int(related)]``)
|
|
|
|
Callable objects are called, and their result is used, before
|
|
proceeding with the resolving.
|
|
"""
|
|
try:
|
|
current = context
|
|
for bit in self.bits:
|
|
try: # dictionary lookup
|
|
current = current[bit]
|
|
except (TypeError, AttributeError, KeyError):
|
|
try: # attribute lookup
|
|
current = getattr(current, bit)
|
|
except (TypeError, AttributeError):
|
|
try: # list-index lookup
|
|
current = current[int(bit)]
|
|
except (IndexError, # list index out of range
|
|
ValueError, # invalid literal for int()
|
|
KeyError, # dict without `int(bit)` key
|
|
TypeError, # unsubscriptable object
|
|
):
|
|
raise ValueError('Failed lookup for key [%s] in %r'
|
|
', when resolving the accessor %s'
|
|
% (bit, current, self))
|
|
if callable(current):
|
|
if safe and getattr(current, 'alters_data', False):
|
|
raise ValueError('refusing to call %s() because `.alters_data = True`'
|
|
% repr(current))
|
|
current = current()
|
|
# important that we break in None case, or a relationship
|
|
# spanning across a null-key will raise an exception in the
|
|
# next iteration, instead of defaulting.
|
|
if current is None:
|
|
break
|
|
return current
|
|
except:
|
|
if not quiet:
|
|
raise
|
|
|
|
@property
|
|
def bits(self):
|
|
if self == '':
|
|
return ()
|
|
return self.split(self.SEPARATOR)
|
|
|
|
|
|
A = Accessor # alias
|
|
|
|
class AttributeDict(dict):
|
|
"""
|
|
A wrapper around `dict` that knows how to render itself as HTML
|
|
style tag attributes.
|
|
|
|
The returned string is marked safe, so it can be used safely in a template.
|
|
See `.as_html` for a usage example.
|
|
"""
|
|
def as_html(self):
|
|
"""
|
|
Render to HTML tag attributes.
|
|
|
|
Example:
|
|
|
|
.. code-block:: python
|
|
|
|
>>> from django_tables2.utils import AttributeDict
|
|
>>> attrs = AttributeDict({'class': 'mytable', 'id': 'someid'})
|
|
>>> attrs.as_html()
|
|
'class="mytable" id="someid"'
|
|
|
|
:rtype: `~django.utils.safestring.SafeUnicode` object
|
|
|
|
"""
|
|
return mark_safe(' '.join(['%s="%s"' % (k, escape(v if not callable(v) else v()))
|
|
for k, v in six.iteritems(self)]))
|
|
|
|
|
|
class Attrs(dict):
|
|
"""
|
|
Backwards compatibility, deprecated.
|
|
"""
|
|
def __init__(self, *args, **kwargs):
|
|
super(Attrs, self).__init__(*args, **kwargs)
|
|
warnings.warn("Attrs class is deprecated, use dict instead.",
|
|
DeprecationWarning)
|
|
|
|
|
|
def segment(sequence, aliases):
|
|
"""
|
|
Translates a flat sequence of items into a set of prefixed aliases.
|
|
|
|
This allows the value set by `.QuerySet.order_by` to be translated into
|
|
a list of columns that would have the same result. These are called
|
|
"order by aliases" which are optionally prefixed column names.
|
|
|
|
e.g.
|
|
|
|
>>> list(segment(("a", "-b", "c"),
|
|
... {"x": ("a"),
|
|
... "y": ("b", "-c"),
|
|
... "z": ("-b", "c")}))
|
|
[("x", "-y"), ("x", "z")]
|
|
|
|
"""
|
|
if not (sequence or aliases):
|
|
return
|
|
for alias, parts in aliases.items():
|
|
variants = {
|
|
# alias: order by tuple
|
|
alias: OrderByTuple(parts),
|
|
OrderBy(alias).opposite: OrderByTuple(parts).opposite,
|
|
}
|
|
for valias, vparts in variants.items():
|
|
if list(sequence[:len(vparts)]) == list(vparts):
|
|
tail_aliases = dict(aliases)
|
|
del tail_aliases[alias]
|
|
tail_sequence = sequence[len(vparts):]
|
|
if tail_sequence:
|
|
for tail in segment(tail_sequence, tail_aliases):
|
|
yield tuple(chain([valias], tail))
|
|
else:
|
|
continue
|
|
else:
|
|
yield tuple([valias])
|
|
|
|
|
|
class cached_property(object): # pylint: disable=C0103
|
|
"""
|
|
Decorator that creates converts a method with a single
|
|
self argument into a property cached on the instance.
|
|
|
|
Taken directly from Django 1.4.
|
|
"""
|
|
def __init__(self, func):
|
|
from functools import wraps
|
|
wraps(func)(self)
|
|
self.func = func
|
|
|
|
def __get__(self, instance, cls):
|
|
res = instance.__dict__[self.func.__name__] = self.func(instance)
|
|
return res
|
|
|
|
|
|
funcs = (name for name in ('getfullargspec', 'getargspec')
|
|
if hasattr(inspect, name))
|
|
getargspec = getattr(inspect, next(funcs))
|
|
del funcs
|
|
|
|
|
|
def build_request(uri='/'):
|
|
"""
|
|
Return a fresh HTTP GET / request.
|
|
|
|
This is essentially a heavily cutdown version of Django 1.3's
|
|
`~django.test.client.RequestFactory`.
|
|
"""
|
|
path, _, querystring = uri.partition('?')
|
|
return WSGIRequest({
|
|
'CONTENT_TYPE': 'text/html; charset=utf-8',
|
|
'PATH_INFO': path,
|
|
'QUERY_STRING': querystring,
|
|
'REMOTE_ADDR': '127.0.0.1',
|
|
'REQUEST_METHOD': 'GET',
|
|
'SCRIPT_NAME': '',
|
|
'SERVER_NAME': 'testserver',
|
|
'SERVER_PORT': '80',
|
|
'SERVER_PROTOCOL': 'HTTP/1.1',
|
|
'wsgi.version': (1, 0),
|
|
'wsgi.url_scheme': 'http',
|
|
'wsgi.input': FakePayload(b''),
|
|
'wsgi.errors': six.StringIO(),
|
|
'wsgi.multiprocess': True,
|
|
'wsgi.multithread': False,
|
|
'wsgi.run_once': False,
|
|
})
|
|
|
|
|
|
def total_ordering(cls):
|
|
"""Class decorator that fills in missing ordering methods"""
|
|
convert = {
|
|
'__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),
|
|
('__le__', lambda self, other: self < other or self == other),
|
|
('__ge__', lambda self, other: not self < other)],
|
|
'__le__': [('__ge__', lambda self, other: not self <= other or self == other),
|
|
('__lt__', lambda self, other: self <= other and not self == other),
|
|
('__gt__', lambda self, other: not self <= other)],
|
|
'__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),
|
|
('__ge__', lambda self, other: self > other or self == other),
|
|
('__le__', lambda self, other: not self > other)],
|
|
'__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),
|
|
('__gt__', lambda self, other: self >= other and not self == other),
|
|
('__lt__', lambda self, other: not self >= other)]
|
|
}
|
|
roots = set(dir(cls)) & set(convert)
|
|
if not roots:
|
|
raise ValueError('must define at least one ordering operation: < > <= >=')
|
|
root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
|
|
for opname, opfunc in convert[root]:
|
|
if opname not in roots:
|
|
opfunc.__name__ = str(opname) # Py2 requires non-unicode, Py3 requires unicode.
|
|
opfunc.__doc__ = getattr(int, opname).__doc__
|
|
setattr(cls, opname, opfunc)
|
|
return cls
|
|
|
|
|
|
def computed_values(d):
|
|
"""
|
|
Computes a new `dict` that has callable values replaced with the return values.
|
|
|
|
Simple example:
|
|
|
|
>>> compute_values({"foo": lambda: "bar"})
|
|
{"foo": "bar"}
|
|
|
|
Arbitrarily deep structures are supported. The logic is as follows:
|
|
|
|
1. If the value is callable, call it and make that the new value.
|
|
2. If the value is an instance of dict, use ComputableDict to compute its keys.
|
|
|
|
Example:
|
|
|
|
>>> def parents():
|
|
... return {
|
|
... "father": lambda: "Foo",
|
|
... "mother": "Bar"
|
|
... }
|
|
...
|
|
>>> a = {
|
|
... "name": "Brad",
|
|
... "parents": parents
|
|
... }
|
|
...
|
|
>>> computed_values(a)
|
|
{"name": "Brad", "parents": {"father": "Foo", "mother": "Bar"}}
|
|
|
|
:rtype: dict
|
|
"""
|
|
result = {}
|
|
for k, v in six.iteritems(d):
|
|
if callable(v):
|
|
v = v()
|
|
if isinstance(v, dict):
|
|
v = computed_values(v)
|
|
result[k] = v
|
|
return result
|