From 1e48ced097066161a6567e1e4ed2cb186a66b14a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 24 Sep 2020 18:19:32 +0530 Subject: [PATCH 01/24] feat(customize form): add links and actions to customize form and cleanup code --- frappe/__init__.py | 1 + frappe/app.py | 1 + .../doctype_action/doctype_action.json | 12 +- .../doctype/doctype_link/doctype_link.json | 21 +- .../doctype/customize_form/customize_form.js | 73 +- .../customize_form/customize_form.json | 92 +- .../doctype/customize_form/customize_form.py | 818 ++++++++++-------- .../customize_form_field.json | 11 +- .../property_setter/property_setter.json | 451 +++------- .../property_setter/property_setter.py | 8 +- .../email/doctype/email_group/email_group.js | 5 - .../doctype/email_group/email_group.json | 12 +- frappe/model/dynamic_links.py | 9 +- frappe/model/meta.py | 65 +- 14 files changed, 811 insertions(+), 768 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index a6a5067ea2..c5f13f2295 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1154,6 +1154,7 @@ def make_property_setter(args, ignore_validate=False, validate_fields_for_doctyp 'doctype_or_field': args.doctype_or_field, 'doc_type': doctype, 'field_name': args.fieldname, + 'row_name': args.row_name, 'property': args.property, 'value': args.value, 'property_type': args.property_type or "Data", diff --git a/frappe/app.py b/frappe/app.py index c4d6a0235a..2c8dcec26b 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -159,6 +159,7 @@ 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.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_action/doctype_action.json b/frappe/core/doctype/doctype_action/doctype_action.json index 0f9da802eb..080755c479 100644 --- a/frappe/core/doctype/doctype_action/doctype_action.json +++ b/frappe/core/doctype/doctype_action/doctype_action.json @@ -9,7 +9,8 @@ "action_type", "action", "group", - "hidden" + "hidden", + "custom" ], "fields": [ { @@ -48,12 +49,19 @@ "fieldname": "hidden", "fieldtype": "Check", "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-21 14:44:03.845315", + "modified": "2020-09-24 14:19:05.549835", "modified_by": "Administrator", "module": "Core", "name": "DocType Action", diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json index 752b4bb5da..2adfd4a6c3 100644 --- a/frappe/core/doctype/doctype_link/doctype_link.json +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -7,7 +7,9 @@ "field_order": [ "link_doctype", "link_fieldname", - "group" + "group", + "hidden", + "custom" ], "fields": [ { @@ -30,10 +32,25 @@ "fieldtype": "Data", "in_list_view": 1, "label": "Group" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "custom", + "fieldtype": "Check", + "hidden": 1, + "label": "Custom" } ], + "index_web_pages_for_search": 1, "istable": 1, - "modified": "2019-09-24 11:41:25.291377", + "links": [], + "modified": "2020-09-24 14:19:23.189511", "modified_by": "Administrator", "module": "Core", "name": "DocType Link", diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index b1743a96a5..7dfec0b0b0 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -5,13 +5,14 @@ frappe.provide("frappe.customize_form"); frappe.ui.form.on("Customize Form", { onload: function(frm) { + frm.disable_save(); frm.set_query("doc_type", function() { return { translate_values: false, filters: [ ['DocType', 'issingle', '=', 0], ['DocType', 'custom', '=', 0], - ['DocType', 'name', 'not in', frappe.model.core_doctypes_list], + //['DocType', 'name', 'not in', frappe.model.core_doctypes_list], ['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] ] }; @@ -27,7 +28,7 @@ frappe.ui.form.on("Customize Form", { }); $(frm.wrapper).on("grid-row-render", function(e, grid_row) { - if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") { + if (grid_row.doc && grid_row.doc.fieldtype=="Section Break") { $(grid_row.row).css({"font-weight": "bold"}); } }); @@ -40,19 +41,25 @@ frappe.ui.form.on("Customize Form", { frm.trigger("setup_sortable"); }); + if (localStorage['customize_doctype']) { + // set default value from customize form + frm.set_value('doc_type', localStorage['customize_doctype']); + } + }, doc_type: function(frm) { - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { return frm.call({ method: "fetch_to_customize", doc: frm.doc, freeze: true, callback: function(r) { - if(r) { - if(r._server_messages && r._server_messages.length) { + if (r) { + if (r._server_messages && r._server_messages.length) { frm.set_value("doc_type", ""); } else { + localStorage['customize_doctype'] = frm.doc.doc_type; frm.refresh(); frm.trigger("setup_sortable"); } @@ -69,7 +76,7 @@ frappe.ui.form.on("Customize Form", { frm.doc.fields.forEach(function(f, i) { var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row'); - if(f.is_custom_field) { + if (f.is_custom_field) { data_row.addClass("highlight"); } else { f._sortable = false; @@ -82,7 +89,7 @@ frappe.ui.form.on("Customize Form", { frm.disable_save(); frm.page.clear_icons(); - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { frappe.customize_form.set_primary_action(frm); frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() { @@ -101,7 +108,7 @@ frappe.ui.form.on("Customize Form", { frappe.set_route('permission-manager', frm.doc.doc_type); }, "fa fa-lock", "btn-default"); - if(frappe.boot.developer_mode) { + if (frappe.boot.developer_mode) { frm.add_custom_button(__('Export Customizations'), function() { frappe.prompt( [ @@ -129,29 +136,29 @@ frappe.ui.form.on("Customize Form", { } // sort order select - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { var fields = $.map(frm.doc.fields, - function(df) { return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; }); + function(df) { return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; }); fields = ["", "name", "modified"].concat(fields); frm.set_df_property("sort_field", "options", fields); } - if(frappe.route_options && frappe.route_options.doc_type) { + if (frappe.route_options && frappe.route_options.doc_type) { setTimeout(function() { frm.set_value("doc_type", frappe.route_options.doc_type); frappe.route_options = null; }, 1000); } - } }); +// can't delete standard fields frappe.ui.form.on("Customize Form Field", { before_fields_remove: function(frm, doctype, name) { var row = frappe.get_doc(doctype, name); - if(!(row.is_custom_field || row.__islocal)) { + if (!(row.is_custom_field || row.__islocal)) { frappe.msgprint(__("Cannot delete standard field. You can hide it if you want")); - throw "cannot delete custom field"; + throw "cannot delete standard field"; } }, fields_add: function(frm, cdt, cdn) { @@ -160,16 +167,46 @@ frappe.ui.form.on("Customize Form Field", { } }); +// can't delete standard links +frappe.ui.form.on("DocType Link", { + before_links_remove: function(frm, doctype, name) { + let row = frappe.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + frappe.msgprint(__("Cannot delete standard link. You can hide it if you want")); + throw "cannot delete standard link"; + } + }, + links_add: function(frm, cdt, cdn) { + let f = frappe.model.get_doc(cdt, cdn); + f.custom = 1; + } +}); + +// can't delete standard actions +frappe.ui.form.on("DocType Action", { + before_actions_remove: function(frm, doctype, name) { + let row = frappe.get_doc(doctype, name); + if (!(row.custom || row.__islocal)) { + frappe.msgprint(__("Cannot delete standard action. You can hide it if you want")); + throw "cannot delete standard action"; + } + }, + actions_add: function(frm, cdt, cdn) { + let f = frappe.model.get_doc(cdt, cdn); + f.custom = 1; + } +}); + frappe.customize_form.set_primary_action = function(frm) { frm.page.set_primary_action(__("Update"), function() { - if(frm.doc.doc_type) { + if (frm.doc.doc_type) { return frm.call({ doc: frm.doc, freeze: true, btn: frm.page.btn_primary, method: "save_customization", callback: function(r) { - if(!r.exc) { + if (!r.exc) { frappe.customize_form.clear_locals_and_refresh(frm); frm.script_manager.trigger("doc_type"); } @@ -180,7 +217,7 @@ frappe.customize_form.set_primary_action = function(frm) { }; frappe.customize_form.confirm = function(msg, frm) { - if(!frm.doc.doc_type) return; + if (!frm.doc.doc_type) return; var d = new frappe.ui.Dialog({ title: 'Reset To Defaults', @@ -192,7 +229,7 @@ frappe.customize_form.confirm = function(msg, frm) { doc: frm.doc, method: "reset_to_defaults", callback: function(r) { - if(r.exc) { + if (r.exc) { frappe.msgprint(r.exc); } else { d.hide(); diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index cd57aa23fe..ff102b3c08 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -10,8 +10,9 @@ "doc_type", "properties", "label", - "default_print_format", "max_attachments", + "search_fields", + "column_break_5", "allow_copy", "istable", "editable_grid", @@ -20,22 +21,27 @@ "track_views", "allow_auto_repeat", "allow_import", - "show_preview_popup", - "image_view", - "column_break_5", + "fields_section_break", + "fields", + "view_settings_section", "title_field", "image_field", - "search_fields", - "section_break_8", - "sort_field", - "column_break_10", - "sort_order", - "section_break_23", + "default_print_format", + "column_break_29", + "show_preview_popup", + "image_view", + "email_settings_section", "email_append_to", "sender_field", "subject_field", - "fields_section_break", - "fields" + "document_actions_section", + "actions", + "document_links_section", + "links", + "section_break_8", + "sort_field", + "column_break_10", + "sort_order" ], "fields": [ { @@ -130,9 +136,11 @@ "label": "Search Fields" }, { + "collapsible": 1, "depends_on": "doc_type", "fieldname": "section_break_8", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "List Settings" }, { "fieldname": "sort_field", @@ -161,7 +169,8 @@ "fieldname": "fields", "fieldtype": "Table", "label": "Fields", - "options": "Customize Form Field" + "options": "Customize Form Field", + "reqd": 1 }, { "default": "0", @@ -200,24 +209,67 @@ "fieldtype": "Check", "label": "Allow document creation via Email" }, - { - "depends_on": "doc_type", - "fieldname": "section_break_23", - "fieldtype": "Section Break" - }, { "default": "0", "fieldname": "show_preview_popup", "fieldtype": "Check", "label": "Show Preview Popup" + }, + { + "collapsible": 1, + "depends_on": "doc_type", + "fieldname": "view_settings_section", + "fieldtype": "Section Break", + "label": "View Settings" + }, + { + "fieldname": "column_break_29", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "collapsible_depends_on": "email_append_to", + "depends_on": "doc_type", + "fieldname": "email_settings_section", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "collapsible": 1, + "collapsible_depends_on": "links", + "depends_on": "doc_type", + "fieldname": "document_links_section", + "fieldtype": "Section Break", + "label": "Document Links" + }, + { + "fieldname": "links", + "fieldtype": "Table", + "label": "Links", + "options": "DocType Link" + }, + { + "collapsible": 1, + "collapsible_depends_on": "actions", + "depends_on": "doc_type", + "fieldname": "document_actions_section", + "fieldtype": "Section Break", + "label": "Document Actions" + }, + { + "fieldname": "actions", + "fieldtype": "Table", + "label": "Actions", + "options": "DocType Action" } ], "hide_toolbar": 1, "icon": "fa fa-glass", "idx": 1, + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-04-10 12:16:01.320411", + "modified": "2020-09-24 14:16:49.594012", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 5c4e16fad7..7f841631e5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -6,9 +6,10 @@ from __future__ import unicode_literals Customize Form is a Single DocType used to mask the Property Setter Thus providing a better UI from user perspective """ +import json import frappe import frappe.translate -from frappe import _ +from frappe import _, scrub from frappe.utils import cint from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list @@ -16,6 +17,440 @@ from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, che from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.model.docfield import supports_translation +class CustomizeForm(Document): + def on_update(self): + frappe.db.sql("delete from tabSingles where doctype='Customize Form'") + frappe.db.sql("delete from `tabCustomize Form Field`") + + def fetch_to_customize(self): + self.clear_existing_doc() + if not self.doc_type: + return + + meta = frappe.get_meta(self.doc_type) + + self.validate_doctype(meta) + + # load the meta properties on the customize (self) object + self.load_properties(meta) + + # load custom translation + translation = self.get_name_translation() + self.label = translation.translated_text if translation else '' + + self.create_auto_repeat_custom_field_if_requried(meta) + + # NOTE doc (self) is sent to clientside by run_method + + def validate_doctype(self, meta): + ''' + 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 meta.issingle: + frappe.throw(_("Single DocTypes cannot be customized.")) + + if meta.custom: + frappe.throw(_("Only standard DocTypes are allowed to be customized from Customize Form.")) + + def load_properties(self, meta): + ''' + Load the customize object (this) with the metadata properties + ''' + # doctype properties + for prop in doctype_properties: + self.set(prop, meta.get(prop)) + + for d in meta.get("fields"): + new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name} + for prop in docfield_properties: + new_d[prop] = d.get(prop) + self.append("fields", new_d) + + for fieldname in ('links', 'actions'): + for d in meta.get(fieldname): + self.append(fieldname, d) + + def create_auto_repeat_custom_field_if_requried(self, meta): + if self.allow_auto_repeat: + if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', + 'dt': self.doc_type}): + insert_after = self.fields[len(self.fields) - 1].fieldname + df = dict( + fieldname='auto_repeat', + label='Auto Repeat', + fieldtype='Link', + options='Auto Repeat', + insert_after=insert_after, + read_only=1, no_copy=1, print_hide=1) + create_custom_field(self.doc_type, df) + + + def get_name_translation(self): + '''Get translation object if exists of current doctype name in the default language''' + return frappe.get_value('Translation', { + 'source_text': self.doc_type, + 'language': frappe.local.lang or 'en' + }, ['name', 'translated_text'], as_dict=True) + + def set_name_translation(self): + '''Create, update custom translation for this doctype''' + current = self.get_name_translation() + if current: + if self.label and current.translated_text != self.label: + frappe.db.set_value('Translation', current.name, 'translated_text', self.label) + frappe.translate.clear_cache() + else: + # clear translation + frappe.delete_doc('Translation', current.name) + + else: + if self.label: + frappe.get_doc(dict(doctype='Translation', + source_text=self.doc_type, + translated_text=self.label, + language_code=frappe.local.lang or 'en')).insert() + + def clear_existing_doc(self): + doc_type = self.doc_type + + for fieldname in self.meta.get_valid_columns(): + self.set(fieldname, None) + + for df in self.meta.get_table_fields(): + self.set(df.fieldname, []) + + self.doc_type = doc_type + self.name = "Customize Form" + + def save_customization(self): + if not self.doc_type: + return + + self.flags.update_db = False + self.flags.rebuild_doctype_for_global_search = False + self.set_property_setters() + self.update_custom_fields() + self.set_name_translation() + validate_fields_for_doctype(self.doc_type) + check_email_append_to(self) + + if self.flags.update_db: + frappe.db.updatedb(self.doc_type) + + if not hasattr(self, 'hide_success') or not self.hide_success: + frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True) + frappe.clear_cache(doctype=self.doc_type) + self.fetch_to_customize() + + if self.flags.rebuild_doctype_for_global_search: + frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', + now=True, doctype=self.doc_type) + + def set_property_setters(self): + meta = frappe.get_meta(self.doc_type) + + # doctype + self.set_property_setters_for_doctype(meta) + + # docfield + for df in self.get("fields"): + meta_df = meta.get("fields", {"fieldname": df.fieldname}) + if not meta_df or meta_df[0].get("is_custom_field"): + continue + self.set_property_setters_for_docfield(meta, df, meta_df) + + # action and links + self.set_property_setters_for_actions_and_links(meta) + + def set_property_setters_for_doctype(self, meta): + for prop, prop_type in doctype_properties.items(): + if self.get(prop) != meta.get(prop): + print(prop, self.get(prop), prop_type) + self.make_property_setter(prop, self.get(prop), prop_type) + + def set_property_setters_for_docfield(self, meta, df, meta_df): + for prop, prop_type in docfield_properties.items(): + if prop != "idx" and (df.get(prop) or '') != (meta_df[0].get(prop) or ''): + if not self.allow_property_change(prop, meta_df, df): + continue + + self.make_property_setter(prop, df.get(prop), prop_type, + fieldname=df.fieldname) + + def allow_property_change(self, prop, meta_df, df): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) + + elif prop == "allow_on_submit" and df.get(prop): + if not frappe.db.get_value("DocField", + {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): + frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ + .format(df.idx)) + return False + + elif prop == "reqd" and \ + ((frappe.db.get_value("DocField", + {"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \ + and (df.get(prop) == 0)): + frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\ + .format(df.idx)) + return False + + elif prop == "in_list_view" and df.get(prop) \ + and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields: + frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}") + .format(df.fieldtype, df.idx)) + return False + + elif prop == "precision" and cint(df.get("precision")) > 6 \ + and cint(df.get("precision")) > cint(meta_df[0].get("precision")): + self.flags.update_db = True + + elif prop == "unique": + self.flags.update_db = True + + elif (prop == "read_only" and cint(df.get("read_only"))==0 + and frappe.db.get_value("DocField", {"parent": self.doc_type, + "fieldname": df.fieldname}, "read_only")==1): + # if docfield has read_only checked and user is trying to make it editable, don't allow it + frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) + return False + + elif prop == "options" and df.get("fieldtype") not in ALLOWED_OPTIONS_CHANGE: + frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) + return False + + elif prop == 'translatable' and not supports_translation(df.get('fieldtype')): + frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label)) + return False + + elif (prop == 'in_global_search' and + df.in_global_search != meta_df[0].get("in_global_search")): + self.flags.rebuild_doctype_for_global_search = True + + return True + + def set_property_setters_for_actions_and_links(self, meta): + ''' + Apply property setters or create custom records for DocType Action and DocType Link + ''' + for doctype, fieldname, field_map in ( + ('DocType Link', 'links', doctype_link_properties), + ('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)): + # 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) + 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) + has_custom = True + + 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') + + + def update_custom_fields(self): + for i, df in enumerate(self.get("fields")): + if df.get("is_custom_field"): + if not frappe.db.exists('Custom Field', {'dt': self.doc_type, 'fieldname': df.fieldname}): + self.add_custom_field(df, i) + self.flags.update_db = True + else: + self.update_in_custom_field(df, i) + + self.delete_custom_fields() + + def add_custom_field(self, df, i): + d = frappe.new_doc("Custom Field") + + d.dt = self.doc_type + + for prop in docfield_properties: + d.set(prop, df.get(prop)) + + if i!=0: + d.insert_after = self.fields[i-1].fieldname + d.idx = i + + d.insert() + df.fieldname = d.fieldname + + def update_in_custom_field(self, df, i): + meta = frappe.get_meta(self.doc_type) + meta_df = meta.get("fields", {"fieldname": df.fieldname}) + if not (meta_df and meta_df[0].get("is_custom_field")): + # not a custom field + return + + custom_field = frappe.get_doc("Custom Field", meta_df[0].name) + changed = False + for prop in docfield_properties: + if df.get(prop) != custom_field.get(prop): + if prop == "fieldtype": + self.validate_fieldtype_change(df, meta_df[0].get(prop), df.get(prop)) + + custom_field.set(prop, df.get(prop)) + changed = True + + # check and update `insert_after` property + if i!=0: + insert_after = self.fields[i-1].fieldname + if custom_field.insert_after != insert_after: + custom_field.insert_after = insert_after + custom_field.idx = i + changed = True + + if changed: + custom_field.db_update() + self.flags.update_db = True + #custom_field.save() + + def delete_custom_fields(self): + meta = frappe.get_meta(self.doc_type) + fields_to_remove = (set([df.fieldname for df in meta.get("fields")]) + - set(df.fieldname for df in self.get("fields"))) + + for fieldname in fields_to_remove: + df = meta.get("fields", {"fieldname": fieldname})[0] + if df.get("is_custom_field"): + frappe.delete_doc("Custom Field", df.name) + + def make_property_setter(self, prop, value, property_type, fieldname=None, + apply_on=None, row_name = None): + self.delete_existing_property_setter(prop, fieldname) + + property_value = self.get_existing_property_value(prop, fieldname) + + if property_value==value: + return + + if not apply_on: + apply_on = "DocField" if fieldname else "DocType" + + # create a new property setter + # ignore validation becuase it will be done at end + frappe.make_property_setter({ + "doctype": self.doc_type, + "doctype_or_field": apply_on, + "fieldname": fieldname, + "row_name": row_name, + "property": prop, + "value": value, + "property_type": property_type + }, ignore_validate=True) + + def delete_existing_property_setter(self, prop, fieldname=None): + # first delete existing property setter + existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.doc_type, + "property": prop, "field_name['']": fieldname or ''}) + + if existing_property_setter: + frappe.db.sql("delete from `tabProperty Setter` where name=%s", existing_property_setter) + + def get_existing_property_value(self, property_name, fieldname=None): + # check if there is any need to make property setter! + if fieldname: + property_value = frappe.db.get_value("DocField", {"parent": self.doc_type, + "fieldname": fieldname}, property_name) + else: + try: + property_value = frappe.db.get_value("DocType", self.doc_type, property_name) + except Exception as e: + if frappe.db.is_column_missing(e): + property_value = None + else: + raise + + return property_value + + def validate_fieldtype_change(self, df, old_value, new_value): + allowed = False + self.check_length_for_fieldtypes = [] + for allowed_changes in ALLOWED_FIELDTYPE_CHANGE: + if (old_value in allowed_changes and new_value in allowed_changes): + allowed = True + old_value_length = cint(frappe.db.type_map.get(old_value)[1]) + new_value_length = cint(frappe.db.type_map.get(new_value)[1]) + + # Ignore fieldtype check validation if new field type has unspecified maxlength + # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated + if new_value_length and (old_value_length > new_value_length): + self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) + self.validate_fieldtype_length() + else: + self.flags.update_db = True + break + if not allowed: + frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) + + def validate_fieldtype_length(self): + for field in self.check_length_for_fieldtypes: + df = field.get('df') + max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) + fieldname = df.fieldname + docs = frappe.db.sql(''' + SELECT name, {fieldname}, LENGTH({fieldname}) AS len + FROM `tab{doctype}` + WHERE LENGTH({fieldname}) > {max_length} + '''.format( + fieldname=fieldname, + doctype=self.doc_type, + max_length=max_length + ), as_dict=True) + links = [] + label = df.label + for doc in docs: + links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name)) + links_str = ', '.join(links) + + if docs: + frappe.throw(_('Value for field {0} is too long in {1}. Length should be lesser than {2} characters') + .format( + frappe.bold(label), + links_str, + frappe.bold(max_length) + ), title=_('Data Too Long'), is_minimizable=len(docs) > 1) + + self.flags.update_db = True + + def reset_to_defaults(self): + if not self.doc_type: + return + + reset_customization(self.doc_type) + self.fetch_to_customize() + +def reset_customization(doctype): + frappe.db.sql(""" + DELETE FROM `tabProperty Setter` WHERE doc_type=%s + and `field_name`!='naming_series' + and `property`!='options' + """, doctype) + frappe.clear_cache(doctype=doctype) + doctype_properties = { 'search_fields': 'Data', 'title_field': 'Data', @@ -82,356 +517,31 @@ docfield_properties = { 'hide_seconds': 'Check' } -allowed_fieldtype_change = (('Currency', 'Float', 'Percent'), ('Small Text', 'Data'), - ('Text', 'Data'), ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), ('Data', 'Select'), - ('Text', 'Small Text'), ('Text', 'Data', 'Barcode'), ('Code', 'Geolocation'), ('Table', 'Table MultiSelect')) - -allowed_fieldtype_for_options_change = ('Read Only', 'HTML', 'Select', 'Data') - -class CustomizeForm(Document): - def on_update(self): - frappe.db.sql("delete from tabSingles where doctype='Customize Form'") - frappe.db.sql("delete from `tabCustomize Form Field`") - - def fetch_to_customize(self): - self.clear_existing_doc() - if not self.doc_type: - return - - meta = frappe.get_meta(self.doc_type) - - if self.doc_type in core_doctypes_list: - return frappe.msgprint(_("Core DocTypes cannot be customized.")) - - if meta.issingle: - return frappe.msgprint(_("Single DocTypes cannot be customized.")) - - if meta.custom: - return frappe.msgprint(_("Only standard DocTypes are allowed to be customized from Customize Form.")) - - # doctype properties - for property in doctype_properties: - self.set(property, meta.get(property)) - - for d in meta.get("fields"): - new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name} - for property in docfield_properties: - new_d[property] = d.get(property) - self.append("fields", new_d) - - # load custom translation - translation = self.get_name_translation() - self.label = translation.translated_text if translation else '' - - #If allow_auto_repeat is set, add auto_repeat custom field. - if self.allow_auto_repeat: - if not frappe.db.exists('Custom Field', {'fieldname': 'auto_repeat', 'dt': self.doc_type}): - insert_after = self.fields[len(self.fields) - 1].fieldname - df = dict(fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', options='Auto Repeat', insert_after=insert_after, read_only=1, no_copy=1, print_hide=1) - create_custom_field(self.doc_type, df) - - # NOTE doc is sent to clientside by run_method - - def get_name_translation(self): - '''Get translation object if exists of current doctype name in the default language''' - return frappe.get_value('Translation', { - 'source_text': self.doc_type, - 'language': frappe.local.lang or 'en' - }, ['name', 'translated_text'], as_dict=True) - - def set_name_translation(self): - '''Create, update custom translation for this doctype''' - current = self.get_name_translation() - if current: - if self.label and current.translated_text != self.label: - frappe.db.set_value('Translation', current.name, 'translated_text', self.label) - frappe.translate.clear_cache() - else: - # clear translation - frappe.delete_doc('Translation', current.name) - - else: - if self.label: - frappe.get_doc(dict(doctype='Translation', - source_text=self.doc_type, - translated_text=self.label, - language_code=frappe.local.lang or 'en')).insert() - - def clear_existing_doc(self): - doc_type = self.doc_type - - for fieldname in self.meta.get_valid_columns(): - self.set(fieldname, None) - - for df in self.meta.get_table_fields(): - self.set(df.fieldname, []) - - self.doc_type = doc_type - self.name = "Customize Form" - - def save_customization(self): - if not self.doc_type: - return - - self.flags.update_db = False - self.flags.rebuild_doctype_for_global_search = False - self.set_property_setters() - self.update_custom_fields() - self.set_name_translation() - validate_fields_for_doctype(self.doc_type) - check_email_append_to(self) - - if self.flags.update_db: - frappe.db.updatedb(self.doc_type) - - if not hasattr(self, 'hide_success') or not self.hide_success: - frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True) - frappe.clear_cache(doctype=self.doc_type) - self.fetch_to_customize() - - if self.flags.rebuild_doctype_for_global_search: - frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', - now=True, doctype=self.doc_type) - - def set_property_setters(self): - meta = frappe.get_meta(self.doc_type) - # doctype property setters - - for property in doctype_properties: - if self.get(property) != meta.get(property): - self.make_property_setter(property=property, value=self.get(property), - property_type=doctype_properties[property]) - - for df in self.get("fields"): - meta_df = meta.get("fields", {"fieldname": df.fieldname}) - - if not meta_df or meta_df[0].get("is_custom_field"): - continue - - for property in docfield_properties: - if property != "idx" and (df.get(property) or '') != (meta_df[0].get(property) or ''): - if property == "fieldtype": - self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) - - elif property == "allow_on_submit" and df.get(property): - if not frappe.db.get_value("DocField", - {"parent": self.doc_type, "fieldname": df.fieldname}, "allow_on_submit"): - frappe.msgprint(_("Row {0}: Not allowed to enable Allow on Submit for standard fields")\ - .format(df.idx)) - continue - - elif property == "reqd" and \ - ((frappe.db.get_value("DocField", - {"parent":self.doc_type,"fieldname":df.fieldname}, "reqd") == 1) \ - and (df.get(property) == 0)): - frappe.msgprint(_("Row {0}: Not allowed to disable Mandatory for standard fields")\ - .format(df.idx)) - continue - - elif property == "in_list_view" and df.get(property) \ - and df.fieldtype!="Attach Image" and df.fieldtype in no_value_fields: - frappe.msgprint(_("'In List View' not allowed for type {0} in row {1}") - .format(df.fieldtype, df.idx)) - continue - - elif property == "precision" and cint(df.get("precision")) > 6 \ - and cint(df.get("precision")) > cint(meta_df[0].get("precision")): - self.flags.update_db = True - - elif property == "unique": - self.flags.update_db = True - - elif (property == "read_only" and cint(df.get("read_only"))==0 - and frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": df.fieldname}, "read_only")==1): - # if docfield has read_only checked and user is trying to make it editable, don't allow it - frappe.msgprint(_("You cannot unset 'Read Only' for field {0}").format(df.label)) - continue - - elif property == "options" and df.get("fieldtype") not in allowed_fieldtype_for_options_change: - frappe.msgprint(_("You can't set 'Options' for field {0}").format(df.label)) - continue - - elif property == 'translatable' and not supports_translation(df.get('fieldtype')): - frappe.msgprint(_("You can't set 'Translatable' for field {0}").format(df.label)) - continue - - elif (property == 'in_global_search' and - df.in_global_search != meta_df[0].get("in_global_search")): - self.flags.rebuild_doctype_for_global_search = True - - self.make_property_setter(property=property, value=df.get(property), - property_type=docfield_properties[property], fieldname=df.fieldname) - - def update_custom_fields(self): - for i, df in enumerate(self.get("fields")): - if df.get("is_custom_field"): - if not frappe.db.exists('Custom Field', {'dt': self.doc_type, 'fieldname': df.fieldname}): - self.add_custom_field(df, i) - self.flags.update_db = True - else: - self.update_in_custom_field(df, i) - - self.delete_custom_fields() - - def add_custom_field(self, df, i): - d = frappe.new_doc("Custom Field") - - d.dt = self.doc_type - - for property in docfield_properties: - d.set(property, df.get(property)) - - if i!=0: - d.insert_after = self.fields[i-1].fieldname - d.idx = i - - d.insert() - df.fieldname = d.fieldname - - def update_in_custom_field(self, df, i): - meta = frappe.get_meta(self.doc_type) - meta_df = meta.get("fields", {"fieldname": df.fieldname}) - if not (meta_df and meta_df[0].get("is_custom_field")): - # not a custom field - return - - custom_field = frappe.get_doc("Custom Field", meta_df[0].name) - changed = False - for property in docfield_properties: - if df.get(property) != custom_field.get(property): - if property == "fieldtype": - self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property)) - - custom_field.set(property, df.get(property)) - changed = True - - # check and update `insert_after` property - if i!=0: - insert_after = self.fields[i-1].fieldname - if custom_field.insert_after != insert_after: - custom_field.insert_after = insert_after - custom_field.idx = i - changed = True - - if changed: - custom_field.db_update() - self.flags.update_db = True - #custom_field.save() - - def delete_custom_fields(self): - meta = frappe.get_meta(self.doc_type) - fields_to_remove = (set([df.fieldname for df in meta.get("fields")]) - - set(df.fieldname for df in self.get("fields"))) - - for fieldname in fields_to_remove: - df = meta.get("fields", {"fieldname": fieldname})[0] - if df.get("is_custom_field"): - frappe.delete_doc("Custom Field", df.name) - - def make_property_setter(self, property, value, property_type, fieldname=None): - self.delete_existing_property_setter(property, fieldname) - - property_value = self.get_existing_property_value(property, fieldname) - - if property_value==value: - return - - # create a new property setter - # ignore validation becuase it will be done at end - frappe.make_property_setter({ - "doctype": self.doc_type, - "doctype_or_field": "DocField" if fieldname else "DocType", - "fieldname": fieldname, - "property": property, - "value": value, - "property_type": property_type - }, ignore_validate=True) - - def delete_existing_property_setter(self, property, fieldname=None): - # first delete existing property setter - existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.doc_type, - "property": property, "field_name['']": fieldname or ''}) - - if existing_property_setter: - frappe.db.sql("delete from `tabProperty Setter` where name=%s", existing_property_setter) - - def get_existing_property_value(self, property_name, fieldname=None): - # check if there is any need to make property setter! - if fieldname: - property_value = frappe.db.get_value("DocField", {"parent": self.doc_type, - "fieldname": fieldname}, property_name) - else: - try: - property_value = frappe.db.get_value("DocType", self.doc_type, property_name) - except Exception as e: - if frappe.db.is_column_missing(e): - property_value = None - else: - raise - - return property_value - - def validate_fieldtype_change(self, df, old_value, new_value): - allowed = False - self.check_length_for_fieldtypes = [] - for allowed_changes in allowed_fieldtype_change: - if (old_value in allowed_changes and new_value in allowed_changes): - allowed = True - old_value_length = cint(frappe.db.type_map.get(old_value)[1]) - new_value_length = cint(frappe.db.type_map.get(new_value)[1]) - - # Ignore fieldtype check validation if new field type has unspecified maxlength - # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated - if new_value_length and (old_value_length > new_value_length): - self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) - self.validate_fieldtype_length() - else: - self.flags.update_db = True - break - if not allowed: - frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) - - def validate_fieldtype_length(self): - for field in self.check_length_for_fieldtypes: - df = field.get('df') - max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) - fieldname = df.fieldname - docs = frappe.db.sql(''' - SELECT name, {fieldname}, LENGTH({fieldname}) AS len - FROM `tab{doctype}` - WHERE LENGTH({fieldname}) > {max_length} - '''.format( - fieldname=fieldname, - doctype=self.doc_type, - max_length=max_length - ), as_dict=True) - links = [] - label = df.label - for doc in docs: - links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name)) - links_str = ', '.join(links) - - if docs: - frappe.throw(_('Value for field {0} is too long in {1}. Length should be lesser than {2} characters') - .format( - frappe.bold(label), - links_str, - frappe.bold(max_length) - ), title=_('Data Too Long'), is_minimizable=len(docs) > 1) - - self.flags.update_db = True - - def reset_to_defaults(self): - if not self.doc_type: - return - - reset_customization(self.doc_type) - self.fetch_to_customize() - -def reset_customization(doctype): - frappe.db.sql(""" - DELETE FROM `tabProperty Setter` WHERE doc_type=%s - and `field_name`!='naming_series' - and `property`!='options' - """, doctype) - frappe.clear_cache(doctype=doctype) \ No newline at end of file +doctype_link_properties = { + 'link_doctype': 'Link', + 'link_fieldname': 'Data', + 'group': 'Data', + 'hidden': 'Check' +} + +doctype_action_properties = { + 'label': 'Link', + 'action_type': 'Select', + 'action': 'Small Text', + 'group': 'Data', + 'hidden': 'Check' +} + + +ALLOWED_FIELDTYPE_CHANGE = ( + ('Currency', 'Float', 'Percent'), + ('Small Text', 'Data'), + ('Text', 'Data'), + ('Text', 'Text Editor', 'Code', 'Signature', 'HTML Editor'), + ('Data', 'Select'), + ('Text', 'Small Text'), + ('Text', 'Data', 'Barcode'), + ('Code', 'Geolocation'), + ('Table', 'Table MultiSelect')) + +ALLOWED_OPTIONS_CHANGE = ('Read Only', 'HTML', 'Select', 'Data') diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 1c7349ef01..1d71e1d1e3 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -11,8 +11,6 @@ "label", "fieldtype", "fieldname", - "hide_seconds", - "hide_days", "reqd", "unique", "in_list_view", @@ -23,6 +21,7 @@ "allow_in_quick_entry", "translatable", "column_break_7", + "default", "precision", "length", "options", @@ -47,8 +46,9 @@ "column_break_33", "read_only_depends_on", "display", - "default", "in_filter", + "hide_seconds", + "hide_days", "column_break_21", "description", "print_hide", @@ -100,6 +100,7 @@ "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", "fieldname": "reqd", "fieldtype": "Check", + "in_list_view": 1, "label": "Mandatory", "oldfieldname": "reqd", "oldfieldtype": "Check", @@ -283,7 +284,7 @@ }, { "fieldname": "default", - "fieldtype": "Text", + "fieldtype": "Small Text", "label": "Default", "oldfieldname": "default", "oldfieldtype": "Text" @@ -419,7 +420,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-28 11:28:59.084060", + "modified": "2020-09-24 14:05:31.093927", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/property_setter/property_setter.json b/frappe/custom/doctype/property_setter/property_setter.json index 5888e11969..b318d92c5a 100644 --- a/frappe/custom/doctype/property_setter/property_setter.json +++ b/frappe/custom/doctype/property_setter/property_setter.json @@ -1,358 +1,133 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-01-10 16:34:04", - "custom": 0, - "description": "Property Setter overrides a standard DocType or Field property", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "actions": [], + "creation": "2013-01-10 16:34:04", + "description": "Property Setter overrides a standard DocType or Field property", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "help", + "sb0", + "doctype_or_field", + "doc_type", + "field_name", + "row_name", + "column_break0", + "property", + "property_type", + "value", + "default_value" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "help", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Help", - "length": 0, - "no_copy": 0, - "options": "
Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
", - "permlevel": 0, - "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 - }, + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "
Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!
" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sb0", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "sb0", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.__islocal", - "fieldname": "doctype_or_field", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "DocType or Field", - "length": 0, - "no_copy": 0, - "options": "\nDocField\nDocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "doctype_or_field", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Applied On", + "options": "\nDocField\nDocType\nDocType Link\nDocType Action", + "read_only_depends_on": "eval:!doc.__islocal", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "New value to be set", - "fieldname": "value", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Set Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "description": "New value to be set", + "fieldname": "value", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Set Value" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break0", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "doc_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "DocType", - "length": 0, - "no_copy": 0, - "options": "DocType", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "doc_type", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.doctype_or_field=='DocField'", - "description": "ID (name) of the entity whose property is to be set", - "fieldname": "field_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Field Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:doc.doctype_or_field=='DocField'", + "description": "ID (name) of the entity whose property is to be set", + "fieldname": "field_name", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Field Name", + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "property", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Property", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "property", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "Property", + "reqd": 1, + "search_index": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "property_type", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Property Type", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 - }, + "fieldname": "property_type", + "fieldtype": "Data", + "label": "Property Type" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_value", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Default Value", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "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 + "fieldname": "default_value", + "fieldtype": "Data", + "label": "Default Value" + }, + { + "description": "For DocType Link / DocType Action", + "fieldname": "row_name", + "fieldtype": "Data", + "label": "Row Name" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-glass", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-12-29 14:39:50.172883", - "modified_by": "Administrator", - "module": "Custom", - "name": "Property Setter", - "owner": "Administrator", + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-09-24 14:42:38.599684", + "modified_by": "Administrator", + "module": "Custom", + "name": "Property Setter", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "doc_type,property", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 + ], + "search_fields": "doc_type,property", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index d8ab5ede73..9ffa48b084 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -11,9 +11,11 @@ not_allowed_fieldtype_change = ['naming_series'] class PropertySetter(Document): def autoname(self): - self.name = self.doc_type + "-" \ - + (self.field_name and (self.field_name + "-") or "") \ - + self.property + self.name = '{doctype}-{field}-{property}'.format( + doctype = self.doc_type, + field = self.field_name or self.row_name or 'main', + property = self.property + ) def validate(self): self.validate_fieldtype_change() diff --git a/frappe/email/doctype/email_group/email_group.js b/frappe/email/doctype/email_group/email_group.js index 63c3832b47..404600c97d 100644 --- a/frappe/email/doctype/email_group/email_group.js +++ b/frappe/email/doctype/email_group/email_group.js @@ -3,11 +3,6 @@ frappe.ui.form.on("Email Group", "refresh", function(frm) { if(!frm.is_new()) { - frm.add_custom_button(__("View Subscribers"), function() { - frappe.route_options = {"email_group": frm.doc.name}; - frappe.set_route("List", "Email Group Member"); - }, __("View")); - frm.add_custom_button(__("Import Subscribers"), function() { frappe.prompt({fieldtype:"Select", options: frm.doc.__onload.import_types, label:__("Import Email From"), fieldname:"doctype", reqd:1}, diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json index 0d784d409a..c49de841e6 100644 --- a/frappe/email/doctype/email_group/email_group.json +++ b/frappe/email/doctype/email_group/email_group.json @@ -5,6 +5,7 @@ "creation": "2015-03-18 06:08:32.729800", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "title", "total_subscribers", @@ -41,8 +42,15 @@ "options": "Email Template" } ], - "links": [], - "modified": "2020-02-21 14:12:48.884738", + "index_web_pages_for_search": 1, + "links": [ + { + "group": "Members", + "link_doctype": "Email Group Member", + "link_fieldname": "email_group" + } + ], + "modified": "2020-09-24 16:41:55.286377", "modified_by": "Administrator", "module": "Email", "name": "Email Group", diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index e5ce9102e2..7404ba407e 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -42,9 +42,12 @@ def get_dynamic_link_map(for_delete=False): # 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) + try: + links = frappe.db.sql_list("""select distinct {options} from `tab{parent}`""".format(**df)) + for doctype in links: + dynamic_link_map.setdefault(doctype, []).append(df) + except frappe.db.TableMissingError: # noqa: E722 + pass frappe.local.dynamic_link_map = dynamic_link_map return frappe.local.dynamic_link_map diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 1cc3abba5b..18b781519f 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -19,7 +19,7 @@ from __future__ import unicode_literals, print_function from datetime import datetime from six.moves import range import frappe, json, os -from frappe.utils import cstr, cint +from frappe.utils import cstr, cint, cast_fieldtype from frappe.model import default_fields, no_value_fields, optional_fields, data_fieldtypes, table_fields from frappe.model.document import Document from frappe.model.base_document import BaseDocument @@ -103,6 +103,7 @@ class Meta(Document): self.sort_fields() self.get_valid_columns() self.set_custom_permissions() + self.add_custom_links_and_actions() def as_dict(self, no_nulls = False): def serialize(doc): @@ -305,6 +306,11 @@ class Meta(Document): self.extend("fields", custom_fields) def apply_property_setters(self): + """ + Property Setters are set via Customize Form. They override standard properties + of the doctype or its child properties like fields, links etc. This method + applies the customized properties over the standard meta object + """ if not frappe.db.table_exists('Property Setter'): return @@ -313,26 +319,50 @@ class Meta(Document): if not property_setters: return - integer_docfield_properties = [d.fieldname for d in frappe.get_meta('DocField').fields - if d.fieldtype in ('Int', 'Check')] - for ps in property_setters: if ps.doctype_or_field=='DocType': - if ps.property_type in ('Int', 'Check'): - ps.value = cint(ps.value) + self.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) - self.set(ps.property, ps.value) - else: - docfield = self.get("fields", {"fieldname":ps.field_name}, limit=1) - if docfield: - docfield = docfield[0] - else: - continue + elif ps.doctype_or_field=='DocField': + for d in self.fields: + if d.fieldname == ps.fieldname: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break - if ps.property in integer_docfield_properties: - ps.value = cint(ps.value) + elif ps.doctype_or_field=='DocType Link': + for d in self.links: + if d.name == ps.row_name: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break - docfield.set(ps.property, ps.value) + elif ps.doctype_or_field=='DocType Action': + for d in self.actions: + if d.name == ps.row_name: + d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) + break + + 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) + + # set the fields in order if specified + # order is saved as `links_order` + order = json.loads(self.get('{}_order'.format(fieldname)) or '[]') + if order: + 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 + + # 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) + + self.set(fieldname, new_list) def sort_fields(self): """sort on basis of insert_after""" @@ -458,6 +488,9 @@ class Meta(Document): for link in dashboard_links: link.added = False + if link.hidden: + continue + for group in data.transactions: group = frappe._dict(group) # group found From 77e79c050f143e844f27eab1e38cadef64992339 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 29 Sep 2020 10:46:44 +0530 Subject: [PATCH 02/24] fix(linting) --- frappe/app.py | 2 +- frappe/custom/doctype/customize_form/customize_form.js | 4 +++- frappe/custom/doctype/customize_form/customize_form.py | 2 +- frappe/website/doctype/blog_category/blog_category.json | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index 2c8dcec26b..1dccb3e7e2 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -159,7 +159,7 @@ def handle_exception(e): response = None http_status_code = getattr(e, "http_status_code", 500) return_as_message = False - print(frappe.get_traceback()) + # 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/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 7dfec0b0b0..0a2b02e6c7 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -138,7 +138,9 @@ frappe.ui.form.on("Customize Form", { // sort order select if (frm.doc.doc_type) { var fields = $.map(frm.doc.fields, - function(df) { return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; }); + function(df) { + return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; + }); fields = ["", "name", "modified"].concat(fields); frm.set_df_property("sort_field", "options", fields); } diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 7f841631e5..fb4ba1bcd6 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -9,7 +9,7 @@ from __future__ import unicode_literals import json import frappe import frappe.translate -from frappe import _, scrub +from frappe import _ from frappe.utils import cint from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list diff --git a/frappe/website/doctype/blog_category/blog_category.json b/frappe/website/doctype/blog_category/blog_category.json index 67e17f49fb..2931107d8f 100644 --- a/frappe/website/doctype/blog_category/blog_category.json +++ b/frappe/website/doctype/blog_category/blog_category.json @@ -43,7 +43,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2020-08-21 11:40:36.919321", + "modified": "2020-09-29 10:45:48.810348", "modified_by": "Administrator", "module": "Website", "name": "Blog Category", @@ -69,6 +69,7 @@ } ], "quick_entry": 1, + "route": "/category", "sort_field": "modified", "sort_order": "DESC", "title_field": "title", From b5cb39bdb12e5c36f3146ee13384887e1eae368b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 29 Sep 2020 16:41:39 +0530 Subject: [PATCH 03/24] fix(minor): fix fieldame in apply_property_setters in model/meta.py --- frappe/model/meta.py | 2 +- frappe/patches/v13_0/web_template_set_module.py | 5 +++++ frappe/tests/test_form_load.py | 5 ++++- frappe/website/doctype/blog_category/blog_category.json | 3 +-- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 18b781519f..af0f574fcf 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -325,7 +325,7 @@ class Meta(Document): elif ps.doctype_or_field=='DocField': for d in self.fields: - if d.fieldname == ps.fieldname: + if d.fieldname == ps.field_name: d.set(ps.property, cast_fieldtype(ps.property_type, ps.value)) break diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py index df008557d8..10e80eeffc 100644 --- a/frappe/patches/v13_0/web_template_set_module.py +++ b/frappe/patches/v13_0/web_template_set_module.py @@ -6,9 +6,14 @@ import frappe def execute(): """Set default module for standard Web Template, if none.""" +<<<<<<< d5ee3032d494ed35a409a36116679a3d3cb96103 frappe.reload_doc('website', 'doctype', 'Web Template Field') frappe.reload_doc('website', 'doctype', 'web_template') +======= + frappe.reload_doc('website', 'doctype', 'Web Template') + frappe.reload_doc('website', 'doctype', 'Web Template Field') +>>>>>>> fix(minor): fix fieldame in apply_property_setters in model/meta.py standard_templates = frappe.get_list('Web Template', {'standard': 1}) for template in standard_templates: doc = frappe.get_doc('Web Template', template.name) diff --git a/frappe/tests/test_form_load.py b/frappe/tests/test_form_load.py index 78562e1055..c962b192b3 100644 --- a/frappe/tests/test_form_load.py +++ b/frappe/tests/test_form_load.py @@ -57,6 +57,7 @@ class TestFormLoad(unittest.TestCase): # have write access on `published` field (or on permlevel 1 fields) blog_doc.published = 1 blog_doc.save() + # since published field has higher permlevel self.assertEqual(blog_doc.published, 0) @@ -94,7 +95,7 @@ class TestFormLoad(unittest.TestCase): user.remove_roles(*user_roles) user.add_roles('Accounts User') - make_property_setter('Contact Phone', 'phone', 'permlevel', 1, 'Data') + make_property_setter('Contact Phone', 'phone', 'permlevel', 1, 'Int') reset('Contact Phone') add('Contact', 'Sales User', 1) update('Contact', 'Sales User', 1, 'write', 1) @@ -124,6 +125,8 @@ class TestFormLoad(unittest.TestCase): user.remove_roles('Accounts User', 'Sales User') user.add_roles(*user_roles) + contact.delete() + def get_blog(blog_name): frappe.response.docs = [] diff --git a/frappe/website/doctype/blog_category/blog_category.json b/frappe/website/doctype/blog_category/blog_category.json index 2931107d8f..a65a7cba29 100644 --- a/frappe/website/doctype/blog_category/blog_category.json +++ b/frappe/website/doctype/blog_category/blog_category.json @@ -43,7 +43,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2020-09-29 10:45:48.810348", + "modified": "2020-09-29 10:48:36.886753", "modified_by": "Administrator", "module": "Website", "name": "Blog Category", @@ -69,7 +69,6 @@ } ], "quick_entry": 1, - "route": "/category", "sort_field": "modified", "sort_order": "DESC", "title_field": "title", From 8a198f363bd0a62b74b62f36f1418d17940c0036 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 29 Sep 2020 23:27:00 +0530 Subject: [PATCH 04/24] fix(default): cast as string as required --- frappe/core/doctype/doctype/doctype.py | 4 ++-- frappe/database/schema.py | 2 +- frappe/model/create_new.py | 6 +++--- frappe/tests/test_db_update.py | 3 ++- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 1aa6beb6a6..92b27e410d 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -752,8 +752,8 @@ def validate_fields(meta): def check_illegal_default(d): if d.fieldtype == "Check" and not d.default: d.default = '0' - if d.fieldtype == "Check" and d.default not in ('0', '1'): - frappe.throw(_("Default for 'Check' type of field must be either '0' or '1'")) + if d.fieldtype == "Check" and cint(d.default) not in (0, 1): + frappe.throw(_("Default for 'Check' type of field {0} must be either '0' or '1'".format(frappe.bold(d.fieldname)))) if d.fieldtype == "Select" and d.default: if not d.options: frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname))) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 52dc2ba917..daabbaa61c 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -186,7 +186,7 @@ class DbColumn: column_def += ' not null default {0}'.format(default_value) elif self.default and (self.default not in frappe.db.DEFAULT_SHORTCUTS) \ - and not self.default.startswith(":") and column_def not in ('text', 'longtext'): + and not cstr(self.default).startswith(":") and column_def not in ('text', 'longtext'): column_def += " default {}".format(frappe.db.escape(self.default)) if self.unique and (column_def not in ('text', 'longtext')): diff --git a/frappe/model/create_new.py b/frappe/model/create_new.py index fcf648e718..e0087a9e40 100644 --- a/frappe/model/create_new.py +++ b/frappe/model/create_new.py @@ -10,7 +10,7 @@ import copy import frappe import frappe.defaults from frappe.model import data_fieldtypes -from frappe.utils import nowdate, nowtime, now_datetime +from frappe.utils import nowdate, nowtime, now_datetime, cstr from frappe.core.doctype.user_permission.user_permission import get_user_permissions from frappe.permissions import filter_allowed_docs_for_doctype @@ -99,7 +99,7 @@ def get_static_default_value(df, doctype_user_permissions, allowed_records): elif df.default == "Today": return nowdate() - elif not df.default.startswith(":"): + elif not cstr(df.default).startswith(":"): # a simple default value is_allowed_default_value = (not user_permissions_exist(df, doctype_user_permissions) or (df.default in allowed_records)) @@ -116,7 +116,7 @@ def set_dynamic_default_values(doc, parent_doc, parentfield): for df in frappe.get_meta(doc["doctype"]).get("fields"): if df.get("default"): - if df.default.startswith(":"): + if cstr(df.default).startswith(":"): default_value = get_default_based_on_another_field(df, user_permissions, parent_doc) if default_value is not None and not doc.get(df.fieldname): doc[df.fieldname] = default_value diff --git a/frappe/tests/test_db_update.py b/frappe/tests/test_db_update.py index f243aa268f..4ae33a2fab 100644 --- a/frappe/tests/test_db_update.py +++ b/frappe/tests/test_db_update.py @@ -1,6 +1,7 @@ import unittest import frappe +from frappe.utils import cstr from frappe.core.utils import find from frappe.custom.doctype.property_setter.property_setter import make_property_setter @@ -31,7 +32,7 @@ class TestDBUpdate(unittest.TestCase): default = field_def.default if field_def.default is not None else fallback_default self.assertEqual(fieldtype, table_column.type) - self.assertIn(table_column.default or 'NULL', [default, "'{}'".format(default)]) + self.assertIn(cstr(table_column.default) or 'NULL', [cstr(default), "'{}'".format(default)]) def get_fieldtype_from_def(field_def): fieldtuple = frappe.db.type_map.get(field_def.fieldtype, ('', 0)) From a0a3606a7f8f5cda5cdc2ee44f2c6deabddb5f4f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 13:09:55 +0530 Subject: [PATCH 05/24] 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) From 6f0104b67e4d2734050f7dc1de279dd9824c7ead Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 13:15:14 +0530 Subject: [PATCH 06/24] fix(minor): conflict --- frappe/patches/v13_0/web_template_set_module.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/patches/v13_0/web_template_set_module.py b/frappe/patches/v13_0/web_template_set_module.py index 10e80eeffc..df008557d8 100644 --- a/frappe/patches/v13_0/web_template_set_module.py +++ b/frappe/patches/v13_0/web_template_set_module.py @@ -6,14 +6,9 @@ import frappe def execute(): """Set default module for standard Web Template, if none.""" -<<<<<<< d5ee3032d494ed35a409a36116679a3d3cb96103 frappe.reload_doc('website', 'doctype', 'Web Template Field') frappe.reload_doc('website', 'doctype', 'web_template') -======= - frappe.reload_doc('website', 'doctype', 'Web Template') - frappe.reload_doc('website', 'doctype', 'Web Template Field') ->>>>>>> fix(minor): fix fieldame in apply_property_setters in model/meta.py standard_templates = frappe.get_list('Web Template', {'standard': 1}) for template in standard_templates: doc = frappe.get_doc('Web Template', template.name) From 473471bedca18dc83029592e1a56528074204bfd Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 15:15:08 +0530 Subject: [PATCH 07/24] fix(tests): assert raises validation --- frappe/custom/doctype/customize_form/customize_form.js | 2 +- .../custom/doctype/customize_form/test_customize_form.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 6b0fbb042d..2d220b864c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -12,7 +12,7 @@ frappe.ui.form.on("Customize Form", { filters: [ ['DocType', 'issingle', '=', 0], ['DocType', 'custom', '=', 0], - //['DocType', 'name', 'not in', frappe.model.core_doctypes_list], + ['DocType', 'name', 'not in', frappe.model.core_doctypes_list], ['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] ] }; diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index b631c81d96..19bbc78e0e 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -186,12 +186,7 @@ class TestCustomizeForm(unittest.TestCase): d.run_method("save_customization") def test_core_doctype_customization(self): - d = self.get_customize_form('User') - e = self.get_customize_form('Custom Field') - - # core doctype is invalid, hence no attributes are set - self.assertEquals(d.get("fields"), []) - self.assertEquals(e.get("fields"), []) + self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User') def test_custom_link(self): try: From 92c30ae2123493717dc28dbfd660fbea61bc5277 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 17:39:01 +0530 Subject: [PATCH 08/24] fix(minor): linting --- frappe/custom/doctype/customize_form/customize_form.py | 2 +- frappe/model/meta.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 034b98a7c2..889fa3f537 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -71,7 +71,7 @@ class CustomizeForm(Document): for fieldname in ('links', 'actions'): for d in meta.get(fieldname): - d1 = self.append(fieldname, d) + self.append(fieldname, d) def create_auto_repeat_custom_field_if_requried(self, meta): if self.allow_auto_repeat: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 8aa761ac21..4299876a77 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -360,7 +360,7 @@ class Meta(Document): # maybe these items were added to the standard product # after the customization was done for d in self.get(fieldname): - if not d in new_list: + if d not in new_list: new_list.append(d) self.set(fieldname, new_list) From 7f9284f3efd0e31d3236f2464ee6901d880d380a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 23:14:48 +0530 Subject: [PATCH 09/24] fix(tests): postgres gotcha, errors will rollback? --- .../doctype/customize_form/customize_form.py | 23 ++++--------------- .../customize_form/test_customize_form.py | 1 + .../property_setter/property_setter.py | 10 ++++---- frappe/database/database.py | 11 ++------- 4 files changed, 12 insertions(+), 33 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 889fa3f537..e6c2840e08 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -168,7 +168,6 @@ class CustomizeForm(Document): def set_property_setters_for_doctype(self, meta): for prop, prop_type in doctype_properties.items(): if self.get(prop) != meta.get(prop): - print(prop, self.get(prop), prop_type) self.make_property_setter(prop, self.get(prop), prop_type) def set_property_setters_for_docfield(self, meta, df, meta_df): @@ -357,8 +356,6 @@ class CustomizeForm(Document): def make_property_setter(self, prop, value, property_type, fieldname=None, apply_on=None, row_name = None): - self.delete_existing_property_setter(prop, fieldname) - property_value = self.get_existing_property_value(prop, fieldname) if property_value==value: @@ -368,7 +365,6 @@ class CustomizeForm(Document): apply_on = "DocField" if fieldname else "DocType" # create a new property setter - # ignore validation becuase it will be done at end frappe.make_property_setter({ "doctype": self.doc_type, "doctype_or_field": apply_on, @@ -377,15 +373,7 @@ class CustomizeForm(Document): "property": prop, "value": value, "property_type": property_type - }, ignore_validate=True) - - def delete_existing_property_setter(self, prop, fieldname=None): - # first delete existing property setter - existing_property_setter = frappe.db.get_value("Property Setter", {"doc_type": self.doc_type, - "property": prop, "field_name['']": fieldname or ''}) - - if existing_property_setter: - frappe.db.sql("delete from `tabProperty Setter` where name=%s", existing_property_setter) + }) def get_existing_property_value(self, property_name, fieldname=None): # check if there is any need to make property setter! @@ -393,13 +381,10 @@ class CustomizeForm(Document): property_value = frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": fieldname}, property_name) else: - try: + if frappe.db.has_column(self.doc_type, property_name): property_value = frappe.db.get_value("DocType", self.doc_type, property_name) - except Exception as e: - if frappe.db.is_column_missing(e): - property_value = None - else: - raise + else: + property_value = None return property_value diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 19bbc78e0e..73d0747015 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -206,6 +206,7 @@ class TestCustomizeForm(unittest.TestCase): 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() diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 9ffa48b084..a1368155b8 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -32,11 +32,11 @@ class PropertySetter(Document): def delete_property_setter(self): """delete other property setters on this, if this is new""" if self.get('__islocal'): - frappe.db.sql("""delete from `tabProperty Setter` where - doctype_or_field = %(doctype_or_field)s - and doc_type = %(doc_type)s - and coalesce(field_name,'') = coalesce(%(field_name)s, '') - and property = %(property)s""", self.get_valid_dict()) + filters = dict(doc_type = self.doc_type, property=self.property) + if self.field_name: + dict['field_name'] = self.field_name + + frappe.db.delete('Property Setter', filters) def get_property_list(self, dt): return frappe.db.get_all('DocField', diff --git a/frappe/database/database.py b/frappe/database/database.py index f94f19c62b..64b234b1d3 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -319,8 +319,7 @@ class Database(object): nres.append(nr) return nres - @staticmethod - def build_conditions(filters): + def build_conditions(self, filters): """Convert filters sent as dict, lists to SQL conditions. filter's key is passed by map function, build conditions like: @@ -346,13 +345,7 @@ class Database(object): values[key] = value[1] if isinstance(value[1], (tuple, list)): # value is a list in tuple ("in", ("A", "B")) - inner_list = [] - for i, v in enumerate(value[1]): - inner_key = "{0}_{1}".format(key, i) - values[inner_key] = v - inner_list.append("%({0})s".format(inner_key)) - - _rhs = " ({0})".format(", ".join(inner_list)) + _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]])) del values[key] if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]: From 1add636324e68d751d49438a655680256e3b4e25 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 23:25:09 +0530 Subject: [PATCH 10/24] fix(tests): fix translation string --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 92b27e410d..8a9c130fbe 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -753,7 +753,7 @@ def validate_fields(meta): if d.fieldtype == "Check" and not d.default: d.default = '0' if d.fieldtype == "Check" and cint(d.default) not in (0, 1): - frappe.throw(_("Default for 'Check' type of field {0} must be either '0' or '1'".format(frappe.bold(d.fieldname)))) + frappe.throw(_("Default for 'Check' type of field {0} must be either '0' or '1'").format(frappe.bold(d.fieldname))) if d.fieldtype == "Select" and d.default: if not d.options: frappe.throw(_("Options for {0} must be set before setting the default value.").format(frappe.bold(d.fieldname))) From 963046296521a3eb8813e6eaf2a45b0953845df2 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 13 Oct 2020 23:39:58 +0530 Subject: [PATCH 11/24] fix(minor): typo --- frappe/custom/doctype/property_setter/property_setter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index a1368155b8..83c0337074 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -34,7 +34,7 @@ class PropertySetter(Document): if self.get('__islocal'): filters = dict(doc_type = self.doc_type, property=self.property) if self.field_name: - dict['field_name'] = self.field_name + filters['field_name'] = self.field_name frappe.db.delete('Property Setter', filters) From 08fe29714d0271516f8b6ab03644f125d0d805ea Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Wed, 14 Oct 2020 09:21:08 +0530 Subject: [PATCH 12/24] fix(minor): delete property setter --- .../doctype/customize_form/customize_form.py | 5 ++++- .../customize_form/test_customize_form.py | 1 + .../property_setter/property_setter.py | 21 ++++++++++--------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index e6c2840e08..61ecdd88b9 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -15,6 +15,7 @@ from frappe.model.document import Document from frappe.model import no_value_fields, core_doctypes_list from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype, check_email_append_to from frappe.custom.doctype.custom_field.custom_field import create_custom_field +from frappe.custom.doctype.property_setter.property_setter import delete_property_setter from frappe.model.docfield import supports_translation class CustomizeForm(Document): @@ -356,6 +357,8 @@ class CustomizeForm(Document): def make_property_setter(self, prop, value, property_type, fieldname=None, apply_on=None, row_name = None): + delete_property_setter(self.doc_type, prop, fieldname) + property_value = self.get_existing_property_value(prop, fieldname) if property_value==value: @@ -381,7 +384,7 @@ class CustomizeForm(Document): property_value = frappe.db.get_value("DocField", {"parent": self.doc_type, "fieldname": fieldname}, property_name) else: - if frappe.db.has_column(self.doc_type, property_name): + if frappe.db.has_column("DocType", property_name): property_value = frappe.db.get_value("DocType", self.doc_type, property_name) else: property_value = None diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 73d0747015..46a2f2f9df 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -25,6 +25,7 @@ class TestCustomizeForm(unittest.TestCase): def setUp(self): self.insert_custom_field() + frappe.db.delete('Property Setter', dict(doc_type='Event')) frappe.db.commit() frappe.clear_cache(doctype="Event") diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 83c0337074..56e5829271 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -19,7 +19,8 @@ class PropertySetter(Document): def validate(self): self.validate_fieldtype_change() - self.delete_property_setter() + if self.is_new(): + delete_property_setter(self.doc_type, self.property, self.field_name) # clear cache frappe.clear_cache(doctype = self.doc_type) @@ -29,15 +30,6 @@ class PropertySetter(Document): self.property == 'fieldtype': frappe.throw(_("Field type cannot be changed for {0}").format(self.field_name)) - def delete_property_setter(self): - """delete other property setters on this, if this is new""" - if self.get('__islocal'): - filters = dict(doc_type = self.doc_type, property=self.property) - if self.field_name: - filters['field_name'] = self.field_name - - frappe.db.delete('Property Setter', filters) - def get_property_list(self, dt): return frappe.db.get_all('DocField', fields=['fieldname', 'label', 'fieldtype'], @@ -91,3 +83,12 @@ def make_property_setter(doctype, fieldname, property, value, property_type, for property_setter.flags.validate_fields_for_doctype = validate_fields_for_doctype property_setter.insert() return property_setter + +def delete_property_setter(doc_type, property, field_name=None): + """delete other property setters on this, if this is new""" + filters = dict(doc_type = doc_type, property=property) + if field_name: + filters['field_name'] = field_name + + frappe.db.delete('Property Setter', filters) + From 8707ed1f2760c87ddafa2ec2c22aec0d67d98428 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 19 Oct 2020 15:18:14 +0530 Subject: [PATCH 13/24] fix(minor): remove "Custom Link" and add patch --- frappe/custom/doctype/custom_link/__init__.py | 0 .../custom/doctype/custom_link/custom_link.js | 20 ------- .../doctype/custom_link/custom_link.json | 52 ------------------- .../custom/doctype/custom_link/custom_link.py | 10 ---- .../doctype/custom_link/test_custom_link.py | 10 ---- frappe/database/database.py | 3 ++ frappe/model/meta.py | 3 -- frappe/patches.txt | 1 + frappe/patches/v13_0/remove_custom_link.py | 15 ++++++ 9 files changed, 19 insertions(+), 95 deletions(-) delete mode 100644 frappe/custom/doctype/custom_link/__init__.py delete mode 100644 frappe/custom/doctype/custom_link/custom_link.js delete mode 100644 frappe/custom/doctype/custom_link/custom_link.json delete mode 100644 frappe/custom/doctype/custom_link/custom_link.py delete mode 100644 frappe/custom/doctype/custom_link/test_custom_link.py create mode 100644 frappe/patches/v13_0/remove_custom_link.py diff --git a/frappe/custom/doctype/custom_link/__init__.py b/frappe/custom/doctype/custom_link/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/custom/doctype/custom_link/custom_link.js b/frappe/custom/doctype/custom_link/custom_link.js deleted file mode 100644 index 8662724b1a..0000000000 --- a/frappe/custom/doctype/custom_link/custom_link.js +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2020, Frappe Technologies and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Custom Link', { - refresh: function(frm) { - frm.set_query("document_type", function () { - return { - filters: { - custom: 0, - istable: 0, - module: ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] - } - }; - }); - - frm.add_custom_button(__('Go to {0} List', [frm.doc.document_type]), function() { - frappe.set_route('List', frm.doc.document_type); - }); - } -}); diff --git a/frappe/custom/doctype/custom_link/custom_link.json b/frappe/custom/doctype/custom_link/custom_link.json deleted file mode 100644 index 350e6b1c2d..0000000000 --- a/frappe/custom/doctype/custom_link/custom_link.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "actions": [], - "autoname": "field:document_type", - "creation": "2020-04-08 15:16:44.342509", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "document_type", - "links" - ], - "fields": [ - { - "fieldname": "document_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Document Type", - "options": "DocType", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "links", - "fieldtype": "Table", - "label": "Links", - "options": "DocType Link" - } - ], - "links": [], - "modified": "2020-04-08 16:42:59.402671", - "modified_by": "Administrator", - "module": "Custom", - "name": "Custom Link", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/frappe/custom/doctype/custom_link/custom_link.py b/frappe/custom/doctype/custom_link/custom_link.py deleted file mode 100644 index 11316d5751..0000000000 --- a/frappe/custom/doctype/custom_link/custom_link.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, 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 CustomLink(Document): - pass diff --git a/frappe/custom/doctype/custom_link/test_custom_link.py b/frappe/custom/doctype/custom_link/test_custom_link.py deleted file mode 100644 index a292f73ad0..0000000000 --- a/frappe/custom/doctype/custom_link/test_custom_link.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies and Contributors -# See license.txt -from __future__ import unicode_literals - -# import frappe -import unittest - -class TestCustomLink(unittest.TestCase): - pass diff --git a/frappe/database/database.py b/frappe/database/database.py index 64b234b1d3..616dd3c3ec 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -780,6 +780,9 @@ class Database(object): """Returns True if table for given doctype exists.""" return ("tab" + doctype) in self.get_tables() + def has_table(self, doctype): + return self.table_exists(doctype) + def get_tables(self): tables = frappe.cache().get_value('db_tables') if not tables: diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 4299876a77..cdd4b34ae8 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -479,9 +479,6 @@ class Meta(Document): if hasattr(self, 'links') and self.links: dashboard_links.extend(self.links) - if frappe.get_all("Custom Link", {"document_type": self.name}): - dashboard_links.extend(frappe.get_doc("Custom Link", self.name).links) - if not data.transactions: # init groups data.transactions = [] diff --git a/frappe/patches.txt b/frappe/patches.txt index 05f067f4b9..2202557b40 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -313,3 +313,4 @@ frappe.patches.v13_0.update_newsletter_content_type execute:frappe.db.set_value('Website Settings', 'Website Settings', {'navbar_template': 'Standard Navbar', 'footer_template': 'Standard Footer'}) frappe.patches.v13_0.delete_event_producer_and_consumer_keys frappe.patches.v13_0.web_template_set_module #2020-10-05 +frappe.patches.v13_0.remove_custom_link diff --git a/frappe/patches/v13_0/remove_custom_link.py b/frappe/patches/v13_0/remove_custom_link.py new file mode 100644 index 0000000000..9c2a441a62 --- /dev/null +++ b/frappe/patches/v13_0/remove_custom_link.py @@ -0,0 +1,15 @@ +import frappe + +def execute(): + ''' + Remove the doctype "Custom Link" that was used to add Custom Links to the + Dashboard since this is now managed by Customize Form. + Update `parent` property to the DocType and delte the doctype + ''' + + if frappe.db.has_table('Custom Link'): + for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']): + frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s', + (custom_link.document_type, custom_link.name)) + + frappe.delete_doc('DocType', 'Custom Link') \ No newline at end of file From 742605542cdab0dcbbc530fc0d86867f3b22e12b Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 19 Oct 2020 15:33:27 +0530 Subject: [PATCH 14/24] fix(minor): added ignore_ddl in frappe.db.get_all to ignore missing tables, columns --- .../core/doctype/doctype_link/doctype_link.json | 2 +- frappe/model/db_query.py | 6 ++++-- frappe/model/meta.py | 3 ++- frappe/model/naming.py | 15 ++++++--------- frappe/patches.txt | 2 +- frappe/patches/v13_0/remove_custom_link.py | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/frappe/core/doctype/doctype_link/doctype_link.json b/frappe/core/doctype/doctype_link/doctype_link.json index 2adfd4a6c3..0453894467 100644 --- a/frappe/core/doctype/doctype_link/doctype_link.json +++ b/frappe/core/doctype/doctype_link/doctype_link.json @@ -50,7 +50,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-09-24 14:19:23.189511", + "modified": "2020-09-24 14:19:25.189511", "modified_by": "Administrator", "module": "Core", "name": "DocType Link", diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 596f69d2dd..bb7ac1bca0 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -38,7 +38,7 @@ class DatabaseQuery(object): join='left join', distinct=False, start=None, page_length=None, limit=None, ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, update=None, add_total_row=None, user_settings=None, reference_doctype=None, - return_query=False, strict=True, pluck=None): + return_query=False, strict=True, pluck=None, ignore_ddl=False): if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) raise frappe.PermissionError(self.doctype) @@ -86,6 +86,7 @@ class DatabaseQuery(object): self.user_settings_fields = copy.deepcopy(self.fields) self.return_query = return_query self.strict = strict + self.ignore_ddl = ignore_ddl # for contextual user permission check # to determine which user permission is applicable on link field of specific doctype @@ -134,7 +135,8 @@ class DatabaseQuery(object): if self.return_query: return query else: - return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, update=self.update) + return frappe.db.sql(query, as_dict=not self.as_list, debug=self.debug, + update=self.update, ignore_ddl=self.ignore_ddl) def prepare_args(self): self.parse_args() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index cdd4b34ae8..8c17a5b19b 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -343,7 +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, fields='*', filters=dict(parent=self.name, custom=1)): + # ignore_ddl because the `custom` column was added later via a patch + for d in frappe.get_all(doctype, fields='*', filters=dict(parent=self.name, custom=1), ignore_ddl=True): self.append(fieldname, d) # set the fields in order if specified diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 9ea5fc0ca4..c2e074990e 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -93,15 +93,12 @@ def set_naming_from_document_naming_rule(doc): if doc.doctype in log_types: return - try: - for d in frappe.get_all('Document Naming Rule', - dict(document_type=doc.doctype, disabled=0), order_by='priority desc'): - frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc) - if doc.name: - break - except frappe.db.TableMissingError: # noqa: E722 - # not yet bootstrapped - pass + # ignore_ddl if naming is not yet bootstrapped + for d in frappe.get_all('Document Naming Rule', + dict(document_type=doc.doctype, disabled=0), order_by='priority desc', ignore_ddl=True): + frappe.get_cached_doc('Document Naming Rule', d.name).apply(doc) + if doc.name: + break def set_name_by_naming_series(doc): """Sets name by the `naming_series` property""" diff --git a/frappe/patches.txt b/frappe/patches.txt index 2202557b40..2a6ba321d5 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -7,7 +7,7 @@ frappe.patches.v7_0.update_auth frappe.patches.v8_0.drop_in_dialog #2017-09-22 frappe.patches.v7_2.remove_in_filter execute:frappe.reload_doc('core', 'doctype', 'doctype_action', force=True) #2019-09-23 -execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2019-09-23 +execute:frappe.reload_doc('core', 'doctype', 'doctype_link', force=True) #2020-10-17 execute:frappe.reload_doc('core', 'doctype', 'doctype', force=True) #2017-09-22 execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2018-02-20 frappe.patches.v11_0.drop_column_apply_user_permissions diff --git a/frappe/patches/v13_0/remove_custom_link.py b/frappe/patches/v13_0/remove_custom_link.py index 9c2a441a62..e1830033af 100644 --- a/frappe/patches/v13_0/remove_custom_link.py +++ b/frappe/patches/v13_0/remove_custom_link.py @@ -6,7 +6,7 @@ def execute(): Dashboard since this is now managed by Customize Form. Update `parent` property to the DocType and delte the doctype ''' - + frappe.reload_doctype('DocType Link') if frappe.db.has_table('Custom Link'): for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']): frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s', From 69d2c10736f7e5255333f9202cd3067722ba40e9 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 19 Oct 2020 15:51:33 +0530 Subject: [PATCH 15/24] fix(minor): db_query (ignore_ddl) --- frappe/model/db_query.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index bb7ac1bca0..942ede10bb 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -327,7 +327,13 @@ class DatabaseQuery(object): def set_optional_columns(self): """Removes optional columns like `_user_tags`, `_comments` etc. if not in table""" - columns = get_table_columns(self.doctype) + try: + columns = get_table_columns(self.doctype) + except frappe.db.TableMissingError: + if self.ignore_ddl: + return + else: + raise # remove from fields to_remove = [] From 6f346a61f4cf08f81b74c511840cfbe4c137615a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 09:59:19 +0530 Subject: [PATCH 16/24] fix(formatter):add link formatter for User and fix patch --- frappe/core/doctype/report/report.py | 6 ++++++ frappe/public/js/frappe/form/formatters.js | 11 ++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index fe3156d995..7620117bd7 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -49,9 +49,15 @@ class Report(Document): self.export_doc() def on_trash(self): +<<<<<<< 69d2c10736f7e5255333f9202cd3067722ba40e9 if (self.is_standard == 'Yes' and not cint(getattr(frappe.local.conf, 'developer_mode', 0)) and not frappe.flags.in_patch): +======= + if (self.is_standard == 'Yes' + and not frappe.flags.in_patch + and not cint(getattr(frappe.local.conf, 'developer_mode',0))): +>>>>>>> fix(formatter):add link formatter for User and fix patch frappe.throw(_("You are not allowed to delete Standard Report")) delete_custom_role('report', self.name) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 39541757a5..3f422d0a9b 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -106,7 +106,7 @@ frappe.form.formatters = { if(frappe.form.link_formatters[doctype]) { // don't apply formatters in case of composite (parent field of same type) if (doc && doctype !== doc.doctype) { - value = frappe.form.link_formatters[doctype](value, doc); + value = frappe.form.link_formatters[doctype](value, doc, docfield); } } @@ -305,7 +305,7 @@ frappe.format = function(value, df, options, doc) { formatted = frappe.dom.remove_script_and_style(formatted); return formatted; -} +}; frappe.get_format_helper = function(doc) { var helper = { @@ -317,4 +317,9 @@ frappe.get_format_helper = function(doc) { }; $.extend(helper, doc); return helper; -} +}; + +frappe.form.link_formatters['User'] = function(value, doc, docfield) { + let full_name = doc && (doc.full_name || (docfield && doc[`${docfield.fieldname}_full_name`])); + return full_name || value; +}; From e820d254c5c62404df5c46447ca049ba1ffa9a17 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 11:07:52 +0530 Subject: [PATCH 17/24] fix(confict): did not save and also fix tabs --- frappe/core/doctype/report/report.py | 10 ++-------- frappe/patches/v13_0/remove_custom_link.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 7620117bd7..9d30409a2a 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -49,15 +49,9 @@ class Report(Document): self.export_doc() def on_trash(self): -<<<<<<< 69d2c10736f7e5255333f9202cd3067722ba40e9 - if (self.is_standard == 'Yes' - and not cint(getattr(frappe.local.conf, 'developer_mode', 0)) - and not frappe.flags.in_patch): -======= if (self.is_standard == 'Yes' - and not frappe.flags.in_patch - and not cint(getattr(frappe.local.conf, 'developer_mode',0))): ->>>>>>> fix(formatter):add link formatter for User and fix patch + and not cint(getattr(frappe.local.conf, 'developer_mode', 0)) + and not frappe.flags.in_patch): frappe.throw(_("You are not allowed to delete Standard Report")) delete_custom_role('report', self.name) diff --git a/frappe/patches/v13_0/remove_custom_link.py b/frappe/patches/v13_0/remove_custom_link.py index e1830033af..f38bb642f0 100644 --- a/frappe/patches/v13_0/remove_custom_link.py +++ b/frappe/patches/v13_0/remove_custom_link.py @@ -1,15 +1,15 @@ import frappe def execute(): - ''' - Remove the doctype "Custom Link" that was used to add Custom Links to the - Dashboard since this is now managed by Customize Form. - Update `parent` property to the DocType and delte the doctype - ''' - frappe.reload_doctype('DocType Link') - if frappe.db.has_table('Custom Link'): - for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']): - frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s', - (custom_link.document_type, custom_link.name)) + ''' + Remove the doctype "Custom Link" that was used to add Custom Links to the + Dashboard since this is now managed by Customize Form. + Update `parent` property to the DocType and delte the doctype + ''' + frappe.reload_doctype('DocType Link') + if frappe.db.has_table('Custom Link'): + for custom_link in frappe.get_all('Custom Link', ['name', 'document_type']): + frappe.db.sql('update `tabDocType Link` set custom=1, parent=%s where parent=%s', + (custom_link.document_type, custom_link.name)) - frappe.delete_doc('DocType', 'Custom Link') \ No newline at end of file + frappe.delete_doc('DocType', 'Custom Link') \ No newline at end of file From 7f1b35b0de88624876b8f5ff04e730025fdf1200 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 12:01:39 +0530 Subject: [PATCH 18/24] fix(tests): test_workflow.py, full cleanup --- .../doctype/workflow/test_workflow.py | 73 +++++++++---------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index adcd98a85d..fe2e3ef66d 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -20,8 +20,7 @@ class TestWorkflow(unittest.TestCase): frappe.set_user('Administrator') def tearDown(self): - frappe.print_sql(False) - self.workflow.db_set('is_active', 0) + frappe.delete_doc('Workflow', 'Test ToDo') def test_default_condition(self): '''test default condition is set''' @@ -34,7 +33,6 @@ class TestWorkflow(unittest.TestCase): def test_approve(self, doc=None): '''test simple workflow''' - frappe.print_sql(True) todo = doc or self.test_default_condition() apply_workflow(todo, 'Approve') @@ -87,7 +85,7 @@ class TestWorkflow(unittest.TestCase): frappe.set_user('test2@example.com') doc = self.test_default_condition() - workflow_actions = frappe.get_all('Workflow Action', fields=['status', 'reference_name']) + workflow_actions = frappe.get_all('Workflow Action', fields=['*']) self.assertEqual(len(workflow_actions), 1) # test if status of workflow actions are updated on approval @@ -128,43 +126,42 @@ class TestWorkflow(unittest.TestCase): def create_todo_workflow(): if frappe.db.exists('Workflow', 'Test ToDo'): - workflow = frappe.get_doc('Workflow', 'Test ToDo').save(ignore_permissions=True) - workflow.db_set('is_active', 1) - return workflow - else: + frappe.delete_doc('Workflow', 'Test ToDo') + + if not frappe.db.exists('Role', 'Test Approver'): frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) - workflow = frappe.new_doc('Workflow') - workflow.workflow_name = 'Test ToDo' - workflow.document_type = 'ToDo' - workflow.workflow_state_field = 'workflow_state' - workflow.is_active = 1 - workflow.send_email_alert = 0 - workflow.append('states', dict( - state = 'Pending', allow_edit = 'All' - )) - workflow.append('states', dict( - state = 'Approved', allow_edit = 'Test Approver', - update_field = 'status', update_value = 'Closed' - )) - workflow.append('states', dict( - state = 'Rejected', allow_edit = 'Test Approver' - )) - workflow.append('transitions', dict( - state = 'Pending', action='Approve', next_state = 'Approved', - allowed='Test Approver', allow_self_approval= 1 - )) - workflow.append('transitions', dict( - state = 'Pending', action='Reject', next_state = 'Rejected', - allowed='Test Approver', allow_self_approval= 1 - )) - workflow.append('transitions', dict( - state = 'Rejected', action='Review', next_state = 'Pending', - allowed='All', allow_self_approval= 1 - )) - workflow.insert(ignore_permissions=True) + workflow = frappe.new_doc('Workflow') + workflow.workflow_name = 'Test ToDo' + workflow.document_type = 'ToDo' + workflow.workflow_state_field = 'workflow_state' + workflow.is_active = 1 + workflow.send_email_alert = 0 + workflow.append('states', dict( + state = 'Pending', allow_edit = 'All' + )) + workflow.append('states', dict( + state = 'Approved', allow_edit = 'Test Approver', + update_field = 'status', update_value = 'Closed' + )) + workflow.append('states', dict( + state = 'Rejected', allow_edit = 'Test Approver' + )) + workflow.append('transitions', dict( + state = 'Pending', action='Approve', next_state = 'Approved', + allowed='Test Approver', allow_self_approval= 1 + )) + workflow.append('transitions', dict( + state = 'Pending', action='Reject', next_state = 'Rejected', + allowed='Test Approver', allow_self_approval= 1 + )) + workflow.append('transitions', dict( + state = 'Rejected', action='Review', next_state = 'Pending', + allowed='All', allow_self_approval= 1 + )) + workflow.insert(ignore_permissions=True) - return workflow + return workflow def create_new_todo(): return frappe.get_doc(dict(doctype='ToDo', description='workflow ' + random_string(10))).insert() From 1df7831d6790388b1af9082a86004c0079a9525f Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 12:24:11 +0530 Subject: [PATCH 19/24] fix(minor): postgres/database.py error handling for missing table --- frappe/database/postgres/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 3d997864e4..4faea78551 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -140,11 +140,11 @@ class PostgresDatabase(Database): @staticmethod def is_table_missing(e): - return e.pgcode == '42P01' + return getattr(e, 'pgcode', None) == '42P01' @staticmethod def is_missing_column(e): - return e.pgcode == '42703' + return getattr(e, 'pgcode', None) == '42703' @staticmethod def is_access_denied(e): From 625ab74883b914b5b7781be2fdccd6f50cc4a01c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 13:01:46 +0530 Subject: [PATCH 20/24] fix(tests): added a new table to avoid conflicts --- .../event_producer/event_producer.json | 9 +--- .../doctype/event_producer/event_producer.py | 20 +++++-- .../event_producer_last_update/__init__.py | 0 .../event_producer_last_update.js | 8 +++ .../event_producer_last_update.json | 52 +++++++++++++++++++ .../event_producer_last_update.py | 10 ++++ .../test_event_producer_last_update.py | 10 ++++ 7 files changed, 98 insertions(+), 11 deletions(-) create mode 100644 frappe/event_streaming/doctype/event_producer_last_update/__init__.py create mode 100644 frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js create mode 100644 frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json create mode 100644 frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py create mode 100644 frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.json b/frappe/event_streaming/doctype/event_producer/event_producer.json index 8fafdc3bb2..d868f6c123 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.json +++ b/frappe/event_streaming/doctype/event_producer/event_producer.json @@ -13,7 +13,6 @@ "api_secret", "column_break_6", "user", - "last_update", "incoming_change" ], "fields": [ @@ -25,12 +24,6 @@ "reqd": 1, "unique": 1 }, - { - "fieldname": "last_update", - "fieldtype": "Data", - "label": "Last Update", - "read_only": 1 - }, { "description": "API Key of the user(Event Subscriber) on the producer site", "fieldname": "api_key", @@ -77,7 +70,7 @@ } ], "links": [], - "modified": "2020-09-08 18:50:57.687979", + "modified": "2020-10-26 13:00:15.361316", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Producer", diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index b0ec998ab9..9e73f51d47 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -79,10 +79,24 @@ class EventProducer(Document): ) if response: response = json.loads(response) - self.last_update = response['last_update'] + self.set_last_update(response['last_update']) else: frappe.throw(_('Failed to create an Event Consumer or an Event Consumer for the current site is already registered.')) + def set_last_update(self, last_update): + last_update_doc_name = frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name)) + if not last_update_doc_name: + frappe.get_doc(dict( + doctype = 'Event Producer Last Update', + event_producer = self.name, + last_update = last_update + )).insert(ignore_permissions=True) + else: + frappe.db.set_value('Event Producer Last Update', last_update_doc_name, 'last_update', last_update) + + def get_last_update(self): + return frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name), 'last_update') + def get_request_data(self): consumer_doctypes = [] for entry in self.producer_doctypes: @@ -184,7 +198,7 @@ def pull_from_node(event_producer): """pull all updates after the last update timestamp from event producer site""" event_producer = frappe.get_doc('Event Producer', event_producer) producer_site = get_producer_site(event_producer.producer_url) - last_update = event_producer.last_update + last_update = event_producer.get_last_update() (doctypes, mapping_config, naming_config) = get_config(event_producer.producer_doctypes) @@ -239,7 +253,7 @@ def sync(update, producer_site, event_producer, in_retry=False): return 'Failed' log_event_sync(update, event_producer.name, 'Failed', frappe.get_traceback()) - event_producer.db_set('last_update', update.creation) + event_producer.set_last_update(update.creation) frappe.db.commit() diff --git a/frappe/event_streaming/doctype/event_producer_last_update/__init__.py b/frappe/event_streaming/doctype/event_producer_last_update/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js new file mode 100644 index 0000000000..15730e4c5f --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Event Producer Last Update', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json new file mode 100644 index 0000000000..af8d45b970 --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json @@ -0,0 +1,52 @@ +{ + "actions": [], + "autoname": "field:event_producer", + "creation": "2020-10-26 12:53:11.940177", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "event_producer", + "last_update" + ], + "fields": [ + { + "fieldname": "event_producer", + "fieldtype": "Link", + "label": "Event Producer", + "options": "Event Producer", + "unique": 1 + }, + { + "fieldname": "last_update", + "fieldtype": "Data", + "label": "Last Update" + } + ], + "in_create": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2020-10-26 12:59:33.112423", + "modified_by": "Administrator", + "module": "Event Streaming", + "name": "Event Producer Last Update", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py new file mode 100644 index 0000000000..02e297bdd5 --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 EventProducerLastUpdate(Document): + pass diff --git a/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py new file mode 100644 index 0000000000..0311cb2df9 --- /dev/null +++ b/frappe/event_streaming/doctype/event_producer_last_update/test_event_producer_last_update.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEventProducerLastUpdate(unittest.TestCase): + pass From 8bd98e6272fdb44e2ce13403328a259d49f2aff3 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 13:22:38 +0530 Subject: [PATCH 21/24] fix(minor): set name for Event Producer Last Update --- .../doctype/event_producer/event_producer.py | 2 +- .../event_producer_last_update.json | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index 9e73f51d47..d458f3c24b 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -88,7 +88,7 @@ class EventProducer(Document): if not last_update_doc_name: frappe.get_doc(dict( doctype = 'Event Producer Last Update', - event_producer = self.name, + event_producer = self.producer_url, last_update = last_update )).insert(ignore_permissions=True) else: diff --git a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json index af8d45b970..27f8ed2f81 100644 --- a/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json +++ b/frappe/event_streaming/doctype/event_producer_last_update/event_producer_last_update.json @@ -12,9 +12,10 @@ "fields": [ { "fieldname": "event_producer", - "fieldtype": "Link", + "fieldtype": "Data", + "in_list_view": 1, "label": "Event Producer", - "options": "Event Producer", + "reqd": 1, "unique": 1 }, { @@ -26,7 +27,7 @@ "in_create": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-26 12:59:33.112423", + "modified": "2020-10-26 13:22:27.056599", "modified_by": "Administrator", "module": "Event Streaming", "name": "Event Producer Last Update", From 373403c97e712f88a4024cbcf49b20abf37c5c75 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 13:44:32 +0530 Subject: [PATCH 22/24] fix(minor): test_workflow.py --- frappe/workflow/doctype/workflow/test_workflow.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py index fe2e3ef66d..9ad0562a86 100644 --- a/frappe/workflow/doctype/workflow/test_workflow.py +++ b/frappe/workflow/doctype/workflow/test_workflow.py @@ -15,8 +15,7 @@ class TestWorkflow(unittest.TestCase): make_test_records("User") def setUp(self): - if not getattr(self, 'workflow', None): - self.workflow = create_todo_workflow() + self.workflow = create_todo_workflow() frappe.set_user('Administrator') def tearDown(self): From 0b93784b32e1dde7c02d260f1405be90e52d84eb Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 14:13:38 +0530 Subject: [PATCH 23/24] fix(minor): test_db.py --- frappe/tests/test_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 6fbf247404..82ddf73c40 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -131,7 +131,7 @@ class TestDB(unittest.TestCase): # Testing read self.assertEqual(list(frappe.get_all("ToDo", fields=[random_field], limit=1)[0])[0], random_field) - self.assertEqual(list(frappe.get_all("ToDo", fields=["{0} as total".format(random_field)], limit=1)[0])[0], "total") + self.assertEqual(list(frappe.get_all("ToDo", fields=["`{0}` as total".format(random_field)], limit=1)[0])[0], "total") # Testing read for distinct and sql functions self.assertEqual(list( From ceb1ecce73ce1f7d6c6f14d3fa57a9e54e1017f8 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 26 Oct 2020 14:16:23 +0530 Subject: [PATCH 24/24] fix(minor): test_db.py --- frappe/tests/test_db.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 82ddf73c40..2925242994 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -104,7 +104,10 @@ class TestDB(unittest.TestCase): "INT", "FORTRAN", "STABLE"] } created_docs = [] - fields = all_keywords[frappe.conf.db_type] + + # edit by rushabh: added [:1] + # don't run every keyword! - if one works, they all do + fields = all_keywords[frappe.conf.db_type][:1] test_doctype = "ToDo" def add_custom_field(field):