Merge pull request #11565 from rmehta/update-customize-form

feat: add links and actions to customize form and cleanup code
This commit is contained in:
Rushabh Mehta 2020-10-26 15:11:47 +05:30 committed by GitHub
commit d56da0156e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1107 additions and 959 deletions

View file

@ -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",

View file

@ -160,6 +160,10 @@ def handle_exception(e):
http_status_code = getattr(e, "http_status_code", 500)
return_as_message = False
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
# if the request is ajax, send back the trace or error message

View file

@ -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)))

View file

@ -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

View file

@ -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",

View file

@ -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:25.189511",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType Link",

View file

@ -49,8 +49,8 @@ class Report(Document):
self.export_doc()
def on_trash(self):
if (self.is_standard == 'Yes'
and not cint(getattr(frappe.local.conf, 'developer_mode', 0))
if (self.is_standard == 'Yes'
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)

View file

@ -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);
});
}
});

View file

@ -5,6 +5,7 @@ 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,
@ -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,26 +89,26 @@ 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() {
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) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(__('Export Customizations'), function() {
frappe.prompt(
[
@ -124,34 +131,36 @@ frappe.ui.form.on("Customize Form", {
});
},
__("Select Module"));
});
}, __('Actions'));
}
}
// 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 +169,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 +219,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 +231,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();

View file

@ -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",

View file

@ -6,6 +6,7 @@ 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 _
@ -14,8 +15,447 @@ 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):
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):
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
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:
# custom - just insert/update
d.parent = self.doc_type
d.custom = 1
d.save(ignore_permissions=True)
has_custom = True
items.append(d.name)
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")):
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):
delete_property_setter(self.doc_type, 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
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
})
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:
if frappe.db.has_column("DocType", property_name):
property_value = frappe.db.get_value("DocType", self.doc_type, property_name)
else:
property_value = None
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 +522,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)
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')

View file

@ -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):
@ -24,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")
@ -185,9 +187,75 @@ 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')
self.assertRaises(frappe.ValidationError, self.get_customize_form, 'User')
# 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)

View file

@ -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",

View file

@ -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": "<div class=\"alert\">Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!</div>",
"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": "<div class=\"alert\">Please don't update it as it can mess up your form. Use the Customize Form View and Custom Fields to set properties!</div>"
},
{
"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
}

View file

@ -11,13 +11,16 @@ 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()
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)
@ -27,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'):
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())
def get_property_list(self, dt):
return frappe.db.get_all('DocField',
fields=['fieldname', 'label', 'fieldtype'],
@ -89,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)

View file

@ -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:
@ -341,18 +340,12 @@ 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)):
# 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"]:
@ -787,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:
@ -959,13 +955,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'))

View file

@ -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):

View file

@ -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')):

View file

@ -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},

View file

@ -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",

View file

@ -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",

View file

@ -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.producer_url,
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()

View file

@ -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) {
// }
});

View file

@ -1,36 +1,36 @@
{
"actions": [],
"autoname": "field:document_type",
"creation": "2020-04-08 15:16:44.342509",
"autoname": "field:event_producer",
"creation": "2020-10-26 12:53:11.940177",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"links"
"event_producer",
"last_update"
],
"fields": [
{
"fieldname": "document_type",
"fieldtype": "Link",
"fieldname": "event_producer",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Document Type",
"options": "DocType",
"label": "Event Producer",
"reqd": 1,
"unique": 1
},
{
"fieldname": "links",
"fieldtype": "Table",
"label": "Links",
"options": "DocType Link"
"fieldname": "last_update",
"fieldtype": "Data",
"label": "Last Update"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-04-08 16:42:59.402671",
"modified": "2020-10-26 13:22:27.056599",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Link",
"module": "Event Streaming",
"name": "Event Producer Last Update",
"owner": "Administrator",
"permissions": [
{
@ -46,6 +46,7 @@
"write": 1
}
],
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class CustomLink(Document):
class EventProducerLastUpdate(Document):
pass

View file

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
class TestCustomLink(unittest.TestCase):
class TestEventProducerLastUpdate(unittest.TestCase):
pass

View file

@ -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

View file

@ -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()
@ -325,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 = []

View file

@ -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

View file

@ -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,52 @@ 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.field_name:
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')):
# 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
# 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:
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 d not in new_list:
new_list.append(d)
self.set(fieldname, new_list)
def sort_fields(self):
"""sort on basis of insert_after"""
@ -448,9 +480,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 = []
@ -458,6 +487,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

View file

@ -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"""

View file

@ -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
@ -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

View file

@ -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
'''
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')

View file

@ -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;
};

View file

@ -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):
@ -131,7 +134,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(

View file

@ -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))

View file

@ -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 = []

View file

@ -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:48:36.886753",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Category",

View file

@ -15,13 +15,11 @@ 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):
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 +32,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 +84,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 +125,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()