@@ -9,6 +9,8 @@ from frappe.model.document import Document | |||||
from frappe.model.db_schema import add_column | from frappe.model.db_schema import add_column | ||||
from frappe.utils import get_fullname | from frappe.utils import get_fullname | ||||
exclude_from_linked_with = True | |||||
class Comment(Document): | class Comment(Document): | ||||
"""Comments are added to Documents via forms or views like blogs etc.""" | """Comments are added to Documents via forms or views like blogs etc.""" | ||||
no_feed_on_delete = True | no_feed_on_delete = True | ||||
@@ -7,6 +7,8 @@ from frappe.model.document import Document | |||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import get_fullname | from frappe.utils import get_fullname | ||||
exclude_from_linked_with = True | |||||
class DocShare(Document): | class DocShare(Document): | ||||
no_feed_on_delete = True | no_feed_on_delete = True | ||||
@@ -17,6 +17,8 @@ import json | |||||
class FolderNotEmpty(frappe.ValidationError): pass | class FolderNotEmpty(frappe.ValidationError): pass | ||||
exclude_from_linked_with = True | |||||
class File(NestedSet): | class File(NestedSet): | ||||
nsm_parent_field = 'folder' | nsm_parent_field = 'folder' | ||||
no_feed_on_delete = True | no_feed_on_delete = True | ||||
@@ -9,6 +9,8 @@ from frappe.model.document import Document | |||||
from frappe.utils import get_fullname | from frappe.utils import get_fullname | ||||
from frappe import _ | from frappe import _ | ||||
exclude_from_linked_with = True | |||||
class Feed(Document): | class Feed(Document): | ||||
pass | pass | ||||
@@ -10,6 +10,7 @@ from frappe.utils import get_fullname | |||||
subject_field = "description" | subject_field = "description" | ||||
sender_field = "sender" | sender_field = "sender" | ||||
exclude_from_linked_with = True | |||||
class ToDo(Document): | class ToDo(Document): | ||||
def validate(self): | def validate(self): | ||||
@@ -0,0 +1,171 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||||
# MIT License. See license.txt | |||||
from __future__ import unicode_literals | |||||
import frappe, json | |||||
from frappe.model.meta import is_single | |||||
from frappe.modules import load_doctype_module | |||||
import frappe.desk.form.meta | |||||
import frappe.desk.form.load | |||||
@frappe.whitelist() | |||||
def get_linked_docs(doctype, name, linkinfo): | |||||
meta = frappe.desk.form.meta.get_meta(doctype) | |||||
results = {} | |||||
if isinstance(linkinfo, basestring): | |||||
# additional fields are added in linkinfo | |||||
linkinfo = json.loads(linkinfo) | |||||
if not linkinfo: | |||||
return results | |||||
me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) | |||||
for dt, link in linkinfo.items(): | |||||
link["doctype"] = dt | |||||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt) | |||||
linkmeta = link_meta_bundle[0] | |||||
if not linkmeta.get("issingle"): | |||||
fields = [d.fieldname for d in linkmeta.get("fields", {"in_list_view":1, | |||||
"fieldtype": ["not in", ["Image", "HTML", "Button", "Table"]]})] \ | |||||
+ ["name", "modified", "docstatus"] | |||||
if link.get("add_fields"): | |||||
fields += link["add_fields"] | |||||
fields = ["`tab{dt}`.`{fn}`".format(dt=dt, fn=sf.strip()) for sf in fields if sf | |||||
and "`tab" not in sf] | |||||
try: | |||||
if link.get("get_parent"): | |||||
if me and me.parent and me.parenttype == dt: | |||||
ret = frappe.get_list(doctype=dt, fields=fields, | |||||
filters=[[dt, "name", '=', me.parent]]) | |||||
else: | |||||
ret = None | |||||
elif link.get("child_doctype"): | |||||
filters = [[link.get('child_doctype'), link.get("fieldname"), '=', name]] | |||||
# dynamic link | |||||
if link.get("doctype_fieldname"): | |||||
filters.append([link.get('child_doctype'), link.get("doctype_fieldname"), "=", doctype]) | |||||
ret = frappe.get_list(doctype=dt, fields=fields, filters=filters) | |||||
else: | |||||
filters = [[dt, link.get("fieldname"), '=', name]] | |||||
# dynamic link | |||||
if link.get("doctype_fieldname"): | |||||
filters.append([dt, link.get("doctype_fieldname"), "=", doctype]) | |||||
ret = frappe.get_list(doctype=dt, fields=fields, filters=filters) | |||||
except frappe.PermissionError: | |||||
if frappe.local.message_log: | |||||
frappe.local.message_log.pop() | |||||
continue | |||||
if ret: | |||||
results[dt] = ret | |||||
return results | |||||
@frappe.whitelist() | |||||
def get_linked_doctypes(doctype): | |||||
"""add list of doctypes this doctype is 'linked' with. | |||||
Example, for Customer: | |||||
{"Address": {"fieldname": "customer"}..} | |||||
""" | |||||
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype)) | |||||
def _get_linked_doctypes(doctype): | |||||
ret = {} | |||||
# find fields where this doctype is linked | |||||
ret.update(get_linked_fields(doctype)) | |||||
ret.update(get_dynamic_linked_fields(doctype)) | |||||
# find links of parents | |||||
links = frappe.db.sql("""select dt from `tabCustom Field` | |||||
where (fieldtype="Table" and options=%s)""", (doctype)) | |||||
links += frappe.db.sql("""select parent from tabDocField | |||||
where (fieldtype="Table" and options=%s)""", (doctype)) | |||||
for dt, in links: | |||||
if not dt in ret: | |||||
ret[dt] = {"get_parent": True} | |||||
for dt in ret.keys(): | |||||
doctype_module = load_doctype_module(dt) | |||||
if getattr(doctype_module, "exclude_from_linked_with", False): | |||||
del ret[dt] | |||||
return ret | |||||
def get_linked_fields(doctype): | |||||
links = frappe.db.sql("""select parent, fieldname from tabDocField | |||||
where (fieldtype="Link" and options=%s) | |||||
or (fieldtype="Select" and options=%s)""", (doctype, "link:"+ doctype)) | |||||
links += frappe.db.sql("""select dt as parent, fieldname from `tabCustom Field` | |||||
where (fieldtype="Link" and options=%s) | |||||
or (fieldtype="Select" and options=%s)""", (doctype, "link:"+ doctype)) | |||||
links = dict(links) | |||||
ret = {} | |||||
if links: | |||||
for dt in links: | |||||
ret[dt] = { "fieldname": links[dt] } | |||||
# find out if linked in a child table | |||||
for parent, options in frappe.db.sql("""select parent, options from tabDocField | |||||
where fieldtype="Table" | |||||
and options in (select name from tabDocType | |||||
where istable=1 and name in (%s))""" % ", ".join(["%s"] * len(links)) ,tuple(links)): | |||||
ret[parent] = {"child_doctype": options, "fieldname": links[options] } | |||||
if options in ret: | |||||
del ret[options] | |||||
return ret | |||||
def get_dynamic_linked_fields(doctype): | |||||
ret = {} | |||||
links = frappe.db.sql("""select parent as doctype, fieldname, options as doctype_fieldname | |||||
from `tabDocField` where fieldtype='Dynamic Link'""", as_dict=True) | |||||
links += frappe.db.sql("""select dt as doctype, fieldname, options as doctype_fieldname | |||||
from `tabCustom Field` where fieldtype='Dynamic Link'""", as_dict=True) | |||||
for df in links: | |||||
if is_single(df.doctype): | |||||
continue | |||||
# optimized to get both link exists and parenttype | |||||
possible_link = frappe.db.sql("""select distinct `{doctype_fieldname}`, parenttype | |||||
from `tab{doctype}` where `{doctype_fieldname}`=%s""".format(**df), doctype, as_dict=True) | |||||
if possible_link: | |||||
for d in possible_link: | |||||
# is child | |||||
if d.parenttype: | |||||
ret[d.parenttype] = { | |||||
"child_doctype": df.doctype, | |||||
"fieldname": df.fieldname, | |||||
"doctype_fieldname": df.doctype_fieldname | |||||
} | |||||
else: | |||||
ret[df.doctype] = { | |||||
"fieldname": df.fieldname, | |||||
"doctype_fieldname": df.doctype_fieldname | |||||
} | |||||
return ret |
@@ -36,7 +36,6 @@ class FormMeta(Meta): | |||||
self.add_linked_document_type() | self.add_linked_document_type() | ||||
if not self.istable: | if not self.istable: | ||||
self.add_linked_with() | |||||
self.add_code() | self.add_code() | ||||
self.load_print_formats() | self.load_print_formats() | ||||
self.load_workflows() | self.load_workflows() | ||||
@@ -130,52 +129,6 @@ class FormMeta(Meta): | |||||
# edge case where options="[Select]" | # edge case where options="[Select]" | ||||
pass | pass | ||||
def add_linked_with(self): | |||||
"""add list of doctypes this doctype is 'linked' with. | |||||
Example, for Customer: | |||||
{"Address": {"fieldname": "customer"}..} | |||||
""" | |||||
# find fields where this doctype is linked | |||||
links = frappe.db.sql("""select parent, fieldname from tabDocField | |||||
where (fieldtype="Link" and options=%s) | |||||
or (fieldtype="Select" and options=%s)""", (self.name, "link:"+ self.name)) | |||||
links += frappe.db.sql("""select dt as parent, fieldname from `tabCustom Field` | |||||
where (fieldtype="Link" and options=%s) | |||||
or (fieldtype="Select" and options=%s)""", (self.name, "link:"+ self.name)) | |||||
links = dict(links) | |||||
ret = {} | |||||
for dt in links: | |||||
ret[dt] = { "fieldname": links[dt] } | |||||
if links: | |||||
# find out if linked in a child table | |||||
for parent, options in frappe.db.sql("""select parent, options from tabDocField | |||||
where fieldtype="Table" | |||||
and options in (select name from tabDocType | |||||
where istable=1 and name in (%s))""" % ", ".join(["%s"] * len(links)) ,tuple(links)): | |||||
ret[parent] = {"child_doctype": options, "fieldname": links[options] } | |||||
if options in ret: | |||||
del ret[options] | |||||
# find links of parents | |||||
links = frappe.db.sql("""select dt from `tabCustom Field` | |||||
where (fieldtype="Table" and options=%s)""", (self.name)) | |||||
links += frappe.db.sql("""select parent from tabDocField | |||||
where (fieldtype="Table" and options=%s)""", (self.name)) | |||||
for dt, in links: | |||||
if not dt in ret: | |||||
ret[dt] = {"get_parent": True} | |||||
self.set("__linked_with", ret, as_value=True) | |||||
def load_print_formats(self): | def load_print_formats(self): | ||||
print_formats = frappe.db.sql("""select * FROM `tabPrint Format` | print_formats = frappe.db.sql("""select * FROM `tabPrint Format` | ||||
WHERE doc_type=%s AND docstatus<2 and ifnull(disabled, 0)=0""", (self.name,), as_dict=1, | WHERE doc_type=%s AND docstatus<2 and ifnull(disabled, 0)=0""", (self.name,), as_dict=1, | ||||
@@ -105,54 +105,3 @@ def get_next(doctype, value, prev, filters=None, order_by="modified desc"): | |||||
else: | else: | ||||
return res[0][0] | return res[0][0] | ||||
@frappe.whitelist() | |||||
def get_linked_docs(doctype, name, metadata_loaded=None, no_metadata=False): | |||||
if not metadata_loaded: metadata_loaded = [] | |||||
meta = frappe.desk.form.meta.get_meta(doctype) | |||||
linkinfo = meta.get("__linked_with") | |||||
results = {} | |||||
if not linkinfo: | |||||
return results | |||||
me = frappe.db.get_value(doctype, name, ["parenttype", "parent"], as_dict=True) | |||||
for dt, link in linkinfo.items(): | |||||
link["doctype"] = dt | |||||
link_meta_bundle = frappe.desk.form.load.get_meta_bundle(dt) | |||||
linkmeta = link_meta_bundle[0] | |||||
if not linkmeta.get("issingle"): | |||||
fields = [d.fieldname for d in linkmeta.get("fields", {"in_list_view":1, | |||||
"fieldtype": ["not in", ["Image", "HTML", "Button", "Table"]]})] \ | |||||
+ ["name", "modified", "docstatus"] | |||||
fields = ["`tab{dt}`.`{fn}`".format(dt=dt, fn=sf.strip()) for sf in fields if sf] | |||||
try: | |||||
if link.get("get_parent"): | |||||
if me and me.parent and me.parenttype == dt: | |||||
ret = frappe.get_list(doctype=dt, fields=fields, | |||||
filters=[[dt, "name", '=', me.parent]]) | |||||
else: | |||||
ret = None | |||||
elif link.get("child_doctype"): | |||||
ret = frappe.get_list(doctype=dt, fields=fields, | |||||
filters=[[link.get('child_doctype'), link.get("fieldname"), '=', name]]) | |||||
else: | |||||
ret = frappe.get_list(doctype=dt, fields=fields, | |||||
filters=[[dt, link.get("fieldname"), '=', name]]) | |||||
except frappe.PermissionError: | |||||
if frappe.local.message_log: | |||||
frappe.local.message_log.pop() | |||||
continue | |||||
if ret: | |||||
results[dt] = ret | |||||
if not no_metadata and not dt in metadata_loaded: | |||||
frappe.local.response.docs.extend(link_meta_bundle) | |||||
return results |
@@ -619,6 +619,10 @@ class Document(BaseDocument): | |||||
self.run_method("on_update_after_submit") | self.run_method("on_update_after_submit") | ||||
frappe.cache().hdel("last_modified", self.doctype) | frappe.cache().hdel("last_modified", self.doctype) | ||||
# to clear linked_with_doctypes of others | |||||
frappe.cache().hdel("linked_with_doctypes", self.doctype) | |||||
self.notify_update() | self.notify_update() | ||||
self.latest = None | self.latest = None | ||||
@@ -332,7 +332,7 @@ def clear_cache(doctype=None): | |||||
frappe.cache().delete_value("is_table") | frappe.cache().delete_value("is_table") | ||||
frappe.cache().delete_value("doctype_modules") | frappe.cache().delete_value("doctype_modules") | ||||
groups = ["meta", "form_meta", "table_columns", "last_modified"] | |||||
groups = ["meta", "form_meta", "table_columns", "last_modified", "linked_doctypes"] | |||||
def clear_single(dt): | def clear_single(dt): | ||||
for name in groups: | for name in groups: | ||||
@@ -16,16 +16,6 @@ frappe.ui.form.LinkedWith = Class.extend({ | |||||
}, | }, | ||||
make_dialog: function() { | make_dialog: function() { | ||||
var me = this; | var me = this; | ||||
this.linked_with = this.frm.meta.__linked_with; | |||||
var links = []; | |||||
$.each(this.linked_with, function(doctype, tmp) { | |||||
if(frappe.model.can_get_report(doctype)) { | |||||
links.push({label: __(doctype), value: doctype}); | |||||
} | |||||
}); | |||||
links = frappe.utils.sort(links, "label"); | |||||
this.dialog = new frappe.ui.Dialog({ | this.dialog = new frappe.ui.Dialog({ | ||||
hide_on_page_refresh: true, | hide_on_page_refresh: true, | ||||
@@ -37,49 +27,119 @@ frappe.ui.form.LinkedWith = Class.extend({ | |||||
this.dialog.$wrapper.find(".modal-dialog").addClass("linked-with-dialog"); | this.dialog.$wrapper.find(".modal-dialog").addClass("linked-with-dialog"); | ||||
if(!links) { | |||||
this.dialog.fields_dict.list.$wrapper.html("<div class='alert alert-warning'>" | |||||
+ this.frm.doctype + ": " | |||||
+ (this.linked_with ? __("Not Linked to any record.") : __("Not enough permission to see links.")) | |||||
+ "</div>") | |||||
return; | |||||
} | |||||
this.dialog.on_page_show = function() { | this.dialog.on_page_show = function() { | ||||
me.dialog.fields_dict.list.$wrapper.html('<div class="text-muted text-center">' | me.dialog.fields_dict.list.$wrapper.html('<div class="text-muted text-center">' | ||||
+ __("Loading") + '...</div>'); | + __("Loading") + '...</div>'); | ||||
frappe.call({ | |||||
method:"frappe.desk.form.utils.get_linked_docs", | |||||
args: { | |||||
doctype: me.frm.doctype, | |||||
name: me.frm.docname, | |||||
metadata_loaded: keys(locals.DocType) | |||||
}, | |||||
callback: function(r) { | |||||
var parent = me.dialog.fields_dict.list.$wrapper.empty(); | |||||
if(keys(r.message || {}).length) { | |||||
$.each(keys(r.message).sort(), function(i, doctype) { | |||||
var listview = frappe.views.get_listview(doctype, me); | |||||
listview.no_delete = true; | |||||
var wrapper = $('<div class="panel panel-default"><div>').appendTo(parent); | |||||
$('<div class="panel-heading">').html(__(doctype).bold()).appendTo(wrapper); | |||||
var body = $('<div class="panel-body">').appendTo(wrapper); | |||||
$.each(r.message[doctype], function(i, d) { | |||||
d.doctype = doctype; | |||||
listview.render($('<div class="list-row"></div>') | |||||
.appendTo(body), d, me); | |||||
}) | |||||
}) | |||||
} else { | |||||
parent.html(__("Not Linked to any record.")); | |||||
// execute ajax calls sequentially | |||||
// 1. get linked doctypes | |||||
// 2. load all doctypes | |||||
// 3. load linked docs | |||||
$.when(me.get_linked_doctypes()) | |||||
.then(function() { return me.load_doctypes() }) | |||||
.then(function() { | |||||
if (me.links_not_permitted_or_missing()) { | |||||
return; | |||||
} | } | ||||
return me.get_linked_docs(); | |||||
}); | |||||
} | |||||
}, | |||||
load_doctypes: function() { | |||||
var me = this; | |||||
var already_loaded = Object.keys(locals.DocType); | |||||
var doctypes_to_load = []; | |||||
$.each(Object.keys(me.frm.__linked_doctypes), function(i, v) { | |||||
if (already_loaded.indexOf(v)===-1) { | |||||
doctypes_to_load.push(v); | |||||
} | |||||
}); | |||||
// load all doctypes sequentially using with_doctype | |||||
return $.when.apply($, $.map(doctypes_to_load, function(dt) { | |||||
return frappe.model.with_doctype(dt, function() { | |||||
if (frappe.listview_settings[dt]) { | |||||
// add additional fields to __linked_doctypes | |||||
me.frm.__linked_doctypes[dt].add_fields = frappe.listview_settings[dt].add_fields; | |||||
} | } | ||||
}) | |||||
}); | |||||
})); | |||||
}, | |||||
links_not_permitted_or_missing: function() { | |||||
var me = this; | |||||
var links = []; | |||||
$.each(me.frm.__linked_doctypes, function(doctype, tmp) { | |||||
if(frappe.model.can_get_report(doctype)) { | |||||
links.push({label: __(doctype), value: doctype}); | |||||
} | |||||
}); | |||||
links = frappe.utils.sort(links, "label"); | |||||
if(!links) { | |||||
me.dialog.fields_dict.list.$wrapper.html("<div class='alert alert-warning'>" | |||||
+ me.frm.doctype + ": " | |||||
+ (me.frm.__linked_doctypes ? __("Not Linked to any record.") : __("Not enough permission to see links.")) | |||||
+ "</div>") | |||||
return true; | |||||
} | } | ||||
return false; | |||||
}, | }, | ||||
get_linked_doctypes: function() { | |||||
var me = this; | |||||
if (this.frm.__linked_doctypes) { | |||||
return; | |||||
} | |||||
return frappe.call({ | |||||
method: "frappe.desk.form.linked_with.get_linked_doctypes", | |||||
args: { | |||||
doctype: this.frm.doctype | |||||
}, | |||||
callback: function(r) { | |||||
me.frm.__linked_doctypes = r.message; | |||||
} | |||||
}); | |||||
}, | |||||
get_linked_docs: function() { | |||||
var me = this; | |||||
return frappe.call({ | |||||
method:"frappe.desk.form.linked_with.get_linked_docs", | |||||
args: { | |||||
doctype: me.frm.doctype, | |||||
name: me.frm.docname, | |||||
linkinfo: me.frm.__linked_doctypes | |||||
}, | |||||
callback: function(r) { | |||||
var parent = me.dialog.fields_dict.list.$wrapper.empty(); | |||||
if(keys(r.message || {}).length) { | |||||
$.each(keys(r.message).sort(), function(i, doctype) { | |||||
var listview = frappe.views.get_listview(doctype, me); | |||||
listview.no_delete = true; | |||||
var wrapper = $('<div class="panel panel-default"><div>').appendTo(parent); | |||||
$('<div class="panel-heading">').html(__(doctype).bold()).appendTo(wrapper); | |||||
var body = $('<div class="panel-body">').appendTo(wrapper); | |||||
$.each(r.message[doctype], function(i, d) { | |||||
d.doctype = doctype; | |||||
listview.render($('<div class="list-row"></div>') | |||||
.appendTo(body), d, me); | |||||
}) | |||||
}) | |||||
} else { | |||||
parent.html(__("Not Linked to any record.")); | |||||
} | |||||
} | |||||
}); | |||||
} | |||||
}); | }); |
@@ -83,7 +83,7 @@ $.extend(frappe.model, { | |||||
with_doctype: function(doctype, callback) { | with_doctype: function(doctype, callback) { | ||||
if(locals.DocType[doctype]) { | if(locals.DocType[doctype]) { | ||||
callback(); | |||||
callback && callback(); | |||||
} else { | } else { | ||||
var cached_timestamp = null; | var cached_timestamp = null; | ||||
if(localStorage["_doctype:" + doctype]) { | if(localStorage["_doctype:" + doctype]) { | ||||
@@ -112,7 +112,7 @@ $.extend(frappe.model, { | |||||
} | } | ||||
frappe.model.init_doctype(doctype); | frappe.model.init_doctype(doctype); | ||||
frappe.defaults.set_user_permissions(r.user_permissions); | frappe.defaults.set_user_permissions(r.user_permissions); | ||||
callback(r); | |||||
callback && callback(r); | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||