* [refactor] a better set-only-once implementation with child tables * [refactor] document.is_child_table_same(fieldname) * [refactor] tests * [refactor] tests * [test] catch timeout reason * [minor] edit in full page more prominent * [minor] testsversion-14
@@ -3,6 +3,7 @@ | |||||
from __future__ import unicode_literals | from __future__ import unicode_literals | ||||
from six import iteritems, string_types | from six import iteritems, string_types | ||||
import datetime | |||||
import frappe, sys | import frappe, sys | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, | from frappe.utils import (cint, flt, now, cstr, strip_html, getdate, get_datetime, to_timedelta, | ||||
@@ -181,7 +182,7 @@ class BaseDocument(object): | |||||
return value | return value | ||||
def get_valid_dict(self, sanitize=True): | |||||
def get_valid_dict(self, sanitize=True, convert_dates_to_str=False): | |||||
d = frappe._dict() | d = frappe._dict() | ||||
for fieldname in self.meta.get_valid_columns(): | for fieldname in self.meta.get_valid_columns(): | ||||
d[fieldname] = self.get(fieldname) | d[fieldname] = self.get(fieldname) | ||||
@@ -215,6 +216,9 @@ class BaseDocument(object): | |||||
if isinstance(d[fieldname], list) and df.fieldtype != 'Table': | if isinstance(d[fieldname], list) and df.fieldtype != 'Table': | ||||
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label))) | frappe.throw(_('Value for {0} cannot be a list').format(_(df.label))) | ||||
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time)): | |||||
d[fieldname] = str(d[fieldname]) | |||||
return d | return d | ||||
def init_valid_columns(self): | def init_valid_columns(self): | ||||
@@ -244,8 +248,8 @@ class BaseDocument(object): | |||||
def is_new(self): | def is_new(self): | ||||
return self.get("__islocal") | return self.get("__islocal") | ||||
def as_dict(self, no_nulls=False, no_default_fields=False): | |||||
doc = self.get_valid_dict() | |||||
def as_dict(self, no_nulls=False, no_default_fields=False, convert_dates_to_str=False): | |||||
doc = self.get_valid_dict(convert_dates_to_str=convert_dates_to_str) | |||||
doc["doctype"] = self.doctype | doc["doctype"] = self.doctype | ||||
for df in self.meta.get_table_fields(): | for df in self.meta.get_table_fields(): | ||||
children = self.get(df.fieldname) or [] | children = self.get(df.fieldname) or [] | ||||
@@ -357,7 +357,10 @@ class Document(BaseDocument): | |||||
def get_doc_before_save(self): | def get_doc_before_save(self): | ||||
if not getattr(self, '_doc_before_save', None): | if not getattr(self, '_doc_before_save', None): | ||||
self._doc_before_save = frappe.get_doc(self.doctype, self.name) | |||||
try: | |||||
self._doc_before_save = frappe.get_doc(self.doctype, self.name) | |||||
except frappe.DoesNotExistError: | |||||
self._doc_before_save = None | |||||
return self._doc_before_save | return self._doc_before_save | ||||
def set_new_name(self, force=False): | def set_new_name(self, force=False): | ||||
@@ -435,7 +438,6 @@ class Document(BaseDocument): | |||||
def _validate(self): | def _validate(self): | ||||
self._validate_mandatory() | self._validate_mandatory() | ||||
self._validate_selects() | self._validate_selects() | ||||
self._validate_constants() | |||||
self._validate_length() | self._validate_length() | ||||
self._extract_images_from_text_editor() | self._extract_images_from_text_editor() | ||||
self._sanitize_content() | self._sanitize_content() | ||||
@@ -444,17 +446,67 @@ class Document(BaseDocument): | |||||
children = self.get_all_children() | children = self.get_all_children() | ||||
for d in children: | for d in children: | ||||
d._validate_selects() | d._validate_selects() | ||||
d._validate_constants() | |||||
d._validate_length() | d._validate_length() | ||||
d._extract_images_from_text_editor() | d._extract_images_from_text_editor() | ||||
d._sanitize_content() | d._sanitize_content() | ||||
d._save_passwords() | d._save_passwords() | ||||
self.validate_set_only_once() | |||||
if self.is_new(): | if self.is_new(): | ||||
# don't set fields like _assign, _comments for new doc | # don't set fields like _assign, _comments for new doc | ||||
for fieldname in optional_fields: | for fieldname in optional_fields: | ||||
self.set(fieldname, None) | self.set(fieldname, None) | ||||
def validate_set_only_once(self): | |||||
'''Validate that fields are not changed if not in insert''' | |||||
set_only_once_fields = self.meta.get_set_only_once_fields() | |||||
if set_only_once_fields and self._doc_before_save: | |||||
# document exists before saving | |||||
for field in set_only_once_fields: | |||||
fail = False | |||||
value = self.get(field.fieldname) | |||||
original_value = self._doc_before_save.get(field.fieldname) | |||||
if field.fieldtype=='Table': | |||||
fail = not self.is_child_table_same(field.fieldname) | |||||
elif field.fieldtype in ('Date', 'Datetime', 'Time'): | |||||
fail = str(value) != str(original_value) | |||||
else: | |||||
fail = value != original_value | |||||
if fail: | |||||
frappe.throw(_("Value cannot be changed for {0}").format(self.meta.get_label(field.fieldname)), | |||||
frappe.CannotChangeConstantError) | |||||
return False | |||||
def is_child_table_same(self, fieldname): | |||||
'''Validate child table is same as original table before saving''' | |||||
value = self.get(fieldname) | |||||
original_value = self._doc_before_save.get(fieldname) | |||||
same = True | |||||
if len(original_value) != len(value): | |||||
same = False | |||||
else: | |||||
# check all child entries | |||||
for i, d in enumerate(original_value): | |||||
new_child = value[i].as_dict(convert_dates_to_str = True) | |||||
original_child = d.as_dict(convert_dates_to_str = True) | |||||
# all fields must be same other than modified and modified_by | |||||
for key in ('modified', 'modified_by', 'creation'): | |||||
del new_child[key] | |||||
del original_child[key] | |||||
if original_child != new_child: | |||||
same = False | |||||
break | |||||
return same | |||||
def apply_fieldlevel_read_permissions(self): | def apply_fieldlevel_read_permissions(self): | ||||
'''Remove values the user is not allowed to read (called when loading in desk)''' | '''Remove values the user is not allowed to read (called when loading in desk)''' | ||||
has_higher_permlevel = False | has_higher_permlevel = False | ||||
@@ -798,10 +850,7 @@ class Document(BaseDocument): | |||||
self.set_title_field() | self.set_title_field() | ||||
self.reset_seen() | self.reset_seen() | ||||
self._doc_before_save = None | |||||
if not self.is_new() and getattr(self.meta, 'track_changes', False): | |||||
self.get_doc_before_save() | |||||
self.load_doc_before_save() | |||||
if self.flags.ignore_validate: | if self.flags.ignore_validate: | ||||
return | return | ||||
@@ -816,6 +865,15 @@ class Document(BaseDocument): | |||||
elif self._action=="update_after_submit": | elif self._action=="update_after_submit": | ||||
self.run_method("before_update_after_submit") | self.run_method("before_update_after_submit") | ||||
def load_doc_before_save(self): | |||||
'''Save load document from db before saving''' | |||||
self._doc_before_save = None | |||||
if not (self.is_new() | |||||
and (getattr(self.meta, 'track_changes', False) | |||||
or self.meta.get_set_only_once_fields())): | |||||
self.get_doc_before_save() | |||||
def run_post_save_methods(self): | def run_post_save_methods(self): | ||||
"""Run standard methods after `INSERT` or `UPDATE`. Standard Methods are: | """Run standard methods after `INSERT` or `UPDATE`. Standard Methods are: | ||||
@@ -96,6 +96,12 @@ class Meta(Document): | |||||
def get_image_fields(self): | def get_image_fields(self): | ||||
return self.get("fields", {"fieldtype": "Attach Image"}) | return self.get("fields", {"fieldtype": "Attach Image"}) | ||||
def get_set_only_once_fields(self): | |||||
'''Return fields with `set_only_once` set''' | |||||
if not hasattr(self, "_set_only_once_fields"): | |||||
self._set_only_once_fields = self.get("fields", {"set_only_once": 1}) | |||||
return self._set_only_once_fields | |||||
def get_table_fields(self): | def get_table_fields(self): | ||||
if not hasattr(self, "_table_fields"): | if not hasattr(self, "_table_fields"): | ||||
if self.name!="DocType": | if self.name!="DocType": | ||||
@@ -152,7 +152,7 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||||
if(me.after_insert) { | if(me.after_insert) { | ||||
me.after_insert(me.dialog.doc); | me.after_insert(me.dialog.doc); | ||||
} else { | } else { | ||||
me.open_from_if_not_list(); | |||||
me.open_form_if_not_list(); | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
@@ -168,11 +168,13 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||||
}); | }); | ||||
}, | }, | ||||
open_from_if_not_list: function() { | |||||
open_form_if_not_list: function() { | |||||
let route = frappe.get_route(); | let route = frappe.get_route(); | ||||
let doc = this.dialog.doc; | let doc = this.dialog.doc; | ||||
if(route && !(route[0]==='List' && route[1]===doc.doctype)) { | |||||
frappe.set_route('Form', doc.doctype, doc.name); | |||||
if (route && !(route[0]==='List' && route[1]===doc.doctype)) { | |||||
frappe.run_serially([ | |||||
() => frappe.set_route('Form', doc.doctype, doc.name) | |||||
]); | |||||
} | } | ||||
}, | }, | ||||
@@ -199,8 +201,8 @@ frappe.ui.form.QuickEntryForm = Class.extend({ | |||||
render_edit_in_full_page_link: function(){ | render_edit_in_full_page_link: function(){ | ||||
var me = this; | var me = this; | ||||
var $link = $('<div class="text-muted small" style="padding-left: 10px; padding-top: 15px;">' + | |||||
__("Ctrl+enter to save") + ' | <a class="edit-full">' + __("Edit in full page") + '</a></div>').appendTo(this.dialog.body); | |||||
var $link = $('<div style="padding-left: 7px; padding-top: 15px; padding-bottom: 10px;">' + | |||||
'<button class="edit-full btn-default btn-sm">' + __("Edit in full page") + '</button></div>').appendTo(this.dialog.body); | |||||
$link.find('.edit-full').on('click', function() { | $link.find('.edit-full').on('click', function() { | ||||
// edit in form | // edit in form | ||||
@@ -56,11 +56,6 @@ class TestPermissions(unittest.TestCase): | |||||
clear_user_permissions_for_doctype("Contact") | clear_user_permissions_for_doctype("Contact") | ||||
clear_user_permissions_for_doctype("Salutation") | clear_user_permissions_for_doctype("Salutation") | ||||
reset('Blogger') | |||||
reset('Blog Post') | |||||
reset('Contact') | |||||
reset('Salutation') | |||||
self.set_ignore_user_permissions_if_missing(0) | self.set_ignore_user_permissions_if_missing(0) | ||||
@staticmethod | @staticmethod | ||||
@@ -197,12 +192,42 @@ class TestPermissions(unittest.TestCase): | |||||
def test_set_only_once(self): | def test_set_only_once(self): | ||||
blog_post = frappe.get_meta("Blog Post") | blog_post = frappe.get_meta("Blog Post") | ||||
blog_post.get_field("title").set_only_once = 1 | |||||
doc = frappe.get_doc("Blog Post", "-test-blog-post-1") | doc = frappe.get_doc("Blog Post", "-test-blog-post-1") | ||||
doc.db_set('title', 'Old') | |||||
blog_post.get_field("title").set_only_once = 1 | |||||
doc.title = "New" | doc.title = "New" | ||||
self.assertRaises(frappe.CannotChangeConstantError, doc.save) | self.assertRaises(frappe.CannotChangeConstantError, doc.save) | ||||
blog_post.get_field("title").set_only_once = 0 | blog_post.get_field("title").set_only_once = 0 | ||||
def test_set_only_once_child_table_rows(self): | |||||
doctype_meta = frappe.get_meta("DocType") | |||||
doctype_meta.get_field("fields").set_only_once = 1 | |||||
doc = frappe.get_doc("DocType", "Blog Post") | |||||
# remove last one | |||||
doc.fields = doc.fields[:-1] | |||||
self.assertRaises(frappe.CannotChangeConstantError, doc.save) | |||||
frappe.clear_cache(doctype='DocType') | |||||
def test_set_only_once_child_table_row_value(self): | |||||
doctype_meta = frappe.get_meta("DocType") | |||||
doctype_meta.get_field("fields").set_only_once = 1 | |||||
doc = frappe.get_doc("DocType", "Blog Post") | |||||
# change one property from the child table | |||||
doc.fields[-1].fieldtype = 'HTML' | |||||
self.assertRaises(frappe.CannotChangeConstantError, doc.save) | |||||
frappe.clear_cache(doctype='DocType') | |||||
def test_set_only_once_child_table_okay(self): | |||||
doctype_meta = frappe.get_meta("DocType") | |||||
doctype_meta.get_field("fields").set_only_once = 1 | |||||
doc = frappe.get_doc("DocType", "Blog Post") | |||||
doc.load_doc_before_save() | |||||
self.assertFalse(doc.validate_set_only_once()) | |||||
frappe.clear_cache(doctype='DocType') | |||||
def test_user_permission_doctypes(self): | def test_user_permission_doctypes(self): | ||||
add_user_permission("Blog Category", "_Test Blog Category 1", | add_user_permission("Blog Category", "_Test Blog Category 1", | ||||
"test2@example.com") | "test2@example.com") | ||||
@@ -18,6 +18,7 @@ class TestTestRunner(unittest.TestCase): | |||||
continue | continue | ||||
timeout = 60 | timeout = 60 | ||||
passed = False | |||||
if '#' in test: | if '#' in test: | ||||
test, comment = test.split('#') | test, comment = test.split('#') | ||||
test = test.strip() | test = test.strip() | ||||
@@ -30,17 +31,19 @@ class TestTestRunner(unittest.TestCase): | |||||
frappe.db.commit() | frappe.db.commit() | ||||
driver.refresh() | driver.refresh() | ||||
driver.set_route('Form', 'Test Runner') | driver.set_route('Form', 'Test Runner') | ||||
driver.click_primary_action() | |||||
driver.wait_for('#frappe-qunit-done', timeout=timeout) | |||||
try: | |||||
driver.click_primary_action() | |||||
driver.wait_for('#frappe-qunit-done', timeout=timeout) | |||||
console = driver.get_console() | |||||
passed = 'Tests Passed' in console | |||||
if frappe.flags.tests_verbose or not passed: | |||||
for line in console: | |||||
print(line) | |||||
print('-' * 40) | |||||
self.assertTrue(passed) | |||||
time.sleep(1) | |||||
console = driver.get_console() | |||||
passed = 'Tests Passed' in console | |||||
finally: | |||||
if frappe.flags.tests_verbose or not passed: | |||||
for line in console: | |||||
print(line) | |||||
print('-' * 40) | |||||
self.assertTrue(passed) | |||||
time.sleep(1) | |||||
frappe.db.set_default('in_selenium', None) | frappe.db.set_default('in_selenium', None) | ||||
driver.close() | driver.close() | ||||