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/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000000..2ff8752871 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,17 @@ +# Each line is a file pattern followed by one or more owners. + +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, + +* @surajshetty3416, @netchampfaris +website/ @scmmishra +templates/ @scmmishra +www/ @scmmishra +integrations/ @Mangesh-Khairnar +patches/ @surajshetty3416 @sahil28297 +dashboard/ @prssanna +email/ @Thunderbottom +event_streaming/ @ruchamahabal +data_import* @netchampfaris +core/ @surajshetty3416 +requirements.txt @gavindsouza \ No newline at end of file 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 7add513b01..58adc6187c 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", @@ -384,7 +386,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", @@ -440,8 +443,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/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 2c8cd240ee..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 @@ -94,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]: @@ -108,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: @@ -124,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: @@ -169,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: @@ -194,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: @@ -204,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', @@ -234,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 @@ -271,7 +263,6 @@ 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() @@ -325,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 @@ -343,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`''' @@ -363,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": @@ -386,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.""" @@ -397,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) @@ -414,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))) @@ -440,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"}) @@ -460,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): @@ -505,7 +487,6 @@ class DocType(Document): except ValueError: pass - @staticmethod def prepare_for_import(docdict): # set order of fields from field_order @@ -528,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) @@ -557,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: @@ -573,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: @@ -642,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 @@ -669,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 @@ -693,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: @@ -734,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)) @@ -763,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' @@ -772,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 @@ -799,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): @@ -814,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: @@ -831,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"): @@ -858,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: @@ -870,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 @@ -878,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 @@ -890,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: @@ -903,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"] @@ -913,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 @@ -926,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""" @@ -938,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] @@ -973,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) @@ -982,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) @@ -994,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) @@ -1009,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: @@ -1103,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: @@ -1134,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))] @@ -1142,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/report/report.py b/frappe/core/doctype/report/report.py index 2d49915f59..967b28b8b2 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -6,7 +6,7 @@ import frappe import json, datetime from frappe import _, scrub import frappe.desk.query_report -from frappe.utils import cint +from frappe.utils import cint, cstr from frappe.model.document import Document from frappe.modules.export_file import export_to_files from frappe.modules import make_boilerplate @@ -92,6 +92,18 @@ class Report(Document): make_boilerplate("controller.py", self, {"name": self.name}) make_boilerplate("controller.js", self, {"name": self.name}) + def execute_query_report(self, filters): + if not self.query: + frappe.throw(_("Must specify a Query to run"), title=_('Report Document Error')) + + if not self.query.lower().startswith("select"): + frappe.throw(_("Query must be a SELECT"), title=_('Report Document Error')) + + result = [list(t) for t in frappe.db.sql(self.query, filters)] + columns = [cstr(c[0]) for c in frappe.db.get_description()] + + return [columns, result] + def execute_script_report(self, filters): # save the timestamp to automatically set to prepared threshold = 30 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/user/user.js b/frappe/core/doctype/user/user.js index c4873ee40e..b17548d994 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -97,6 +97,48 @@ frappe.ui.form.on('User', { }); }, __("Password")); + frappe.db.get_single_value("LDAP Settings", "enabled").then((value) => { + if (value === 1 && frm.doc.name != "Administrator") { + frm.add_custom_button(__("Reset LDAP Password"), function() { + const d = new frappe.ui.Dialog({ + title: __("Reset LDAP Password"), + fields: [ + { + label: __("New Password"), + fieldtype: "Password", + fieldname: "new_password", + reqd: 1 + }, + { + label: __("Confirm New Password"), + fieldtype: "Password", + fieldname: "confirm_password", + reqd: 1 + }, + { + label: __("Logout All Sessions"), + fieldtype: "Check", + fieldname: "logout_sessions" + } + ], + primary_action: (values) => { + d.hide(); + if (values.new_password !== values.confirm_password) { + frappe.throw(__("Passwords do not match!")); + } + frappe.call( + "frappe.integrations.doctype.ldap_settings.ldap_settings.reset_password", { + user: frm.doc.email, + password: values.new_password, + logout: values.logout_sessions + }); + } + }); + d.show(); + }, __("Password")); + } + }); + frm.add_custom_button(__("Reset OTP Secret"), function() { frappe.call({ method: "frappe.core.doctype.user.user.reset_otp_secret", diff --git a/frappe/core/page/dashboard/dashboard.js b/frappe/core/page/dashboard/dashboard.js index 511aac7010..88bfba9e84 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,30 +76,42 @@ class Dashboard { } refresh() { - this.get_dashboard_doc().then((doc) => { - this.dashboard_doc = doc; - this.charts = this.dashboard_doc.charts - .map(chart => { - return { - chart_name: chart.chart, - label: chart.chart, - ...chart - } - }); + this.get_permitted_dashboard_charts().then(charts => { + if (!charts.length) { + frappe.msgprint(__('No Permitted Charts on this Dashboard'), __('No Permitted Charts')) + } - this.chart_group = new frappe.widget.WidgetGroup({ - title: null, - container: this.container, - type: "chart", - columns: 2, - allow_sorting: false, - widgets: this.charts, - }); + frappe.dashboard_utils.get_dashboard_settings().then((settings) => { + let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {}; + this.charts = + charts.map(chart => { + return { + chart_name: chart.chart, + label: chart.chart, + chart_settings: chart_config[chart.chart] || {}, + ...chart + } + }); + this.chart_group = new frappe.widget.WidgetGroup({ + title: null, + container: this.container, + type: "chart", + columns: 2, + allow_sorting: false, + widgets: this.charts, + }); + }) }); } - 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/core/utils.py b/frappe/core/utils.py index 55767ffe34..55cfbc34d7 100644 --- a/frappe/core/utils.py +++ b/frappe/core/utils.py @@ -67,3 +67,19 @@ def find_all(list_of_dict, match_function): if match_function(entry): found.append(entry) return found + +def ljust_list(_list, length, fill_word=None): + """ + Similar to ljust but for list. + + Usage: + $ ljust_list([1, 2, 3], 5) + > [1, 2, 3, None, None] + """ + # make a copy to avoid mutation of passed list + _list = list(_list) + fill_length = length - len(_list) + if fill_length > 0: + _list.extend([fill_word] * fill_length) + + return _list diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 5c9fa37ef5..b1743a96a5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -143,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 3259085781..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 = { @@ -170,6 +173,7 @@ class CustomizeForm(Document): 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) 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/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/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/dashboard_settings/__init__.py b/frappe/desk/doctype/dashboard_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.js b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js new file mode 100644 index 0000000000..8e7966366d --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Dashboard Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.json b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json new file mode 100644 index 0000000000..e1eb75db47 --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-31 19:41:45.785014", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "chart_config" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "chart_config", + "fieldtype": "Code", + "label": "Chart Configuration", + "options": "JSON", + "read_only": 1 + } + ], + "in_create": 1, + "links": [], + "modified": "2020-04-01 00:07:26.489561", + "modified_by": "Administrator", + "module": "Desk", + "name": "Dashboard Settings", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "read_only": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py new file mode 100644 index 0000000000..4697d897fc --- /dev/null +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document +import frappe +import json + +class DashboardSettings(Document): + pass + + +@frappe.whitelist() +def create_dashboard_settings(user): + if not frappe.db.exists("Dashboard Settings", user): + doc = frappe.new_doc('Dashboard Settings') + doc.name = user + doc.insert(ignore_permissions=True) + frappe.db.commit() + return doc + +def get_permission_query_conditions(user): + if not user: user = frappe.session.user + + return '''(`tabDashboard Settings`.name = '{user}')'''.format(user=user) + +@frappe.whitelist() +def save_chart_config(reset, config, chart_name): + reset = frappe.parse_json(reset) + doc = frappe.get_doc('Dashboard Settings', frappe.session.user) + chart_config = frappe.parse_json(doc.chart_config) or {} + + if reset: + chart_config[chart_name] = {} + else: + config = frappe.parse_json(config) + if not chart_name in chart_config: + chart_config[chart_name] = {} + chart_config[chart_name].update(config) + + frappe.db.set_value('Dashboard Settings', frappe.session.user, 'chart_config', json.dumps(chart_config)) \ No newline at end of file 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 d210af02fd..aaf859e7fd 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,7 +8,7 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, cstr, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview @@ -16,6 +16,7 @@ from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta from frappe.utils import gzip_decompress +from frappe.core.utils import ljust_list def get_report_doc(report_name): doc = frappe.get_doc("Report", report_name) @@ -42,44 +43,32 @@ def get_report_doc(report_name): return doc -def generate_report_result(report, filters=None, user=None): - status = None - if not user: - user = frappe.session.user - if not filters: - filters = [] +def generate_report_result(report, filters=None, user=None, custom_columns=None): + user = user or frappe.session.user + filters = filters or [] if filters and isinstance(filters, string_types): filters = json.loads(filters) - columns, result, message, chart, report_summary, skip_total_row = [], [], None, None, None, 0 + + res = [] + if report.report_type == "Query Report": - if not report.query: - status = "error" - frappe.msgprint(_("Must specify a Query to run"), raise_exception=True) - - if not report.query.lower().startswith("select"): - status = "error" - frappe.msgprint(_("Query must be a SELECT"), raise_exception=True) - - result = [list(t) for t in frappe.db.sql(report.query, filters)] - columns = [cstr(c[0]) for c in frappe.db.get_description()] + res = report.execute_query_report(filters) elif report.report_type == 'Script Report': res = report.execute_script_report(filters) - columns, result = res[0], res[1] - if len(res) > 2: - message = res[2] - if len(res) > 3: - chart = res[3] - if len(res) > 4: - report_summary = res[4] - if len(res) > 5: - skip_total_row = cint(res[5]) + columns, result, message, chart, report_summary, skip_total_row = \ + ljust_list(res, 6) if report.custom_columns: columns = json.loads(report.custom_columns) result = add_data_to_custom_columns(columns, result) + if custom_columns: + result = add_data_to_custom_columns(custom_columns, result) + + for custom_column in custom_columns: + columns.insert(custom_column['insert_after_index'] + 1, custom_column) if result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -93,8 +82,8 @@ def generate_report_result(report, filters=None, user=None): "message": message, "chart": chart, "report_summary": report_summary, - "skip_total_row": skip_total_row, - "status": status, + "skip_total_row": skip_total_row or 0, + "status": None, "execution_time": frappe.cache().hget('report_execution_time', report.name) or 0 } @@ -161,7 +150,7 @@ def get_script(report_name): @frappe.whitelist() @frappe.read_only() -def run(report_name, filters=None, user=None, ignore_prepared_report=False): +def run(report_name, filters=None, user=None, ignore_prepared_report=False, custom_columns=None): report = get_report_doc(report_name) if not user: @@ -183,7 +172,7 @@ def run(report_name, filters=None, user=None, ignore_prepared_report=False): dn = "" result = get_prepared_report_result(report, filters, dn, user) else: - result = generate_report_result(report, filters, user) + result = generate_report_result(report, filters, user, custom_columns) result["add_total_row"] = report.add_total_row and not result.get('skip_total_row', False) @@ -294,6 +283,8 @@ def export_query(): if isinstance(data.get("file_format_type"), string_types): file_format_type = data["file_format_type"] + custom_columns = frappe.parse_json(data["custom_columns"]) + include_indentation = data["include_indentation"] if isinstance(data.get("visible_idx"), string_types): visible_idx = json.loads(data.get("visible_idx")) @@ -301,7 +292,7 @@ def export_query(): visible_idx = None if file_format_type == "Excel": - data = run(report_name, filters) + data = run(report_name, filters, custom_columns=custom_columns) data = frappe._dict(data) if not data.columns: frappe.respond_as_web_page(_("No data to export"), 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..4f65303be9 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -86,7 +86,9 @@ permission_query_conditions = { "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions", "ToDo": "frappe.desk.doctype.todo.todo.get_permission_query_conditions", "User": "frappe.core.doctype.user.user.get_permission_query_conditions", + "Dashboard Settings": "frappe.desk.doctype.dashboard_settings.dashboard_settings.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 +103,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/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index c0f12df04a..558f7117c0 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe import _ +from frappe import _, safe_encode from frappe.model.document import Document @@ -19,7 +19,7 @@ class LDAPSettings(Document): else: frappe.throw(_("LDAP Search String needs to end with a placeholder, eg sAMAccountName={0}")) - def connect_to_ldap(self, base_dn, password): + def connect_to_ldap(self, base_dn, password, read_only=True): try: import ldap3 import ssl @@ -44,7 +44,7 @@ class LDAPSettings(Document): user=base_dn, password=password, auto_bind=bind_type, - read_only=True, + read_only=read_only, raise_exceptions=True) return conn @@ -170,6 +170,36 @@ class LDAPSettings(Document): else: frappe.throw(_("Invalid username or password")) + def reset_password(self, user, password, logout_sessions=False): + from ldap3 import HASHED_SALTED_SHA, MODIFY_REPLACE + from ldap3.utils.hashed import hashed + + search_filter = "({0}={1})".format(self.ldap_email_field, user) + + conn = self.connect_to_ldap(self.base_dn, self.get_password(raise_exception=False), + read_only=False) + + if conn.search( + search_base=self.organizational_unit, + search_filter=search_filter, + attributes=self.get_ldap_attributes() + ): + if conn.entries and conn.entries[0]: + entry_dn = conn.entries[0].entry_dn + hashed_password = hashed(HASHED_SALTED_SHA, safe_encode(password)) + changes = {'userPassword': [(MODIFY_REPLACE, [hashed_password])]} + if conn.modify(entry_dn, changes=changes): + if logout_sessions: + from frappe.sessions import clear_sessions + clear_sessions(user=user, force=True) + frappe.msgprint(_("Password changed successfully.")) + else: + frappe.throw(_("Failed to change password.")) + else: + frappe.throw(_("No Entry for the User {0} found within LDAP!").format(user)) + else: + frappe.throw(_("No LDAP User found for email: {0}").format(user)) + def convert_ldap_entry_to_dict(self, user_entry): # support multiple email values @@ -211,3 +241,11 @@ def login(): # because of a GET request! frappe.db.commit() + + +@frappe.whitelist() +def reset_password(user, password, logout): + ldap = frappe.get_doc("LDAP Settings") + if not ldap.enabled: + frappe.throw(_("LDAP is not enabled.")) + ldap.reset_password(user, password, logout_sessions=int(logout)) 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 66dd7e3c58..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() diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 5065684311..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'): + 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 + \ diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index b134f2f8dc..4384e7c8f5 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''' @@ -185,7 +194,7 @@ def bulk_workflow_approval(docnames, doctype, action): from collections import defaultdict # dictionaries for logging - errored_transactions = defaultdict(list) + failed_transactions = defaultdict(list) successful_transactions = defaultdict(list) # WARN: message log is cleared @@ -206,7 +215,7 @@ def bulk_workflow_approval(docnames, doctype, action): if e.args: message += " : {0}".format(e.args[0]) message_dict = {"docname": docname, "message": message} - errored_transactions[docname].append(message_dict) + failed_transactions[docname].append(message_dict) frappe.db.rollback() frappe.log_error(frappe.get_traceback(), "Workflow {0} threw an error for {1} {2}".format(action, doctype, docname)) @@ -219,20 +228,20 @@ def bulk_workflow_approval(docnames, doctype, action): message_dict = {"docname": docname, "message": message.get("message")} if message.get("raise_exception", False): - errored_transactions[docname].append(message_dict) + failed_transactions[docname].append(message_dict) else: successful_transactions[docname].append(message_dict) else: successful_transactions[docname].append({"docname": docname, "message": None}) - if errored_transactions and successful_transactions: + if failed_transactions and successful_transactions: indicator = "orange" - elif errored_transactions: + elif failed_transactions: indicator = "red" else: indicator = "green" - print_workflow_log(errored_transactions, _("Errored Transactions"), doctype, indicator) + print_workflow_log(failed_transactions, _("Failed Transactions"), doctype, indicator) print_workflow_log(successful_transactions, _("Successful Transactions"), doctype, indicator) def print_workflow_log(messages, title, doctype, indicator): 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..beec168dfd 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -561,10 +561,9 @@ frappe.ui.form.Timeline = class Timeline { } let updater_reference_link = null; - - if (!$.isEmptyObject(data.updater_reference)) { + let updater_reference = data.updater_reference; + if (!$.isEmptyObject(updater_reference)) { let label = updater_reference.label || __('via {0}', [updater_reference.doctype]); - let updater_reference = data.updater_reference; updater_reference_link = frappe.utils.get_form_link( updater_reference.doctype, updater_reference.docname, @@ -703,7 +702,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/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index ef559a2e99..1a118be5db 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -161,6 +161,10 @@ $.extend(frappe.model, { user_default = frappe.boot.user.last_selected_values[df.options]; } + if (!user_default && default_doc) { + user_default = default_doc; + } + var is_allowed_user_default = user_default && (!has_user_permissions || allowed_records.includes(user_default)); diff --git a/frappe/public/js/frappe/utils/dashboard_utils.js b/frappe/public/js/frappe/utils/dashboard_utils.js index 7e64f5c143..66f3d5e21f 100644 --- a/frappe/public/js/frappe/utils/dashboard_utils.js +++ b/frappe/public/js/frappe/utils/dashboard_utils.js @@ -54,5 +54,26 @@ frappe.dashboard_utils = { } else { return Promise.resolve(); } + }, + + get_dashboard_settings() { + return frappe.model.with_doc('Dashboard Settings', frappe.session.user).then(settings => { + if (!settings) { + return this.create_dashboard_settings().then(settings => { + return settings; + }); + } else { + return settings; + } + }); + }, + + create_dashboard_settings() { + return frappe.xcall( + 'frappe.desk.doctype.dashboard_settings.dashboard_settings.create_dashboard_settings', + {user: frappe.session.user} + ).then(settings => { + return settings; + }); } }; \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index 0f73526e04..16f87b872f 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -44,6 +44,10 @@ window.validate_email = function(txt) { return frappe.utils.validate_type(txt, "email"); }; +window.validate_phone = function(txt) { + return frappe.utils.validate_type(txt, "phone"); +}; + window.nth = function(number) { number = cint(number); var s = 'th'; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 278c80897e..1af37c1f60 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -232,6 +232,9 @@ Object.assign(frappe.utils, { var regExp; switch ( type ) { + case "phone": + regExp = /^([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$/; + break; case "number": regExp = /^-?(?:\d+|\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/; break; diff --git a/frappe/public/js/frappe/views/desktop/desktop.js b/frappe/public/js/frappe/views/desktop/desktop.js index 54a25c3771..d0993e2e8c 100644 --- a/frappe/public/js/frappe/views/desktop/desktop.js +++ b/frappe/public/js/frappe/views/desktop/desktop.js @@ -165,11 +165,18 @@ class DesktopPage { this.allow_customization = this.data.allow_customization || false; - !this.sections["onboarding"] && - this.data.charts.items.length && - this.make_charts(); - this.data.shortcuts.items.length && this.make_shortcuts(); - this.data.cards.items.length && this.make_cards(); + let create_shortcuts_and_cards = () => { + this.data.shortcuts.items.length && this.make_shortcuts(); + this.data.cards.items.length && this.make_cards(); + }; + + if (!this.sections["onboarding"] && this.data.charts.items.length) { + this.make_charts().then(() => { + create_shortcuts_and_cards(); + }); + } else { + create_shortcuts_and_cards(); + } }); } @@ -224,13 +231,22 @@ class DesktopPage { } make_charts() { - this.sections["charts"] = new frappe.widget.WidgetGroup({ - title: this.data.charts.label || `${this.page_name} Dashboard`, - container: this.page, - type: "chart", - columns: 1, - allow_sorting: false, - widgets: this.data.charts.items + return frappe.dashboard_utils.get_dashboard_settings().then(settings => { + let chart_config = settings.chart_config? JSON.parse(settings.chart_config): {}; + if (this.data.charts.items) { + this.data.charts.items.map(chart => { + chart.chart_settings = chart_config[chart.chart_name] || {}; + }); + } + + this.sections["charts"] = new frappe.widget.WidgetGroup({ + title: this.data.charts.label || `${this.page_name} Dashboard`, + container: this.page, + type: "chart", + columns: 1, + allow_sorting: false, + widgets: this.data.charts.items + }); }); } diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1d7065e70d..2276bc07d6 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -616,6 +616,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { prepare_report_data(data) { this.raw_data = data; this.columns = this.prepare_columns(data.columns); + this.custom_columns = []; this.data = this.prepare_data(data.result); this.linked_doctypes = this.get_linked_doctypes(); this.tree_report = this.data.some(d => 'indent' in d); @@ -1110,6 +1111,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const args = { cmd: 'frappe.desk.query_report.export_query', report_name: this.report_name, + custom_columns: this.custom_columns.length? this.custom_columns: [], file_format_type: file_format, filters: filters, visible_idx, @@ -1275,16 +1277,20 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { primary_action: (values) => { const custom_columns = []; let df = frappe.meta.get_docfield(values.doctype, values.field); + const insert_after_index = this.columns + .findIndex(column => column.label === values.insert_after); custom_columns.push({ fieldname: df.fieldname, fieldtype: df.fieldtype, label: df.label, + insert_after_index: insert_after_index, link_field: this.doctype_field_map[values.doctype], doctype: values.doctype, options: df.fieldtype === "Link" ? df.options : undefined, width: 100 }); + this.custom_columns = this.custom_columns.concat(custom_columns); frappe.call({ method: 'frappe.desk.query_report.get_data_for_custom_field', args: { @@ -1294,7 +1300,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { callback: (r) => { const custom_data = r.message; const link_field = this.doctype_field_map[values.doctype]; - this.add_custom_column(custom_columns, custom_data, link_field, values.field, values.insert_after); + + this.add_custom_column(custom_columns, custom_data, link_field, values.field, insert_after_index); d.hide(); } }); @@ -1369,11 +1376,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - add_custom_column(custom_column, custom_data, link_field, column_field, insert_after) { + add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) { const column = this.prepare_columns(custom_column); - const insert_after_index = this.columns - .findIndex(column => column.label === insert_after); this.columns.splice(insert_after_index + 1, 0, column[0]); this.data.forEach(row => { diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 3388890776..174f0ae666 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -62,6 +62,9 @@ export default class ChartWidget extends Widget { make_chart() { this.get_settings().then(() => { + if (!this.chart_settings) { + this.chart_settings = {}; + } this.setup_container(); this.prepare_chart_object(); this.action_area.empty(); @@ -89,7 +92,7 @@ export default class ChartWidget extends Widget { render_time_series_filters() { let filters = [ { - label: this.chart_doc.timespan, + label: this.chart_settings.timespan || this.chart_doc.timespan, options: [ "Select Date Range", "Last Year", @@ -114,15 +117,22 @@ export default class ChartWidget extends Widget { this.head.css('flex-direction', "row"); } + this.save_chart_config_for_user({ + 'timespan': this.selected_timespan, + 'from_date': null, + 'to_date': null + + }); this.fetch_and_update_chart(); } } }, { - label: this.chart_doc.time_interval, + label: this.chart_settings.time_interval || this.chart_doc.time_interval, options: ["Yearly", "Quarterly", "Monthly", "Weekly", "Daily"], action: selected_item => { this.selected_time_interval = selected_item; + this.save_chart_config_for_user({'time_interval': this.selected_time_interval}); this.fetch_and_update_chart(); } } @@ -138,10 +148,10 @@ export default class ChartWidget extends Widget { fetch_and_update_chart() { this.args = { - timespan: this.selected_timespan, - time_interval: this.selected_time_interval, - from_date: this.selected_from_date, - to_date: this.selected_to_date + timespan: this.selected_timespan || this.chart_settings.timespan, + time_interval: this.selected_time_interval || this.chart_settings.time_interval, + from_date: this.selected_from_date || this.chart_settings.from_date, + to_date: this.selected_to_date || this.chart_settings.to_date }; this.fetch(this.filters, true, this.args).then(data => { @@ -176,16 +186,19 @@ export default class ChartWidget extends Widget { fieldname: "from_date", placeholder: "Date Range", input_class: "input-xs", + default: [this.chart_settings.from_date, this.chart_settings.to_date], reqd: 1, change: () => { let selected_date_range = this.date_range_field.get_value(); this.selected_from_date = selected_date_range[0]; this.selected_to_date = selected_date_range[1]; - if ( - selected_date_range && - selected_date_range.length == 2 - ) { + if (selected_date_range && selected_date_range.length == 2) { + this.save_chart_config_for_user({ + 'timespan': this.selected_timespan, + 'from_date': this.selected_from_date, + 'to_date': this.selected_to_date, + }); this.fetch_and_update_chart(); } } @@ -235,7 +248,7 @@ export default class ChartWidget extends Widget { } }, { - label: __("Edit..."), + label: __("Edit"), action: "action-edit", handler: () => { frappe.set_route( @@ -244,6 +257,15 @@ export default class ChartWidget extends Widget { this.chart_doc.name ); } + }, + { + label: __("Reset Chart"), + action: "action-list", + handler: () => { + this.reset_chart(); + delete this.dashboard_chart; + this.make_chart(); + } } ]; @@ -334,6 +356,7 @@ export default class ChartWidget extends Widget { } else { me.filters = values; } + me.save_chart_config_for_user({'filters': me.filters}); me.fetch_and_update_chart(); } }, @@ -350,6 +373,21 @@ export default class ChartWidget extends Widget { dialog.set_values(this.filters); } + reset_chart() { + this.save_chart_config_for_user(null, 1); + this.chart_settings = {}; + this.filters = null; + } + + save_chart_config_for_user(config, reset=0) { + Object.assign(this.chart_settings, config); + frappe.xcall('frappe.desk.doctype.dashboard_settings.dashboard_settings.save_chart_config', { + 'reset': reset, + 'config': this.chart_settings, + 'chart_name': this.chart_doc.chart_name + }); + } + create_filter_group_and_add_filters(parent) { this.filter_group = new frappe.ui.FilterGroup({ parent: parent, @@ -406,10 +444,10 @@ export default class ChartWidget extends Widget { filters: filters, refresh: refresh ? 1 : 0, time_interval: - args && args.time_interval ? args.time_interval : null, - timespan: args && args.timespan ? args.timespan : null, - from_date: args && args.from_date ? args.from_date : null, - to_date: args && args.to_date ? args.to_date : null + args && args.time_interval? args.time_interval: null, + timespan: args && args.timespan? args.timespan: null, + from_date: args && args.from_date? args.from_date: null, + to_date: args && args.to_date? args.to_date: null }; } return frappe.xcall(method, args); @@ -481,8 +519,9 @@ export default class ChartWidget extends Widget { } prepare_chart_object() { + let saved_filters = this.chart_settings.filters || null; this.filters = - this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); + saved_filters || this.filters || JSON.parse(this.chart_doc.filters_json || "[]"); } get_settings() { diff --git a/frappe/templates/styles/card_style.css b/frappe/templates/styles/card_style.css index f5b002a17f..595d974011 100644 --- a/frappe/templates/styles/card_style.css +++ b/frappe/templates/styles/card_style.css @@ -28,4 +28,12 @@ } .page-card p { font-size: 14px; -} \ No newline at end of file +} + +.ellipsis { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; + vertical-align: middle; +} diff --git a/frappe/tests/test_global_search.py b/frappe/tests/test_global_search.py index 01067c85dd..5cffbaaf64 100644 --- a/frappe/tests/test_global_search.py +++ b/frappe/tests/test_global_search.py @@ -191,3 +191,6 @@ class TestGlobalSearch(unittest.TestCase): frappe.db.commit() results = global_search.web_search('unsubscribe') self.assertTrue('Unsubscribe' in results[0].content) + results = global_search.web_search(text='unsubscribe', + scope="manufacturing\" UNION ALL SELECT 1,2,3,4,doctype from __global_search") + self.assertTrue(results == []) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 82e6ea1b45..649d3bf72c 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -75,11 +75,18 @@ def extract_email_id(email): email_id = email_id.decode("utf-8", "ignore") return email_id -def validate_email_add(email_str, throw=False): - """ - validate_email_add will be renamed to the validate_email_address in v12 - """ - return validate_email_address(email_str, throw=False) +def validate_phone_number(phone_number, throw=False): + """Returns True if valid phone number""" + if not phone_number: + return False + + phone_number = phone_number.strip() + match = re.match("([0-9\ \+\_\-\,\.\*\#\(\)]){1,20}$", phone_number) + + if not match and throw: + frappe.throw(frappe._("{0} is not a valid Phone Number").format(phone_number), frappe.InvalidPhoneNumberError) + + return bool(match) def validate_email_address(email_str, throw=False): """Validates the email string""" @@ -98,15 +105,15 @@ def validate_email_address(email_str, throw=False): _valid = False else: - e = extract_email_id(e) - match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", e.lower()) if e else None + email_id = extract_email_id(e) + match = re.match("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", email_id.lower()) if email_id else None if not match: _valid = False else: matched = match.group(0) if match: - match = matched==e.lower() + match = matched==email_id.lower() if not _valid: if throw: @@ -691,4 +698,4 @@ def get_html_for_route(route): set_request(method='GET', path=route) response = render.render() html = frappe.safe_decode(response.get_data()) - return html \ No newline at end of file + return html diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index b81d802a07..e65fa44253 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -151,6 +151,10 @@ app_license = "{app_license}" # web_include_css = "/assets/{app_name}/css/{app_name}.css" # web_include_js = "/assets/{app_name}/js/{app_name}.js" +# include js, css files in header of web form +# webform_include_js = {"doctype": "public/js/doctype.js"} +# webform_include_css = {"doctype": "public/css/doctype.css"} + # include js in page # page_js = {{"page" : "public/js/file.js"}} diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index 4b50745a74..3c4b9583f8 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -499,22 +499,29 @@ def web_search(text, scope=None, start=0, limit=20): common_query = ''' SELECT `doctype`, `name`, `content`, `title`, `route` FROM `__global_search` WHERE {conditions} - LIMIT {limit} OFFSET {start}''' + LIMIT %(limit)s OFFSET %(start)s''' - scope_condition = '`route` like "{}%" AND '.format(scope) if scope else '' + scope_condition = '`route` like %(scope)s AND ' if scope else '' published_condition = '`published` = 1 AND ' mariadb_conditions = postgres_conditions = ' '.join([published_condition, scope_condition]) # https://mariadb.com/kb/en/library/full-text-index-overview/#in-boolean-mode text = '"{}"'.format(text) - mariadb_conditions += 'MATCH(`content`) AGAINST ({} IN BOOLEAN MODE)'.format(frappe.db.escape(text)) - postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY({})'.format(frappe.db.escape(text)) + mariadb_conditions += 'MATCH(`content`) AGAINST (%(text)s IN BOOLEAN MODE)' + postgres_conditions += 'TO_TSVECTOR("content") @@ PLAINTO_TSQUERY(%(text)s)' + + values = { + "scope": "".join([scope, "%"]) if scope else '', + "limit": limit, + "start": start, + "text": text + } result = frappe.db.multisql({ - 'mariadb': common_query.format(conditions=mariadb_conditions, limit=limit, start=start), - 'postgres': common_query.format(conditions=postgres_conditions, limit=limit, start=start) - }, as_dict=True) - tmp_result=[] + 'mariadb': common_query.format(conditions=mariadb_conditions), + 'postgres': common_query.format(conditions=postgres_conditions) + }, values=values, as_dict=True) + tmp_result = [] for i in result: if i in results or not results: tmp_result.append(i) diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index b0c0990e85..385bac8e4a 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -43,7 +43,7 @@ class RedisWrapper(redis.Redis): try: if expires_in_sec: - self.setex(key, expires_in_sec, pickle.dumps(val)) + self.setex(name=key, time=expires_in_sec, value=pickle.dumps(val)) else: self.set(key, pickle.dumps(val)) diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index ae2c27c420..a188503ea3 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -2,18 +2,23 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe, json, os -from frappe.website.website_generator import WebsiteGenerator + +import json +import os + +from six import iteritems +from six.moves.urllib.parse import urlencode + +import frappe from frappe import _, scrub +from frappe.core.doctype.file.file import get_max_file_size, remove_file_by_url +from frappe.custom.doctype.customize_form.customize_form import docfield_properties +from frappe.desk.form.meta import get_code_files_via_hooks +from frappe.integrations.utils import get_payment_gateway_controller +from frappe.modules.utils import export_module_json, get_doc_module from frappe.utils import cstr from frappe.website.utils import get_comment_list -from frappe.custom.doctype.customize_form.customize_form import docfield_properties -from frappe.core.doctype.file.file import get_max_file_size -from frappe.core.doctype.file.file import remove_file_by_url -from frappe.modules.utils import export_module_json, get_doc_module -from six.moves.urllib.parse import urlencode -from frappe.integrations.utils import get_payment_gateway_controller -from six import iteritems +from frappe.website.website_generator import WebsiteGenerator class WebForm(WebsiteGenerator): @@ -237,11 +242,23 @@ def get_context(context): js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.js') if os.path.exists(js_path): - context.script = frappe.render_template(open(js_path, 'r').read(), context) + script = frappe.render_template(open(js_path, 'r').read(), context) + + for path in get_code_files_via_hooks("webform_include_js", context.doc_type): + custom_js = frappe.render_template(open(path, 'r').read(), context) + script = "\n\n".join([script, custom_js]) + + context.script = script css_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + '.css') if os.path.exists(css_path): - context.style = open(css_path, 'r').read() + style = open(css_path, 'r').read() + + for path in get_code_files_via_hooks("webform_include_css", context.doc_type): + custom_css = open(path, 'r').read() + style = "\n\n".join([style, custom_css]) + + context.style = style def get_layout(self): layout = [] @@ -589,4 +606,3 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals else: raise frappe.PermissionError('Not Allowed, {0}'.format(doctype)) - diff --git a/frappe/website/utils.py b/frappe/website/utils.py index 495c36c92a..f5e35976eb 100644 --- a/frappe/website/utils.py +++ b/frappe/website/utils.py @@ -3,11 +3,15 @@ from __future__ import unicode_literals import functools -import frappe, re, os +import re +import os +import frappe + from six import iteritems from past.builtins import cmp from frappe.utils import markdown + def delete_page_cache(path): cache = frappe.cache() cache.delete_value('full_index') @@ -20,7 +24,7 @@ def delete_page_cache(path): cache.delete_key(name) def find_first_image(html): - m = re.finditer("""]*src\s?=\s?['"]([^'"]*)['"]""", html) + m = re.finditer(r"""]*src\s?=\s?['"]([^'"]*)['"]""", html) try: return next(m).groups()[0] except StopIteration: @@ -33,16 +37,34 @@ def can_cache(no_cache=False): return False return not no_cache + def get_comment_list(doctype, name): - return frappe.get_all('Comment', - fields = ['name', 'creation', 'owner', 'comment_email', 'comment_by', 'content'], - filters = dict( - reference_doctype = doctype, - reference_name = name, - comment_type = 'Comment', - published = 1 - ), - order_by = 'creation asc') + comments = frappe.get_all('Comment', + fields=['name', 'creation', 'owner', + 'comment_email', 'comment_by', 'content'], + filters=dict( + reference_doctype=doctype, + reference_name=name, + comment_type='Comment', + ), + or_filters=[ + ['owner', '=', frappe.session.user], + ['published', '=', 1]]) + + communications = frappe.get_all("Communication", + fields=['name', 'creation', 'owner', 'owner as comment_email', + 'sender_full_name as comment_by', 'content', 'recipients'], + filters=dict( + reference_doctype=doctype, + reference_name=name, + ), + or_filters=[ + ['recipients', 'like', '%{0}%'.format(frappe.session.user)], + ['cc', 'like', '%{0}%'.format(frappe.session.user)], + ['bcc', 'like', '%{0}%'.format(frappe.session.user)]]) + + return sorted((comments + communications), key=lambda comment: comment['creation'], reverse=True) + def get_home_page(): if frappe.local.flags.home_page: @@ -92,7 +114,7 @@ def cleanup_page_name(title): return '' name = title.lower() - name = re.sub('[~!@#$%^&*+()<>,."\'\?]', '', name) + name = re.sub(r'[~!@#$%^&*+()<>,."\'\?]', '', name) name = re.sub('[:/]', '-', name) name = '-'.join(name.split()) @@ -194,7 +216,6 @@ def abs_url(path): def get_toc(route, url_prefix=None, app=None): '''Insert full index (table of contents) for {index} tag''' - from frappe.website.utils import get_full_index full_index = get_full_index(app=app) diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py index 3d4c106f29..68ee7cd2ef 100644 --- a/frappe/workflow/doctype/workflow_action/workflow_action.py +++ b/frappe/workflow/doctype/workflow_action/workflow_action.py @@ -9,8 +9,8 @@ from frappe.utils import get_url, get_datetime from frappe.desk.form.utils import get_pdf_link from frappe.utils.verified_command import get_signed_params, verify_request from frappe import _ -from frappe.model.workflow import apply_workflow, get_workflow_name, \ - has_approval_access, get_workflow_state_field, send_email_alert, get_workflow_field_value +from frappe.model.workflow import apply_workflow, get_workflow_name, has_approval_access, \ + get_workflow_state_field, send_email_alert, get_workflow_field_value, is_transition_condition_satisfied from frappe.desk.notifications import clear_doctype_notifications from frappe.utils.user import get_users_with_role @@ -155,10 +155,10 @@ def get_next_possible_transitions(workflow_name, state, doc=None): for transition in transitions: is_next_state_optional = get_state_optional_field_value(workflow_name, transition.next_state) # skip transition if next state of the transition is optional - if transition.condition and not frappe.safe_eval(transition.condition, None, {'doc': doc.as_dict()}): - continue if is_next_state_optional: continue + if not is_transition_condition_satisfied(transition, doc): + continue transitions_to_return.append(transition) return transitions_to_return diff --git a/frappe/www/desk.py b/frappe/www/desk.py index 689bf725f0..6cb7c8a077 100644 --- a/frappe/www/desk.py +++ b/frappe/www/desk.py @@ -12,8 +12,9 @@ from frappe import _ import frappe.sessions def get_context(context): - if (frappe.session.user == "Guest" or - frappe.db.get_value("User", frappe.session.user, "user_type")=="Website User"): + if frappe.session.user == "Guest": + frappe.throw(_("Log in to access this page."), frappe.PermissionError) + elif frappe.db.get_value("User", frappe.session.user, "user_type") == "Website User": frappe.throw(_("You are not permitted to access this page."), frappe.PermissionError) hooks = frappe.get_hooks() diff --git a/frappe/www/message.html b/frappe/www/message.html index 2919592901..87d311d9cc 100644 --- a/frappe/www/message.html +++ b/frappe/www/message.html @@ -24,7 +24,7 @@ html, body { {{ title or _("Message") }} -
+
{% block message_body %}

{{ message or "" }}

{% if primary_action %}