# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import functools
try:
from urllib import quote, unquote
except ImportError:
from urllib.parse import quote, unquote
from decimal import Decimal
from dateutil.parser import parse
from bottle import request, MultiDict, _parse_qsl
from .common import (to_bytes, to_unicode, attr_escape, html_escape,
basestring, unicode)
SIZES = 'KMGTP'
FERR_CLS = 'form-errors'
FERR_ONE_CLS = 'form-error'
ERR_CLS = 'field-error'
HTTP_PORTS = (80, 443)
urlquote = lambda value: quote(to_bytes(value))
urlunquote = lambda value: to_unicode(unquote(value))
[docs]class QueryDict(MultiDict):
"""
Represents a query string in ``bottle.MultiDict`` format.
This class differs from the base ``bottle.MultiDict`` class in two ways.
First, it is instantiated with raw query string, rather than a list of
two-tuples::
>>> q = QueryDict('a=1&b=2')
The query string is parsed and converted to ``MultiDict`` format. This
works exactly the same way as ``request.query``.
Second difference is the way string coercion is handled. ``QueryDict``
instances can be converted back into a query string by coercing them into
string or bytestring::
>>> str(q)
'a=1&b=2'
The coercion also happens when using the ``+`` operator to concatenate with
other strings::
>>> 'foo' + q
'foo?a=1&b=2'
Notice that the '?' character is inserted when using the ``+`` operator.
.. note::
When converting back to string, the order of parameters in the
resulting query string may differ from the original.
Furthermore, additional methods have been added to provide chaining
capability in conjunction with ``*_qparam()`` functions in this module.
For instance::
>>> q = QueryDict('a=1&b=2')
>>> q.del_qparam('a').set_qparam(b=3).add_qparam(d=2, k=12)
>>> str(s)
'b=3&d=2&k=12'
When used with functions like :py:func:`~add_qparam`, this provides a more
intuitive API::
>>> qs = 'a=1&b=2'
>>> q = add_qparam(qs, c=2).set_qparam(a=2)
>>> str(q)
'a=2&b=2&c=2'
Since this class is a ``bottle.MultiDict`` subclass, you can expect it to
behave the same way as a regular ``MultiDict`` object. You can assign
values to keys, get values by key, get all items as a list of key-value
tuples, and so on. Please consult the Bottle documentation for more
information on how ``MultiDict`` objects work.
"""
def __init__(self, qs=''):
super(QueryDict, self).__init__(_parse_qsl(qs))
[docs] def add_qparam(self, **params):
"""
Add query parameter. Any keyword arguments passed to this function will
be converted to query parameters.
Returns the instance for further chaining.
"""
for param, value in params.items():
self.append(param, to_unicode(value))
return self
[docs] def set_qparam(self, **params):
"""
Replace or add parameter. Any keyword arguments passed to this function
will be converted to query parameters.
Returns the instance for further chaining.
"""
for param, value in params.items():
self.replace(param, to_unicode(value))
return self
[docs] def del_qparam(self, *params):
"""
Remove a query parameter. Takes any number of parameter names to be
removed.
Returns the instance for further chaining.
"""
for param in params:
try:
del self[param]
except KeyError:
pass
return self
[docs] def to_qs(self):
"""
Return the string representation of the query string with prepended
'?' character.
"""
return '?' + str(self)
def __radd__(self, other):
return other + str(self.to_qs())
def __add__(self, other):
return str(self.to_qs()) + other
def __str__(self):
return '&'.join(['{}={}'.format(urlquote(k), urlquote(v))
for k, v in self.allitems()])
# DATA FORMATTING
[docs]def plur(word, n, plural=lambda n: n != 1,
convert=lambda w, p: w + 's' if p else w):
"""
Pluralize word based on number of items. This function provides rudimentary
pluralization support. It is quite flexible, but not a replacement for
functions like ``ngettext``.
This function takes two optional arguments, ``plural()`` and ``convert()``,
which can be customized to change the way plural form is derived from the
original string. The default implementation is a naive version of English
language plural, which uses plural form if number is not 1, and derives the
plural form by simply adding 's' to the word. While this works in most
cases, it doesn't always work even for English.
The ``plural(n)`` function takes the value of the ``n`` argument and its
return value is fed into the ``convert()`` function. The latter takes the
source word as first argument, and return value of ``plural()`` call as
second argument, and returns a string representing the pluralized word.
Return value of the ``convert(w, p)`` call is returned from this function.
Here are some simple examples::
>>> plur('book', 1)
'book'
>>> plur('book', 2)
'books'
# But it's a bit naive
>>> plur('box', 2)
'boxs'
The latter can be fixed like this::
>>> exceptions = ['box']
>>> def pluralize(word, is_plural):
... if not is_plural:
... return word
... if word in exceptions:
... return word + 'es'
... return word + 's'
>>> plur('book', 2)
'books'
>>> plur('box', 2, convert=pluralize)
'boxes'
"""
return convert(word, plural(n))
[docs]def hsize(size, unit='B', step=1024, rounding=2, sep=' '):
"""
Given size in unit produce size with human-friendly units. This is a simple
formatting function that takes a value, a unit in which the value is
expressed, and the size of multiple (kilo, mega, giga, etc).
This function rounds values to 2 decimal places and does not handle
fractions. It also uses metric prefixes (K, M, G, etc) and only goes up to
Peta (P, quadrillion) prefix. The number of decimal places can be
customized using the ``rounding`` argument.
The size multiple (``step`` parameter) is 1024 by default, suitable for
expressing values related to size of data on disk.
The ``sep`` argument represents a separator between values and units.
Example::
>>> hsize(12)
'12.00 B'
>>> hsize(1030)
'1.01 KB'
>>> hsize(1536)
'1.50 KB'
>>> hsize(2097152)
'2.00 MB'
>>> hsize(12, sep='')
'12.00B'
"""
size = Decimal(size)
order = -1
while size > step:
size /= step
order += 1
if order < 0:
return '%.{}f%s%s'.format(rounding) % (round(size, rounding), sep,
unit)
return '%.{}f%s%s%s'.format(rounding) % (round(size, rounding), sep,
SIZES[order], unit)
[docs]def trunc(s, chars):
"""
Trucante string at ``n`` characters. This function hard-trucates a string
at specified number of characters and appends an elipsis to the end.
The truncating does not take into account words or markup. Elipsis is not
appended if the string is shorter than the specified number of characters.
::
>>> trunc('foobarbaz', 6)
'foobar...'
.. note::
Keep in mind that the trucated string is always 3 characters longer than
``n`` because of the appended elipsis.
"""
if len(s) <= chars:
return s
return s[:chars] + '...'
[docs]def yesno(val, yes='yes', no='no'):
"""
Return ``yes`` or ``no`` depending on value. This function takes the value
and returns either yes or no depending on whether the value evaluates to
``True``.
Examples::
>>> yesno(True)
'yes'
>>> yesno(False)
'no'
>>> yesno(True, 'available', 'not available')
'available'
"""
return yes if val else no
[docs]def strft(ts, fmt):
"""
Reformat string datestamp/timestamp. This function parses a string
representation of a date and/or time and reformats it using specified
format.
The format is standard strftime format used in Python's
``datetime.datetime.strftime()`` call.
Actual parsing of the input is delegated to
`python-dateutil <https://pypi.python.org/pypi/python-dateutil>`_ library.
"""
return parse(ts).strftime(fmt)
# HTML
def attr(name, value=None):
"""
Render HTML attribute. This function is used as part of :py:func:`~tag`
function to render HTML attributes, but can be used on its own as well. It
converts the value into Unicode string and sanitizes it before returning
the markup.
Basic usage may look like this::
>>> attr('src', '/images?src=foo.png&w=12')
'src="/images?src=foo.png&w=12"'
All attribute values are double-quoted and any double quotes found inside
the value are escaped. User-supplied values can be used reasonably safely.
If the value is ``None``, only the attribute name is rendered, otherwise a
normal 'attribute="value"' pair is returned::
>>> attr('src', None)
'src'
>>> attr('src', '')
'src=""'
Therefore, to suppress attribute values completely (i.e., not even have the
``=""`` part, use ``None`` as the value.
"""
if value is not None:
value = to_unicode(value)
value = attr_escape(value)
return '%s="%s"' % (name, value)
return name
[docs]def tag(name, content='', nonclosing=False, **attrs):
"""
Wraps content in a HTML tag with optional attributes. This function
provides a Pythonic interface for writing HTML tags with a few bells and
whistles.
The basic usage looks like this::
>>> tag('p', 'content', _class="note", _id="note1")
'<p class="note" id="note1">content</p>'
Any attribute names with any number of leading underscores (e.g., '_class')
will have the underscores strpped away.
If content is an iterable, the tag will be generated once per each member.
>>> tag('span', ['a', 'b', 'c'])
'<span>a</span><span>b</span><span>c</span>'
It does not sanitize the tag names, though, so it is possible to specify
invalid tag names::
>>> tag('not valid')
'<not valid></not valid>
.. warning::
Please ensure that ``name`` argument does not come from user-specified
data, or, if it does, that it is properly sanitized (best way is to use
a whitelist of allowed names).
Because attributes are specified using keyword arguments, which are then
treated as a dictionary, there is no guarantee of attribute order. If
attribute order is important, don't use this function.
This module contains a few partially applied aliases for this function.
These mostly have hard-wired first argument (tag name), and are all
uppercase:
- ``A`` - alias for ``<a>`` tag
- ``BUTTON`` - alias for ``<button>`` tag
- ``HIDDEN`` - alias for ``<input>`` tag with ``type="hidden"`` attribute
- ``INPUT`` - alias for ``<input>`` tag with ``nonclosing`` set to ``True``
- ``LI`` - alias for ``<li>`` tag
- ``OPTION`` - alias for ``<option>`` tag
- ``P`` - alias for ``<p>`` tag
- ``SELECT`` - alias for ``<select>`` tag
- ``SPAN`` - alias for ``<span>`` tag
- ``SUBMIT`` - alias for ``<button>`` tag with ``type="submit"`` attribute
- ``TEXTAREA`` - alias for ``<textarea>`` tag
- ``UL`` - alias for ``<ul>`` tag
"""
open_tag = '<%s>' % name
close_tag = '</%s>' % name
attrs = ' '.join([attr(k.lstrip('_'), to_unicode(v))
for k, v in attrs.items()])
if attrs:
open_tag = '<%s %s>' % (name, attrs)
if nonclosing:
content = ''
close_tag = ''
if not isinstance(content, basestring):
try:
return ''.join(['%s%s%s' % (open_tag, to_unicode(c), close_tag)
for c in content])
except TypeError:
pass
return '%s%s%s' % (open_tag, to_unicode(content), close_tag)
SPAN = functools.partial(tag, 'span')
UL = functools.partial(tag, 'ul')
LI = functools.partial(tag, 'li')
P = functools.partial(tag, 'p')
A = functools.partial(tag, 'a')
INPUT = functools.partial(tag, 'input', nonclosing=True)
BUTTON = functools.partial(tag, 'button')
SUBMIT = functools.partial(BUTTON, _type='submit')
HIDDEN = lambda n, v: INPUT(_name=n, value=v, _type='hidden')
TEXTAREA = functools.partial(tag, 'textarea')
BUTTON = functools.partial(tag, 'button')
OPTION = functools.partial(tag, 'option')
SELECT = functools.partial(tag, 'select')
[docs]def varea(name, values, **attrs):
"""
Render textarea with bound value. Textareas use a somewhat different markup
to that of regular inputs, so a separate function is used for binding
values to this form control.::
>>> varea('foo', {'foo': 'bar'})
'<textarea name="foo" id="foo">bar</textarea>'
This function works the same way as :py:func:`~vinput` function, so please
look at it for more information. The primary difference is in the generated
markup.
"""
attrs.setdefault('_id', name)
value = values.get(name)
if value is None:
return TEXTAREA(_name=name, **attrs)
return TEXTAREA(value, _name=name, **attrs)
[docs]def vcheckbox(name, value, values, default=False, **attrs):
"""
Render checkbox with bound value. This function renders a checkbox which is
checked or unchecked depending on whether its own name-value combination
appears in the provided form values dictionary.
Because there are many ways to think about checkboxes in general, this
particular function may or may not work for you. It treats checkboxes as a
list of alues which are all named the same.
Let's say we have markup that looks like this::
<input type="checkbox" name="foo" value="1">
<input type="checkbox" name="foo" value="2">
<input type="checkbox" name="foo" value="3">
If user checks all of them, we consider it a list ``foo=['1', '2', '3']``.
If user checks only the first and last, we have ``foo=['1', '3']``. And so
on.
This function assumes that you are using this pattern.
The ``values`` map can either map the checkbox name to a single value, or a
list of multiple values. In the former case, if the single value matches
the value of the checkbox, the checkbox is checked. In the latter case, if
value of the checkbox is found in the list of values, the checkbox is
checked.::
>>> vcheckbox('foo', 'bar', {'foo': 'bar'})
'<input type="checkbox" name="foo" id="foo" value="bar" checked>'
>>> vcheckbox('foo', 'bar', {'foo': ['foo', 'bar', 'baz']})
'<input type="checkbox" name="foo" id="foo" value="bar" checked>'
>>> vcheckbox('foo', 'bar', {'foo': ['foo', 'baz']})
'<input type="checkbox" name="foo" id="foo" value="bar">'
When the field values dictionary doesn't contain a key that matches the
checkbox name, the value of ``default`` keyword argument determines whether
the checkbox should be checked::
>>> vcheckbox('foo', 'bar', {}, default=True)
'<input type="checkbox" name="foo" id="foo" value="bar" checked>'
"""
attrs.setdefault('_id', name)
if name in values:
try:
values = values.getall(name)
except AttributeError:
values = values.get(name, [])
if isinstance(values, basestring):
if unicode(value) == unicode(values):
attrs['checked'] = None
elif unicode(value) in [unicode(v) for v in values]:
attrs['checked'] = None
elif default:
if default:
attrs['checked'] = None
elif 'checked' in attrs:
del attrs['checked']
return INPUT(_type='checkbox', _name=name, value=value, **attrs)
[docs]def vselect(name, choices, values, empty=None, **attrs):
"""
Render select list with bound value. This function renders the select list
with option elements with appropriate element selected based on field
values that are passed.
The values and labels for option elemnets are specified using an iterable
of two-tuples::
>>> vselect('foo', ((1, 'one'), (2, 'two'),), {})
'<select name="foo" id="foo"><option value="1">one</option><option...'
There is no mechanism for default value past what browsers support, so you
should generally assume that most browsers will render the select with
frist value preselected. Using an empty string or ``None`` as option value
will render an option element without value::
>>> vselect('foo', ((None, '---'), (1, 'one'),), {})
'<select name="foo" id="foo"><option value>---</option><option val...'
>>> vselect('foo', (('', '---'), (1, 'one'),), {})
'<select name="foo" id="foo"><option value="">---</option><option ...'
When specifying values, keep in mind that only ``None`` is special, in that
it will crete a ``value`` attribute without any value. All other Python
types become strings in the HTML markup, and are submitted as such. You
will need to convert the values back to their appropriate Python type
manually.
If the choices iterable does not contain an element representing the empty
value (``None``), you can specify it using the ``empty`` parameter. The
argument for ``empty`` should be a label, and the matching value is
``None``. The emtpy value is always inseted at the beginning of the list.
"""
attrs.setdefault('_id', name)
value = values.get(name)
options = []
for val, label in choices:
if unicode(val) == unicode(value):
options.append(OPTION(label, value=val, selected=None))
else:
options.append(OPTION(label, value=val))
if empty:
options.insert(0, OPTION(empty, value=None))
return SELECT(''.join(options), _name=name, **attrs)
[docs]def link_other(label, url, path, wrapper=SPAN, **kwargs):
"""
Only wrap label in anchor if given target URL, ``url``, does not match the
``path``. Given a label, this function will match the page URL against the
path to which the anchor should point, and generate the anchor element
markup as necessary. If the paths, match, ``wrapper`` will be used to
generate the markup around the label.
Any additional keyword arguments are passed to the function that generates
the anchor markup, which is :py:func:`~A` alias of the :py:func:`~tag`
function.
If the URLs match (meaning the page URL matches the target path), the label
will be passed to the wrapper function. The default wrapper function is
:py:func:`~SPAN`, so the label is wrapped in SPAN tag when the URLs
matches.::
>>> link_other('foo', '/here', '/there')
'<a href="/target">foo</a>'
>>> link_other('foo', '/there', '/there')
'<span>foo</span>'
You can customize the appearance of the label in the case URLs match by
customizing the wrapper::
>>> link_other('foo', '/there', '/there',
... wrapper=lambda l, **kw: l + 'bar')
'foobar'
Note that the wrapper lambda function has wild-card keyword arguments. The
wrapper function accepts the same extra keyword arguments that the anchor
function does, so if you have common classes and similar attributes, you
can specify them as extra keyword arguments and use any of the helper
functions in this module.::
>>> link_other('foo', '/here', '/there', wrapper=BUTTON, _class='cls')
'<a class="cls" href="/target">foo</a>'
>>> link_other('foo', '/there', '/there', wrapper=BUTTON, _class='cls')
'<button class="cls">foo</button>'
"""
if url == path:
return wrapper(label, **kwargs)
return A(label, href=url, **kwargs)
[docs]def quote_dict(mapping):
"""URL quote keys and values of the passed in dict-like object.
:param mapping: ``bottle.MultiDict`` or ``dict``-like object
:returns: dict with url quoted values
"""
try:
pairs = mapping.allitems()
except AttributeError:
pairs = mapping.items()
qdict = QueryDict()
for k, v in pairs:
qdict[urlquote(k)] = urlquote(v)
return qdict
[docs]def quoted_url(route, **params):
"""Return matching URL with it's query parameters quoted."""
return request.app.get_url(route, **quote_dict(params))
def full_url(path='', with_scheme=False):
"""
Return full URL with an optional scheme, the domain, the port if it's not a
typical http port, and an optional path.
Note that the path is added to the URL verbatim, therefore it must start
with a forward slash and must be a string.
"""
try:
domain, _ = request.urlparts.netloc.rsplit(':')
except ValueError:
domain = request.urlparts.netloc
if request.urlparts.port in HTTP_PORTS or request.urlparts.port is None:
port = ''
else:
port = ':' + str(request.urlparts.port)
scheme = str(request.urlparts.scheme + "://") if with_scheme else '//'
return ''.join([scheme, domain, port, path])
def to_qs(mapping):
"""
Convert a mapping object to query string appended to current path. This
function takes a ``bottle.MultiDict`` object or a ``dict``-like object that
supports ``items()`` call, and converts it to a query string appeneded to
the path in the current request context.
The values for each parameter is encoded as UTF-8 and escaped.
"""
qs = ['{}={}'.format(k, v) for k, v in quote_dict(mapping).allitems()]
return request.path + '?' + '&'.join(qs)
_to_qdict = lambda qs: QueryDict(qs) if isinstance(qs, basestring) else qs
[docs]def add_qparam(qs=None, **params):
"""
Add parameter to query string
If query string is omitted ``request.query_string`` is used.
Any keyword arguments passed to this function will be converted to query
parameters.
The returned object is a :py:class:`~QueryDict` instance, which is a
``bottle.MultiDict`` subclass.
Example::
>>> q = add_qparam('a=1', b=2)
>>> str(q)
'a=1&b=2'
>> q = add_qparam('a=1', a=2)
>>> str(q)
'a=1&a=2'
"""
qs = qs or request.query_string
qs = _to_qdict(qs)
return qs.add_qparam(**params)
[docs]def set_qparam(qs=None, **params):
"""
Replace or add parameters to query string
If query string is omitted ``request.query_string`` is used.
Any keyword arguments passed to this function will be converted to query
parameters.
The returned object is a :py:class:`~QueryDict` instance, which is a
``bottle.MultiDict`` subclass.
"""
qs = qs or request.query_string
qs = _to_qdict(qs)
return qs.set_qparam(**params)
[docs]def del_qparam(qs=None, *params):
"""
Remove query string parameters
If query string is ``None`` or empty, ``request.query_string`` is used.
Second and subsequent positional arguments are query parameter names to be
removed from the query string.
The returned object is a :py:class:`~QueryDict` instance, which is a
``bottle.MultiDict`` subclass.
"""
qs = qs or request.query_string
qs = _to_qdict(qs)
return qs.del_qparam(*params)
def perc_range(n, min_val, max_val, rounding=2):
"""
Return percentage of `n` within `min_val` to `max_val` range. The
``rounding`` argument is used to specify the number of decimal places to
include after the floating point.
Example::
>>> perc_range(40, 20, 60)
50
"""
return round(
min([1, max([0, n - min_val]) / (max_val - min_val)]) * 100, rounding)