Source code for flask_datatables.utils
# -*- coding: utf-8 -*-
# pylint: disable=unsubscriptable-object
"""Utilities & Auxiliaries
=============================
We provided some auxiliary functions for ``Flask-DataTables``.
* :func:`~flask_datatables.utils.render_macro` renders a given
``macro`` from the Jinja templates
* :func:`~flask_datatables.utils.prepare_response` is the default
built-in method for ``factory`` parameter of
:meth:`Model.search <flask_datatables.model.Model.search>`
* :func:`~flask_datatables.utils.parse_request` is the utility
function to parse `DataTables`_ client-side query parameters
from the URL
.. _DataTables: https://datatables.net/
"""
import contextlib
import urllib.parse
from typing import TYPE_CHECKING, cast
import flask
import peewee
import werkzeug.datastructures
import werkzeug.exceptions
if TYPE_CHECKING:
from typing import Any, Callable, List, Optional, Union
from jinja2 import Environment
from peewee import Model
from werkzeug.datastructures import ImmutableMultiDict
from .typing import ObjectData, Query
__all__ = [
'render_macro',
'prepare_response',
'parse_request',
]
[docs]
def render_macro(template_name_or_list: 'Union[str, List[str]]', macro: str, **context: 'Any') -> str:
"""Evaluates and renders a **macro** from the template.
Args:
template_name_or_list: The name of the template to be rendered, or an iterable with template names
the first one existing will be rendered.
macro: The name of macro to be called.
Keyword Args:
**context: The variables that should be available in the context of the template.
Returns:
The rendered macro.
"""
template = cast('Environment', flask.current_app.jinja_env).get_or_select_template(template_name_or_list) # type: ignore[arg-type] # pylint: disable=line-too-long
macro_func = getattr(template.module, macro)
return macro_func(**context)
[docs]
def prepare_response(template: 'Union[str, List[str]]') -> 'Callable[[Model], ObjectData]':
"""Prepare response object data.
The function returns a wrapper function to use the ``template`` as a factory to
render HTML response blocks. The Jinja templates should have **macro** blocks
for each target field named after ``render_{field_name}`` and takes only one
argument ``record`` as the selected data model record.
Args:
template: Path to the macro template.
Returns:
Prepared response object data.
See Also:
See :func:`flask_datatables.utils.render_macro` for more information.
"""
def wrapper(record: peewee.Model) -> 'ObjectData':
data = {} # type: ObjectData
for field in record.__data__.keys():
try:
data[field] = render_macro(template, f'render_{field}', record=record) # type: ignore[misc]
except Exception:
data[field] = getattr(record, field) # type: ignore[misc]
return data
return wrapper
def _parse_int(arg: 'Optional[str]') -> int:
"""Parse argument as :obj:`int`.
Args:
arg: Original request argument.
Returns:
Parsed query argument.
"""
if arg is not None:
with contextlib.suppress(Exception):
return int(arg)
return -1
def _parse_bool(arg: 'Optional[str]') -> bool:
"""Parse argument as :obj:`bool`.
Args:
arg: Original request argument.
Returns:
Parsed query argument.
"""
if isinstance(arg, str):
arg = arg.casefold()
if arg == 'true':
return True
if arg == 'false':
return False
return False
def _parse_str(arg: 'Optional[str]') -> str:
"""Parse argument as :obj:`str`.
Args:
arg: Original request argument.
Returns:
Parsed query argument.
"""
if arg is None:
return ''
return arg
[docs]
def parse_request(args: 'Optional[ImmutableMultiDict]' = None) -> 'Query':
"""Parse :attr:`flask.request.args <flask.Request.args>` as :class:`~tekid.ext.datatables.Query`.
Args:
args: Original request arguments. The default value is inferred from
:attr:`request.args <flask.Request.args>`.
Returns:
Parsed query dictionary.
"""
if args is None:
args = flask.request.args
query = {
'draw': _parse_int(args.get('draw')),
'columns': [],
'order': [],
'start': _parse_int(args.get('start')),
'length': _parse_int(args.get('length')),
'search': {
'value': _parse_str(args.get('search[value]')),
'regex': _parse_bool(args.get('search[regex]')),
},
'_': _parse_int(args.get('_')),
} # type: Query
index = 0
while True:
try:
data = args[f'columns[{index}][data]']
except werkzeug.exceptions.BadRequestKeyError:
break
query['columns'].append({
'data': _parse_str(data),
'name': _parse_str(args.get(f'columns[{index}][data]')),
'searchable': _parse_bool(args.get(f'columns[{index}][searchable]')),
'orderable': _parse_bool(args.get(f'columns[{index}][orderable]')),
'search': {
'value': _parse_str(args.get(f'columns[{index}][search][value]')),
'regex': _parse_bool(args.get(f'columns[{index}][search][regex]')),
},
})
index += 1
index = 0
while True:
try:
column = args[f'order[{index}][column]']
except werkzeug.exceptions.BadRequestKeyError:
break
query['order'].append({
'column': _parse_int(column),
'dir': _parse_str(args.get(f'order[{index}][dir]')), # type: ignore[typeddict-item]
})
index += 1
return query
def build_cache(query_string: 'Optional[str]' = None) -> str:
"""Build a key to cache the query parameters.
Args:
query_string: Query parameters in string form. The default value is inferred
from :attr:`request.query_string <flask.Request.query_string>`.
Returns:
A string literal representing the query parameters.
"""
if query_string is None:
query_string = flask.request.query_string.decode()
query_parsed = urllib.parse.parse_qsl(query_string)
query = werkzeug.datastructures.MultiDict(query_parsed).to_dict()
if 'draw' in query:
del query['draw']
query_sorted = sorted(query.items(), key=lambda kv: kv[0])
return urllib.parse.urlencode(query_sorted)