Merge branch 'dynamic-links' of https://github.com/rmehta/frappe into rmehta-dynamic-links
This commit is contained in:
commit
737d81e45d
8 changed files with 199 additions and 59 deletions
|
|
@ -5,7 +5,6 @@ from __future__ import unicode_literals
|
|||
import frappe
|
||||
from frappe.utils import cstr
|
||||
from frappe import _
|
||||
import json
|
||||
from frappe.model.document import Document
|
||||
|
||||
class CustomField(Document):
|
||||
|
|
|
|||
|
|
@ -16,11 +16,6 @@ test_user = "test1@example.com"
|
|||
|
||||
|
||||
class TestEvent(unittest.TestCase):
|
||||
# def setUp(self):
|
||||
# user = frappe.get_doc("User", test_user)
|
||||
# user.add_roles("Website Manager")
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ from __future__ import unicode_literals
|
|||
|
||||
import frappe
|
||||
import frappe.model.meta
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
import frappe.defaults
|
||||
from frappe.utils.file_manager import remove_all
|
||||
from frappe import _
|
||||
from rename_doc import dynamic_link_queries
|
||||
from frappe.model.naming import revert_series_if_last
|
||||
|
||||
def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reload=False,
|
||||
|
|
@ -71,9 +71,19 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
|
|||
if not ignore_on_trash:
|
||||
doc.run_method("on_trash")
|
||||
|
||||
delete_linked_todos(doc)
|
||||
delete_linked_communications(doc)
|
||||
delete_shared(doc)
|
||||
dynamic_linked_doctypes = [df.parent for df in get_dynamic_link_map().get(doc.doctype, [])]
|
||||
if "ToDo" in dynamic_linked_doctypes:
|
||||
delete_linked_todos(doc)
|
||||
|
||||
if "Communication" in dynamic_linked_doctypes:
|
||||
delete_linked_communications(doc)
|
||||
|
||||
if "DocShare" in dynamic_linked_doctypes:
|
||||
delete_shared(doc)
|
||||
|
||||
if "Email Unsubscribe":
|
||||
delete_email_subscribe(doc)
|
||||
|
||||
# check if links exist
|
||||
if not force:
|
||||
check_if_doc_is_linked(doc)
|
||||
|
|
@ -159,39 +169,47 @@ def check_if_doc_is_linked(doc, method="Delete"):
|
|||
frappe.LinkExistsError)
|
||||
|
||||
def check_if_doc_is_dynamically_linked(doc, method="Delete"):
|
||||
for query in dynamic_link_queries:
|
||||
for df in frappe.db.sql(query, as_dict=True):
|
||||
if frappe.get_meta(df.parent).issingle:
|
||||
'''Raise `frappe.LinkExistsError` if the document is dynamically linked'''
|
||||
for df in get_dynamic_link_map().get(doc.doctype, []):
|
||||
if df.parent in ("Communication", "ToDo", "DocShare", "Email Unsubscribe"):
|
||||
# don't check for communication and todo!
|
||||
continue
|
||||
|
||||
# dynamic link in single doc
|
||||
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||
if (refdoc.get(df.options)==doc.doctype
|
||||
and refdoc.get(df.fieldname)==doc.name
|
||||
and ((method=="Delete" and refdoc.docstatus < 2)
|
||||
or (method=="Cancel" and refdoc.docstatus==1))
|
||||
):
|
||||
meta = frappe.get_meta(df.parent)
|
||||
if meta.issingle:
|
||||
# dynamic link in single doc
|
||||
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||
if (refdoc.get(df.options)==doc.doctype
|
||||
and refdoc.get(df.fieldname)==doc.name
|
||||
and ((method=="Delete" and refdoc.docstatus < 2)
|
||||
or (method=="Cancel" and refdoc.docstatus==1))
|
||||
):
|
||||
# raise exception only if
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype,
|
||||
doc.name, df.parent, ""), frappe.LinkExistsError)
|
||||
else:
|
||||
# dynamic link in table
|
||||
for refdoc in frappe.db.sql("""select name, docstatus from `tab{parent}` where
|
||||
{options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name), as_dict=True):
|
||||
|
||||
if ((method=="Delete" and refdoc.docstatus < 2) or (method=="Cancel" and refdoc.docstatus==1)):
|
||||
# raise exception only if
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}").format(doc.doctype,
|
||||
doc.name, df.parent, ""), frappe.LinkExistsError)
|
||||
else:
|
||||
# dynamic link in table
|
||||
for refdoc in frappe.db.sql("""select name, docstatus from `tab{parent}` where
|
||||
{options}=%s and {fieldname}=%s""".format(**df), (doc.doctype, doc.name), as_dict=True):
|
||||
|
||||
if ((method=="Delete" and refdoc.docstatus < 2) or (method=="Cancel" and refdoc.docstatus==1)):
|
||||
# raise exception only if
|
||||
# linked to an non-cancelled doc when deleting
|
||||
# or linked to a submitted doc when cancelling
|
||||
frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}")\
|
||||
.format(doc.doctype, doc.name, df.parent, refdoc.name), frappe.LinkExistsError)
|
||||
frappe.throw(_("Cannot delete or cancel because {0} {1} is linked with {2} {3}")\
|
||||
.format(doc.doctype, doc.name, df.parent, refdoc.name), frappe.LinkExistsError)
|
||||
|
||||
def delete_linked_todos(doc):
|
||||
delete_doc("ToDo", frappe.db.sql_list("""select name from `tabToDo`
|
||||
where reference_type=%s and reference_name=%s""", (doc.doctype, doc.name)),
|
||||
ignore_permissions=True)
|
||||
|
||||
def delete_email_subscribe(doc):
|
||||
frappe.db.sql('''delete from `tabEmail Unsubscribe`
|
||||
where reference_doctype=%s and reference_name=%s''', (doc.doctype, doc.name))
|
||||
|
||||
def delete_linked_communications(doc):
|
||||
# delete comments
|
||||
frappe.db.sql("""delete from `tabCommunication`
|
||||
|
|
|
|||
41
frappe/model/dynamic_links.py
Normal file
41
frappe/model/dynamic_links.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
dynamic_link_queries = [
|
||||
"""select parent, fieldname, options from tabDocField where fieldtype='Dynamic Link'""",
|
||||
"""select dt as parent, fieldname, options from `tabCustom Field` where fieldtype='Dynamic Link'""",
|
||||
]
|
||||
|
||||
def get_dynamic_link_map(for_delete=False):
|
||||
'''Build a map of all dynamically linked tables. For example,
|
||||
if Note is dynamically linked to ToDo, the function will return
|
||||
`{"Note": ["ToDo"], "Sales Invoice": ["Journal Entry Detail"]}`
|
||||
|
||||
Note: Will not map single doctypes
|
||||
'''
|
||||
if getattr(frappe.local, 'dynamic_link_map', None)==None or frappe.flags.in_test:
|
||||
# Build from scratch
|
||||
dynamic_link_map = {}
|
||||
for df in get_dynamic_links():
|
||||
meta = frappe.get_meta(df.parent)
|
||||
if meta.issingle:
|
||||
# always check in Single DocTypes
|
||||
dynamic_link_map.setdefault(meta.name, []).append(df)
|
||||
else:
|
||||
links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df))
|
||||
for doctype in links:
|
||||
dynamic_link_map.setdefault(doctype, []).append(df)
|
||||
|
||||
frappe.local.dynamic_link_map = dynamic_link_map
|
||||
|
||||
return frappe.local.dynamic_link_map
|
||||
|
||||
def get_dynamic_links():
|
||||
'''Return list of dynamic link fields as DocField.
|
||||
Uses cache if possible'''
|
||||
df = []
|
||||
for query in dynamic_link_queries:
|
||||
df += frappe.db.sql(query, as_dict=True)
|
||||
return df
|
||||
|
|
@ -61,7 +61,9 @@ class Meta(Document):
|
|||
return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]})
|
||||
|
||||
def get_dynamic_link_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Dynamic Link"})
|
||||
if not hasattr(self, '_dynamic_link_fields'):
|
||||
self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"})
|
||||
return self._dynamic_link_fields
|
||||
|
||||
def get_select_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Select", "options":["not in",
|
||||
|
|
@ -358,8 +360,8 @@ def trim_tables():
|
|||
def clear_cache(doctype=None):
|
||||
cache = frappe.cache()
|
||||
|
||||
cache.delete_value("is_table")
|
||||
cache.delete_value("doctype_modules")
|
||||
for key in ('is_table', 'doctype_modules'):
|
||||
cache.delete_value(key)
|
||||
|
||||
groups = ["meta", "form_meta", "table_columns", "last_modified", "linked_doctypes"]
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from frappe.model.naming import validate_name
|
||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||
|
||||
@frappe.whitelist()
|
||||
def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=False):
|
||||
|
|
@ -297,33 +298,26 @@ def update_parenttype_values(old, new):
|
|||
where parenttype=%s""" % (doctype, '%s', '%s'),
|
||||
(new, old))
|
||||
|
||||
dynamic_link_queries = [
|
||||
"""select parent, fieldname, options from tabDocField where fieldtype='Dynamic Link'""",
|
||||
"""select dt as parent, fieldname, options from `tabCustom Field` where fieldtype='Dynamic Link'""",
|
||||
]
|
||||
|
||||
def rename_dynamic_links(doctype, old, new):
|
||||
for query in dynamic_link_queries:
|
||||
for df in frappe.db.sql(query, as_dict=True):
|
||||
for df in get_dynamic_link_map().get(doctype, []):
|
||||
# dynamic link in single, just one value to check
|
||||
if frappe.get_meta(df.parent).issingle:
|
||||
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||
if refdoc.get(df.options)==doctype and refdoc.get(df.fieldname)==old:
|
||||
|
||||
# dynamic link in single, just one value to check
|
||||
if frappe.get_meta(df.parent).issingle:
|
||||
refdoc = frappe.db.get_singles_dict(df.parent)
|
||||
if refdoc.get(df.options)==doctype and refdoc.get(df.fieldname)==old:
|
||||
frappe.db.sql("""update tabSingles set value=%s where
|
||||
field=%s and value=%s and doctype=%s""", (new, df.fieldname, old, df.parent))
|
||||
else:
|
||||
# because the table hasn't been renamed yet!
|
||||
parent = df.parent if df.parent != new else old
|
||||
|
||||
frappe.db.sql("""update tabSingles set value=%s where
|
||||
field=%s and value=%s and doctype=%s""", (new, df.fieldname, old, df.parent))
|
||||
else:
|
||||
# because the table hasn't been renamed yet!
|
||||
parent = df.parent if df.parent != new else old
|
||||
# replace for each value where renamed
|
||||
for to_change in frappe.db.sql_list("""select name from `tab{parent}` where
|
||||
{options}=%s and {fieldname}=%s""".format(parent=parent, options=df.options,
|
||||
fieldname=df.fieldname), (doctype, old)):
|
||||
|
||||
# replace for each value where renamed
|
||||
for to_change in frappe.db.sql_list("""select name from `tab{parent}` where
|
||||
{options}=%s and {fieldname}=%s""".format(parent=parent, options=df.options,
|
||||
fieldname=df.fieldname), (doctype, old)):
|
||||
|
||||
frappe.db.sql("""update `tab{parent}` set {fieldname}=%s
|
||||
where name=%s""".format(**df), (new, to_change))
|
||||
frappe.db.sql("""update `tab{parent}` set {fieldname}=%s
|
||||
where name=%s""".format(**df), (new, to_change))
|
||||
|
||||
def bulk_rename(doctype, rows=None, via_console = False):
|
||||
"""Bulk rename documents
|
||||
|
|
|
|||
73
frappe/tests/test_dynamic_links.py
Normal file
73
frappe/tests/test_dynamic_links.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe, unittest
|
||||
|
||||
class TestDynamicLinks(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql('delete from `tabEmail Unsubscribe`')
|
||||
|
||||
def test_delete_normal(self):
|
||||
event = frappe.get_doc({
|
||||
'doctype': 'Event',
|
||||
'subject':'test-for-delete',
|
||||
'starts_on': '2014-01-01',
|
||||
'event_type': 'Public'
|
||||
}).insert()
|
||||
|
||||
unsub = frappe.get_doc({
|
||||
'doctype': 'Email Unsubscribe',
|
||||
'email': 'test@example.com',
|
||||
'reference_doctype': event.doctype,
|
||||
'reference_name': event.name
|
||||
}).insert()
|
||||
|
||||
event.delete()
|
||||
|
||||
self.assertFalse(frappe.db.exists('Email Unsubscribe', unsub.name))
|
||||
|
||||
def test_delete_with_comment(self):
|
||||
event = frappe.get_doc({
|
||||
'doctype': 'Event',
|
||||
'subject':'test-for-delete-1',
|
||||
'starts_on': '2014-01-01',
|
||||
'event_type': 'Public'
|
||||
}).insert()
|
||||
event.add_comment('Comment', 'test')
|
||||
|
||||
self.assertTrue(frappe.get_all('Communication',
|
||||
filters={'reference_doctype':'Event', 'reference_name':event.name}))
|
||||
event.delete()
|
||||
self.assertFalse(frappe.get_all('Communication',
|
||||
filters={'reference_doctype':'Event', 'reference_name':event.name}))
|
||||
|
||||
def test_custom_fields(self):
|
||||
from frappe.utils.testutils import add_custom_field, clear_custom_fields
|
||||
add_custom_field('Event', 'test_ref_doc', 'Link', 'DocType')
|
||||
add_custom_field('Event', 'test_ref_name', 'Dynamic Link', 'test_ref_doc')
|
||||
|
||||
unsub = frappe.get_doc({
|
||||
'doctype': 'Email Unsubscribe',
|
||||
'email': 'test@example.com',
|
||||
'global_unsubscribe': 1
|
||||
}).insert()
|
||||
|
||||
event = frappe.get_doc({
|
||||
'doctype': 'Event',
|
||||
'subject':'test-for-delete-2',
|
||||
'starts_on': '2014-01-01',
|
||||
'event_type': 'Public',
|
||||
'test_ref_doc': unsub.doctype,
|
||||
'test_ref_name': unsub.name
|
||||
}).insert()
|
||||
|
||||
self.assertRaises(frappe.LinkExistsError, unsub.delete)
|
||||
|
||||
event.test_ref_doc = None
|
||||
event.test_ref_name = None
|
||||
event.save()
|
||||
|
||||
unsub.delete()
|
||||
|
||||
clear_custom_fields('Event')
|
||||
18
frappe/utils/testutils.py
Normal file
18
frappe/utils/testutils.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# MIT License. See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
|
||||
def add_custom_field(doctype, fieldname, fieldtype='Data', options=None):
|
||||
frappe.get_doc({
|
||||
"doctype": "Custom Field",
|
||||
"dt": doctype,
|
||||
"fieldname": fieldname,
|
||||
"fieldtype": fieldtype,
|
||||
"options": options
|
||||
}).insert()
|
||||
|
||||
def clear_custom_fields(doctype):
|
||||
frappe.db.sql('delete from `tabCustom Field` where dt=%s', doctype)
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
Loading…
Add table
Reference in a new issue