# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals import frappe import re from frappe.utils import cint, strip_html_tags from frappe.model.base_document import get_controller def setup_global_search_table(): '''Creates __global_seach table''' if not '__global_search' in frappe.db.get_tables(): frappe.db.sql('''create table __global_search( doctype varchar(100), name varchar(255), title varchar(140), content text, fulltext(content), route varchar(140), published int(1) not null default 0, unique `doctype_name` (doctype, name)) COLLATE=utf8mb4_unicode_ci ENGINE=MyISAM CHARACTER SET=utf8mb4''') def reset(): '''Deletes all data in __global_search''' frappe.db.sql('delete from __global_search') def get_doctypes_with_global_search(with_child_tables=True): '''Return doctypes with global search fields''' def _get(): global_search_doctypes = [] if not with_child_tables: filters = {"istable": ["!=", 1]} for d in frappe.get_all('DocType', fields=['name', 'module'], filters=filters): meta = frappe.get_meta(d.name) if len(meta.get_global_search_fields()) > 0: global_search_doctypes.append(d) installed_apps = frappe.get_installed_apps() doctypes = [d.name for d in global_search_doctypes if frappe.local.module_app[frappe.scrub(d.module)] in installed_apps] return doctypes return frappe.cache().get_value('doctypes_with_global_search', _get) def rebuild_for_doctype(doctype): '''Rebuild entries of doctype's documents in __global_search on change of searchable fields :param doctype: Doctype ''' def _get_filters(): filters = frappe._dict({ "docstatus": ["!=", 1] }) if meta.has_field("enabled"): filters.enabled = 1 if meta.has_field("disabled"): filters.disabled = 0 return filters meta = frappe.get_meta(doctype) if cint(meta.istable) == 1: parent_doctypes = frappe.get_all("DocField", fields="parent", filters={ "fieldtype": "Table", "options": doctype }) for p in parent_doctypes: rebuild_for_doctype(p.parent) return # Delete records delete_global_search_records_for_doctype(doctype) parent_search_fields = meta.get_global_search_fields() fieldnames = get_selected_fields(meta, parent_search_fields) # Get all records from parent doctype table all_records = frappe.get_all(doctype, fields=fieldnames, filters=_get_filters()) # Children data all_children, child_search_fields = get_children_data(doctype, meta) all_contents = [] for doc in all_records: content = [] for field in parent_search_fields: value = doc.get(field.fieldname) if value: content.append(get_formatted_value(value, field)) # get children data for child_doctype, records in all_children.get(doc.name, {}).items(): for field in child_search_fields.get(child_doctype): for r in records: if r.get(field.fieldname): content.append(get_formatted_value(r.get(field.fieldname), field)) if content: # if doctype published in website, push title, route etc. published = 0 title, route = "", "" if hasattr(get_controller(doctype), "is_website_published") and meta.allow_guest_to_view: d = frappe.get_doc(doctype, doc.name) published = 1 if d.is_website_published() else 0 title = d.get_title() route = d.get("route") all_contents.append({ "doctype": frappe.db.escape(doctype), "name": frappe.db.escape(doc.name), "content": frappe.db.escape(' ||| '.join(content or '')), "published": published, "title": frappe.db.escape(title or ''), "route": frappe.db.escape(route or '') }) if all_contents: insert_values_for_multiple_docs(all_contents) def delete_global_search_records_for_doctype(doctype): frappe.db.sql(''' delete from __global_search where doctype = %s''', doctype, as_dict=True) def get_selected_fields(meta, global_search_fields): fieldnames = [df.fieldname for df in global_search_fields] if meta.istable==1: fieldnames.append("parent") elif "name" not in fieldnames: fieldnames.append("name") if meta.has_field("is_website_published"): fieldnames.append("is_website_published") return fieldnames def get_children_data(doctype, meta): """ Get all records from all the child tables of a doctype all_children = { "parent1": { "child_doctype1": [ { "field1": val1, "field2": val2 } ] } } """ all_children = frappe._dict() child_search_fields = frappe._dict() for child in meta.get_table_fields(): child_meta = frappe.get_meta(child.options) search_fields = child_meta.get_global_search_fields() if search_fields: child_search_fields.setdefault(child.options, search_fields) child_fieldnames = get_selected_fields(child_meta, search_fields) child_records = frappe.get_all(child.options, fields=child_fieldnames, filters={ "docstatus": ["!=", 1], "parenttype": doctype }) for record in child_records: all_children.setdefault(record.parent, frappe._dict())\ .setdefault(child.options, []).append(record) return all_children, child_search_fields def insert_values_for_multiple_docs(all_contents): values = [] for content in all_contents: values.append("( '{doctype}', '{name}', '{content}', '{published}', '{title}', '{route}')" .format(**content)) # ignoring duplicate keys for doctype_name frappe.db.sql(''' insert ignore into __global_search (doctype, name, content, published, title, route) values {0} '''.format(", ".join(values))) def update_global_search(doc): '''Add values marked with `in_global_search` to `frappe.flags.update_global_search` from given doc :param doc: Document to be added to global search''' if doc.docstatus > 1 or (doc.meta.has_field("enabled") and not doc.get("enabled")) \ or doc.get("disabled"): return if frappe.flags.update_global_search==None: frappe.flags.update_global_search = [] content = [] for field in doc.meta.get_global_search_fields(): if doc.get(field.fieldname) and field.fieldtype != "Table": content.append(get_formatted_value(doc.get(field.fieldname), field)) # Get children for child in doc.meta.get_table_fields(): for d in doc.get(child.fieldname): if d.parent == doc.name: for field in d.meta.get_global_search_fields(): if d.get(field.fieldname): content.append(get_formatted_value(d.get(field.fieldname), field)) if content: published = 0 if hasattr(doc, 'is_website_published') and doc.meta.allow_guest_to_view: published = 1 if doc.is_website_published() else 0 frappe.flags.update_global_search.append( dict(doctype=doc.doctype, name=doc.name, content=' ||| '.join(content or ''), published=published, title=doc.get_title(), route=doc.get('route'))) def get_formatted_value(value, field): '''Prepare field from raw data''' from HTMLParser import HTMLParser if(getattr(field, 'fieldtype', None) in ["Text", "Text Editor"]): h = HTMLParser() value = h.unescape(value) value = (re.subn(r'<[\s]*(script|style).*?(?s)', '', unicode(value))[0]) value = ' '.join(value.split()) return field.label + " : " + strip_html_tags(unicode(value)) def sync_global_search(): '''Add values from `frappe.flags.update_global_search` to __global_search. This is called internally at the end of the request.''' for value in frappe.flags.update_global_search: frappe.db.sql(''' insert into __global_search (doctype, name, content, published, title, route) values (%(doctype)s, %(name)s, %(content)s, %(published)s, %(title)s, %(route)s) on duplicate key update content = %(content)s''', value) frappe.flags.update_global_search = [] def delete_for_document(doc): '''Delete the __global_search entry of a document that has been deleted :param doc: Deleted document''' frappe.db.sql(''' delete from __global_search where doctype = %s and name = %s''', (doc.doctype, doc.name), as_dict=True) @frappe.whitelist() def search(text, start=0, limit=20, doctype=""): '''Search for given text in __global_search :param text: phrase to be searched :param start: start results at, default 0 :param limit: number of results to return, default 20 :return: Array of result objects''' text = "+" + text + "*" if not doctype: results = frappe.db.sql(''' select doctype, name, content from __global_search where match(content) against (%s IN BOOLEAN MODE) limit {start}, {limit}'''.format(start=start, limit=limit), text+"*", as_dict=True) else: results = frappe.db.sql(''' select doctype, name, content from __global_search where doctype = %s AND match(content) against (%s IN BOOLEAN MODE) limit {start}, {limit}'''.format(start=start, limit=limit), (doctype, text), as_dict=True) for r in results: if frappe.get_meta(r.doctype).image_field: doc = frappe.get_doc(r.doctype, r.name) r.image = doc.get(doc.meta.image_field) return results @frappe.whitelist(allow_guest=True) def web_search(text, start=0, limit=20): '''Search for given text in __global_search where published = 1 :param text: phrase to be searched :param start: start results at, default 0 :param limit: number of results to return, default 20 :return: Array of result objects''' text = "+" + text + "*" results = frappe.db.sql(''' select doctype, name, content, title, route from __global_search where published = 1 and match(content) against (%s IN BOOLEAN MODE) limit {start}, {limit}'''.format(start=start, limit=limit), text, as_dict=True) return results