[fix] Customize Title Field using Customize Form. Validate title field customization. Fixes frappe/erpnext#4177version-14
@@ -3,6 +3,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import re | |||||
import frappe | import frappe | ||||
from frappe import _ | from frappe import _ | ||||
@@ -12,6 +13,9 @@ from frappe.model.document import Document | |||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter | from frappe.custom.doctype.property_setter.property_setter import make_property_setter | ||||
from frappe.desk.notifications import delete_notification_count_for | from frappe.desk.notifications import delete_notification_count_for | ||||
from frappe.modules import make_boilerplate | from frappe.modules import make_boilerplate | ||||
from frappe.model.db_schema import validate_column_name | |||||
class InvalidFieldNameError(frappe.ValidationError): pass | |||||
form_grid_templates = { | form_grid_templates = { | ||||
"fields": "templates/form_grid/fields.html" | "fields": "templates/form_grid/fields.html" | ||||
@@ -36,7 +40,6 @@ class DocType(Document): | |||||
frappe.throw(_("{0} not allowed in name").format(c)) | frappe.throw(_("{0} not allowed in name").format(c)) | ||||
self.validate_series() | self.validate_series() | ||||
self.scrub_field_names() | self.scrub_field_names() | ||||
self.validate_title_field() | |||||
self.validate_document_type() | self.validate_document_type() | ||||
validate_fields(self) | validate_fields(self) | ||||
@@ -77,13 +80,6 @@ class DocType(Document): | |||||
else: | else: | ||||
d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) | ||||
def validate_title_field(self): | |||||
"""Throw exception if `title_field` is not a valid field.""" | |||||
if self.title_field and \ | |||||
self.title_field not in [d.fieldname for d in self.get("fields")]: | |||||
frappe.throw(_("Title field must be a valid fieldname")) | |||||
def validate_series(self, autoname=None, name=None): | def validate_series(self, autoname=None, name=None): | ||||
"""Validate if `autoname` property is correctly set.""" | """Validate if `autoname` property is correctly set.""" | ||||
if not autoname: autoname = self.autoname | if not autoname: autoname = self.autoname | ||||
@@ -207,7 +203,7 @@ class DocType(Document): | |||||
return max_idx and max_idx[0][0] or 0 | return max_idx and max_idx[0][0] or 0 | ||||
def validate_fields_for_doctype(doctype): | def validate_fields_for_doctype(doctype): | ||||
validate_fields(frappe.get_meta(doctype)) | |||||
validate_fields(frappe.get_meta(doctype, cached=False)) | |||||
# this is separate because it is also called via custom field | # this is separate because it is also called via custom field | ||||
def validate_fields(meta): | def validate_fields(meta): | ||||
@@ -223,13 +219,11 @@ def validate_fields(meta): | |||||
9. Precision is set in numeric fields and is between 1 & 6. | 9. Precision is set in numeric fields and is between 1 & 6. | ||||
10. Fold is not at the end (if set). | 10. Fold is not at the end (if set). | ||||
11. `search_fields` are valid. | 11. `search_fields` are valid. | ||||
12. `title_field` and title field pattern are valid. | |||||
:param meta: `frappe.model.meta.Meta` object to check.""" | :param meta: `frappe.model.meta.Meta` object to check.""" | ||||
def check_illegal_characters(fieldname): | def check_illegal_characters(fieldname): | ||||
for c in ['.', ',', ' ', '-', '&', '%', '=', '"', "'", '*', '$', | |||||
'(', ')', '[', ']', '/']: | |||||
if c in fieldname: | |||||
frappe.throw(_("{0} not allowed in fieldname {1}").format(c, fieldname)) | |||||
validate_column_name(fieldname) | |||||
def check_unique_fieldname(fieldname): | def check_unique_fieldname(fieldname): | ||||
duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)) | duplicates = filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)) | ||||
@@ -305,6 +299,7 @@ def validate_fields(meta): | |||||
frappe.throw(_("Fold can not be at the end of the form")) | frappe.throw(_("Fold can not be at the end of the form")) | ||||
def check_search_fields(meta): | def check_search_fields(meta): | ||||
"""Throw exception if `search_fields` don't contain valid fields.""" | |||||
if not meta.search_fields: | if not meta.search_fields: | ||||
return | return | ||||
@@ -314,7 +309,32 @@ def validate_fields(meta): | |||||
if fieldname not in fieldname_list: | if fieldname not in fieldname_list: | ||||
frappe.throw(_("Search Fields should contain valid fieldnames")) | frappe.throw(_("Search Fields should contain valid fieldnames")) | ||||
def check_title_field(meta): | |||||
"""Throw exception if `title_field` isn't a valid fieldname.""" | |||||
if not meta.title_field: | |||||
return | |||||
fieldname_list = [d.fieldname for d in fields] | |||||
if meta.title_field not in fieldname_list: | |||||
frappe.throw(_("Title field must be a valid fieldname"), InvalidFieldNameError) | |||||
def _validate_title_field_pattern(pattern): | |||||
if not pattern: | |||||
return | |||||
for fieldname in re.findall("{(.*?)}", pattern, re.UNICODE): | |||||
if fieldname.startswith("{"): | |||||
# edge case when double curlies are used for escape | |||||
continue | |||||
if fieldname not in fieldname_list: | |||||
frappe.throw(_("{{{0}}} is not a valid fieldname pattern. It should be {{field_name}}.").format(fieldname), | |||||
InvalidFieldNameError) | |||||
df = meta.get_field(meta.title_field) | |||||
_validate_title_field_pattern(df.options) | |||||
_validate_title_field_pattern(df.default) | |||||
fields = meta.get("fields") | fields = meta.get("fields") | ||||
for d in fields: | for d in fields: | ||||
@@ -334,6 +354,7 @@ def validate_fields(meta): | |||||
check_fold(fields) | check_fold(fields) | ||||
check_search_fields(meta) | check_search_fields(meta) | ||||
check_title_field(meta) | |||||
def validate_permissions_for_doctype(doctype, for_remove=False): | def validate_permissions_for_doctype(doctype, for_remove=False): | ||||
"""Validates if permissions are set correctly.""" | """Validates if permissions are set correctly.""" | ||||
@@ -15,6 +15,12 @@ class TestFile(unittest.TestCase): | |||||
self.delete_test_data() | self.delete_test_data() | ||||
self.upload_file() | self.upload_file() | ||||
def tearDown(self): | |||||
try: | |||||
frappe.get_doc("File", {"file_name": "file_copy.txt"}).delete() | |||||
except frappe.DoesNotExistError: | |||||
pass | |||||
def delete_test_data(self): | def delete_test_data(self): | ||||
for f in frappe.db.sql('''select name, file_name from tabFile where | for f in frappe.db.sql('''select name, file_name from tabFile where | ||||
is_home_folder = 0 and is_attachments_folder = 0 order by rgt-lft asc'''): | is_home_folder = 0 and is_attachments_folder = 0 order by rgt-lft asc'''): | ||||
@@ -78,14 +78,35 @@ | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"description": "Fields separated by comma (,) will be included in the \"Search By\" list of Search dialog box", | |||||
"fieldname": "search_fields", | |||||
"fieldtype": "Data", | |||||
"depends_on": "", | |||||
"fieldname": "max_attachments", | |||||
"fieldtype": "Int", | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_list_view": 1, | |||||
"label": "Search Fields", | |||||
"in_list_view": 0, | |||||
"label": "Max Attachments", | |||||
"no_copy": 0, | |||||
"permlevel": 0, | |||||
"print_hide": 0, | |||||
"read_only": 0, | |||||
"report_hide": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"set_only_once": 0, | |||||
"unique": 0 | |||||
}, | |||||
{ | |||||
"allow_on_submit": 0, | |||||
"bold": 0, | |||||
"collapsible": 0, | |||||
"fieldname": "allow_copy", | |||||
"fieldtype": "Check", | |||||
"hidden": 0, | |||||
"ignore_user_permissions": 0, | |||||
"in_filter": 0, | |||||
"in_list_view": 0, | |||||
"label": "Hide Copy", | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"permlevel": 0, | "permlevel": 0, | ||||
"print_hide": 0, | "print_hide": 0, | ||||
@@ -121,16 +142,17 @@ | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"depends_on": "", | |||||
"fieldname": "max_attachments", | |||||
"fieldtype": "Int", | |||||
"description": "Use this fieldname to generate title", | |||||
"fieldname": "title_field", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"label": "Max Attachments", | |||||
"label": "Title Field", | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"permlevel": 0, | "permlevel": 0, | ||||
"precision": "", | |||||
"print_hide": 0, | "print_hide": 0, | ||||
"read_only": 0, | "read_only": 0, | ||||
"report_hide": 0, | "report_hide": 0, | ||||
@@ -143,13 +165,14 @@ | |||||
"allow_on_submit": 0, | "allow_on_submit": 0, | ||||
"bold": 0, | "bold": 0, | ||||
"collapsible": 0, | "collapsible": 0, | ||||
"fieldname": "allow_copy", | |||||
"fieldtype": "Check", | |||||
"description": "Fields separated by comma (,) will be included in the \"Search By\" list of Search dialog box", | |||||
"fieldname": "search_fields", | |||||
"fieldtype": "Data", | |||||
"hidden": 0, | "hidden": 0, | ||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_list_view": 0, | |||||
"label": "Hide Copy", | |||||
"in_list_view": 1, | |||||
"label": "Search Fields", | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"permlevel": 0, | "permlevel": 0, | ||||
"print_hide": 0, | "print_hide": 0, | ||||
@@ -234,7 +257,7 @@ | |||||
"ignore_user_permissions": 0, | "ignore_user_permissions": 0, | ||||
"in_filter": 0, | "in_filter": 0, | ||||
"in_list_view": 0, | "in_list_view": 0, | ||||
"label": "Sort Order", | |||||
"label": "Sort Order", | |||||
"no_copy": 0, | "no_copy": 0, | ||||
"options": "ASC\nDESC", | "options": "ASC\nDESC", | ||||
"permlevel": 0, | "permlevel": 0, | ||||
@@ -301,7 +324,7 @@ | |||||
"is_submittable": 0, | "is_submittable": 0, | ||||
"issingle": 1, | "issingle": 1, | ||||
"istable": 0, | "istable": 0, | ||||
"modified": "2015-10-02 07:17:18.939161", | |||||
"modified": "2015-10-21 08:11:19.151364", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Customize Form", | "name": "Customize Form", | ||||
@@ -16,6 +16,7 @@ from frappe.core.doctype.doctype.doctype import validate_fields_for_doctype | |||||
class CustomizeForm(Document): | class CustomizeForm(Document): | ||||
doctype_properties = { | doctype_properties = { | ||||
'search_fields': 'Data', | 'search_fields': 'Data', | ||||
'title_field': 'Data', | |||||
'sort_field': 'Data', | 'sort_field': 'Data', | ||||
'sort_order': 'Data', | 'sort_order': 'Data', | ||||
'default_print_format': 'Data', | 'default_print_format': 'Data', | ||||
@@ -4,10 +4,12 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
import frappe, unittest, json | import frappe, unittest, json | ||||
from frappe.test_runner import make_test_records_for_doctype | from frappe.test_runner import make_test_records_for_doctype | ||||
from frappe.core.doctype.doctype.doctype import InvalidFieldNameError | |||||
test_dependencies = ["Custom Field", "Property Setter"] | test_dependencies = ["Custom Field", "Property Setter"] | ||||
class TestCustomizeForm(unittest.TestCase): | class TestCustomizeForm(unittest.TestCase): | ||||
def insert_custom_field(self): | def insert_custom_field(self): | ||||
frappe.delete_doc_if_exists("Custom Field", "User-test_custom_field") | |||||
frappe.get_doc({ | frappe.get_doc({ | ||||
"doctype": "Custom Field", | "doctype": "Custom Field", | ||||
"dt": "User", | "dt": "User", | ||||
@@ -174,3 +176,29 @@ class TestCustomizeForm(unittest.TestCase): | |||||
# allow for custom field | # allow for custom field | ||||
self.assertEquals(d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1) | self.assertEquals(d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1) | ||||
def test_title_field_pattern(self): | |||||
d = self.get_customize_form("Web Form") | |||||
df = d.get("fields", {"fieldname": "title"})[0] | |||||
# invalid fieldname | |||||
df.options = """{doc_type} - {introduction_test}""" | |||||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | |||||
# space in formatter | |||||
df.options = """{doc_type} - {introduction text}""" | |||||
self.assertRaises(InvalidFieldNameError, d.run_method, "save_customization") | |||||
# valid fieldname | |||||
df.options = """{doc_type} - {introduction_text}""" | |||||
d.run_method("save_customization") | |||||
# valid fieldname with escaped curlies | |||||
df.options = """{{ {doc_type} }} - {introduction_text}""" | |||||
d.run_method("save_customization") | |||||
# undo | |||||
df.options = None | |||||
d.run_method("save_customization") | |||||
@@ -58,6 +58,11 @@ class TestEmailAccount(unittest.TestCase): | |||||
attachments = get_attachments(comm.doctype, comm.name) | attachments = get_attachments(comm.doctype, comm.name) | ||||
self.assertTrue("erpnext-conf-14.png" in [f.file_name for f in attachments]) | self.assertTrue("erpnext-conf-14.png" in [f.file_name for f in attachments]) | ||||
# cleanup | |||||
existing_file = frappe.get_doc({'doctype': 'File', 'file_name': 'erpnext-conf-14.png'}) | |||||
frappe.delete_doc("File", existing_file.name) | |||||
delete_file_from_filesystem(existing_file) | |||||
def test_outgoing(self): | def test_outgoing(self): | ||||
frappe.flags.sent_mail = None | frappe.flags.sent_mail = None | ||||
make(subject = "test-mail-000", content="test mail 000", recipients="test_receiver@example.com", | make(subject = "test-mail-000", content="test mail 000", recipients="test_receiver@example.com", | ||||
@@ -164,9 +164,10 @@ def save_file(fname, content, dt, dn, folder=None, decode=False): | |||||
f = frappe.get_doc(file_data) | f = frappe.get_doc(file_data) | ||||
f.flags.ignore_permissions = True | f.flags.ignore_permissions = True | ||||
try: | try: | ||||
f.insert(); | |||||
f.insert() | |||||
except frappe.DuplicateEntryError: | except frappe.DuplicateEntryError: | ||||
return frappe.get_doc("File", f.duplicate_entry) | return frappe.get_doc("File", f.duplicate_entry) | ||||
return f | return f | ||||
def get_file_data_from_hash(content_hash): | def get_file_data_from_hash(content_hash): | ||||