Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

553 řádky
18 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # MIT License. See license.txt
  3. from __future__ import unicode_literals
  4. """build query for doclistview and return results"""
  5. import frappe, json, copy
  6. import frappe.defaults
  7. import frappe.share
  8. import frappe.permissions
  9. from frappe.utils import flt, cint, getdate, get_datetime, get_time, make_filter_tuple, get_filter, add_to_date
  10. from frappe import _
  11. from frappe.model import optional_fields
  12. from frappe.model.utils.user_settings import get_user_settings, update_user_settings
  13. from datetime import datetime
  14. class DatabaseQuery(object):
  15. def __init__(self, doctype):
  16. self.doctype = doctype
  17. self.tables = []
  18. self.conditions = []
  19. self.or_conditions = []
  20. self.fields = None
  21. self.user = None
  22. self.ignore_ifnull = False
  23. self.flags = frappe._dict()
  24. def execute(self, query=None, fields=None, filters=None, or_filters=None,
  25. docstatus=None, group_by=None, order_by=None, limit_start=False,
  26. limit_page_length=None, as_list=False, with_childnames=False, debug=False,
  27. ignore_permissions=False, user=None, with_comment_count=False,
  28. join='left join', distinct=False, start=None, page_length=None, limit=None,
  29. ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
  30. update=None, add_total_row=None, user_settings=None):
  31. if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user):
  32. raise frappe.PermissionError, self.doctype
  33. # fitlers and fields swappable
  34. # its hard to remember what comes first
  35. if (isinstance(fields, dict)
  36. or (isinstance(fields, list) and fields and isinstance(fields[0], list))):
  37. # if fields is given as dict/list of list, its probably filters
  38. filters, fields = fields, filters
  39. elif fields and isinstance(filters, list) \
  40. and len(filters) > 1 and isinstance(filters[0], basestring):
  41. # if `filters` is a list of strings, its probably fields
  42. filters, fields = fields, filters
  43. if fields:
  44. self.fields = fields
  45. else:
  46. self.fields = ["`tab{0}`.`name`".format(self.doctype)]
  47. if start: limit_start = start
  48. if page_length: limit_page_length = page_length
  49. if limit: limit_page_length = limit
  50. self.filters = filters or []
  51. self.or_filters = or_filters or []
  52. self.docstatus = docstatus or []
  53. self.group_by = group_by
  54. self.order_by = order_by
  55. self.limit_start = 0 if (limit_start is False) else cint(limit_start)
  56. self.limit_page_length = cint(limit_page_length) if limit_page_length else None
  57. self.with_childnames = with_childnames
  58. self.debug = debug
  59. self.join = join
  60. self.distinct = distinct
  61. self.as_list = as_list
  62. self.ignore_ifnull = ignore_ifnull
  63. self.flags.ignore_permissions = ignore_permissions
  64. self.user = user or frappe.session.user
  65. self.update = update
  66. self.user_settings_fields = copy.deepcopy(self.fields)
  67. #self.debug = True
  68. if user_settings:
  69. self.user_settings = json.loads(user_settings)
  70. if query:
  71. result = self.run_custom_query(query)
  72. else:
  73. result = self.build_and_run()
  74. if with_comment_count and not as_list and self.doctype:
  75. self.add_comment_count(result)
  76. if save_user_settings:
  77. self.save_user_settings_fields = save_user_settings_fields
  78. self.update_user_settings()
  79. return result
  80. def build_and_run(self):
  81. args = self.prepare_args()
  82. args.limit = self.add_limit()
  83. if args.conditions:
  84. args.conditions = "where " + args.conditions
  85. if self.distinct:
  86. args.fields = 'distinct ' + args.fields
  87. query = """select %(fields)s from %(tables)s %(conditions)s
  88. %(group_by)s %(order_by)s %(limit)s""" % args
  89. return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, update=self.update)
  90. def prepare_args(self):
  91. self.parse_args()
  92. self.extract_tables()
  93. self.set_optional_columns()
  94. self.build_conditions()
  95. args = frappe._dict()
  96. if self.with_childnames:
  97. for t in self.tables:
  98. if t != "`tab" + self.doctype + "`":
  99. self.fields.append(t + ".name as '%s:name'" % t[4:-1])
  100. # query dict
  101. args.tables = self.tables[0]
  102. # left join parent, child tables
  103. for child in self.tables[1:]:
  104. args.tables += " {join} {child} on ({child}.parent = {main}.name)".format(join=self.join,
  105. child=child, main=self.tables[0])
  106. if self.grouped_or_conditions:
  107. self.conditions.append("({0})".format(" or ".join(self.grouped_or_conditions)))
  108. args.conditions = ' and '.join(self.conditions)
  109. if self.or_conditions:
  110. args.conditions += (' or ' if args.conditions else "") + \
  111. ' or '.join(self.or_conditions)
  112. self.set_field_tables()
  113. args.fields = ', '.join(self.fields)
  114. self.set_order_by(args)
  115. self.validate_order_by_and_group_by(args.order_by)
  116. args.order_by = args.order_by and (" order by " + args.order_by) or ""
  117. self.validate_order_by_and_group_by(self.group_by)
  118. args.group_by = self.group_by and (" group by " + self.group_by) or ""
  119. return args
  120. def parse_args(self):
  121. """Convert fields and filters from strings to list, dicts"""
  122. if isinstance(self.fields, basestring):
  123. if self.fields == "*":
  124. self.fields = ["*"]
  125. else:
  126. try:
  127. self.fields = json.loads(self.fields)
  128. except ValueError:
  129. self.fields = [f.strip() for f in self.fields.split(",")]
  130. for filter_name in ["filters", "or_filters"]:
  131. filters = getattr(self, filter_name)
  132. if isinstance(filters, basestring):
  133. filters = json.loads(filters)
  134. if isinstance(filters, dict):
  135. fdict = filters
  136. filters = []
  137. for key, value in fdict.iteritems():
  138. filters.append(make_filter_tuple(self.doctype, key, value))
  139. setattr(self, filter_name, filters)
  140. def extract_tables(self):
  141. """extract tables from fields"""
  142. self.tables = ['`tab' + self.doctype + '`']
  143. # add tables from fields
  144. if self.fields:
  145. for f in self.fields:
  146. if ( not ("tab" in f and "." in f) ) or ("locate(" in f): continue
  147. table_name = f.split('.')[0]
  148. if table_name.lower().startswith('group_concat('):
  149. table_name = table_name[13:]
  150. if table_name.lower().startswith('ifnull('):
  151. table_name = table_name[7:]
  152. if not table_name[0]=='`':
  153. table_name = '`' + table_name + '`'
  154. if not table_name in self.tables:
  155. self.append_table(table_name)
  156. def append_table(self, table_name):
  157. self.tables.append(table_name)
  158. doctype = table_name[4:-1]
  159. if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)):
  160. raise frappe.PermissionError, doctype
  161. def set_field_tables(self):
  162. '''If there are more than one table, the fieldname must not be ambigous.
  163. If the fieldname is not explicitly mentioned, set the default table'''
  164. if len(self.tables) > 1:
  165. for i, f in enumerate(self.fields):
  166. if '.' not in f:
  167. self.fields[i] = '{0}.{1}'.format(self.tables[0], f)
  168. def set_optional_columns(self):
  169. """Removes optional columns like `_user_tags`, `_comments` etc. if not in table"""
  170. columns = frappe.db.get_table_columns(self.doctype)
  171. # remove from fields
  172. to_remove = []
  173. for fld in self.fields:
  174. for f in optional_fields:
  175. if f in fld and not f in columns:
  176. to_remove.append(fld)
  177. for fld in to_remove:
  178. del self.fields[self.fields.index(fld)]
  179. # remove from filters
  180. to_remove = []
  181. for each in self.filters:
  182. if isinstance(each, basestring):
  183. each = [each]
  184. for element in each:
  185. if element in optional_fields and element not in columns:
  186. to_remove.append(each)
  187. for each in to_remove:
  188. if isinstance(self.filters, dict):
  189. del self.filters[each]
  190. else:
  191. self.filters.remove(each)
  192. def build_conditions(self):
  193. self.conditions = []
  194. self.grouped_or_conditions = []
  195. self.build_filter_conditions(self.filters, self.conditions)
  196. self.build_filter_conditions(self.or_filters, self.grouped_or_conditions)
  197. # match conditions
  198. if not self.flags.ignore_permissions:
  199. match_conditions = self.build_match_conditions()
  200. if match_conditions:
  201. self.conditions.append("(" + match_conditions + ")")
  202. def build_filter_conditions(self, filters, conditions):
  203. """build conditions from user filters"""
  204. if isinstance(filters, dict):
  205. filters = [filters]
  206. for f in filters:
  207. if isinstance(f, basestring):
  208. conditions.append(f)
  209. else:
  210. conditions.append(self.prepare_filter_condition(f))
  211. def prepare_filter_condition(self, f):
  212. """Returns a filter condition in the format:
  213. ifnull(`tabDocType`.`fieldname`, fallback) operator "value"
  214. """
  215. f = get_filter(self.doctype, f)
  216. tname = ('`tab' + f.doctype + '`')
  217. if not tname in self.tables:
  218. self.append_table(tname)
  219. if 'ifnull(' in f.fieldname:
  220. column_name = f.fieldname
  221. else:
  222. column_name = '{tname}.{fname}'.format(tname=tname,
  223. fname=f.fieldname)
  224. can_be_null = True
  225. # prepare in condition
  226. if f.operator in ('in', 'not in'):
  227. values = f.value
  228. if not isinstance(values, (list, tuple)):
  229. values = values.split(",")
  230. fallback = "''"
  231. value = (frappe.db.escape((v or '').strip(), percent=False) for v in values)
  232. value = '("{0}")'.format('", "'.join(value))
  233. else:
  234. df = frappe.get_meta(f.doctype).get("fields", {"fieldname": f.fieldname})
  235. df = df[0] if df else None
  236. if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
  237. can_be_null = False
  238. if f.operator=='Between' and \
  239. (f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))):
  240. value = "'%s' AND '%s'" % (
  241. get_datetime(f.value[0]).strftime("%Y-%m-%d %H:%M:%S.%f"),
  242. add_to_date(get_datetime(f.value[1]),days=1).strftime("%Y-%m-%d %H:%M:%S.%f"))
  243. fallback = "'0000-00-00 00:00:00'"
  244. elif df and df.fieldtype=="Date":
  245. value = getdate(f.value).strftime("%Y-%m-%d")
  246. fallback = "'0000-00-00'"
  247. elif (df and df.fieldtype=="Datetime") or isinstance(f.value, datetime):
  248. value = get_datetime(f.value).strftime("%Y-%m-%d %H:%M:%S.%f")
  249. fallback = "'0000-00-00 00:00:00'"
  250. elif df and df.fieldtype=="Time":
  251. value = get_time(f.value).strftime("%H:%M:%S.%f")
  252. fallback = "'00:00:00'"
  253. elif f.operator in ("like", "not like") or (isinstance(f.value, basestring) and
  254. (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
  255. value = "" if f.value==None else f.value
  256. fallback = '""'
  257. if f.operator in ("like", "not like") and isinstance(value, basestring):
  258. # because "like" uses backslash (\) for escaping
  259. value = value.replace("\\", "\\\\").replace("%", "%%")
  260. else:
  261. value = flt(f.value)
  262. fallback = 0
  263. # put it inside double quotes
  264. if isinstance(value, basestring) and not f.operator=='Between':
  265. value = '"{0}"'.format(frappe.db.escape(value, percent=False))
  266. if (self.ignore_ifnull
  267. or not can_be_null
  268. or (f.value and f.operator in ('=', 'like'))
  269. or 'ifnull(' in column_name.lower()):
  270. condition = '{column_name} {operator} {value}'.format(
  271. column_name=column_name, operator=f.operator,
  272. value=value)
  273. else:
  274. condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format(
  275. column_name=column_name, fallback=fallback, operator=f.operator,
  276. value=value)
  277. return condition
  278. def build_match_conditions(self, as_condition=True):
  279. """add match conditions if applicable"""
  280. self.match_filters = []
  281. self.match_conditions = []
  282. only_if_shared = False
  283. if not self.user:
  284. self.user = frappe.session.user
  285. if not self.tables: self.extract_tables()
  286. meta = frappe.get_meta(self.doctype)
  287. role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user)
  288. self.shared = frappe.share.get_shared(self.doctype, self.user)
  289. if not meta.istable and not role_permissions.get("read") and not self.flags.ignore_permissions:
  290. only_if_shared = True
  291. if not self.shared:
  292. frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
  293. else:
  294. self.conditions.append(self.get_share_condition())
  295. else:
  296. # apply user permissions?
  297. if role_permissions.get("apply_user_permissions", {}).get("read"):
  298. # get user permissions
  299. user_permissions = frappe.defaults.get_user_permissions(self.user)
  300. self.add_user_permissions(user_permissions,
  301. user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read"))
  302. if role_permissions.get("if_owner", {}).get("read"):
  303. self.match_conditions.append("`tab{0}`.owner = '{1}'".format(self.doctype,
  304. frappe.db.escape(self.user, percent=False)))
  305. if as_condition:
  306. conditions = ""
  307. if self.match_conditions:
  308. # will turn out like ((blog_post in (..) and blogger in (...)) or (blog_category in (...)))
  309. conditions = "((" + ") or (".join(self.match_conditions) + "))"
  310. doctype_conditions = self.get_permission_query_conditions()
  311. if doctype_conditions:
  312. conditions += (' and ' + doctype_conditions) if conditions else doctype_conditions
  313. # share is an OR condition, if there is a role permission
  314. if not only_if_shared and self.shared and conditions:
  315. conditions = "({conditions}) or ({shared_condition})".format(
  316. conditions=conditions, shared_condition=self.get_share_condition())
  317. return conditions
  318. else:
  319. return self.match_filters
  320. def get_share_condition(self):
  321. return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["'%s'"] * len(self.shared))) % \
  322. tuple([frappe.db.escape(s, percent=False) for s in self.shared])
  323. def add_user_permissions(self, user_permissions, user_permission_doctypes=None):
  324. user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions)
  325. meta = frappe.get_meta(self.doctype)
  326. for doctypes in user_permission_doctypes:
  327. match_filters = {}
  328. match_conditions = []
  329. # check in links
  330. for df in meta.get_fields_to_check_permissions(doctypes):
  331. user_permission_values = user_permissions.get(df.options, [])
  332. condition = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname)
  333. if user_permission_values:
  334. condition += """ or `tab{doctype}`.`{fieldname}` in ({values})""".format(
  335. doctype=self.doctype, fieldname=df.fieldname,
  336. values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values])
  337. )
  338. match_conditions.append("({condition})".format(condition=condition))
  339. match_filters[df.options] = user_permission_values
  340. if match_conditions:
  341. self.match_conditions.append(" and ".join(match_conditions))
  342. if match_filters:
  343. self.match_filters.append(match_filters)
  344. def get_permission_query_conditions(self):
  345. condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, [])
  346. if condition_methods:
  347. conditions = []
  348. for method in condition_methods:
  349. c = frappe.call(frappe.get_attr(method), self.user)
  350. if c:
  351. conditions.append(c)
  352. return " and ".join(conditions) if conditions else None
  353. def run_custom_query(self, query):
  354. if '%(key)s' in query:
  355. query = query.replace('%(key)s', 'name')
  356. return frappe.db.sql(query, as_dict = (not self.as_list))
  357. def set_order_by(self, args):
  358. meta = frappe.get_meta(self.doctype)
  359. if self.order_by:
  360. args.order_by = self.order_by
  361. else:
  362. args.order_by = ""
  363. # don't add order by from meta if a mysql group function is used without group by clause
  364. group_function_without_group_by = (len(self.fields)==1 and
  365. ( self.fields[0].lower().startswith("count(")
  366. or self.fields[0].lower().startswith("min(")
  367. or self.fields[0].lower().startswith("max(")
  368. ) and not self.group_by)
  369. if not group_function_without_group_by:
  370. sort_field = sort_order = None
  371. if meta.sort_field and ',' in meta.sort_field:
  372. # multiple sort given in doctype definition
  373. # Example:
  374. # `idx desc, modified desc`
  375. # will covert to
  376. # `tabItem`.`idx` desc, `tabItem`.`modified` desc
  377. args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype,
  378. f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
  379. else:
  380. sort_field = meta.sort_field or 'modified'
  381. sort_order = (meta.sort_field and meta.sort_order) or 'desc'
  382. args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
  383. # draft docs always on top
  384. if meta.is_submittable:
  385. args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by)
  386. def validate_order_by_and_group_by(self, parameters):
  387. """Check order by, group by so that atleast one column is selected and does not have subquery"""
  388. if not parameters:
  389. return
  390. _lower = parameters.lower()
  391. if 'select' in _lower and ' from ' in _lower:
  392. frappe.throw(_('Cannot use sub-query in order by'))
  393. for field in parameters.split(","):
  394. if "." in field and field.strip().startswith("`tab"):
  395. tbl = field.strip().split('.')[0]
  396. if tbl not in self.tables:
  397. if tbl.startswith('`'):
  398. tbl = tbl[4:-1]
  399. frappe.throw(_("Please select atleast 1 column from {0} to sort/group").format(tbl))
  400. def add_limit(self):
  401. if self.limit_page_length:
  402. return 'limit %s, %s' % (self.limit_start, self.limit_page_length)
  403. else:
  404. return ''
  405. def add_comment_count(self, result):
  406. for r in result:
  407. if not r.name:
  408. continue
  409. r._comment_count = 0
  410. if "_comments" in r:
  411. r._comment_count = len(json.loads(r._comments or "[]"))
  412. def update_user_settings(self):
  413. # update user settings if new search
  414. user_settings = json.loads(get_user_settings(self.doctype))
  415. if hasattr(self, 'user_settings'):
  416. user_settings.update(self.user_settings)
  417. if self.save_user_settings_fields:
  418. user_settings['fields'] = self.user_settings_fields
  419. update_user_settings(self.doctype, user_settings)
  420. def get_order_by(doctype, meta):
  421. order_by = ""
  422. sort_field = sort_order = None
  423. if meta.sort_field and ',' in meta.sort_field:
  424. # multiple sort given in doctype definition
  425. # Example:
  426. # `idx desc, modified desc`
  427. # will covert to
  428. # `tabItem`.`idx` desc, `tabItem`.`modified` desc
  429. order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype,
  430. f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
  431. else:
  432. sort_field = meta.sort_field or 'modified'
  433. sort_order = (meta.sort_field and meta.sort_order) or 'desc'
  434. order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc")
  435. # draft docs always on top
  436. if meta.is_submittable:
  437. order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)
  438. return order_by