diff --git a/.eslintrc b/.eslintrc index 7e469f7672..e79571f556 100644 --- a/.eslintrc +++ b/.eslintrc @@ -78,6 +78,7 @@ "has_common": true, "has_words": true, "validate_email": true, + "validate_phone": true, "get_number_format": true, "format_number": true, "format_currency": true, diff --git a/frappe/api.py b/frappe/api.py index 1b1861b78f..6655ebc4d8 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -81,10 +81,8 @@ def handle(): frappe.local.response.update({"data": doc}) if frappe.local.request.method=="PUT": - if frappe.local.form_dict.data is None: - data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) - else: - data = json.loads(frappe.local.form_dict.data) + data = get_request_form_data() + doc = frappe.get_doc(doctype, name) if "flags" in data: @@ -123,10 +121,7 @@ def handle(): }) if frappe.local.request.method == "POST": - if frappe.local.form_dict.data is None: - data = json.loads(frappe.safe_decode(frappe.local.request.get_data())) - else: - data = json.loads(frappe.local.form_dict.data) + data = get_request_form_data() data.update({ "doctype": doctype }) @@ -142,6 +137,13 @@ def handle(): return build_response("json") +def get_request_form_data(): + if frappe.local.form_dict.data is None: + data = frappe.safe_decode(frappe.local.request.get_data()) + else: + data = frappe.local.form_dict.data + + return frappe.parse_json(data) def validate_oauth(): """ authentication using oauth """ diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index b9239dc1f6..82311b19d4 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -69,23 +69,25 @@ class Contact(Document): return True def add_email(self, email_id, is_primary=0, autosave=False): - self.append("email_ids", { - "email_id": email_id, - "is_primary": is_primary - }) + if not frappe.db.exists("Contact Email", {"email_id": email_id, "parent": self.name}): + self.append("email_ids", { + "email_id": email_id, + "is_primary": is_primary + }) - if autosave: - self.save(ignore_permissions=True) + if autosave: + self.save(ignore_permissions=True) def add_phone(self, phone, is_primary_phone=0, is_primary_mobile_no=0, autosave=False): - self.append("phone_nos", { - "phone": phone, - "is_primary_phone": is_primary_phone, - "is_primary_mobile_no": is_primary_mobile_no - }) + if not frappe.db.exists("Contact Phone", {"phone": phone, "parent": self.name}): + self.append("phone_nos", { + "phone": phone, + "is_primary_phone": is_primary_phone, + "is_primary_mobile_no": is_primary_mobile_no + }) - if autosave: - self.save(ignore_permissions=True) + if autosave: + self.save(ignore_permissions=True) def set_primary_email(self): if not self.email_ids: diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index 5e34804b93..ae6fb164ec 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -1,9 +1,11 @@ { + "actions": [], "allow_import": 1, "creation": "2013-01-29 10:47:14", "description": "Keeps track of all communications", "doctype": "DocType", "document_type": "Setup", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "subject", @@ -383,7 +385,8 @@ ], "icon": "fa fa-comment", "idx": 1, - "modified": "2019-10-09 14:22:27.664645", + "links": [], + "modified": "2019-12-27 14:44:04.880373", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -430,8 +433,10 @@ } ], "search_fields": "subject", + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "subject", "title_field": "subject", "track_changes": 1, "track_seen": 1 diff --git a/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html b/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html index 3cc8ce2c2d..66fe744830 100644 --- a/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html +++ b/frappe/core/doctype/doctype/boilerplate/templates/controller_row.html @@ -1,4 +1,4 @@
- {{{{ title }}}} + {{{{ doc.title or doc.name }}}}
- \ No newline at end of file + diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index 9a19185cfc..b3469abf29 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -53,7 +53,7 @@ frappe.ui.form.on('DocType', { frm.events.autoname(frm); }, - autoname(frm) { + autoname: function(frm) { frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt'); } }) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 4e3f2fd84a..379ea227cb 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -54,6 +54,10 @@ "color", "show_preview_popup", "show_name_in_global_search", + "email_settings_sb", + "email_append_to", + "sender_field", + "subject_field", "sb2", "permissions", "restrict_to_domain", @@ -488,11 +492,37 @@ "fieldtype": "Table", "label": "Links", "options": "DocType Link" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "collapsible": 1, + "fieldname": "email_settings_sb", + "fieldtype": "Section Break", + "label": "Email Settings" } ], "icon": "fa fa-bolt", "idx": 6, - "modified": "2019-11-25 17:24:03.690192", + "links": [], + "modified": "2020-03-27 14:51:44.581128", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index da1b184cc1..f970f51419 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -15,7 +15,7 @@ import frappe import frappe.website.render from frappe import _ from frappe.utils import now, cint -from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields +from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options from frappe.model.document import Document from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.custom_field.custom_field import create_custom_field @@ -24,6 +24,7 @@ from frappe.modules import make_boilerplate, get_doc_path from frappe.database.schema import validate_column_name, validate_column_length from frappe.model.docfield import supports_translation from frappe.modules.import_file import get_file_path +from frappe.model.meta import Meta class InvalidFieldNameError(frappe.ValidationError): pass @@ -93,10 +94,11 @@ class DocType(Document): if not self.is_new(): self.setup_fields_to_fetch() + check_email_append_to(self) + if self.default_print_format and not self.custom: frappe.throw(_('Standard DocType cannot have default print format, use Customize Form')) - def set_default_in_list_view(self): '''Set default in-list-view for first 4 mandatory fields''' if not [d.fieldname for d in self.fields if d.in_list_view]: @@ -107,14 +109,12 @@ class DocType(Document): cnt += 1 if cnt == 4: break - def set_default_translatable(self): '''Ensure that non-translatable never will be translatable''' for d in self.fields: if d.translatable and not supports_translation(d.fieldtype): d.translatable = 0 - def check_developer_mode(self): """Throw exception if not developer mode or via patch""" if frappe.flags.in_patch or frappe.flags.in_test: @@ -123,7 +123,6 @@ class DocType(Document): if not frappe.conf.get("developer_mode") and not self.custom: frappe.throw(_("Not in Developer Mode! Set in site_config.json or make 'Custom' DocType."), CannotCreateStandardDoctypeError) - def setup_fields_to_fetch(self): '''Setup query to update values for newly set fetch values''' try: @@ -168,21 +167,18 @@ class DocType(Document): ) ) - def update_fields_to_fetch(self): '''Update fetch values based on queries setup''' if self.flags.update_fields_to_fetch_queries and not self.issingle: for query in self.flags.update_fields_to_fetch_queries: frappe.db.sql(query) - def validate_document_type(self): if self.document_type=="Transaction": self.document_type = "Document" if self.document_type=="Master": self.document_type = "Setup" - def validate_website(self): """Ensure that website generator has field 'route'""" if self.has_web_view: @@ -193,7 +189,6 @@ class DocType(Document): # clear website cache frappe.website.render.clear_cache() - def change_modified_of_parent(self): """Change the timestamp of parent DocType if the current one is a child to clear caches.""" if frappe.flags.in_import: @@ -203,7 +198,6 @@ class DocType(Document): for p in parent_list: frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent)) - def scrub_field_names(self): """Sluggify fieldnames if not set from Label.""" restricted = ('name','parent','creation','modified','modified_by', @@ -233,7 +227,6 @@ class DocType(Document): # unique is automatically an index if d.unique: d.search_index = 0 - def validate_series(self, autoname=None, name=None): """Validate if `autoname` property is correctly set.""" if not autoname: autoname = self.autoname @@ -270,12 +263,11 @@ class DocType(Document): if used_in: frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0])) - def on_update(self): """Update database schema, make controller templates if `custom` is not set and clear cache.""" self.delete_duplicate_custom_fields() try: - frappe.db.updatedb(self.name, self) + frappe.db.updatedb(self.name, Meta(self)) except Exception as e: print("\n\nThere was an issue while migrating the DocType: {}\n".format(self.name)) raise e @@ -324,7 +316,6 @@ class DocType(Document): dt = {0} and fieldname in ({1}) '''.format('%s', ', '.join(['%s'] * len(fields))), tuple([self.name] + fields), as_dict=True) - def sync_global_search(self): '''If global search settings are changed, rebuild search properties for this table''' global_search_fields_before_update = [d.fieldname for d in @@ -342,7 +333,6 @@ class DocType(Document): frappe.enqueue('frappe.utils.global_search.rebuild_for_doctype', now=now, doctype=self.name) - def set_base_class_for_controller(self): '''Updates the controller class to subclass from `WebsiteGenertor`, if it is a subclass of `Document`''' @@ -362,14 +352,12 @@ class DocType(Document): with open(controller_path, 'w') as f: f.write(code) - def run_module_method(self, method): from frappe.modules import load_doctype_module module = load_doctype_module(self.name, self.module) if hasattr(module, method): getattr(module, method)() - def before_rename(self, old, new, merge=False): """Throw exception if merge. DocTypes cannot be merged.""" if not self.custom and frappe.session.user != "Administrator": @@ -385,7 +373,6 @@ class DocType(Document): if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: self.rename_files_and_folders(old, new) - def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" @@ -396,7 +383,6 @@ class DocType(Document): else: frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) - def rename_files_and_folders(self, old, new): # move files new_path = get_doc_path(self.module, 'doctype', new) @@ -413,7 +399,6 @@ class DocType(Document): self.rename_inside_controller(new, old, new_path) frappe.msgprint(_('Renamed files and replaced code in controllers, please check!')) - def rename_inside_controller(self, new, old, new_path): for fname in ('{}.js', '{}.py', '{}_list.js', '{}_calendar.js', 'test_{}.py', 'test_{}.js'): fname = os.path.join(new_path, fname.format(frappe.scrub(new))) @@ -439,7 +424,6 @@ class DocType(Document): if not (self.issingle and self.istable): self.preserve_naming_series_options_in_property_setter() - def preserve_naming_series_options_in_property_setter(self): """Preserve naming_series as property setter if it does not exist""" naming_series = self.get("fields", {"fieldname": "naming_series"}) @@ -459,7 +443,6 @@ class DocType(Document): if naming_series[0].default: make_property_setter(self.name, "naming_series", "default", naming_series[0].default, "Text", validate_fields_for_doctype=False) - def before_export(self, docdict): # remove null and empty fields def remove_null_fields(o): @@ -504,7 +487,6 @@ class DocType(Document): except ValueError: pass - @staticmethod def prepare_for_import(docdict): # set order of fields from field_order @@ -527,19 +509,16 @@ class DocType(Document): if "field_order" in docdict: del docdict["field_order"] - def export_doc(self): """Export to standard folder `[module]/doctype/[name]/[name].json`.""" from frappe.modules.export_file import export_to_files export_to_files(record_list=[['DocType', self.name]], create_init=True) - def import_doc(self): """Import from standard folder `[module]/doctype/[name]/[name].json`.""" from frappe.modules.import_module import import_from_files import_from_files(record_list=[[self.module, 'doctype', self.name]]) - def make_controller_template(self): """Make boilerplate controller template.""" make_boilerplate("controller._py", self) @@ -556,7 +535,6 @@ class DocType(Document): make_boilerplate('templates/controller.html', self.as_dict()) make_boilerplate('templates/controller_row.html', self.as_dict()) - def make_amendable(self): """If is_submittable is set, add amended_from docfields.""" if self.is_submittable: @@ -572,7 +550,6 @@ class DocType(Document): "no_copy": 1 }) - def make_repeatable(self): """If allow_auto_repeat is set, add auto_repeat custom field.""" if self.allow_auto_repeat: @@ -641,14 +618,12 @@ class DocType(Document): }) self.nsm_parent_field = parent_field_name - def get_max_idx(self): """Returns the highest `idx`""" max_idx = frappe.db.sql("""select max(idx) from `tabDocField` where parent = %s""", self.name) return max_idx and max_idx[0][0] or 0 - def validate_name(self, name=None): if not name: name = self.name @@ -668,7 +643,6 @@ def validate_fields_for_doctype(doctype): doc.delete_duplicate_custom_fields() validate_fields(frappe.get_meta(doctype, cached=False)) - # this is separate because it is also called via custom field def validate_fields(meta): """Validate doctype fields. Checks @@ -692,29 +666,24 @@ def validate_fields(meta): def check_illegal_characters(fieldname): validate_column_name(fieldname) - def check_invalid_fieldnames(docname, fieldname): invalid_fields = ('doctype',) if fieldname in invalid_fields: frappe.throw(_("{0}: Fieldname cannot be one of {1}") .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields]))) - def check_unique_fieldname(docname, fieldname): duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields))) if len(duplicates) > 1: frappe.throw(_("{0}: Fieldname {1} appears multiple times in rows {2}").format(docname, fieldname, ", ".join(duplicates)), UniqueFieldnameError) - def check_fieldname_length(fieldname): validate_column_length(fieldname) - def check_illegal_mandatory(docname, d): if (d.fieldtype in no_value_fields) and d.fieldtype not in table_fields and d.reqd: frappe.throw(_("{0}: Field {1} of type {2} cannot be mandatory").format(docname, d.label, d.fieldtype), IllegalMandatoryError) - def check_link_table_options(docname, d): if frappe.flags.in_patch: return if d.fieldtype in ("Link",) + table_fields: @@ -733,28 +702,23 @@ def validate_fields(meta): # fix case d.options = options - def check_hidden_and_mandatory(docname, d): if d.hidden and d.reqd and not d.default: frappe.throw(_("{0}: Field {1} in row {2} cannot be hidden and mandatory without default").format(docname, d.label, d.idx), HiddenAndMandatoryWithoutDefaultError) - def check_width(d): if d.fieldtype == "Currency" and cint(d.width) < 100: frappe.throw(_("Max width for type Currency is 100px in row {0}").format(d.idx)) - def check_in_list_view(d): if d.in_list_view and (d.fieldtype in not_allowed_in_list_view): frappe.throw(_("'In List View' not allowed for type {0} in row {1}").format(d.fieldtype, d.idx)) - def check_in_global_search(d): if d.in_global_search and d.fieldtype in no_value_fields: frappe.throw(_("'In Global Search' not allowed for type {0} in row {1}") .format(d.fieldtype, d.idx)) - def check_dynamic_link_options(d): if d.fieldtype=="Dynamic Link": doctype_pointer = list(filter(lambda df: df.fieldname==d.options, fields)) @@ -762,7 +726,6 @@ def validate_fields(meta): or (doctype_pointer[0].fieldtype=="Link" and doctype_pointer[0].options!="DocType"): frappe.throw(_("Options 'Dynamic Link' type of field must point to another Link Field with options as 'DocType'")) - def check_illegal_default(d): if d.fieldtype == "Check" and not d.default: d.default = '0' @@ -771,12 +734,10 @@ def validate_fields(meta): if d.fieldtype == "Select" and d.default and (d.default not in d.options.split("\n")): frappe.throw(_("Default for {0} must be an option").format(d.fieldname)) - def check_precision(d): if d.fieldtype in ("Currency", "Float", "Percent") and d.precision is not None and not (1 <= cint(d.precision) <= 6): frappe.throw(_("Precision should be between 1 and 6")) - def check_unique_and_text(docname, d): if meta.issingle: d.unique = 0 @@ -798,7 +759,6 @@ def validate_fields(meta): if d.search_index and d.fieldtype in ("Text", "Long Text", "Small Text", "Code", "Text Editor"): frappe.throw(_("{0}:Fieldtype {1} for {2} cannot be indexed").format(docname, d.fieldtype, d.label), CannotIndexedError) - def check_fold(fields): fold_exists = False for i, f in enumerate(fields): @@ -813,7 +773,6 @@ def validate_fields(meta): else: frappe.throw(_("Fold can not be at the end of the form")) - def check_search_fields(meta, fields): """Throw exception if `search_fields` don't contain valid fields.""" if not meta.search_fields: @@ -830,7 +789,6 @@ def validate_fields(meta): (fieldname not in fieldname_list): frappe.throw(_("Search field {0} is not valid").format(fieldname)) - def check_title_field(meta): """Throw exception if `title_field` isn't a valid fieldname.""" if not meta.get("title_field"): @@ -857,7 +815,6 @@ def validate_fields(meta): _validate_title_field_pattern(df.options) _validate_title_field_pattern(df.default) - def check_image_field(meta): '''check image_field exists and is of type "Attach Image"''' if not meta.image_field: @@ -869,7 +826,6 @@ def validate_fields(meta): if df[0].fieldtype != 'Attach Image': frappe.throw(_("Image field must be of type Attach Image"), InvalidFieldNameError) - def check_is_published_field(meta): if not meta.is_published_field: return @@ -877,7 +833,6 @@ def validate_fields(meta): if meta.is_published_field not in fieldname_list: frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError) - def check_timeline_field(meta): if not meta.timeline_field: return @@ -889,7 +844,6 @@ def validate_fields(meta): if df.fieldtype not in ("Link", "Dynamic Link"): frappe.throw(_("Timeline field must be a Link or Dynamic Link"), InvalidFieldNameError) - def check_sort_field(meta): '''Validate that sort_field(s) is a valid field''' if meta.sort_field: @@ -902,7 +856,6 @@ def validate_fields(meta): frappe.throw(_("Sort field {0} must be a valid fieldname").format(fieldname), InvalidFieldNameError) - def check_illegal_depends_on_conditions(docfield): ''' assignment operation should not be allowed in the depends on condition.''' depends_on_fields = ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"] @@ -912,7 +865,6 @@ def validate_fields(meta): re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on): frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError) - def check_table_multiselect_option(docfield): '''check if the doctype provided in Option has atleast 1 Link field''' if not docfield.fieldtype == 'Table MultiSelect': return @@ -925,7 +877,6 @@ def validate_fields(meta): frappe.throw(_('DocType {0} provided for the field {1} must have atleast one Link field') .format(doctype, docfield.fieldname), frappe.ValidationError) - def scrub_options_in_select(field): """Strip options for whitespaces""" @@ -937,11 +888,20 @@ def validate_fields(meta): options_list.append(_option) field.options = '\n'.join(options_list) - def scrub_fetch_from(field): if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'): field.fetch_from = field.fetch_from.strip('\n').strip() + def validate_data_field_type(docfield): + if docfield.fieldtype == "Data": + if docfield.options and (docfield.options not in data_field_options): + df_str = frappe.bold(_(docfield.label)) + text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
" + df_options_str = "" + + frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True) + + fields = meta.get("fields") fieldname_list = [d.fieldname for d in fields] @@ -972,6 +932,7 @@ def validate_fields(meta): check_table_multiselect_option(d) scrub_options_in_select(d) scrub_fetch_from(d) + validate_data_field_type(d) check_fold(fields) check_search_fields(meta, fields) @@ -981,7 +942,6 @@ def validate_fields(meta): check_sort_field(meta) check_image_field(meta) - def validate_permissions_for_doctype(doctype, for_remove=False): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) @@ -993,7 +953,6 @@ def validate_permissions_for_doctype(doctype, for_remove=False): clear_permissions_cache(doctype.name) - def clear_permissions_cache(doctype): frappe.clear_cache(doctype=doctype) delete_notification_count_for(doctype) @@ -1008,7 +967,6 @@ def clear_permissions_cache(doctype): """, doctype): frappe.clear_cache(user=user) - def validate_permissions(doctype, for_remove=False): permissions = doctype.get("permissions") if not permissions: @@ -1102,7 +1060,6 @@ def validate_permissions(doctype, for_remove=False): check_level_zero_is_set(d) remove_rights_for_single(d) - def make_module_and_roles(doc, perm_fieldname="permissions"): """Make `Module Def` and `Role` records if already not made. Called while installing.""" try: @@ -1133,7 +1090,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): else: raise - def check_if_fieldname_conflicts_with_methods(doctype, fieldname): doc = frappe.get_doc({"doctype": doctype}) method_list = [method for method in dir(doc) if isinstance(method, str) and callable(getattr(doc, method))] @@ -1141,6 +1097,38 @@ def check_if_fieldname_conflicts_with_methods(doctype, fieldname): if fieldname in method_list: frappe.throw(_("Fieldname {0} conflicting with meta object").format(fieldname)) - def clear_linked_doctype_cache(): frappe.cache().delete_value('linked_doctypes_without_ignore_user_permissions_enabled') + +def check_email_append_to(doc): + if not hasattr(doc, "email_append_to") or not doc.email_append_to: + return + + # Subject Field + doc.subject_field = doc.subject_field.strip() if doc.subject_field else None + subject_field = get_field(doc, doc.subject_field) + + if doc.subject_field and not subject_field: + frappe.throw(_("Select a valid Subject field for creating documents from Email")) + + if subject_field and subject_field.fieldtype not in ["Data", "Text", "Long Text", "Small Text", "Text Editor"]: + frappe.throw(_("Subject Field type should be Data, Text, Long Text, Small Text, Text Editor")) + + # Sender Field is mandatory + doc.sender_field = doc.sender_field.strip() if doc.sender_field else None + sender_field = get_field(doc, doc.sender_field) + + if doc.sender_field and not sender_field: + frappe.throw(_("Select a valid Sender Field for creating documents from Email")) + + if not sender_field.options == "Email": + frappe.throw(_("Sender Field should have Email in options")) + + +def get_field(doc, fieldname): + if not (doc or fieldname): + return + + for field in doc.fields: + if field.fieldname == fieldname: + return field diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 969a71ab7d..fe9f88b9b3 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -113,6 +113,32 @@ class TestDocType(unittest.TestCase): if condition: self.assertFalse(re.match(pattern, condition)) + def test_data_field_options(self): + doctype_name = "Test Data Fields" + valid_data_field_options = frappe.model.data_field_options + ("",) + invalid_data_field_options = ("Invalid Option 1", frappe.utils.random_string(5)) + + for field_option in (valid_data_field_options + invalid_data_field_options): + test_doctype = frappe.get_doc({ + "doctype": "DocType", + "name": doctype_name, + "module": "Core", + "custom": 1, + "fields": [{ + "fieldname": "{0}_field".format(field_option), + "fieldtype": "Data", + "options": field_option + }] + }) + + if field_option in invalid_data_field_options: + # assert that only data options in frappe.model.data_field_options are valid + self.assertRaises(frappe.ValidationError, test_doctype.insert) + else: + test_doctype.insert() + self.assertEqual(test_doctype.name, doctype_name) + test_doctype.delete() + def test_sync_field_order(self): from frappe.modules.import_file import get_file_path import os @@ -349,4 +375,4 @@ class TestDocType(unittest.TestCase): # delete doctype link_doc.delete() doc.delete() - frappe.db.commit() \ No newline at end of file + frappe.db.commit() diff --git a/frappe/core/doctype/role_profile/role_profile.js b/frappe/core/doctype/role_profile/role_profile.js index 09aead670a..d31618cc4a 100644 --- a/frappe/core/doctype/role_profile/role_profile.js +++ b/frappe/core/doctype/role_profile/role_profile.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Role Profile', { - setup: function(frm) { + refresh: function(frm) { if(has_common(frappe.user_roles, ["Administrator", "System Manager"])) { if(!frm.roles_editor) { var role_area = $('
') diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index eb20802624..f2c62ad1a3 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -37,6 +37,7 @@ "allow_login_using_user_name", "allow_error_traceback", "password_settings", + "logout_on_password_reset", "force_user_to_reset_password", "column_break_31", "enable_password_policy", @@ -407,6 +408,12 @@ "fieldname": "dormant_days", "fieldtype": "Int", "label": "Run Jobs only Daily if Inactive For (Days)" + }, + { + "default": "1", + "fieldname": "logout_on_password_reset", + "fieldtype": "Check", + "label": "Logout All Sessions on Password Reset" } ], "icon": "fa fa-cog", diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 0d981c9e9e..d4c0fa98ed 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -224,3 +224,4 @@ class TestUser(unittest.TestCase): def delete_contact(user): frappe.db.sql("DELETE FROM `tabContact` WHERE `email_id`= %s", user) + frappe.db.sql("DELETE FROM `tabContact Email` WHERE `email_id`= %s", user) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index b71c9555e1..5ebde7e7bd 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "creation": "2014-03-11 14:55:00", @@ -178,7 +179,7 @@ { "fieldname": "time_zone", "fieldtype": "Select", - "label": "Timezone" + "label": "Time Zone" }, { "description": "Get your globally recognized avatar from Gravatar.com", @@ -302,7 +303,7 @@ "default": "0", "fieldname": "logout_all_sessions", "fieldtype": "Check", - "label": "Logout from all devices while changing Password" + "label": "Logout From All Devices After Changing Password" }, { "fieldname": "reset_password_key", @@ -338,7 +339,7 @@ "default": "0", "fieldname": "document_follow_notify", "fieldtype": "Check", - "label": "Send Notifications for documents followed by me" + "label": "Send Notifications For Documents Followed By Me" }, { "default": "Daily", @@ -359,7 +360,7 @@ "default": "1", "fieldname": "thread_notify", "fieldtype": "Check", - "label": "Send Notifications for Email threads" + "label": "Send Notifications For Email Threads" }, { "default": "0", @@ -496,7 +497,7 @@ "description": "If enabled, user can login from any IP Address using Two Factor Auth, this can also be set for all users in System Settings", "fieldname": "bypass_restrict_ip_check_if_2fa_enabled", "fieldtype": "Check", - "label": "Bypass restricted IP Address check If Two Factor Auth Enabled" + "label": "Bypass Restricted IP Address Check If Two Factor Auth Enabled" }, { "fieldname": "column_break1", @@ -585,8 +586,9 @@ "icon": "fa fa-user", "idx": 413, "image_field": "user_image", + "links": [], "max_attachments": 5, - "modified": "2019-10-22 14:16:34.810223", + "modified": "2020-03-23 22:59:26.154985", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5740485e16..ddad3a91fb 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -555,7 +555,8 @@ def update_password(new_password, logout_all_sessions=0, key=None, old_password= else: user = res['user'] - _update_password(user, new_password, logout_all_sessions=int(logout_all_sessions)) + logout_all_sessions = cint(logout_all_sessions) or frappe.db.get_single_value("System Settings", "logout_on_password_reset") + _update_password(user, new_password, logout_all_sessions=cint(logout_all_sessions)) user_doc, redirect_url = reset_user_data(user) diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index 511aac7010..8705804014 100644 --- a/frappe/core/page/dashboard/dashboard.js +++ b/frappe/core/page/dashboard/dashboard.js @@ -59,7 +59,7 @@ class Dashboard { } show_dashboard(current_dashboard_name) { - if(this.dashboard_name !== current_dashboard_name) { + if (this.dashboard_name !== current_dashboard_name) { this.dashboard_name = current_dashboard_name; let title = this.dashboard_name; if (!this.dashboard_name.toLowerCase().includes(__('dashboard'))) { @@ -76,9 +76,11 @@ class Dashboard { } refresh() { - this.get_dashboard_doc().then((doc) => { - this.dashboard_doc = doc; - this.charts = this.dashboard_doc.charts + this.get_permitted_dashboard_charts().then(charts => { + if (!charts.length) { + frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts')) + } + this.charts = charts .map(chart => { return { chart_name: chart.chart, @@ -98,8 +100,14 @@ class Dashboard { }); } - get_dashboard_doc() { - return frappe.model.with_doc('Dashboard', this.dashboard_name); + get_permitted_dashboard_charts() { + return frappe.xcall( + 'frappe.desk.doctype.dashboard.dashboard.get_permitted_charts', + { + dashboard_name: this.dashboard_name + }).then(charts => { + return charts; + }); } set_dropdown() { diff --git a/frappe/core/page/dashboard/dashboard.json b/frappe/core/page/dashboard/dashboard.json index 891dcb26f8..58fda5a34c 100644 --- a/frappe/core/page/dashboard/dashboard.json +++ b/frappe/core/page/dashboard/dashboard.json @@ -4,7 +4,7 @@ "docstatus": 0, "doctype": "Page", "idx": 0, - "modified": "2019-01-08 19:19:48.073410", + "modified": "2020-03-26 13:30:44.603948", "modified_by": "Administrator", "module": "Core", "name": "dashboard", diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 710bb51680..ed3b0d17db 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -217,6 +217,7 @@ frappe.PermissionEngine = Class.extend({ me.rights.forEach(r => { if (!d.is_submittable && ['submit', 'cancel', 'amend'].includes(r)) return; + if (d.in_create && ['create', 'write', 'delete'].includes(r)) return; me.add_check(perm_container, d, r); }); diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 1afd7bb423..637b526d5c 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -66,6 +66,7 @@ def get_permissions(doctype=None, role=None): meta = frappe.get_meta(d.parent) if meta: d.is_submittable = meta.is_submittable + d.in_create = meta.in_create return out diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 63b094615a..b1743a96a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -85,6 +85,10 @@ frappe.ui.form.on("Customize Form", { 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); + }); + frm.add_custom_button(__('Refresh Form'), function() { frm.script_manager.trigger("doc_type"); }, "fa fa-refresh", "btn-default"); @@ -139,8 +143,7 @@ frappe.ui.form.on("Customize Form", { }, 1000); } - }, - + } }); frappe.ui.form.on("Customize Form Field", { diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 0b1df62f9d..51a5c0b85f 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "DL.####", "creation": "2013-01-29 17:55:08", "doctype": "DocType", @@ -28,6 +29,10 @@ "sort_field", "column_break_10", "sort_order", + "section_break_23", + "email_append_to", + "sender_field", + "subject_field", "fields_section_break", "fields" ], @@ -174,13 +179,38 @@ "fieldname": "allow_import", "fieldtype": "Check", "label": "Allow Import (via Data Import Tool)" + }, + { + "depends_on": "email_append_to", + "fieldname": "subject_field", + "fieldtype": "Data", + "label": "Subject Field" + }, + { + "depends_on": "email_append_to", + "fieldname": "sender_field", + "fieldtype": "Data", + "label": "Sender Field", + "mandatory_depends_on": "email_append_to" + }, + { + "default": "0", + "fieldname": "email_append_to", + "fieldtype": "Check", + "label": "Allow document creation via Email" + }, + { + "depends_on": "doc_type", + "fieldname": "section_break_23", + "fieldtype": "Section Break" } ], "hide_toolbar": 1, "icon": "fa fa-glass", "idx": 1, "issingle": 1, - "modified": "2019-10-08 11:16:36.698006", + "links": [], + "modified": "2020-03-27 15:06:35.443861", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 8d47a075ba..68848d26f6 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -12,7 +12,7 @@ from frappe import _ from frappe.utils import cint 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 +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.model.docfield import supports_translation @@ -31,7 +31,10 @@ doctype_properties = { 'track_changes': 'Check', 'track_views': 'Check', 'allow_auto_repeat': 'Check', - 'allow_import': 'Check' + 'allow_import': 'Check', + 'email_append_to': 'Check', + 'subject_field': 'Data', + 'sender_field': 'Data' } docfield_properties = { @@ -166,11 +169,11 @@ class CustomizeForm(Document): 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) @@ -362,13 +365,49 @@ class CustomizeForm(Document): 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 + if frappe.db.type_map.get(old_value)[1] > frappe.db.type_map.get(new_value)[1]: + 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 = 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 diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 1cd71ea05d..cace25a03d 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -46,7 +46,7 @@ class TestCustomizeForm(unittest.TestCase): d = self.get_customize_form("Event") self.assertEquals(d.doc_type, "Event") - self.assertEquals(len(d.get("fields")), 35) + self.assertEquals(len(d.get("fields")), 36) d = self.get_customize_form("Event") self.assertEquals(d.doc_type, "Event") diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index dbe53df4e4..46940cc846 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -217,6 +217,9 @@ CREATE TABLE `tabDocType` ( `allow_guest_to_view` int(1) NOT NULL DEFAULT 0, `route` varchar(255) DEFAULT NULL, `is_published_field` varchar(255) DEFAULT NULL, + `email_append_to` int(1) NOT NULL DEFAULT 0, + `subject_field` varchar(255) DEFAULT NULL, + `sender_field` varchar(255) DEFAULT NULL, PRIMARY KEY (`name`), KEY `parent` (`parent`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 243d0f934e..e30ef3293f 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -92,7 +92,7 @@ class PostgresDatabase(Database): # pylint: disable=W0221 def sql(self, *args, **kwargs): - if len(args): + if args: # since tuple is immutable args = list(args) args[0] = modify_query(args[0]) @@ -276,13 +276,13 @@ class PostgresDatabase(Database): # pylint: disable=W1401 return self.sql(''' SELECT a.column_name AS name, - CASE a.data_type + CASE LOWER(a.data_type) WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')') - WHEN 'timestamp without TIME zone' THEN 'timestamp' + WHEN 'timestamp without time zone' THEN 'timestamp' ELSE a.data_type END AS type, COUNT(b.indexdef) AS Index, - COALESCE(a.column_default, NULL) AS default, + SPLIT_PART(COALESCE(a.column_default, NULL), '::', 1) AS default, BOOL_OR(b.unique) AS unique FROM information_schema.columns a LEFT JOIN diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 457f6c906a..26760dbcc9 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -222,6 +222,9 @@ CREATE TABLE "tabDocType" ( "allow_guest_to_view" smallint NOT NULL DEFAULT 0, "route" varchar(255) DEFAULT NULL, "is_published_field" varchar(255) DEFAULT NULL, + "email_append_to" smallint NOT NULL DEFAULT 0, + "subject_field" varchar(255) DEFAULT NULL, + "sender_field" varchar(255) DEFAULT NULL, PRIMARY KEY ("name") ) ; diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 88cda9340b..28e055f382 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -13,7 +13,7 @@ class DBTable: def __init__(self, doctype, meta=None): self.doctype = doctype self.table_name = 'tab{}'.format(doctype) - self.meta = meta or frappe.get_meta(doctype) + self.meta = meta or frappe.get_meta(doctype, False) self.columns = {} self.current_columns = {} @@ -65,64 +65,35 @@ class DBTable: """ get columns from docfields and custom fields """ - fl = frappe.db.sql("SELECT * FROM `tabDocField` WHERE parent = %s", self.doctype, as_dict = 1) - lengths = {} - precisions = {} - uniques = {} + fields = self.meta.get_fieldnames_with_value(True) # optional fields like _comments - if not self.meta.istable: + if not self.meta.get('istable'): for fieldname in frappe.db.OPTIONAL_COLUMNS: - fl.append({ + fields.append({ "fieldname": fieldname, "fieldtype": "Text" }) # add _seen column if track_seen - if getattr(self.meta, 'track_seen', False): - fl.append({ + if self.meta.get('track_seen'): + fields.append({ 'fieldname': '_seen', 'fieldtype': 'Text' }) - if (not frappe.flags.in_install_db - and (frappe.flags.in_install != "frappe" - or frappe.flags.ignore_in_install)): - custom_fl = frappe.db.sql(""" - SELECT * FROM `tabCustom Field` - WHERE dt = %s AND docstatus < 2 - """, (self.doctype,), as_dict=1) - if custom_fl: fl += custom_fl - - # apply length, precision and unique from property setters - for ps in frappe.get_all("Property Setter", - fields=["field_name", "property", "value"], - filters={ - "doc_type": self.doctype, - "doctype_or_field": "DocField", - "property": ["in", ["precision", "length", "unique"]] - }): - - if ps.property=="length": - lengths[ps.field_name] = cint(ps.value) - - elif ps.property=="precision": - precisions[ps.field_name] = cint(ps.value) - - elif ps.property=="unique": - uniques[ps.field_name] = cint(ps.value) - - for f in fl: - self.columns[f['fieldname']] = DbColumn(self, - f['fieldname'], - f['fieldtype'], - lengths.get(f["fieldname"]) or f.get('length'), - f.get('default'), - f.get('search_index'), - f.get('options'), - uniques.get(f["fieldname"], - f.get('unique')), - precisions.get(f['fieldname']) or f.get('precision')) + for field in fields: + self.columns[field.get('fieldname')] = DbColumn( + self, + field.get('fieldname'), + field.get('fieldtype'), + field.get('length'), + field.get('default'), + field.get('search_index'), + field.get('options'), + field.get('unique'), + field.get('precision') + ) def validate(self): """Check if change in varchar length isn't truncating the columns""" diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index ef84114745..1cb03355c6 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -146,8 +146,9 @@ class Workspace: charts = charts + self.extended_charts for chart in charts: - chart.label = chart.label if chart.label else chart.chart_name - all_charts.append(chart) + if frappe.has_permission('Dashboard Chart', doc=chart.chart_name): + chart.label = chart.label if chart.label else chart.chart_name + all_charts.append(chart) return all_charts diff --git a/frappe/desk/doctype/dashboard/dashboard.json b/frappe/desk/doctype/dashboard/dashboard.json index 239f35bea8..c177ee70ac 100644 --- a/frappe/desk/doctype/dashboard/dashboard.json +++ b/frappe/desk/doctype/dashboard/dashboard.json @@ -34,7 +34,7 @@ } ], "links": [], - "modified": "2020-01-26 20:00:10.069817", + "modified": "2020-03-25 21:09:37.080132", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard", @@ -51,6 +51,27 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "quick_entry": 1, diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index c8f22d29b9..5c344956bf 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -12,3 +12,12 @@ class Dashboard(Document): # make all other dashboards non-default frappe.db.sql('''update tabDashboard set is_default = 0 where name != %s''', self.name) + +@frappe.whitelist() +def get_permitted_charts(dashboard_name): + permitted_charts = [] + dashboard = frappe.get_doc('Dashboard', dashboard_name) + for chart in dashboard.charts: + if frappe.has_permission('Dashboard Chart', doc=chart.chart): + permitted_charts.append(chart) + return permitted_charts diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index 0a017a0de2..9652ae3945 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -215,7 +215,7 @@ } ], "links": [], - "modified": "2020-03-13 19:19:37.162771", + "modified": "2020-03-31 16:00:01.987059", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -232,6 +232,27 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Dashboard Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 } ], "sort_field": "modified", diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index f01c976b9c..b2a6f0a0ff 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -10,8 +10,51 @@ import json from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.utils import nowdate, add_to_date, getdate, get_last_day, formatdate, get_datetime from frappe.model.naming import append_number_if_name_exists +from frappe.boot import get_allowed_reports from frappe.model.document import Document + +def get_permission_query_conditions(user): + + if not user: + user = frappe.session.user + + if user == 'Administrator': + return + + roles = frappe.get_roles(user) + if "System Manager" in roles: + return None + + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()]) + + return ''' + `tabDashboard Chart`.`document_type` in {allowed_doctypes} + or `tabDashboard Chart`.`report_name` in {allowed_reports} + '''.format( + allowed_doctypes=allowed_doctypes, + allowed_reports=allowed_reports + ) + + +def has_permission(doc, ptype, user): + roles = frappe.get_roles(user) + if "System Manager" in roles: + return True + + + if doc.chart_type == 'Report': + allowed_reports = tuple([key.encode('UTF8') for key in get_allowed_reports()]) + if doc.report_name in allowed_reports: + return True + else: + allowed_doctypes = tuple(frappe.permissions.get_doctypes_with_read()) + if doc.document_type in allowed_doctypes: + return True + + return False + @frappe.whitelist() @cache_source def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None, diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 032030ddef..5768f00f32 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -1,9 +1,11 @@ { + "actions": [], "allow_import": 1, "autoname": "EV.#####", "creation": "2013-06-10 13:17:47", "doctype": "DocType", "document_type": "Document", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "details", @@ -17,6 +19,7 @@ "starts_on", "ends_on", "status", + "sender", "all_day", "sync_with_google_calendar", "sb_00", @@ -262,11 +265,19 @@ "fieldtype": "Check", "label": "Pulled from Google Calendar", "read_only": 1 + }, + { + "fieldname": "sender", + "fieldtype": "Data", + "label": "Sender", + "options": "Email", + "read_only": 1 } ], "icon": "fa fa-calendar", "idx": 1, - "modified": "2019-08-08 16:01:19.489396", + "links": [], + "modified": "2020-01-14 21:47:15.825287", "modified_by": "Administrator", "module": "Desk", "name": "Event", @@ -297,8 +308,10 @@ } ], "read_only": 1, + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "subject", "title_field": "subject", "track_changes": 1, "track_seen": 1, diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 508720a488..15e0e4abe1 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -1,8 +1,10 @@ { + "actions": [], "autoname": "hash", "creation": "2012-07-03 13:30:35", "doctype": "DocType", "document_type": "Setup", + "email_append_to": 1, "engine": "InnoDB", "field_order": [ "description_and_status", @@ -142,7 +144,8 @@ "fieldname": "sender", "fieldtype": "Data", "hidden": 1, - "label": "Sender" + "label": "Sender", + "options": "Email" }, { "fieldname": "assignment_rule", @@ -154,7 +157,8 @@ ], "icon": "fa fa-check", "idx": 2, - "modified": "2019-09-10 14:34:59.161750", + "links": [], + "modified": "2020-01-14 17:04:36.971002", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", @@ -185,9 +189,11 @@ ], "quick_entry": 1, "search_fields": "description, reference_type, reference_name", + "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "subject_field": "description", "title_field": "description", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 6cd7c68368..8e8102d093 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -8,8 +8,6 @@ import json from frappe.model.document import Document from frappe.utils import get_fullname -subject_field = "description" -sender_field = "sender" exclude_from_linked_with = True class ToDo(Document): diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 498ab50645..4c3bab2e23 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -56,18 +56,20 @@ def validate_link(): frappe.response['valid_value'] = valid_value frappe.response['message'] = 'Ok' + @frappe.whitelist() -def add_comment(reference_doctype, reference_name, content, comment_email): +def add_comment(reference_doctype, reference_name, content, comment_email, comment_by): """allow any logged user to post a comment""" doc = frappe.get_doc(dict( - doctype = 'Comment', - reference_doctype = reference_doctype, - reference_name = reference_name, - comment_email = comment_email, - comment_type = 'Comment' + doctype='Comment', + reference_doctype=reference_doctype, + reference_name=reference_name, + comment_email=comment_email, + comment_type='Comment', + comment_by=comment_by )) doc.content = extract_images_from_html(doc, content) - doc.insert(ignore_permissions = True) + doc.insert(ignore_permissions=True) follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user) return doc.as_dict() diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 301c37cc21..d210af02fd 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -77,9 +77,9 @@ def generate_report_result(report, filters=None, user=None): if len(res) > 5: skip_total_row = cint(res[5]) - if report.custom_columns: - columns = json.loads(report.custom_columns) - result = add_data_to_custom_columns(columns, result) + if report.custom_columns: + columns = json.loads(report.custom_columns) + result = add_data_to_custom_columns(columns, result) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index f3eb2188b7..c0a198f5e5 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -452,16 +452,15 @@ class EmailAccount(Document): def set_sender_field_and_subject_field(self): '''Identify the sender and subject fields from the `append_to` DocType''' # set subject_field and sender_field - meta_module = frappe.get_meta_module(self.append_to) meta = frappe.get_meta(self.append_to) + self.subject_field = None + self.sender_field = None - self.subject_field = getattr(meta_module, "subject_field", "subject") - if not meta.get_field(self.subject_field): - self.subject_field = None + if hasattr(meta, "subject_field"): + self.subject_field = meta.subject_field - self.sender_field = getattr(meta_module, "sender_field", "sender") - if not meta.get_field(self.sender_field): - self.sender_field = None + if hasattr(meta, "sender_field"): + self.sender_field = meta.sender_field def find_parent_based_on_subject_and_sender(self, communication, email): '''Find parent document based on subject and sender match''' @@ -675,8 +674,21 @@ class EmailAccount(Document): @frappe.whitelist() def get_append_to(doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None): - if not txt: txt = "" - return [[d] for d in frappe.get_hooks("email_append_to") if txt in d] + txt = txt if txt else "" + email_append_to_list = [] + + # Set Email Append To DocTypes via DocType + filters = {"istable": 0, "issingle": 0, "email_append_to": 1} + for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]): + email_append_to_list.append(dt.name) + + # Set Email Append To DocTypes set via Customize Form + for dt in frappe.get_list("Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]): + email_append_to_list.append(dt.doc_type) + + email_append_to = [[d] for d in set(email_append_to_list) if txt in d] + + return email_append_to def test_internet(host="8.8.8.8", port=53, timeout=3): """Returns True if internet is connected diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 3d63f4b2b4..732fc39e9a 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -78,6 +78,7 @@ class TimestampMismatchError(ValidationError): pass class EmptyTableError(ValidationError): pass class LinkExistsError(ValidationError): pass class InvalidEmailAddressError(ValidationError): pass +class InvalidPhoneNumberError(ValidationError): pass class TemplateNotFoundError(ValidationError): pass class UniqueValidationError(ValidationError): pass class AppNotInstalledError(ValidationError): pass diff --git a/frappe/hooks.py b/frappe/hooks.py index c44c05fdf4..733cec7a08 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -87,6 +87,7 @@ permission_query_conditions = { "ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", "User": "frappe.core.doctype.user.user.get_permission_query_conditions", "Notification Log": "frappe.desk.doctype.notification_log.notification_log.get_permission_query_conditions", + "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.get_permission_query_conditions", "Notification Settings": "frappe.desk.doctype.notification_settings.notification_settings.get_permission_query_conditions", "Note": "frappe.desk.doctype.note.note.get_permission_query_conditions", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.get_permission_query_conditions", @@ -101,6 +102,7 @@ has_permission = { "ToDo": "frappe.desk.doctype.todo.todo.has_permission", "User": "frappe.core.doctype.user.user.has_permission", "Note": "frappe.desk.doctype.note.note.has_permission", + "Dashboard Chart": "frappe.desk.doctype.dashboard_chart.dashboard_chart.has_permission", "Kanban Board": "frappe.desk.doctype.kanban_board.kanban_board.has_permission", "Contact": "frappe.contacts.address_and_contact.has_permission", "Address": "frappe.contacts.address_and_contact.has_permission", diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index 1fe92d7a67..7af987f4bc 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -48,6 +48,7 @@ table_fields = ('Table', 'Table MultiSelect') core_doctypes_list = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link', 'User', 'Role', 'Has Role', 'Page', 'Module Def', 'Print Format', 'Report', 'Customize Form', 'Customize Form Field', 'Property Setter', 'Custom Field', 'Custom Script') +data_field_options = ('Email', 'Phone') def copytables(srctype, src, srcfield, tartype, tar, tarfield, srcfields, tarfields=[]): if not tarfields: diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 569cea9d5f..4af502f844 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -544,6 +544,23 @@ class BaseDocument(object): frappe.throw(_('{0} {1} cannot be "{2}". It should be one of "{3}"').format(prefix, label, value, comma_options)) + def _validate_data_fields(self): + from frappe.core.doctype.user.user import STANDARD_USERS + + # data_field options defined in frappe.model.data_field_options + for data_field in self.meta.get_data_fields(): + data = self.get(data_field.fieldname) + data_field_options = data_field.get("options") + + if data_field_options == "Email": + if (self.owner in STANDARD_USERS) and (data in STANDARD_USERS): + return + for email_address in frappe.utils.split_emails(data): + frappe.utils.validate_email_address(email_address, throw=True) + + if data_field_options == "Phone": + frappe.utils.validate_phone_number(data, throw=True) + def _validate_constants(self): if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants: return diff --git a/frappe/model/document.py b/frappe/model/document.py index 41f946efd9..03b21ea667 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -468,6 +468,7 @@ class Document(BaseDocument): def _validate(self): self._validate_mandatory() + self._validate_data_fields() self._validate_selects() self._validate_length() self._extract_images_from_text_editor() @@ -477,6 +478,7 @@ class Document(BaseDocument): children = self.get_all_children() for d in children: + d._validate_data_fields() d._validate_selects() d._validate_length() d._extract_images_from_text_editor() @@ -978,7 +980,7 @@ class Document(BaseDocument): def reset_seen(self): """Clear _seen property and set current user as seen""" if getattr(self.meta, 'track_seen', False): - self.db_set('_seen', json.dumps([frappe.session.user]), update_modified=False) + frappe.db.set_value(self.doctype, self.name, "_seen", json.dumps([frappe.session.user]), update_modified=False) def notify_update(self): """Publish realtime that the current document is modified""" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 1938a4a96c..9c71f8c0b1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -68,7 +68,7 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def") + special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link') def __init__(self, doctype): self._fields = {} @@ -128,6 +128,9 @@ class Meta(Document): def get_link_fields(self): return self.get("fields", {"fieldtype": "Link", "options":["!=", "[Select]"]}) + def get_data_fields(self): + return self.get("fields", {"fieldtype": "Data"}) + def get_dynamic_link_fields(self): if not hasattr(self, '_dynamic_link_fields'): self._dynamic_link_fields = self.get("fields", {"fieldtype": "Dynamic Link"}) @@ -165,7 +168,8 @@ class Meta(Document): def get_valid_columns(self): if not hasattr(self, "_valid_columns"): - if self.name in ("DocType", "DocField", "DocPerm", 'DocType Action', 'DocType Link', "Property Setter"): + table_exists = frappe.db.table_exists(self.name) + if self.name in self.special_doctypes and table_exists: self._valid_columns = get_table_columns(self.name) else: self._valid_columns = self.default_fields + \ @@ -290,17 +294,20 @@ class Meta(Document): return get_workflow_name(self.name) 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, - update={"is_custom_field": 1})) - except Exception as e: - if frappe.db.is_table_missing(e): - return - else: - raise + if not frappe.db.table_exists('Custom Field'): + return + + custom_fields = frappe.db.sql(""" + SELECT * FROM `tabCustom Field` + WHERE dt = %s AND docstatus < 2 + """, (self.name,), as_dict=1, update={"is_custom_field": 1}) + + self.extend("fields", custom_fields) def apply_property_setters(self): + if not frappe.db.table_exists('Property Setter'): + return + property_setters = frappe.db.sql("""select * from `tabProperty Setter` where doc_type=%s""", (self.name,), as_dict=1) @@ -378,8 +385,9 @@ class Meta(Document): if custom_perms: self.permissions = [Document(d) for d in custom_perms] - def get_fieldnames_with_value(self): - return [df.fieldname for df in self.fields if df.fieldtype not in no_value_fields] + def get_fieldnames_with_value(self, with_field_meta=False): + return [df if with_field_meta else df.fieldname \ + for df in self.fields if df.fieldtype not in no_value_fields] def get_fields_to_check_permissions(self, user_permission_doctypes): diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index b134f2f8dc..3f3711af9d 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -45,20 +45,29 @@ def get_transitions(doc, workflow = None, raise_exception=False): transitions = [] for transition in workflow.transitions: if transition.state == current_state and transition.allowed in roles: - if transition.condition: - # if condition, evaluate - # access to frappe.db.get_value and frappe.db.get_list - success = frappe.safe_eval(transition.condition, - dict(frappe = frappe._dict( - db = frappe._dict(get_value = frappe.db.get_value, get_list=frappe.db.get_list), - session = frappe.session - )), - dict(doc = doc)) - if not success: - continue + if not is_transition_condition_satisfied(transition, doc): + continue transitions.append(transition.as_dict()) return transitions +def get_workflow_safe_globals(): + # access to frappe.db.get_value and frappe.db.get_list + return dict( + frappe=frappe._dict( + db=frappe._dict( + get_value=frappe.db.get_value, + get_list=frappe.db.get_list + ), + session=frappe.session + ) + ) + +def is_transition_condition_satisfied(transition, doc): + if not transition.condition: + return True + else: + return frappe.safe_eval(transition.condition, get_workflow_safe_globals(), dict(doc=doc.as_dict())) + @frappe.whitelist() def apply_workflow(doc, action): '''Allow workflow action on the current doc''' diff --git a/frappe/patches.txt b/frappe/patches.txt index a33b4d68b0..fc4f3ae998 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -270,3 +270,4 @@ execute:frappe.delete_doc_if_exists('DocType', 'GSuite Settings') execute:frappe.delete_doc_if_exists('DocType', 'GSuite Templates') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Account') execute:frappe.delete_doc_if_exists('DocType', 'GCalendar Settings') +frappe.patches.v12_0.remove_parent_and_parenttype_from_print_formats diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index b18a7487f3..4388d3c849 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -5,7 +5,11 @@ def execute(): SELECT `name`, `email_id`, `phone`, `mobile_no`, `modified_by`, `creation`, `modified` FROM `tabContact` + where not exists (select * from `tabContact Email` + where `tabContact Email`.parent=`tabContact`.name + and `tabContact Email`.email_id=`tabContact`.email_id) """, as_dict=True) + frappe.reload_doc("contacts", "doctype", "contact_email") frappe.reload_doc("contacts", "doctype", "contact_phone") frappe.reload_doc("contacts", "doctype", "contact") @@ -15,7 +19,6 @@ def execute(): for count, contact_detail in enumerate(contact_details): phone_counter = 1 is_primary = 1 - if contact_detail.email_id: email_values.append(( 1, diff --git a/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py new file mode 100644 index 0000000000..1a3c56da59 --- /dev/null +++ b/frappe/patches/v12_0/remove_parent_and_parenttype_from_print_formats.py @@ -0,0 +1,14 @@ +import frappe + +def execute(): + frappe.db.sql(""" + UPDATE + `tabPrint Format` + SET + `tabPrint Format`.`parent`='', + `tabPrint Format`.`parenttype`='', + `tabPrint Format`.parentfield='' + WHERE + `tabPrint Format`.parent != '' + OR `tabPrint Format`.parenttype != '' + """) \ No newline at end of file diff --git a/frappe/permissions.py b/frappe/permissions.py index a0d1677fac..0d766aec8d 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -307,7 +307,7 @@ def has_controller_permissions(doc, ptype, user=None): return None def get_doctypes_with_read(): - return list(set([p.parent for p in get_valid_perms()])) + return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()])) def get_valid_perms(doctype=None, user=None): '''Get valid permissions for the current user from DocPerm and Custom DocPerm''' diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 2adb5435e3..c1ba41ab16 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -152,6 +152,7 @@ frappe.ui.form.Control = Class.extend({ () => me.set_model_value(value), () => { me.set_mandatory && me.set_mandatory(value); + me.set_invalid && me.set_invalid(); if(me.df.change || me.df.onchange) { // onchange event specified in df diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 8a8ac271c7..0dbaaeb63c 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -179,6 +179,9 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ set_mandatory: function(value) { this.$wrapper.toggleClass("has-error", (this.df.reqd && is_null(value)) ? true : false); }, + set_invalid: function () { + this.$wrapper.toggleClass("has-error", (this.df.invalid ? true : false)); + }, set_bold: function() { if(this.$input) { this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd)); diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 6dc8c3d387..a7f0050d65 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -87,56 +87,29 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ return val==null ? "" : val; }, validate: function(v) { + if (!v) { + return ''; + } if(this.df.is_filter) { return v; } if(this.df.options == 'Phone') { - if(v+''=='') { - return ''; - } - var v1 = ''; - // phone may start with + and must only have numbers later, '-' and ' ' are stripped - v = v.replace(/ /g, '').replace(/-/g, '').replace(/\(/g, '').replace(/\)/g, ''); - - // allow initial +,0,00 - if(v && v.substr(0,1)=='+') { - v1 = '+'; v = v.substr(1); - } - if(v && v.substr(0,2)=='00') { - v1 += '00'; v = v.substr(2); - } - if(v && v.substr(0,1)=='0') { - v1 += '0'; v = v.substr(1); - } - v1 += cint(v) + ''; - return v1; + this.df.invalid = !validate_phone(v); + return v; } else if(this.df.options == 'Email') { - if(v+''=='') { - return ''; - } - var email_list = frappe.utils.split_emails(v); if (!email_list) { - // invalid email return ''; } else { - var invalid_email = false; + let email_invalid = false; email_list.forEach(function(email) { if (!validate_email(email)) { - frappe.msgprint(__("Invalid Email: {0}", [email])); - invalid_email = true; + email_invalid = true; } }); - - if (invalid_email) { - // at least 1 invalid email - return ''; - } else { - // all good - return v; - } + this.df.invalid = email_invalid; + return v; } - } else { return v; } diff --git a/frappe/public/js/frappe/form/controls/markdown_editor.js b/frappe/public/js/frappe/form/controls/markdown_editor.js index ee00fef0f7..81e47a0924 100644 --- a/frappe/public/js/frappe/form/controls/markdown_editor.js +++ b/frappe/public/js/frappe/form/controls/markdown_editor.js @@ -6,6 +6,8 @@ frappe.ui.form.ControlMarkdownEditor = frappe.ui.form.ControlCode.extend({ this.ace_editor_target.wrap(`
`); this.markdown_container = this.$input_wrapper.find(`.${this.editor_class}-container`); + this.editor.getSession().setUseWrapMode(true); + this.showing_preview = false; this.preview_toggle_btn = $(``) .click(e => { diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 593f987a9a..d27c65548d 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -703,7 +703,8 @@ frappe.ui.form.Timeline = class Timeline { reference_doctype: this.frm.doctype, reference_name: this.frm.docname, content: comment, - comment_email: frappe.session.user + comment_email: frappe.session.user, + comment_by: frappe.session.user_fullname }, btn: btn, callback: function(r) { diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index f8e868da20..880a91cf81 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -10,6 +10,7 @@