Refactored Customize Form frappe/frappe#478

This commit is contained in:
Anand Doshi 2014-04-03 18:35:13 +05:30
parent 441c83ae00
commit f2d4f65f8d
10 changed files with 370 additions and 259 deletions

View file

@ -26,6 +26,9 @@ class CustomField(Document):
if not self.idx:
self.idx = len(frappe.get_meta(self.dt).get("fields")) + 1
if not self.fieldname:
frappe.throw(_("Fieldname not set for Custom Field"))
def on_update(self):
# validate field
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
@ -38,9 +41,10 @@ class CustomField(Document):
self.create_property_setter()
# update the schema
from frappe.model.db_schema import updatedb
updatedb(self.dt)
if not frappe.flags.in_test:
from frappe.model.db_schema import updatedb
updatedb(self.dt)
def on_trash(self):
# delete property setter entries
frappe.db.sql("""\

View file

@ -0,0 +1,11 @@
[
{
"dt": "User",
"label": "Test Custom Field",
"description": "A Custom Field for Testing",
"fieldtype": "Select",
"in_list_view": 1,
"options": "\nCustom 1\nCustom 2\nCustom 3",
"default": "Custom 3"
}
]

View file

@ -4,7 +4,6 @@
frappe.provide("frappe.customize_form");
frappe.ui.form.on("Customize Form", "onload", function(frm) {
frm.fields_dict.fields.grid.cannot_add_rows = true;
frappe.customize_form.add_fields_help(frm);
frm.set_query("doc_type", function() {
@ -215,7 +214,7 @@ frappe.customize_form.add_fields_help = function(frm) {
<tr>\
<td></td>\
<td><a class='link_type' \
onclick='frm.fields_help_dialog.hide()'\
onclick='frappe.customize_form.fields_help_dialog.hide()'\
style='color:grey'>Press Esc to close</a>\
</td>\
</tr>\
@ -227,7 +226,7 @@ frappe.customize_form.add_fields_help = function(frm) {
d.show();
frm.fields_help_dialog = d;
frappe.customize_form.fields_help_dialog = d;
});
}

View file

@ -98,7 +98,7 @@
"permlevel": 0
},
{
"fieldname": "fields",
"fieldname": "customize_form_fields",
"fieldtype": "Table",
"label": "Fields",
"no_copy": 0,
@ -111,7 +111,7 @@
"icon": "icon-glass",
"idx": 1,
"issingle": 1,
"modified": "2014-01-15 16:16:24.000000",
"modified": "2014-01-15 16:16:22.000000",
"modified_by": "Administrator",
"module": "Core",
"name": "Customize Form",

View file

@ -7,56 +7,52 @@ from __future__ import unicode_literals
Thus providing a better UI from user perspective
"""
import frappe, json
from frappe import _
from frappe.utils import cstr
from frappe.model.document import Document
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
class CustomizeForm(Document):
doctype_properties = (
'search_fields',
'default_print_format',
'read_only_onload',
'allow_print',
'allow_email',
'allow_copy',
'allow_attach',
'max_attachments'
)
doctype_properties = {
'search_fields': 'Data',
'default_print_format': 'Data',
'read_only_onload': 'Check',
'allow_print': 'Check',
'allow_email': 'Check',
'allow_copy': 'Check',
'allow_attach': 'Check',
'max_attachments': 'Int'
}
docfield_properties = (
'idx',
'label',
'fieldtype',
'fieldname',
'options',
'permlevel',
'width',
'print_width',
'reqd',
'ignore_restrictions',
'in_filter',
'in_list_view',
'hidden',
'print_hide',
'report_hide',
'allow_on_submit',
'depends_on',
'description',
'default',
'name',
)
docfield_properties = {
'idx': 'Int',
'label': 'Data',
'fieldtype': 'Select',
'options': 'Text',
'permlevel': 'Int',
'width': 'Data',
'print_width': 'Data',
'reqd': 'Check',
'ignore_restrictions': 'Check',
'in_filter': 'Check',
'in_list_view': 'Check',
'hidden': 'Check',
'print_hide': 'Check',
'report_hide': 'Check',
'allow_on_submit': 'Check',
'depends_on': 'Data',
'description': 'Text',
'default': 'Text'
}
allowed_fieldtype_change = (('Currency', 'Float'), ('Small Text', 'Data'),
('Text', 'Text Editor', 'Code'))
# property_restrictions = {
# 'fieldtype': (('Currency', 'Float'), ('Small Text', 'Data'), ('Text', 'Text Editor', 'Code')),
# }
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:
@ -65,14 +61,14 @@ class CustomizeForm(Document):
meta = frappe.get_meta(self.doc_type)
# doctype properties
for fieldname in self.doctype_properties:
self.set(fieldname, meta.get(fieldname))
for property in self.doctype_properties:
self.set(property, meta.get(property))
for d in meta.get("fields"):
new_d = {}
for fieldname in self.docfield_properties:
new_d[fieldname] = d.get(fieldname)
self.append("fields", new_d)
new_d = {"fieldname": d.fieldname, "is_custom_field": d.get("is_custom_field"), "name": d.name}
for property in self.docfield_properties:
new_d[property] = d.get(property)
self.append("customize_form_fields", new_d)
# NOTE doc is sent to clientside by run_method
@ -91,9 +87,150 @@ class CustomizeForm(Document):
def save_customization(self):
if not self.doc_type:
return
self.set_property_setters()
self.update_custom_fields()
self.set_idx_property_setter()
validate_fields_for_doctype(self.doc_type)
frappe.msgprint("{}: {} {}.".format(_("DocType"), _(self.doc_type), _("updated")))
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()
def set_property_setters(self):
meta = frappe.get_meta(self.doc_type)
# doctype property setters
for property in self.doctype_properties:
if self.get(property) != meta.get(property):
self.make_property_setter(property=property, value=self.get(property),
property_type=self.doctype_properties[property])
for df in self.get("customize_form_fields"):
if df.get("__islocal"):
continue
meta_df = meta.get("fields", {"fieldname": df.fieldname})
if not meta_df or meta_df[0].get("is_custom_field"):
continue
for property in self.docfield_properties:
if df.get(property) != meta_df[0].get(property):
if property == "fieldtype":
self.validate_fieldtype_change(df, meta_df[0].get(property), df.get(property))
self.make_property_setter(property=property, value=df.get(property),
property_type=self.docfield_properties[property], fieldname=df.fieldname)
def update_custom_fields(self):
for df in self.get("customize_form_fields"):
if df.get("__islocal"):
self.add_custom_field(df)
else:
self.update_in_custom_field(df)
self.delete_custom_fields()
def add_custom_field(self, df):
d = frappe.new_doc("Custom Field")
d.dt = self.doc_type
for property in self.docfield_properties:
d.set(property, df.get(property))
d.insert()
df.fieldname = d.fieldname
def update_in_custom_field(self, df):
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")):
return
custom_field = frappe.get_doc("Custom Field", meta_df[0].name)
changed = False
for property in self.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
if changed:
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("customize_form_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 set_idx_property_setter(self):
meta = frappe.get_meta(self.doc_type)
field_order_has_changed = [df.fieldname for df in meta.get("fields")] != \
[d.fieldname for d in self.get("customize_form_fields")]
if field_order_has_changed:
_idx = []
for df in sorted(self.get("customize_form_fields"), key=lambda x: x.idx):
_idx.append(df.fieldname)
self.make_property_setter(property="_idx", value=json.dumps(_idx), property_type="Text")
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
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
})
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.delete_doc("Property Setter", existing_property_setter)
def get_existing_property_value(self, property, 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)
else:
try:
property_value = frappe.db.get_value("DocType", self.doc_type, property)
except Exception, e:
if e.args[0]==1054:
property_value = None
else:
raise
return property_value
def validate_fieldtype_change(self, df, old_value, new_value):
for allowed_changes in self.allowed_fieldtype_change:
if ((old_value in allowed_changes and new_value in allowed_changes)
or (old_value not in allowed_changes and new_value not in allowed_changes)):
continue
else:
frappe.throw("{row} # {num}, {label}: {msg} {allowed_changes}".format(
row=_("Row"), num=df.idx, label=_(df.label), msg=_("Field Type can be one of"),
allowed_changes=", ".join([_(fieldtype) for fieldtype in allowed_changes])))
def reset_to_defaults(self):
if not self.doc_type:
@ -102,208 +239,5 @@ class CustomizeForm(Document):
frappe.db.sql("""delete from `tabProperty Setter` where doc_type=%s""", self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()
def post(self):
"""
Save diff between Customize Form Bean and DocType Bean as property setter entries
"""
if self.doc_type:
from frappe.model import doc
from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype
this_doclist = self
ref_doclist = frappe.get_meta(self.doc_type)
dt_doclist = frappe.get_doc('DocType', self.doc_type)
# get a list of property setter docs
self.idx_dirty = False
diff_list = self.diff(this_doclist, ref_doclist, dt_doclist)
if self.idx_dirty:
self.make_idx_property_setter(this_doclist, diff_list)
self.set_properties(diff_list)
validate_fields_for_doctype(self.doc_type)
frappe.clear_cache(doctype=self.doc_type)
frappe.msgprint("Updated")
def diff(self, new_dl, ref_dl, dt_dl):
"""
Get difference between new_dl doclist and ref_dl doclist
then check how it differs from dt_dl i.e. default doclist
"""
import re
self.defaults = self.get_defaults()
diff_list = []
for new_d in new_dl:
for ref_d in ref_dl:
if ref_d.doctype == 'DocField' and new_d.name == ref_d.name:
for prop in self.docfield_properties:
# do not set forbidden properties like idx
if prop=="idx":
if ref_d.idx != new_d.idx:
self.idx_dirty = True
continue
# check if its custom field:
if ref_d.get("__custom_field"):
# update custom field
if self.has_property_changed(ref_d, new_d, prop):
# using set_value not doc because validations are called
# in the end anyways
frappe.db.set_value("Custom Field", ref_d.name, prop, new_d.get(prop))
else:
d = self.prepare_to_set(prop, new_d, ref_d, dt_dl)
if d: diff_list.append(d)
break
elif ref_d.doctype == 'DocType' and new_d.doctype == 'Customize Form':
for prop in self.doctype_properties:
d = self.prepare_to_set(prop, new_d, ref_d, dt_dl)
if d: diff_list.append(d)
break
return diff_list
def get_defaults(self):
"""
Get fieldtype and default value for properties of a field
"""
df_defaults = frappe.db.sql("""
SELECT fieldname, fieldtype, `default`, label
FROM `tabDocField`
WHERE parent='DocField' or parent='DocType'""", as_dict=1)
defaults = {}
for d in df_defaults:
defaults[d['fieldname']] = d
defaults['idx'] = {'fieldname' : 'idx', 'fieldtype' : 'Int', 'default' : 1, 'label' : 'idx'}
defaults['previous_field'] = {'fieldname' : 'previous_field', 'fieldtype' : 'Data', 'default' : None, 'label' : 'Previous Field'}
return defaults
def has_property_changed(self, ref_d, new_d, prop):
return new_d.get(prop) != ref_d.get(prop) \
and not \
( \
new_d.get(prop) in [None, 0] \
and ref_d.get(prop) in [None, 0] \
) and not \
( \
new_d.get(prop) in [None, ''] \
and ref_d.get(prop) in [None, ''] \
)
def prepare_to_set(self, prop, new_d, ref_d, dt_doclist, delete=0):
"""
Prepares docs of property setter
sets delete property if it is required to be deleted
"""
# Check if property has changed compared to when it was loaded
if self.has_property_changed(ref_d, new_d, prop):
#frappe.msgprint("new: " + str(new_d.get(prop)) + " | old: " + str(ref_d.get(prop)))
# Check if the new property is same as that in original doctype
# If yes, we need to delete the property setter entry
for dt_d in dt_doclist:
if dt_d.name == ref_d.name \
and (new_d.get(prop) == dt_d.get(prop) \
or \
( \
new_d.get(prop) in [None, 0] \
and dt_d.get(prop) in [None, 0] \
) or \
( \
new_d.get(prop) in [None, ''] \
and dt_d.get(prop) in [None, ''] \
)):
delete = 1
break
value = new_d.get(prop)
if prop in self.property_restrictions:
allow_change = False
for restrict_list in self.property_restrictions.get(prop):
if value in restrict_list and \
ref_d.get(prop) in restrict_list:
allow_change = True
break
if not allow_change:
frappe.msgprint("""\
You cannot change '%s' of '%s' from '%s' to '%s'.
%s can only be changed among %s.
<i>Ignoring this change and saving.</i>""" % \
(self.defaults.get(prop, {}).get("label") or prop,
new_d.get("label") or new_d.get("idx"),
ref_d.get(prop), value,
self.defaults.get(prop, {}).get("label") or prop,
" -or- ".join([", ".join(r) for r in \
self.property_restrictions.get(prop)])), raise_exception=True)
return None
# If the above conditions are fulfilled,
# create a property setter doc, but dont save it yet.
d = frappe.get_doc('Property Setter')
d.doctype_or_field = ref_d.doctype=='DocField' and 'DocField' or 'DocType'
d.doc_type = self.doc_type
d.field_name = ref_d.fieldname
d.property = prop
d.value = value
d.property_type = self.defaults[prop]['fieldtype']
#d.default_value = self.defaults[prop]['default']
if delete: d.delete = 1
if d.select_item:
d.select_item = self.remove_forbidden(d.select_item)
# return the property setter doc
return d
else: return None
def make_idx_property_setter(self, doclist, diff_list):
fields = []
doclist.sort(lambda a, b: a.idx < b.idx)
for d in doclist:
if d.doctype=="Customize Form Field":
fields.append(d.fieldname)
d = frappe.get_doc('Property Setter')
d.doctype_or_field = 'DocType'
d.doc_type = self.doc_type
d.property = "_idx"
d.value = json.dumps(fields)
d.property_type = "Text"
diff_list.append(d)
def set_properties(self, ps_doclist):
"""
* Delete a property setter entry
+ if it already exists
+ if marked for deletion
* Save the property setter doc in the list
"""
for d in ps_doclist:
# Delete existing property setter entry
if not d.get("field_name"):
frappe.db.sql("""
DELETE FROM `tabProperty Setter`
WHERE doc_type = %(doc_type)s
AND property = %(property)s""", d.as_dict())
else:
frappe.db.sql("""
DELETE FROM `tabProperty Setter`
WHERE doc_type = %(doc_type)s
AND field_name = %(field_name)s
AND property = %(property)s""", d.as_dict())
# Save the property setter doc if not marked for deletion i.e. delete=0
if not d.delete:
d.insert()

View file

@ -0,0 +1,145 @@
# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals
import frappe, unittest, json
from frappe.test_runner import make_test_records_for_doctype
test_dependencies = ["Custom Field", "Property Setter"]
class TestCustomizeForm(unittest.TestCase):
def setUp(self):
frappe.clear_cache(doctype="User")
def tearDown(self):
frappe.clear_cache(doctype="User")
def get_customize_form(self, doctype=None):
d = frappe.get_doc("Customize Form")
if doctype:
d.doc_type = doctype
d.run_method("fetch_to_customize")
return d
def test_fetch_to_customize(self):
d = self.get_customize_form()
self.assertEquals(d.doc_type, None)
self.assertEquals(len(d.get("customize_form_fields")), 0)
d = self.get_customize_form("Event")
self.assertEquals(d.doc_type, "Event")
self.assertEquals(len(d.get("customize_form_fields")), 30)
d = self.get_customize_form("User")
self.assertEquals(d.doc_type, "User")
self.assertEquals(len(d.get("customize_form_fields")), 53)
self.assertEquals(d.get("customize_form_fields")[-1].fieldname, "test_custom_field")
self.assertEquals(d.get("customize_form_fields", {"fieldname": "location"})[0].in_list_view, 1)
return d
def test_save_customization_idx(self):
d = self.get_customize_form("User")
original_sequence = [df.fieldname for df in d.get("customize_form_fields")]
# move field to last
location_field = d.get("customize_form_fields", {"fieldname": "location"})[0]
d.get("customize_form_fields").remove(location_field)
d.append("customize_form_fields", location_field)
d.run_method("save_customization")
frappe.clear_cache(doctype=d.doc_type)
property_setter_name, _idx = frappe.db.get_value("Property Setter",
{"doc_type": d.doc_type, "property": "_idx"}, ("name", "value"))
self.assertTrue(_idx)
_idx = json.loads(_idx)
for i, df in enumerate(frappe.get_meta(d.doc_type).get("fields")):
self.assertEquals(_idx[i], df.fieldname)
frappe.delete_doc("Property Setter", property_setter_name)
frappe.clear_cache(doctype=d.doc_type)
for i, df in enumerate(frappe.get_meta(d.doc_type).get("fields")):
self.assertEquals(original_sequence[i], df.fieldname)
def test_save_customization_property(self):
d = self.get_customize_form("User")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "allow_copy"}, "value"), None)
d.allow_copy = 1
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "allow_copy"}, "value"), '1')
d.allow_copy = 0
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "allow_copy"}, "value"), None)
def test_save_customization_field_property(self):
d = self.get_customize_form("User")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "reqd", "field_name": "location"}, "value"), None)
location_field = d.get("customize_form_fields", {"fieldname": "location"})[0]
location_field.reqd = 1
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "reqd", "field_name": "location"}, "value"), '1')
location_field = d.get("customize_form_fields", {"fieldname": "location"})[0]
location_field.reqd = 0
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Property Setter",
{"doc_type": "User", "property": "reqd", "field_name": "location"}, "value"), '0')
def test_save_customization_custom_field_property(self):
d = self.get_customize_form("User")
self.assertEquals(frappe.db.get_value("Custom Field", "User-test_custom_field", "reqd"), None)
custom_field = d.get("customize_form_fields", {"fieldname": "test_custom_field"})[0]
custom_field.reqd = 1
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Custom Field", "User-test_custom_field", "reqd"), 1)
custom_field = d.get("customize_form_fields", {"is_custom_field": True})[0]
custom_field.reqd = 0
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Custom Field", "User-test_custom_field", "reqd"), 0)
def test_save_customization_new_field(self):
d = self.get_customize_form("User")
d.append("customize_form_fields", {
"label": "Test Add Custom Field Via Customize Form",
"fieldtype": "Data",
"__islocal": 1
})
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Custom Field",
"User-test_add_custom_field_via_customize_form", "fieldtype"), "Data")
frappe.delete_doc("Custom Field", "User-test_add_custom_field_via_customize_form")
self.assertEquals(frappe.db.get_value("Custom Field",
"User-test_add_custom_field_via_customize_form"), None)
def test_save_customization_remove_field(self):
d = self.get_customize_form("User")
custom_field = d.get("customize_form_fields", {"fieldname": "test_custom_field"})[0]
d.get("customize_form_fields").remove(custom_field)
d.run_method("save_customization")
self.assertEquals(frappe.db.get_value("Custom Field", custom_field.name), None)
frappe.local.test_objects["Custom Field"] = []
make_test_records_for_doctype("Custom Field")
def test_reset_to_defaults(self):
d = frappe.get_doc("Customize Form")
d.doc_type = "User"
d.run_method('reset_to_defaults')
self.assertEquals(d.get("customize_form_fields", {"fieldname": "location"})[0].in_list_view, None)
frappe.local.test_objects["Property Setter"] = []
make_test_records_for_doctype("Property Setter")

