2019-04-15 22:42:28 +00:00
|
|
|
import json
|
2019-04-15 21:51:20 +00:00
|
|
|
import numbers
|
2019-04-15 22:42:28 +00:00
|
|
|
|
2019-05-04 02:15:14 +00:00
|
|
|
from .utils import detect_json1, escape_sqlite
|
2019-04-15 21:51:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Filter:
|
|
|
|
key = None
|
|
|
|
display = None
|
|
|
|
no_argument = False
|
|
|
|
|
|
|
|
def where_clause(self, table, column, value, param_counter):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
def human_clause(self, column, value):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
class TemplatedFilter(Filter):
|
2019-05-04 02:15:14 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
key,
|
|
|
|
display,
|
|
|
|
sql_template,
|
|
|
|
human_template,
|
|
|
|
format="{}",
|
|
|
|
numeric=False,
|
|
|
|
no_argument=False,
|
|
|
|
):
|
2019-04-15 21:51:20 +00:00
|
|
|
self.key = key
|
|
|
|
self.display = display
|
|
|
|
self.sql_template = sql_template
|
|
|
|
self.human_template = human_template
|
|
|
|
self.format = format
|
|
|
|
self.numeric = numeric
|
|
|
|
self.no_argument = no_argument
|
|
|
|
|
|
|
|
def where_clause(self, table, column, value, param_counter):
|
|
|
|
converted = self.format.format(value)
|
|
|
|
if self.numeric and converted.isdigit():
|
|
|
|
converted = int(converted)
|
|
|
|
if self.no_argument:
|
2019-05-04 02:15:14 +00:00
|
|
|
kwargs = {"c": column}
|
2019-04-15 21:51:20 +00:00
|
|
|
converted = None
|
|
|
|
else:
|
2019-05-04 02:15:14 +00:00
|
|
|
kwargs = {"c": column, "p": "p{}".format(param_counter), "t": table}
|
2019-04-15 21:51:20 +00:00
|
|
|
return self.sql_template.format(**kwargs), converted
|
|
|
|
|
|
|
|
def human_clause(self, column, value):
|
|
|
|
if callable(self.human_template):
|
|
|
|
template = self.human_template(column, value)
|
|
|
|
else:
|
|
|
|
template = self.human_template
|
|
|
|
if self.no_argument:
|
|
|
|
return template.format(c=column)
|
|
|
|
else:
|
|
|
|
return template.format(c=column, v=value)
|
|
|
|
|
|
|
|
|
2019-04-15 22:42:28 +00:00
|
|
|
class InFilter(Filter):
|
2019-05-04 02:15:14 +00:00
|
|
|
key = "in"
|
|
|
|
display = "in"
|
2019-04-15 22:42:28 +00:00
|
|
|
|
|
|
|
def split_value(self, value):
|
|
|
|
if value.startswith("["):
|
|
|
|
return json.loads(value)
|
|
|
|
else:
|
|
|
|
return [v.strip() for v in value.split(",")]
|
|
|
|
|
|
|
|
def where_clause(self, table, column, value, param_counter):
|
|
|
|
values = self.split_value(value)
|
|
|
|
params = [":p{}".format(param_counter + i) for i in range(len(values))]
|
|
|
|
sql = "{} in ({})".format(escape_sqlite(column), ", ".join(params))
|
|
|
|
return sql, values
|
|
|
|
|
|
|
|
def human_clause(self, column, value):
|
|
|
|
return "{} in {}".format(column, json.dumps(self.split_value(value)))
|
|
|
|
|
|
|
|
|
2019-04-15 21:51:20 +00:00
|
|
|
class Filters:
|
2019-05-04 02:15:14 +00:00
|
|
|
_filters = (
|
|
|
|
[
|
|
|
|
# key, display, sql_template, human_template, format=, numeric=, no_argument=
|
|
|
|
TemplatedFilter(
|
|
|
|
"exact",
|
|
|
|
"=",
|
|
|
|
'"{c}" = :{p}',
|
|
|
|
lambda c, v: "{c} = {v}" if v.isdigit() else '{c} = "{v}"',
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"not",
|
|
|
|
"!=",
|
|
|
|
'"{c}" != :{p}',
|
|
|
|
lambda c, v: "{c} != {v}" if v.isdigit() else '{c} != "{v}"',
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"contains",
|
|
|
|
"contains",
|
|
|
|
'"{c}" like :{p}',
|
|
|
|
'{c} contains "{v}"',
|
|
|
|
format="%{}%",
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"endswith",
|
|
|
|
"ends with",
|
|
|
|
'"{c}" like :{p}',
|
|
|
|
'{c} ends with "{v}"',
|
|
|
|
format="%{}",
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"startswith",
|
|
|
|
"starts with",
|
|
|
|
'"{c}" like :{p}',
|
|
|
|
'{c} starts with "{v}"',
|
|
|
|
format="{}%",
|
|
|
|
),
|
|
|
|
TemplatedFilter("gt", ">", '"{c}" > :{p}', "{c} > {v}", numeric=True),
|
|
|
|
TemplatedFilter(
|
|
|
|
"gte", "\u2265", '"{c}" >= :{p}', "{c} \u2265 {v}", numeric=True
|
|
|
|
),
|
|
|
|
TemplatedFilter("lt", "<", '"{c}" < :{p}', "{c} < {v}", numeric=True),
|
|
|
|
TemplatedFilter(
|
|
|
|
"lte", "\u2264", '"{c}" <= :{p}', "{c} \u2264 {v}", numeric=True
|
|
|
|
),
|
|
|
|
TemplatedFilter("like", "like", '"{c}" like :{p}', '{c} like "{v}"'),
|
|
|
|
TemplatedFilter("glob", "glob", '"{c}" glob :{p}', '{c} glob "{v}"'),
|
|
|
|
InFilter(),
|
|
|
|
]
|
|
|
|
+ (
|
|
|
|
[
|
|
|
|
TemplatedFilter(
|
|
|
|
"arraycontains",
|
|
|
|
"array contains",
|
|
|
|
"""rowid in (
|
2019-04-15 21:51:20 +00:00
|
|
|
select {t}.rowid from {t}, json_each({t}.{c}) j
|
|
|
|
where j.value = :{p}
|
2019-05-04 02:15:14 +00:00
|
|
|
)""",
|
|
|
|
'{c} contains "{v}"',
|
|
|
|
)
|
|
|
|
]
|
|
|
|
if detect_json1()
|
|
|
|
else []
|
|
|
|
)
|
|
|
|
+ [
|
2019-05-21 05:36:22 +00:00
|
|
|
TemplatedFilter(
|
|
|
|
"date", "date", 'date("{c}") = :{p}', '"{c}" is on date {v}'
|
|
|
|
),
|
2019-05-04 02:15:14 +00:00
|
|
|
TemplatedFilter(
|
|
|
|
"isnull", "is null", '"{c}" is null', "{c} is null", no_argument=True
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"notnull",
|
|
|
|
"is not null",
|
|
|
|
'"{c}" is not null',
|
|
|
|
"{c} is not null",
|
|
|
|
no_argument=True,
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"isblank",
|
|
|
|
"is blank",
|
|
|
|
'("{c}" is null or "{c}" = "")',
|
|
|
|
"{c} is blank",
|
|
|
|
no_argument=True,
|
|
|
|
),
|
|
|
|
TemplatedFilter(
|
|
|
|
"notblank",
|
|
|
|
"is not blank",
|
|
|
|
'("{c}" is not null and "{c}" != "")',
|
|
|
|
"{c} is not blank",
|
|
|
|
no_argument=True,
|
|
|
|
),
|
|
|
|
]
|
|
|
|
)
|
|
|
|
_filters_by_key = {f.key: f for f in _filters}
|
2019-04-15 21:51:20 +00:00
|
|
|
|
|
|
|
def __init__(self, pairs, units={}, ureg=None):
|
|
|
|
self.pairs = pairs
|
|
|
|
self.units = units
|
|
|
|
self.ureg = ureg
|
|
|
|
|
|
|
|
def lookups(self):
|
|
|
|
"Yields (lookup, display, no_argument) pairs"
|
|
|
|
for filter in self._filters:
|
|
|
|
yield filter.key, filter.display, filter.no_argument
|
|
|
|
|
|
|
|
def human_description_en(self, extra=None):
|
|
|
|
bits = []
|
|
|
|
if extra:
|
|
|
|
bits.extend(extra)
|
|
|
|
for column, lookup, value in self.selections():
|
|
|
|
filter = self._filters_by_key.get(lookup, None)
|
|
|
|
if filter:
|
|
|
|
bits.append(filter.human_clause(column, value))
|
|
|
|
# Comma separated, with an ' and ' at the end
|
|
|
|
and_bits = []
|
|
|
|
commas, tail = bits[:-1], bits[-1:]
|
|
|
|
if commas:
|
2019-05-04 02:15:14 +00:00
|
|
|
and_bits.append(", ".join(commas))
|
2019-04-15 21:51:20 +00:00
|
|
|
if tail:
|
|
|
|
and_bits.append(tail[0])
|
2019-05-04 02:15:14 +00:00
|
|
|
s = " and ".join(and_bits)
|
2019-04-15 21:51:20 +00:00
|
|
|
if not s:
|
2019-05-04 02:15:14 +00:00
|
|
|
return ""
|
|
|
|
return "where {}".format(s)
|
2019-04-15 21:51:20 +00:00
|
|
|
|
|
|
|
def selections(self):
|
|
|
|
"Yields (column, lookup, value) tuples"
|
|
|
|
for key, value in self.pairs:
|
2019-05-04 02:15:14 +00:00
|
|
|
if "__" in key:
|
|
|
|
column, lookup = key.rsplit("__", 1)
|
2019-04-15 21:51:20 +00:00
|
|
|
else:
|
|
|
|
column = key
|
2019-05-04 02:15:14 +00:00
|
|
|
lookup = "exact"
|
2019-04-15 21:51:20 +00:00
|
|
|
yield column, lookup, value
|
|
|
|
|
|
|
|
def has_selections(self):
|
|
|
|
return bool(self.pairs)
|
|
|
|
|
|
|
|
def convert_unit(self, column, value):
|
|
|
|
"If the user has provided a unit in the query, convert it into the column unit, if present."
|
|
|
|
if column not in self.units:
|
|
|
|
return value
|
|
|
|
|
|
|
|
# Try to interpret the value as a unit
|
|
|
|
value = self.ureg(value)
|
|
|
|
if isinstance(value, numbers.Number):
|
|
|
|
# It's just a bare number, assume it's the column unit
|
|
|
|
return value
|
|
|
|
|
|
|
|
column_unit = self.ureg(self.units[column])
|
|
|
|
return value.to(column_unit).magnitude
|
|
|
|
|
|
|
|
def build_where_clauses(self, table):
|
|
|
|
sql_bits = []
|
|
|
|
params = {}
|
|
|
|
i = 0
|
|
|
|
for column, lookup, value in self.selections():
|
|
|
|
filter = self._filters_by_key.get(lookup, None)
|
|
|
|
if filter:
|
2019-05-04 02:15:14 +00:00
|
|
|
sql_bit, param = filter.where_clause(
|
|
|
|
table, column, self.convert_unit(column, value), i
|
|
|
|
)
|
2019-04-15 21:51:20 +00:00
|
|
|
sql_bits.append(sql_bit)
|
|
|
|
if param is not None:
|
|
|
|
if not isinstance(param, list):
|
|
|
|
param = [param]
|
|
|
|
for individual_param in param:
|
2019-05-04 02:15:14 +00:00
|
|
|
param_id = "p{}".format(i)
|
2019-04-15 21:51:20 +00:00
|
|
|
params[param_id] = individual_param
|
|
|
|
i += 1
|
|
|
|
return sql_bits, params
|