Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 
 

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