您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

632 行
20 KiB

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