View file

@ -45,7 +45,7 @@
"label": "DocType",
"options": "DocType",
"permlevel": 0,
"reqd": 0,
"reqd": 1,
"search_index": 1
},
{

View file

@ -0,0 +1,10 @@
[
{
"doc_type": "User",
"doctype_or_field": "DocField",
"field_name": "location",
"property": "in_list_view",
"property_type": "Check",
"value": "1"
}
]

View file

@ -23,6 +23,7 @@ class Meta(Document):
_metaclass = True
default_fields = default_fields[1:]
special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def")
def __init__(self, doctype):
self._fields = {}
super(Meta, self).__init__("DocType", doctype)
@ -98,7 +99,8 @@ class Meta(Document):
def add_custom_fields(self):
try:
self.extend("fields", frappe.db.sql("""SELECT * FROM `tabCustom Field`
WHERE dt = %s AND docstatus < 2""", (self.name,), as_dict=1))
WHERE dt = %s AND docstatus < 2""", (self.name,), as_dict=1,
update={"is_custom_field": True}))
except Exception, e:
if e.args[0]==1146:
return

View file

@ -133,8 +133,14 @@ def make_test_records_for_doctype(doctype, verbose=0):
elif hasattr(test_module, "test_records"):
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_module.test_records, verbose)
elif verbose:
print_mandatory_fields(doctype)
else:
test_records = frappe.get_test_records(doctype)
if test_records:
frappe.local.test_objects[doctype] += make_test_objects(doctype, test_records, verbose)
elif verbose:
print_mandatory_fields(doctype)
def make_test_objects(doctype, test_records, verbose=None):