25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

574 lines
19 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
  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], basestring):
  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, basestring):
  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, basestring):
  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, basestring):
  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, basestring):
  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. from_date = None
  246. to_date = None
  247. if f.value and isinstance(f.value, (list, tuple)):
  248. if len(f.value) >= 1: from_date = f.value[0]
  249. if len(f.value) >= 2: to_date = f.value[1]
  250. value = "'%s' AND '%s'" % (
  251. add_to_date(get_datetime(from_date),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"),
  252. get_datetime(to_date).strftime("%Y-%m-%d %H:%M:%S.%f"))
  253. fallback = "'0000-00-00 00:00:00'"
  254. elif df and df.fieldtype=="Date":
  255. value = getdate(f.value).strftime("%Y-%m-%d")
  256. fallback = "'0000-00-00'"
  257. elif (df and df.fieldtype=="Datetime") or isinstance(f.value, datetime):
  258. value = get_datetime(f.value).strftime("%Y-%m-%d %H:%M:%S.%f")
  259. fallback = "'0000-00-00 00:00:00'"
  260. elif df and df.fieldtype=="Time":
  261. value = get_time(f.value).strftime("%H:%M:%S.%f")
  262. fallback = "'00:00:00'"
  263. elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, basestring) and
  264. (not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
  265. value = "" if f.value==None else f.value
  266. fallback = '""'
  267. if f.operator.lower() in ("like", "not like") and isinstance(value, basestring):
  268. # because "like" uses backslash (\) for escaping
  269. value = value.replace("\\", "\\\\").replace("%", "%%")
  270. else:
  271. value = flt(f.value)
  272. fallback = 0
  273. # put it inside double quotes
  274. if isinstance(value, basestring) and not f.operator.lower() == 'between':
  275. value = '"{0}"'.format(frappe.db.escape(value, percent=False))
  276. if (self.ignore_ifnull
  277. or not can_be_null
  278. or (f.value and f.operator.lower() in ('=', 'like'))
  279. or 'ifnull(' in column_name.lower()):
  280. condition = '{column_name} {operator} {value}'.format(
  281. column_name=column_name, operator=f.operator,
  282. value=value)
  283. else:
  284. condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format(
  285. column_name=column_name, fallback=fallback, operator=f.operator,
  286. value=value)
  287. return condition
  288. def build_match_conditions(self, as_condition=True):
  289. """add match conditions if applicable"""
  290. self.match_filters = []
  291. self.match_conditions = []
  292. only_if_shared = False
  293. if not self.user:
  294. self.user = frappe.session.user
  295. if not self.tables: self.extract_tables()
  296. meta = frappe.get_meta(self.doctype)
  297. role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user)
  298. self.shared = frappe.share.get_shared(self.doctype, self.user)
  299. if not meta.istable and not role_permissions.get("read") and not self.flags.ignore_permissions:
  300. only_if_shared = True
  301. if not self.shared:
  302. frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
  303. else:
  304. self.conditions.append(self.get_share_condition())
  305. else:
  306. # apply user permissions?
  307. if role_permissions.get("apply_user_permissions", {}).get("read"):
  308. # get user permissions
  309. user_permissions = frappe.defaults.get_user_permissions(self.user)
  310. self.add_user_permissions(user_permissions,
  311. user_permission_doctypes=role_permissions.get("user_permission_doctypes").get("read"))
  312. if role_permissions.get("if_owner", {}).get("read"):
  313. self.match_conditions.append("`tab{0}`.owner = '{1}'".format(self.doctype,
  314. frappe.db.escape(self.user, percent=False)))
  315. if as_condition:
  316. conditions = ""
  317. if self.match_conditions:
  318. # will turn out like ((blog_post in (..) and blogger in (...)) or (blog_category in (...)))
  319. conditions = "((" + ") or (".join(self.match_conditions) + "))"
  320. doctype_conditions = self.get_permission_query_conditions()
  321. if doctype_conditions:
  322. conditions += (' and ' + doctype_conditions) if conditions else doctype_conditions
  323. # share is an OR condition, if there is a role permission
  324. if not only_if_shared and self.shared and conditions:
  325. conditions = "({conditions}) or ({shared_condition})".format(
  326. conditions=conditions, shared_condition=self.get_share_condition())
  327. return conditions
  328. else:
  329. return self.match_filters
  330. def get_share_condition(self):
  331. return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["'%s'"] * len(self.shared))) % \
  332. tuple([frappe.db.escape(s, percent=False) for s in self.shared])
  333. def add_user_permissions(self, user_permissions, user_permission_doctypes=None):
  334. user_permission_doctypes = frappe.permissions.get_user_permission_doctypes(user_permission_doctypes, user_permissions)
  335. meta = frappe.get_meta(self.doctype)
  336. for doctypes in user_permission_doctypes:
  337. match_filters = {}
  338. match_conditions = []
  339. # check in links
  340. for df in meta.get_fields_to_check_permissions(doctypes):
  341. user_permission_values = user_permissions.get(df.options, [])
  342. cond = 'ifnull(`tab{doctype}`.`{fieldname}`, "")=""'.format(doctype=self.doctype, fieldname=df.fieldname)
  343. if user_permission_values:
  344. if not cint(frappe.get_system_settings("apply_strict_user_permissions")):
  345. condition = cond + " or "
  346. else:
  347. condition = ""
  348. condition += """`tab{doctype}`.`{fieldname}` in ({values})""".format(
  349. doctype=self.doctype, fieldname=df.fieldname,
  350. values=", ".join([('"'+frappe.db.escape(v, percent=False)+'"') for v in user_permission_values]))
  351. else:
  352. condition = cond
  353. match_conditions.append("({condition})".format(condition=condition))
  354. match_filters[df.options] = user_permission_values
  355. if match_conditions:
  356. self.match_conditions.append(" and ".join(match_conditions))
  357. if match_filters:
  358. self.match_filters.append(match_filters)
  359. def get_permission_query_conditions(self):
  360. condition_methods = frappe.get_hooks("permission_query_conditions", {}).get(self.doctype, [])
  361. if condition_methods:
  362. conditions = []
  363. for method in condition_methods:
  364. c = frappe.call(frappe.get_attr(method), self.user)
  365. if c:
  366. conditions.append(c)
  367. return " and ".join(conditions) if conditions else None
  368. def run_custom_query(self, query):
  369. if '%(key)s' in query:
  370. query = query.replace('%(key)s', 'name')
  371. return frappe.db.sql(query, as_dict = (not self.as_list))
  372. def set_order_by(self, args):
  373. meta = frappe.get_meta(self.doctype)
  374. if self.order_by:
  375. args.order_by = self.order_by
  376. else:
  377. args.order_by = ""
  378. # don't add order by from meta if a mysql group function is used without group by clause
  379. group_function_without_group_by = (len(self.fields)==1 and
  380. ( self.fields[0].lower().startswith("count(")
  381. or self.fields[0].lower().startswith("min(")
  382. or self.fields[0].lower().startswith("max(")
  383. ) and not self.group_by)
  384. if not group_function_without_group_by:
  385. sort_field = sort_order = None
  386. if meta.sort_field and ',' in meta.sort_field:
  387. # multiple sort given in doctype definition
  388. # Example:
  389. # `idx desc, modified desc`
  390. # will covert to
  391. # `tabItem`.`idx` desc, `tabItem`.`modified` desc
  392. args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype,
  393. f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
  394. else:
  395. sort_field = meta.sort_field or 'modified'
  396. sort_order = (meta.sort_field and meta.sort_order) or 'desc'
  397. args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
  398. # draft docs always on top
  399. if meta.is_submittable:
  400. args.order_by = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.order_by)
  401. def validate_order_by_and_group_by(self, parameters):
  402. """Check order by, group by so that atleast one column is selected and does not have subquery"""
  403. if not parameters:
  404. return
  405. _lower = parameters.lower()
  406. if 'select' in _lower and ' from ' in _lower:
  407. frappe.throw(_('Cannot use sub-query in order by'))
  408. for field in parameters.split(","):
  409. if "." in field and field.strip().startswith("`tab"):
  410. tbl = field.strip().split('.')[0]
  411. if tbl not in self.tables:
  412. if tbl.startswith('`'):
  413. tbl = tbl[4:-1]
  414. frappe.throw(_("Please select atleast 1 column from {0} to sort/group").format(tbl))
  415. def add_limit(self):
  416. if self.limit_page_length:
  417. return 'limit %s, %s' % (self.limit_start, self.limit_page_length)
  418. else:
  419. return ''
  420. def add_comment_count(self, result):
  421. for r in result:
  422. if not r.name:
  423. continue
  424. r._comment_count = 0
  425. if "_comments" in r:
  426. r._comment_count = len(json.loads(r._comments or "[]"))
  427. def update_user_settings(self):
  428. # update user settings if new search
  429. user_settings = json.loads(get_user_settings(self.doctype))
  430. if hasattr(self, 'user_settings'):
  431. user_settings.update(self.user_settings)
  432. if self.save_user_settings_fields:
  433. user_settings['fields'] = self.user_settings_fields
  434. update_user_settings(self.doctype, user_settings)
  435. def get_order_by(doctype, meta):
  436. order_by = ""
  437. sort_field = sort_order = None
  438. if meta.sort_field and ',' in meta.sort_field:
  439. # multiple sort given in doctype definition
  440. # Example:
  441. # `idx desc, modified desc`
  442. # will covert to
  443. # `tabItem`.`idx` desc, `tabItem`.`modified` desc
  444. order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype,
  445. f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
  446. else:
  447. sort_field = meta.sort_field or 'modified'
  448. sort_order = (meta.sort_field and meta.sort_order) or 'desc'
  449. order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc")
  450. # draft docs always on top
  451. if meta.is_submittable:
  452. order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)
  453. return order_by