From a0a3606a7f8f5cda5cdc2ee44f2c6deabddb5f4f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 13:09:55 +0530 Subject: [PATCH] fix(tests): add test cases for custom_link and custom_action --- frappe/app.py | 5 +- frappe/core/doctype/doctype/test_doctype.py | 82 ++++++++++--------- .../doctype/customize_form/customize_form.js | 12 +-- .../doctype/customize_form/customize_form.py | 61 +++++++++----- .../customize_form/test_customize_form.py | 71 ++++++++++++++++ frappe/database/database.py | 6 +- frappe/model/meta.py | 11 +-- 7 files changed, 173 insertions(+), 75 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index 1dccb3e7e2..82471c4e32 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -159,7 +159,10 @@ def handle_exception(e): response = None http_status_code = getattr(e, "http_status_code", 500) return_as_message = False - # print(frappe.get_traceback()) + + if frappe.conf.get('developer_mode'): + # don't fail silently + print(frappe.get_traceback()) if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')): # handle ajax responses first diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 00e80ce4e7..6f4a400577 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -12,41 +12,22 @@ from frappe.core.doctype.doctype.doctype import UniqueFieldnameError, IllegalMan class TestDocType(unittest.TestCase): - def new_doctype(self, name, unique=0, depends_on=''): - return frappe.get_doc({ - "doctype": "DocType", - "module": "Core", - "custom": 1, - "fields": [{ - "label": "Some Field", - "fieldname": "some_fieldname", - "fieldtype": "Data", - "unique": unique, - "depends_on": depends_on, - }], - "permissions": [{ - "role": "System Manager", - "read": 1, - }], - "name": name - }) - def test_validate_name(self): - self.assertRaises(frappe.NameError, self.new_doctype("_Some DocType").insert) - self.assertRaises(frappe.NameError, self.new_doctype("8Some DocType").insert) - self.assertRaises(frappe.NameError, self.new_doctype("Some (DocType)").insert) + self.assertRaises(frappe.NameError, new_doctype("_Some DocType").insert) + self.assertRaises(frappe.NameError, new_doctype("8Some DocType").insert) + self.assertRaises(frappe.NameError, new_doctype("Some (DocType)").insert) for name in ("Some DocType", "Some_DocType"): if frappe.db.exists("DocType", name): frappe.delete_doc("DocType", name) - doc = self.new_doctype(name).insert() + doc = new_doctype(name).insert() doc.delete() def test_doctype_unique_constraint_dropped(self): if frappe.db.exists("DocType", "With_Unique"): frappe.delete_doc("DocType", "With_Unique") - dt = self.new_doctype("With_Unique", unique=1) + dt = new_doctype("With_Unique", unique=1) dt.insert() doc1 = frappe.new_doc("With_Unique") @@ -67,7 +48,7 @@ class TestDocType(unittest.TestCase): doc2.delete() def test_validate_search_fields(self): - doc = self.new_doctype("Test Search Fields") + doc = new_doctype("Test Search Fields") doc.search_fields = "some_fieldname" doc.insert() self.assertEqual(doc.name, "Test Search Fields") @@ -85,7 +66,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(frappe.ValidationError, doc.save) def test_depends_on_fields(self): - doc = self.new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") + doc = new_doctype("Test Depends On", depends_on="eval:doc.__islocal == 0") doc.insert() # check if the assignment operation is allowed in depends_on @@ -261,7 +242,7 @@ class TestDocType(unittest.TestCase): frappe.flags.allow_doctype_export = 0 def test_unique_field_name_for_two_fields(self): - doc = self.new_doctype('Test Unique Field') + doc = new_doctype('Test Unique Field') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Data' @@ -273,7 +254,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(UniqueFieldnameError, doc.insert) def test_fieldname_is_not_name(self): - doc = self.new_doctype('Test Name Field') + doc = new_doctype('Test Name Field') field_1 = doc.append('fields', {}) field_1.label = 'Name' field_1.fieldtype = 'Data' @@ -283,7 +264,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(InvalidFieldNameError, doc.save) def test_illegal_mandatory_validation(self): - doc = self.new_doctype('Test Illegal mandatory') + doc = new_doctype('Test Illegal mandatory') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Section Break' @@ -292,7 +273,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(IllegalMandatoryError, doc.insert) def test_link_with_wrong_and_no_options(self): - doc = self.new_doctype('Test link') + doc = new_doctype('Test link') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Link' @@ -304,7 +285,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(WrongOptionsDoctypeLinkError, doc.insert) def test_hidden_and_mandatory_without_default(self): - doc = self.new_doctype('Test hidden and mandatory') + doc = new_doctype('Test hidden and mandatory') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Data' @@ -314,7 +295,7 @@ class TestDocType(unittest.TestCase): self.assertRaises(HiddenAndMandatoryWithoutDefaultError, doc.insert) def test_field_can_not_be_indexed_validation(self): - doc = self.new_doctype('Test index') + doc = new_doctype('Test index') field_1 = doc.append('fields', {}) field_1.fieldname = 'some_fieldname_1' field_1.fieldtype = 'Long Text' @@ -327,14 +308,14 @@ class TestDocType(unittest.TestCase): from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs #create doctype - link_doc = self.new_doctype('Test Linked Doctype') + link_doc = new_doctype('Test Linked Doctype') link_doc.is_submittable = 1 for data in link_doc.get('permissions'): data.submit = 1 data.cancel = 1 link_doc.insert() - doc = self.new_doctype('Test Doctype') + doc = new_doctype('Test Doctype') doc.is_submittable = 1 field_2 = doc.append('fields', {}) field_2.label = 'Test Linked Doctype' @@ -377,12 +358,12 @@ class TestDocType(unittest.TestCase): doc.delete() frappe.db.commit() - def test_ignore_cancelation_of_linked_doctype_during_cancell(self): + def test_ignore_cancelation_of_linked_doctype_during_cancel(self): import json from frappe.desk.form.linked_with import get_submitted_linked_docs, cancel_all_linked_docs #create linked doctype - link_doc = self.new_doctype('Test Linked Doctype 1') + link_doc = new_doctype('Test Linked Doctype 1') link_doc.is_submittable = 1 for data in link_doc.get('permissions'): data.submit = 1 @@ -390,7 +371,7 @@ class TestDocType(unittest.TestCase): link_doc.insert() #create first parent doctype - test_doc_1 = self.new_doctype('Test Doctype 1') + test_doc_1 = new_doctype('Test Doctype 1') test_doc_1.is_submittable = 1 field_2 = test_doc_1.append('fields', {}) @@ -405,7 +386,7 @@ class TestDocType(unittest.TestCase): test_doc_1.insert() #crete second parent doctype - doc = self.new_doctype('Test Doctype 2') + doc = new_doctype('Test Doctype 2') doc.is_submittable = 1 field_2 = doc.append('fields', {}) @@ -469,3 +450,28 @@ class TestDocType(unittest.TestCase): doc.delete() test_doc_1.delete() frappe.db.commit() + +def new_doctype(name, unique=0, depends_on='', fields=None): + doc = frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "custom": 1, + "fields": [{ + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data", + "unique": unique, + "depends_on": depends_on, + }], + "permissions": [{ + "role": "System Manager", + "read": 1, + }], + "name": name + }) + + if fields: + for f in fields: + doc.append('fields', f) + + return doc \ No newline at end of file diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 0a2b02e6c7..6b0fbb042d 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -94,19 +94,19 @@ frappe.ui.form.on("Customize Form", { frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() { frappe.set_route('List', frm.doc.doc_type); - }); + }, __('Actions')); - frm.add_custom_button(__('Refresh Form'), function() { + frm.add_custom_button(__('Reload'), function() { frm.script_manager.trigger("doc_type"); - }, "fa fa-refresh", "btn-default"); + }, __('Actions')); frm.add_custom_button(__('Reset to defaults'), function() { frappe.customize_form.confirm(__('Remove all customizations?'), frm); - }, "fa fa-eraser", "btn-default"); + }, __('Actions')); frm.add_custom_button(__('Set Permissions'), function() { frappe.set_route('permission-manager', frm.doc.doc_type); - }, "fa fa-lock", "btn-default"); + }, __('Actions')); if (frappe.boot.developer_mode) { frm.add_custom_button(__('Export Customizations'), function() { @@ -131,7 +131,7 @@ frappe.ui.form.on("Customize Form", { }); }, __("Select Module")); - }); + }, __('Actions')); } } diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index fb4ba1bcd6..034b98a7c2 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -46,8 +46,8 @@ class CustomizeForm(Document): ''' Check if the doctype is allowed to be customized. ''' - #if self.doc_type in core_doctypes_list: - # frappe.throw(_("Core DocTypes cannot be customized.")) + if self.doc_type in core_doctypes_list: + frappe.throw(_("Core DocTypes cannot be customized.")) if meta.issingle: frappe.throw(_("Single DocTypes cannot be customized.")) @@ -71,7 +71,7 @@ class CustomizeForm(Document): for fieldname in ('links', 'actions'): for d in meta.get(fieldname): - self.append(fieldname, d) + d1 = self.append(fieldname, d) def create_auto_repeat_custom_field_if_requried(self, meta): if self.allow_auto_repeat: @@ -242,35 +242,52 @@ class CustomizeForm(Document): ('DocType Action', 'actions', doctype_action_properties) ): has_custom = False - for d in self.get(fieldname): - if not (d.custom and frappe.db.exists(doctype, d.name)): + items = [] + for i, d in enumerate(self.get(fieldname) or []): + d.idx = i + if frappe.db.exists(doctype, d.name) and not d.custom: # check property and apply property setter original = frappe.get_doc(doctype, d.name) for prop, prop_type in field_map.items(): if d.get(prop) != original.get(prop): self.make_property_setter(prop, d.get(prop), prop_type, apply_on=doctype, row_name=d.name) + items.append(d.name) else: - # add or update custom object - if frappe.db.exists(doctype, d.name): - doc = frappe.get_doc(doctype, d.name) - else: - doc = frappe.new_doc(doctype) - doc.parent = self.doc_type - doc.parenttype = '_Custom' # dummy parenttype since its mandatory - doc.custom = 1 - - for prop, prop_type in field_map.items(): - doc.set(prop, d.get(prop)) - - doc.save(ignore_permissions=True) + # custom - just insert/update + d.parent = self.doc_type + d.custom = 1 + d.save(ignore_permissions=True) has_custom = True + items.append(d.name) - if has_custom: - # save the order of the actions and links - self.make_property_setter('{}_order'.format(fieldname), - json.dumps([d.name for d in self.get(fieldname)]), 'Small Text') + self.update_order_property_setter(has_custom, fieldname) + self.clear_removed_items(doctype, items) + def update_order_property_setter(self, has_custom, fieldname): + ''' + We need to maintain the order of the link/actions if the user has shuffled them. + So we create a new property (ex `links_order`) to keep a list of items. + ''' + property_name = '{}_order'.format(fieldname) + if has_custom: + # save the order of the actions and links + self.make_property_setter(property_name, + json.dumps([d.name for d in self.get(fieldname)]), 'Small Text') + else: + frappe.db.delete('Property Setter', dict(property=property_name, + doc_type=self.doc_type)) + + + def clear_removed_items(self, doctype, items): + ''' + Clear rows that do not appear in `items`. These have been removed by the user. + ''' + if items: + frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1, + name=('not in', items))) + else: + frappe.db.delete(doctype, dict(parent=self.doc_type, custom=1)) def update_custom_fields(self): for i, df in enumerate(self.get("fields")): diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index cace25a03d..b631c81d96 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, unittest, json from frappe.test_runner import make_test_records_for_doctype from frappe.core.doctype.doctype.doctype import InvalidFieldNameError +from frappe.core.doctype.doctype.test_doctype import new_doctype test_dependencies = ["Custom Field", "Property Setter"] class TestCustomizeForm(unittest.TestCase): @@ -191,3 +192,73 @@ class TestCustomizeForm(unittest.TestCase): # core doctype is invalid, hence no attributes are set self.assertEquals(d.get("fields"), []) self.assertEquals(e.get("fields"), []) + + def test_custom_link(self): + try: + # create a dummy doctype linked to Event + testdt_name = 'Test Link for Event' + testdt = new_doctype(testdt_name, fields=[ + dict(fieldtype='Link', fieldname='event', options='Event') + ]).insert() + + testdt_name1 = 'Test Link for Event 1' + testdt1 = new_doctype(testdt_name1, fields=[ + dict(fieldtype='Link', fieldname='event', options='Event') + ]).insert() + + # add a custom link + d = self.get_customize_form("Event") + + d.append('links', dict(link_doctype=testdt_name, link_fieldname='event', group='Tests')) + d.append('links', dict(link_doctype=testdt_name1, link_fieldname='event', group='Tests')) + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + # check links exist + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name]) + self.assertTrue([d.name for d in event.links if d.link_doctype == testdt_name1]) + + # check order + order = json.loads(event.links_order) + self.assertListEqual(order, [d.name for d in event.links]) + + # remove the link + d = self.get_customize_form("Event") + d.links = [] + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + self.assertFalse([d.name for d in (event.links or []) if d.link_doctype == testdt_name]) + finally: + testdt.delete() + testdt1.delete() + + def test_custom_action(self): + test_route = '#List/DocType' + + # create a dummy action (route) + d = self.get_customize_form("Event") + d.append('actions', dict(label='Test Action', action_type='Route', action=test_route)) + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + # check if added to meta + action = [d for d in event.actions if d.label=='Test Action'] + self.assertEqual(len(action), 1) + self.assertEqual(action[0].action, test_route) + + # clear the action + d = self.get_customize_form("Event") + d.actions = [] + d.run_method("save_customization") + + frappe.clear_cache() + event = frappe.get_meta('Event') + + action = [d for d in event.actions if d.label=='Test Action'] + self.assertEqual(len(action), 0) diff --git a/frappe/database/database.py b/frappe/database/database.py index d9755abd33..f94f19c62b 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -341,7 +341,7 @@ class Database(object): value = filters.get(key) values[key] = value if isinstance(value, (list, tuple)): - # value is a tuble like ("!=", 0) + # value is a tuple like ("!=", 0) _operator = value[0] values[key] = value[1] if isinstance(value[1], (tuple, list)): @@ -959,13 +959,13 @@ class Database(object): query = sql_dict.get(current_dialect) return self.sql(query, values, **kwargs) - def delete(self, doctype, conditions): + def delete(self, doctype, conditions, debug=False): if conditions: conditions, values = self.build_conditions(conditions) return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format( doctype=doctype, conditions=conditions - ), values) + ), values, debug=debug) else: frappe.throw(_('No conditions provided')) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index af0f574fcf..8aa761ac21 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -343,8 +343,8 @@ class Meta(Document): def add_custom_links_and_actions(self): for doctype, fieldname in (('DocType Link', 'links'), ('DocType Action', 'actions')): - for d in frappe.get_all(doctype, dict(parent=self.name, custom=1)): - self.get(fieldname).append(d) + for d in frappe.get_all(doctype, fields='*', filters=dict(parent=self.name, custom=1)): + self.append(fieldname, d) # set the fields in order if specified # order is saved as `links_order` @@ -353,14 +353,15 @@ class Meta(Document): name_map = {d.name:d for d in self.get(fieldname)} new_list = [] for name in order: - new_list.append(name_map[name]) - name_map[name].__added = True + if name in name_map: + new_list.append(name_map[name]) # add the missing items that have not be added # maybe these items were added to the standard product # after the customization was done for d in self.get(fieldname): - if not d.__added: new_list.append(d) + if not d in new_list: + new_list.append(d) self.set(fieldname, new_list)