Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 
 

330 rindas
9.7 KiB

  1. # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: GNU General Public License v3. See license.txt
  3. from __future__ import unicode_literals
  4. import frappe
  5. import re
  6. from frappe.utils import cint, strip_html_tags
  7. from frappe.model.base_document import get_controller
  8. def setup_global_search_table():
  9. '''Creates __global_seach table'''
  10. if not '__global_search' in frappe.db.get_tables():
  11. frappe.db.sql('''create table __global_search(
  12. doctype varchar(100),
  13. name varchar(140),
  14. title varchar(140),
  15. content text,
  16. fulltext(content),
  17. route varchar(140),
  18. published int(1) not null default 0,
  19. unique `doctype_name` (doctype, name))
  20. COLLATE=utf8mb4_unicode_ci
  21. ENGINE=MyISAM
  22. CHARACTER SET=utf8mb4''')
  23. def reset():
  24. '''Deletes all data in __global_search'''
  25. frappe.db.sql('delete from __global_search')
  26. def get_doctypes_with_global_search(with_child_tables=True):
  27. '''Return doctypes with global search fields'''
  28. def _get():
  29. global_search_doctypes = []
  30. filters = {}
  31. if not with_child_tables:
  32. filters = {"istable": ["!=", 1], "issingle": ["!=", 1]}
  33. for d in frappe.get_all('DocType', fields=['name', 'module'], filters=filters):
  34. meta = frappe.get_meta(d.name)
  35. if len(meta.get_global_search_fields()) > 0:
  36. global_search_doctypes.append(d)
  37. installed_apps = frappe.get_installed_apps()
  38. doctypes = [d.name for d in global_search_doctypes
  39. if frappe.local.module_app[frappe.scrub(d.module)] in installed_apps]
  40. return doctypes
  41. return frappe.cache().get_value('doctypes_with_global_search', _get)
  42. def rebuild_for_doctype(doctype):
  43. '''Rebuild entries of doctype's documents in __global_search on change of
  44. searchable fields
  45. :param doctype: Doctype '''
  46. def _get_filters():
  47. filters = frappe._dict({ "docstatus": ["!=", 2] })
  48. if meta.has_field("enabled"):
  49. filters.enabled = 1
  50. if meta.has_field("disabled"):
  51. filters.disabled = 0
  52. return filters
  53. meta = frappe.get_meta(doctype)
  54. if cint(meta.istable) == 1:
  55. parent_doctypes = frappe.get_all("DocField", fields="parent", filters={
  56. "fieldtype": "Table",
  57. "options": doctype
  58. })
  59. for p in parent_doctypes:
  60. rebuild_for_doctype(p.parent)
  61. return
  62. # Delete records
  63. delete_global_search_records_for_doctype(doctype)
  64. parent_search_fields = meta.get_global_search_fields()
  65. fieldnames = get_selected_fields(meta, parent_search_fields)
  66. # Get all records from parent doctype table
  67. all_records = frappe.get_all(doctype, fields=fieldnames, filters=_get_filters())
  68. # Children data
  69. all_children, child_search_fields = get_children_data(doctype, meta)
  70. all_contents = []
  71. for doc in all_records:
  72. content = []
  73. for field in parent_search_fields:
  74. value = doc.get(field.fieldname)
  75. if value:
  76. content.append(get_formatted_value(value, field))
  77. # get children data
  78. for child_doctype, records in all_children.get(doc.name, {}).items():
  79. for field in child_search_fields.get(child_doctype):
  80. for r in records:
  81. if r.get(field.fieldname):
  82. content.append(get_formatted_value(r.get(field.fieldname), field))
  83. if content:
  84. # if doctype published in website, push title, route etc.
  85. published = 0
  86. title, route = "", ""
  87. try:
  88. if hasattr(get_controller(doctype), "is_website_published") and meta.allow_guest_to_view:
  89. d = frappe.get_doc(doctype, doc.name)
  90. published = 1 if d.is_website_published() else 0
  91. title = d.get_title()
  92. route = d.get("route")
  93. except ImportError:
  94. # some doctypes has been deleted via future patch, hence controller does not exists
  95. pass
  96. all_contents.append({
  97. "doctype": frappe.db.escape(doctype),
  98. "name": frappe.db.escape(doc.name),
  99. "content": frappe.db.escape(' ||| '.join(content or '')),
  100. "published": published,
  101. "title": frappe.db.escape(title or ''),
  102. "route": frappe.db.escape(route or '')
  103. })
  104. if all_contents:
  105. insert_values_for_multiple_docs(all_contents)
  106. def delete_global_search_records_for_doctype(doctype):
  107. frappe.db.sql('''
  108. delete
  109. from __global_search
  110. where
  111. doctype = %s''', doctype, as_dict=True)
  112. def get_selected_fields(meta, global_search_fields):
  113. fieldnames = [df.fieldname for df in global_search_fields]
  114. if meta.istable==1:
  115. fieldnames.append("parent")
  116. elif "name" not in fieldnames:
  117. fieldnames.append("name")
  118. if meta.has_field("is_website_published"):
  119. fieldnames.append("is_website_published")
  120. return fieldnames
  121. def get_children_data(doctype, meta):
  122. """
  123. Get all records from all the child tables of a doctype
  124. all_children = {
  125. "parent1": {
  126. "child_doctype1": [
  127. {
  128. "field1": val1,
  129. "field2": val2
  130. }
  131. ]
  132. }
  133. }
  134. """
  135. all_children = frappe._dict()
  136. child_search_fields = frappe._dict()
  137. for child in meta.get_table_fields():
  138. child_meta = frappe.get_meta(child.options)
  139. search_fields = child_meta.get_global_search_fields()
  140. if search_fields:
  141. child_search_fields.setdefault(child.options, search_fields)
  142. child_fieldnames = get_selected_fields(child_meta, search_fields)
  143. child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={
  144. "docstatus": ["!=", 1],
  145. "parenttype": doctype
  146. })
  147. for record in child_records:
  148. all_children.setdefault(record.parent, frappe._dict())\
  149. .setdefault(child.options, []).append(record)
  150. return all_children, child_search_fields
  151. def insert_values_for_multiple_docs(all_contents):
  152. values = []
  153. for content in all_contents:
  154. values.append("( '{doctype}', '{name}', '{content}', '{published}', '{title}', '{route}')"
  155. .format(**content))
  156. batch_size = 50000
  157. for i in range(0, len(values), batch_size):
  158. batch_values = values[i:i + batch_size]
  159. # ignoring duplicate keys for doctype_name
  160. frappe.db.sql('''
  161. insert ignore into __global_search
  162. (doctype, name, content, published, title, route)
  163. values
  164. {0}
  165. '''.format(", ".join(batch_values)))
  166. def update_global_search(doc):
  167. '''Add values marked with `in_global_search` to
  168. `frappe.flags.update_global_search` from given doc
  169. :param doc: Document to be added to global search'''
  170. if doc.docstatus > 1 or (doc.meta.has_field("enabled") and not doc.get("enabled")) \
  171. or doc.get("disabled"):
  172. return
  173. if frappe.flags.update_global_search==None:
  174. frappe.flags.update_global_search = []
  175. content = []
  176. for field in doc.meta.get_global_search_fields():
  177. if doc.get(field.fieldname) and field.fieldtype != "Table":
  178. content.append(get_formatted_value(doc.get(field.fieldname), field))
  179. # Get children
  180. for child in doc.meta.get_table_fields():
  181. for d in doc.get(child.fieldname):
  182. if d.parent == doc.name:
  183. for field in d.meta.get_global_search_fields():
  184. if d.get(field.fieldname):
  185. content.append(get_formatted_value(d.get(field.fieldname), field))
  186. if content:
  187. published = 0
  188. if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view:
  189. published = 1 if doc.is_website_published() else 0
  190. frappe.flags.update_global_search.append(
  191. dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''),
  192. published=published, title=doc.get_title(), route=doc.get('route')))
  193. def get_formatted_value(value, field):
  194. '''Prepare field from raw data'''
  195. from HTMLParser import HTMLParser
  196. if(getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]):
  197. h = HTMLParser()
  198. value = h.unescape(value)
  199. value = (re.subn(r'<[\s]*(script|style).*?</\1>(?s)', '', unicode(value))[0])
  200. value = ' '.join(value.split())
  201. return field.label + " : " + strip_html_tags(unicode(value))
  202. def sync_global_search():
  203. '''Add values from `frappe.flags.update_global_search` to __global_search.
  204. This is called internally at the end of the request.'''
  205. for value in frappe.flags.update_global_search:
  206. frappe.db.sql('''
  207. insert into __global_search
  208. (doctype, name, content, published, title, route)
  209. values
  210. (%(doctype)s, %(name)s, %(content)s, %(published)s, %(title)s, %(route)s)
  211. on duplicate key update
  212. content = %(content)s''', value)
  213. frappe.flags.update_global_search = []
  214. def delete_for_document(doc):
  215. '''Delete the __global_search entry of a document that has
  216. been deleted
  217. :param doc: Deleted document'''
  218. frappe.db.sql('''
  219. delete
  220. from __global_search
  221. where
  222. doctype = %s and
  223. name = %s''', (doc.doctype, doc.name), as_dict=True)
  224. @frappe.whitelist()
  225. def search(text, start=0, limit=20, doctype=""):
  226. '''Search for given text in __global_search
  227. :param text: phrase to be searched
  228. :param start: start results at, default 0
  229. :param limit: number of results to return, default 20
  230. :return: Array of result objects'''
  231. text = "+" + text + "*"
  232. if not doctype:
  233. results = frappe.db.sql('''
  234. select
  235. doctype, name, content
  236. from
  237. __global_search
  238. where
  239. match(content) against (%s IN BOOLEAN MODE)
  240. limit {start}, {limit}'''.format(start=start, limit=limit), text+"*", as_dict=True)
  241. else:
  242. results = frappe.db.sql('''
  243. select
  244. doctype, name, content
  245. from
  246. __global_search
  247. where
  248. doctype = %s AND
  249. match(content) against (%s IN BOOLEAN MODE)
  250. limit {start}, {limit}'''.format(start=start, limit=limit), (doctype, text), as_dict=True)
  251. for r in results:
  252. if frappe.get_meta(r.doctype).image_field:
  253. doc = frappe.get_doc(r.doctype, r.name)
  254. r.image = doc.get(doc.meta.image_field)
  255. return results
  256. @frappe.whitelist(allow_guest=True)
  257. def web_search(text, start=0, limit=20):
  258. '''Search for given text in __global_search where published = 1
  259. :param text: phrase to be searched
  260. :param start: start results at, default 0
  261. :param limit: number of results to return, default 20
  262. :return: Array of result objects'''
  263. text = "+" + text + "*"
  264. results = frappe.db.sql('''
  265. select
  266. doctype, name, content, title, route
  267. from
  268. __global_search
  269. where
  270. published = 1 and
  271. match(content) against (%s IN BOOLEAN MODE)
  272. limit {start}, {limit}'''.format(start=start, limit=limit),
  273. text, as_dict=True)
  274. return results