Source code for bottle_utils.html

# -*- 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&amp;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 vinput(name, values, **attrs): """ Render input with bound value. This function can be used to bind values to form inputs. By default it will result in HTML markup for a generic input. The generated input has a ``name`` attribute set to specified name, and an ``id`` attribute that has the same value. :: >>> vinput('foo', {}) '<input name="foo" id="foo">' If the supplied dictionary of field values contains a key that matches the specified name (case-sensitive), the value of that key will be used as the value of the input:: >>> vinput('foo', {'foo': 'bar'}) '<input name="foo" id="foo" value="bar">' All values are properly sanitized before they are added to the markup. Any additional keyword arguments that are passed to this function are passed on the :py:func:`~tag` function. Since the generated input markup is for generic text input, some of the other usual input types can be specified using ``_type`` parameter:: >>> input('foo', {}, _type='email') '<input name="foo" id="foo" type="email">' """ attrs.setdefault('_id', name) value = values.get(name) if value is None: return INPUT(_name=name, **attrs) return INPUT(_name=name, value=value, **attrs)
[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 form(method=None, action=None, csrf=False, multipart=False, **attrs): """ Render open form tag. This function renders the open form tag with additional features, such as faux HTTP methods, CSRF token, and multipart support. All parameters are optional. Using this function without any argument has the same effect as naked form tag without any attributes. Method names can be either lowercase or uppercase. The methods other than GET and POST are faked using a hidden input with ``_method`` name and uppercase name of the HTTP method. The form will use POST method in this case. Server-side support is required for this feature to work. Any additional keyword arguments will be used as attributes for the form tag. """ method = method and method.upper() if not method: faux_method = False elif method in ['GET', 'POST']: attrs['method'] = method faux_method = False else: attrs['method'] = 'POST' faux_method = True if multipart: attrs['enctype'] = 'multipart/form-data' if action is not None: attrs['action'] = action s = tag('form', nonclosing=True, **attrs) if faux_method: s += HIDDEN('_method', method) if csrf: # Import csrf_tag here to avoid circular dependency, since the csrf # module uses functions from this module from .csrf import csrf_tag s += csrf_tag() return s
[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)