feat: Global Tags

This commit is contained in:
Himanshu Warekar 2019-09-24 01:03:11 +05:30
parent 4510b6eca7
commit f6d1ce2194
24 changed files with 481 additions and 255 deletions

View file

@ -1,11 +1,133 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import json
from frappe.model.document import Document
from frappe.utils.global_tags import update_global_tags
from frappe import _
class Tag(Document):
def validate(self):
self.tag_name = self.tag_name.title()
def on_trash(self):
if self.count > 0:
frappe.throw(_("Cannot delete Tag {0} since it is linked to Documents.").format(frappe.bold(self.name)))
def check_user_tags(dt):
"if the user does not have a tags column, then it creates one"
try:
frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
except Exception as e:
if frappe.db.is_column_missing(e):
DocTags(dt).setup()
@frappe.whitelist()
def add_tag(tag, dt, dn, color=None):
"adds a new tag to a record, and creates the Tag master"
DocTags(dt).add(dn, tag)
return tag
@frappe.whitelist()
def remove_tag(tag, dt, dn):
"removes tag from the record"
DocTags(dt).remove(dn, tag)
@frappe.whitelist()
def get_tagged_docs(doctype, tag):
frappe.has_permission(doctype, throw=True)
return frappe.db.sql("""SELECT name
FROM `tab{0}`
WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
@frappe.whitelist()
def get_tags(doctype, txt, cat_tags):
tags = json.loads(cat_tags)
tag = frappe.get_list("Tag", filters=[["name", "like", "%{}%".format(txt)]])
tags.extend([t.name for t in tag])
return sorted(filter(lambda t: t and txt.lower() in t.lower(), list(set(tags))))
class DocTags:
"""Tags for a particular doctype"""
def __init__(self, dt):
self.dt = dt
def get_tag_fields(self):
"""returns tag_fields property"""
return frappe.db.get_value('DocType', self.dt, 'tag_fields')
def get_tags(self, dn):
"""returns tag for a particular item"""
return (frappe.db.get_value(self.dt, dn, '_user_tags', ignore=1) or '').strip()
def add(self, dn, tag):
"""add a new user tag"""
tl = self.get_tags(dn).split(',')
if not tag in tl:
tl.append(tag)
if not frappe.db.exists("Tag", tag):
frappe.get_doc({"doctype": "Tag", "name": tag, "count": 1}).insert(ignore_permissions=True)
else:
update_tag_count(tags=tag)
self.update(dn, tl)
def remove(self, dn, tag):
"""remove a user tag"""
tl = self.get_tags(dn).split(',')
update_tag_count(tags=tag, increment=False)
self.update(dn, filter(lambda x:x.lower()!=tag.lower(), tl))
def remove_all(self, dn):
"""remove all user tags (call before delete)"""
update_tag_count(tags=tag, increment=False, dt=self.dt, dn=dn)
self.update(dn, [])
def update(self, dn, tl):
"""updates the _user_tag column in the table"""
if not tl:
tags = ''
else:
tl = list(set(filter(lambda x: x, tl)))
tags = ',' + ','.join(tl)
try:
frappe.db.sql("update `tab%s` set _user_tags=%s where name=%s" % \
(self.dt,'%s','%s'), (tags , dn))
doc= frappe.get_doc(self.dt, dn)
update_global_tags(doc, tags)
except Exception as e:
if frappe.db.is_column_missing(e):
if not tags:
# no tags, nothing to do
return
self.setup()
self.update(dn, tl)
else: raise
def setup(self):
"""adds the _user_tags column if not exists"""
from frappe.database.schema import add_column
add_column(self.dt, "_user_tags", "Data")
def update_tag_count(tags, increment=True, dt=None, dn=None):
"""
Used to Increase or Decrease the count of documents linked with a certain tag
"""
_user_tags = [tags]
if tags == [] and dt and dn:
_user_tags = frappe.db.get_value(dt, dn, '_user_tags', ignore=1).split(",")
_user_tags = [t.strip() for t in _user_tags if t]
for tag in _user_tags:
tag_count = frappe.db.get_value("Tag", tag, "count")
if increment:
tag_count+=1
else:
tag_count-=1
frappe.db.set_value("Tag", tag, "count", tag_count)

View file

@ -1,9 +0,0 @@
// Copyright (c) 2016, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Tag', {
tag_name:function(frm){
for (var i = 0 ;i<frm.doc.tags.length;i++){
frm.doc.tags[i].tag_name = toTitle(frm.doc.tags[i].tag_name)
}
}
});

View file

@ -1,147 +0,0 @@
{
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "field:category_name",
"beta": 0,
"creation": "2016-05-25 09:49:07.125394",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Category Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tags",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tags",
"length": 0,
"no_copy": 0,
"options": "Tag",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "tagdocs",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Doctypes",
"length": 0,
"no_copy": 0,
"options": "Tag Doc Category",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-12-29 14:40:37.489085",
"modified_by": "Administrator",
"module": "Core",
"name": "Tag Category",
"name_case": "Title Case",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 0,
"export": 1,
"if_owner": 0,
"import": 1,
"is_custom": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View file

@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
# test_records = frappe.get_test_records('Tag Categories')
class TestTagCategories(unittest.TestCase):
pass

View file

@ -1,58 +0,0 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"creation": "2016-05-25 13:09:20.996154",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "tagdoc",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Doctype to Assign Tags",
"length": 0,
"no_copy": 0,
"options": "DocType",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-05-30 15:04:45.454688",
"modified_by": "Administrator",
"module": "Core",
"name": "Tag Doc Category",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class TagDocCategory(Document):
pass

View file

@ -205,6 +205,18 @@ class MariaDBDatabase(Database):
ENGINE=MyISAM
CHARACTER SET=utf8mb4'''.format(self.VARCHAR_LEN))
def create_global_tags_table(self):
if not '__global_tags' in self.get_tables():
self.sql('''create table __global_tags(
doctype varchar(100),
name varchar({0}),
title varchar({0}),
tags varchar({0}),
unique `doctype_name` (doctype, name))
COLLATE=utf8mb4_unicode_ci
ENGINE=MyISAM
CHARACTER SET=utf8mb4'''.format(self.VARCHAR_LEN))
def create_user_settings_table(self):
self.sql_ddl("""create table if not exists __UserSettings (
`user` VARCHAR(180) NOT NULL,

View file

@ -194,6 +194,15 @@ class PostgresDatabase(Database):
published int not null default 0,
unique (doctype, name))'''.format(self.VARCHAR_LEN))
def create_global_tags_table(self):
if not '__global_tags' in self.get_tables():
self.sql('''create table __global_tags(
doctype varchar(100),
name varchar({0}),
title varchar({0}),
tags varchar({0}),
unique (doctype, name))'''.format(self.VARCHAR_LEN))
def create_user_settings_table(self):
self.sql_ddl("""create table if not exists "__UserSettings" (
"user" VARCHAR(180) NOT NULL,

View file

@ -0,0 +1,8 @@
// Copyright (c) 2019, Frappe Technologies and contributors
// For license information, please see license.txt
frappe.ui.form.on('Tag', {
// refresh: function(frm) {
// }
});

View file

@ -0,0 +1,56 @@
{
"autoname": "Prompt",
"creation": "2016-05-25 09:43:44.767581",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"description",
"count"
],
"fields": [
{
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"fieldname": "count",
"fieldtype": "Int",
"label": "Count",
"read_only": 1
}
],
"modified": "2019-09-24 00:53:13.488147",
"modified_by": "Administrator",
"module": "Desk",
"name": "Tag",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "All",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View file

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
# import frappe
from frappe.model.document import Document
class TagCategory(Document):
class Tag(Document):
pass

View file

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestTag(unittest.TestCase):
pass

View file

@ -261,13 +261,8 @@ def delete_bulk(doctype, items):
@frappe.whitelist()
@frappe.read_only()
def get_sidebar_stats(stats, doctype, filters=[]):
cat_tags = frappe.db.sql("""select `tag`.parent as `category`, `tag`.tag_name as `tag`
from `tabTag Doc Category` as `docCat`
INNER JOIN `tabTag` as `tag` on `tag`.parent = `docCat`.parent
where `docCat`.tagdoc=%s
ORDER BY `tag`.parent asc, `tag`.idx""", doctype, as_dict=1)
return {"defined_cat":cat_tags, "stats":get_stats(stats, doctype, filters)}
return {"defined_cat": [], "stats": get_stats(stats, doctype, filters)}
@frappe.whitelist()
@frappe.read_only()

View file

@ -39,6 +39,7 @@ def install_db(root_login="root", root_password=None, db_name=None, source_sql=N
frappe.db.create_auth_table()
frappe.db.create_global_search_table()
frappe.db.create_global_tags_table()
frappe.db.create_user_settings_table()
frappe.flags.in_install_db = False

View file

@ -16,6 +16,7 @@ from frappe.core.doctype.file.file import remove_all
from frappe.utils.password import delete_all_passwords_for
from frappe.model.naming import revert_series_if_last
from frappe.utils.global_search import delete_for_document
from frappe.utils.global_tags import delete_tags_for_document
from frappe.exceptions import FileNotFoundError
@ -116,6 +117,8 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
# delete global search entry
delete_for_document(doc)
# delete tags from __global_tags
delete_tags_for_document(doc)
if doc and not for_reload:
add_to_deleted_document(doc)

View file

@ -186,6 +186,7 @@
"public/js/frappe/ui/toolbar/awesome_bar.js",
"public/js/frappe/ui/toolbar/energy_points_notifications.js",
"public/js/frappe/ui/toolbar/search.js",
"public/js/frappe/ui/toolbar/global_tags.js",
"public/js/frappe/ui/toolbar/search.html",
"public/js/frappe/ui/toolbar/search_header.html",
"public/js/frappe/ui/toolbar/search_utils.js",

View file

@ -147,6 +147,8 @@ frappe.Application = Class.extend({
});
}, 300000); // check every 5 minutes
}
this.set_global_tags();
},
setup_frappe_vue() {
@ -599,6 +601,10 @@ frappe.Application = Class.extend({
frappe.show_alert(message);
});
},
set_global_tags() {
frappe.global_tags.utils.set_tags();
}
});
frappe.get_module = function(m, default_module) {

View file

@ -35,13 +35,14 @@ frappe.ui.TagEditor = Class.extend({
onTagAdd: (tag) => {
if(me.initialized && !me.refreshing) {
return frappe.call({
method: 'frappe.desk.tags.add_tag',
method: "frappe.desk.doctype.tag.tag.add_tag",
args: me.get_args(tag),
callback: function(r) {
var user_tags = me.user_tags ? me.user_tags.split(",") : [];
user_tags.push(tag)
me.user_tags = user_tags.join(",");
me.on_change && me.on_change(me.user_tags);
frappe.global_tags.utils.set_tags();
}
});
}
@ -49,13 +50,14 @@ frappe.ui.TagEditor = Class.extend({
onTagRemove: (tag) => {
if(!me.refreshing) {
return frappe.call({
method: 'frappe.desk.tags.remove_tag',
method: "frappe.desk.doctype.tag.tag.remove_tag",
args: me.get_args(tag),
callback: function(r) {
var user_tags = me.user_tags.split(",");
user_tags.splice(user_tags.indexOf(tag), 1);
me.user_tags = user_tags.join(",");
me.on_change && me.on_change(me.user_tags);
frappe.global_tags.utils.set_tags();
}
});
}
@ -82,7 +84,7 @@ frappe.ui.TagEditor = Class.extend({
$input.on("input", function(e) {
var value = e.target.value;
frappe.call({
method:"frappe.desk.tags.get_tags",
method: "frappe.desk.doctype.tag.tag.get_tags",
args:{
doctype: me.frm.doctype,
txt: value.toLowerCase(),

View file

@ -1,6 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.provide('frappe.search');
frappe.provide('frappe.global_tags');
frappe.search.AwesomeBar = Class.extend({
setup: function(element) {
@ -140,6 +141,8 @@ frappe.search.AwesomeBar = Class.extend({
__("document type..., e.g. customer")+'</td></tr>\
<tr><td>'+__("Search in a document type")+'</td><td>'+
__("text in document type")+'</td></tr>\
<tr><td>'+__("Tags")+'</td><td>'+
__("tag name..., e.g. #tag")+'</td></tr>\
<tr><td>'+__("Open a module or tool")+'</td><td>'+
__("module name...")+'</td></tr>\
<tr><td>'+__("Calculate")+'</td><td>'+
@ -177,6 +180,9 @@ frappe.search.AwesomeBar = Class.extend({
frappe.search.utils.get_recent_pages(txt || ""),
frappe.search.utils.get_executables(txt)
);
if (txt.charAt(0) === "#") {
options = frappe.global_tags.utils.get_tags(txt);
}
var out = this.deduplicate(options);
return out.sort(function(a, b) {
return b.index - a.index;
@ -215,6 +221,11 @@ frappe.search.AwesomeBar = Class.extend({
make_global_search: function(txt) {
var me = this;
if (txt.charAt(0) === "#") {
return;
}
this.options.push({
label: __("Search for '{0}'", [txt.bold()]),
value: __("Search for '{0}'", [txt]),

View file

@ -0,0 +1,136 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
frappe.provide("frappe.global_tags");
frappe.provide("locals.global_tags");
frappe.global_tags.GlobalTagsDialog = class GlobalTags {
constructor(opts) {
$.extend(this, opts);
this.show();
}
show() {
if (!this.dialog) {
this.make_dialog();
}
$(this.dialog.body).html(
`<div class="text-muted text-center" style="padding: 30px 0px">
${__("Loading")}...
</div>`);
this.dialog.show();
}
make_dialog() {
let title = __("Tag {0}", ["#".concat(this.tag)]);
this.dialog = new frappe.ui.Dialog({
hide_on_page_refresh: true,
minimizable: true,
title: title
});
this.dialog.on_page_show = () => {
this.get_documents_for_tag()
.then(() => this.make_html());
};
}
make_html() {
const results = this.results;
let html = '';
const linked_doctypes = Object.keys(results);
if (linked_doctypes.length === 0) {
html = __("Not Linked to any record");
} else {
html = linked_doctypes.map(doctype => {
const docs = results[doctype];
return `
<div class="list-item-table margin-bottom">
${this.make_doc_head(doctype)}
${docs.map(doc => this.make_doc_row(doc.name, doctype, doc.title)).join('')}
</div>
`;
}).join('');
}
$(this.dialog.body).html(html);
}
get_documents_for_tag() {
return new Promise((resolve) => {
frappe.call({
method: "frappe.utils.global_tags.get_documents_for_tag",
args: {
tag: this.tag
},
callback: (r) => {
this.results = r.message;
resolve();
}
});
});
}
make_doc_head(heading) {
return `
<header class="level list-row list-row-head text-muted small">
<div>${__(heading)}</div>
</header>
`;
}
make_doc_row(docname, doctype, title) {
return `<div class="list-row-container">
<div class="level list-row small">
<div class="level-left bold">
<a href="#Form/${doctype}/${docname}">${docname}</a>
</div>
<div class="level-left">
<p>${title}</p>
</div>
</div>
</div>`;
}
};
frappe.global_tags.utils = {
get_tags: function(txt) {
txt = txt.slice(1);
let out = [];
for (let i in locals.global_tags) {
let tag = locals.global_tags[i];
let level = frappe.search.utils.fuzzy_search(txt, tag);
if (level) {
out.push({
type: "Tag",
label: __("#{0}", [frappe.search.utils.bolden_match_part(__(tag), txt)]),
value: __("#{0}", [__(tag)]),
index: 1 + level,
match: tag,
onclick: function() {
new frappe.global_tags.GlobalTagsDialog({"tag": tag});
}
});
}
}
return out;
},
set_tags: function() {
frappe.call({
method: "frappe.utils.global_tags.get_tags_list_for_awesomebar",
callback: function(r) {
if (r && r.message) {
locals.global_tags = $.extend([], r.message);
}
}
});
}
}

View file

@ -242,10 +242,6 @@ def update_global_search(doc):
if doc.get(field.fieldname) and field.fieldtype not in frappe.model.table_fields:
content.append(get_formatted_value(doc.get(field.fieldname), field))
tags = (doc.get('_user_tags') or '').strip()
if tags:
content.extend(list(filter(lambda x: x, tags.split(','))))
# Get children
for child in doc.meta.get_table_fields():
for d in doc.get(child.fieldname):

View file

@ -0,0 +1,94 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def setup_global_tags_table():
"""
Creates __global_search table
:return:
"""
frappe.db.create_global_tags_table()
def reset():
"""
Deletes all data in __global_tags
:return:
"""
frappe.db.sql('DELETE FROM `__global_tags`')
def delete_tags_for_document(doc):
"""
Delete the __global_tags 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))
def update_global_tags(doc, tags):
"""
Adds tags for documents
:param doc: Document to be added to global tags
"""
if frappe.local.conf.get('disable_global_tags') or not doc.get("_user_tags"):
return
value = {
"doctype": doc.doctype,
"name": doc.name,
"title": (doc.get_title() or '')[:int(frappe.db.VARCHAR_LEN)],
"tags": tags.lower()
}
frappe.db.multisql({
'mariadb': '''INSERT INTO `__global_tags`
(`doctype`, `name`, `title`, `tags`)
VALUES (%(doctype)s, %(name)s, %(title)s, %(tags)s)
ON DUPLICATE key UPDATE
`tags`=%(tags)s
''',
'postgres': '''INSERT INTO `__global_tags`
(`doctype`, `name`, `title`, `tags`)
VALUES (%(doctype)s, %(name)s, %(title)s, %(tags)s)
ON CONFLICT("doctype", "name") DO UPDATE SET
`tags`=%(tags)s
'''
}, value)
@frappe.whitelist()
def get_documents_for_tag(tag):
"""
Search for given text in __global_tags
:param tag: tag to be searched
"""
# remove hastag # from tag
results = {}
tag = frappe.db.escape('%{0}%'.format(tag.lower()), False)
common_query = '''
SELECT `doctype`, `name`, `title`, `tags`
FROM `__global_tags`
WHERE `tags` LIKE {tag}
'''
result = frappe.db.multisql({
'mariadb': common_query.format(tag=tag),
'postgres': common_query.format(tag=tag)
}, as_dict=True)
for res in result:
if res.doctype in results.keys():
results[res.doctype].append(res)
else:
results[res.doctype] = [res]
return results
@frappe.whitelist()
def get_tags_list_for_awesomebar():
return [t.name for t in frappe.get_list("Tag")]