diff --git a/.travis.yml b/.travis.yml index 7e2c0d5e1a..e9e5c0e183 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ install: - cp -r $TRAVIS_BUILD_DIR/test_sites/test_site ~/frappe-bench/sites/ script: + - set -e - bench --verbose run-tests - bench reinstall --yes - testcafe chrome apps/frappe/frappe/tests/testcafe/ diff --git a/frappe/__init__.py b/frappe/__init__.py index 7697f14a56..3a1c6cc6b0 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -13,7 +13,7 @@ import os, sys, importlib, inspect, json from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template -__version__ = '8.0.43' +__version__ = '8.0.44' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 53a942a32d..5eae544c75 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -7,6 +7,11 @@ def get_data(): "label": _("Payments"), "icon": "fa fa-star", "items": [ + { + "type": "doctype", + "name": "Stripe Settings", + "description": _("Stripe payment gateway settings"), + }, { "type": "doctype", "name": "PayPal Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index d742681469..0a84b1a791 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -902,7 +902,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-05-01 15:27:11.079447", + "modified": "2017-05-11 15:27:11.079447", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 3d3f03ef9c..30a5bcef5f 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -13,7 +13,9 @@ frappe.ui.form.on('Custom Field', { if(user!=="Administrator") { filters.push(['DocType', 'module', '!=', 'Core']) } - return filters + return { + "filters": filters + } }); }, refresh: function(frm) { diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index 4d9072e9e4..062b72590c 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -118,10 +118,13 @@ def update_order(board_name, order): def quick_kanban_board(doctype, board_name, field_name): '''Create new KanbanBoard quickly with default options''' doc = frappe.new_doc('Kanban Board') - options = frappe.get_value('DocField', dict( - parent=doctype, - fieldname=field_name - ), 'options') + + meta = frappe.get_meta(doctype) + + options = '' + for field in meta.fields: + if field.fieldname == field_name: + options = field.options columns = [] if options: @@ -198,4 +201,4 @@ def set_indicator(board_name, column_name, indicator): def save_filters(board_name, filters): '''Save filters silently''' frappe.db.set_value('Kanban Board', board_name, 'filters', - filters, update_modified=False) + filters, update_modified=False) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 0bb6ac0a0b..734da033a3 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -3,7 +3,7 @@ # Search from __future__ import unicode_literals -import frappe +import frappe, json from frappe.utils import cstr, unique # this is called by the Link Field @@ -16,7 +16,7 @@ def search_link(doctype, txt, query=None, filters=None, page_len=20, searchfield # this is called by the search box @frappe.whitelist() def search_widget(doctype, txt, query=None, searchfield=None, start=0, - page_len=10, filters=None, as_dict=False): + page_len=10, filters=None, filter_fields=None, as_dict=False): if isinstance(filters, basestring): import json filters = json.loads(filters) @@ -76,20 +76,24 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, if meta.get("fields", {"fieldname":"disabled", "fieldtype":"Check"}): filters.append([doctype, "disabled", "!=", 1]) + # format a list of fields combining search fields and filter fields fields = get_std_fields_list(meta, searchfield or "name") + if filter_fields: + fields = list(set(fields + json.loads(filter_fields))) + formatted_fields = ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in fields] # find relevance as location of search term from the beginning of string `name`. used for sorting results. - fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( + formatted_fields.append("""locate("{_txt}", `tab{doctype}`.`name`) as `_relevance`""".format( _txt=frappe.db.escape((txt or "").replace("%", "")), doctype=frappe.db.escape(doctype))) - + # In order_by, `idx` gets second priority, because it stores link count from frappe.model.db_query import get_order_by order_by_based_on_meta = get_order_by(doctype, meta) order_by = "if(_relevance, _relevance, 99999), idx desc, {0}".format(order_by_based_on_meta) - + values = frappe.get_list(doctype, - filters=filters, fields=fields, + filters=filters, fields=formatted_fields, or_filters = or_filters, limit_start = start, limit_page_length=page_len, order_by=order_by, @@ -97,6 +101,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, as_list=not as_dict) # remove _relevance from results + frappe.response["fields"] = fields frappe.response["values"] = [r[:-1] for r in values] def get_std_fields_list(meta, key): @@ -107,7 +112,7 @@ def get_std_fields_list(meta, key): if not key in sflist: sflist = sflist + [key] - return ['`tab%s`.`%s`' % (meta.name, f.strip()) for f in sflist] + return sflist def build_for_autosuggest(res): results = [] diff --git a/frappe/email/doctype/email_alert/email_alert.py b/frappe/email/doctype/email_alert/email_alert.py index 666ae25dbc..f9212cfe88 100755 --- a/frappe/email/doctype/email_alert/email_alert.py +++ b/frappe/email/doctype/email_alert/email_alert.py @@ -217,7 +217,14 @@ def evaluate_alert(doc, alert, event): return if event=="Value Change" and not doc.is_new(): - db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) + try: + db_value = frappe.db.get_value(doc.doctype, doc.name, alert.value_changed) + except frappe.DatabaseOperationalError as e: + if e.args[0]==1054: + alert.db_set('enabled', 0) + frappe.log_error('Email Alert {0} has been disabled due to missing field'.format(alert.name)) + return + db_value = parse_val(db_value) if (doc.get(alert.value_changed) == db_value) or \ (not db_value and not doc.get(alert.value_changed)): diff --git a/frappe/email/doctype/email_alert/test_email_alert.py b/frappe/email/doctype/email_alert/test_email_alert.py index 57686dd984..1f7c457ae2 100755 --- a/frappe/email/doctype/email_alert/test_email_alert.py +++ b/frappe/email/doctype/email_alert/test_email_alert.py @@ -87,6 +87,36 @@ class TestEmailAlert(unittest.TestCase): self.assertTrue(frappe.db.get_value("Email Queue", {"reference_doctype": "Event", "reference_name": event.name, "status":"Not Sent"})) + def test_alert_disabled_on_wrong_field(self): + frappe.set_user('Administrator') + email_alert = frappe.get_doc({ + "doctype": "Email Alert", + "subject":"_Test Email Alert for wrong field", + "document_type": "Event", + "event": "Value Change", + "attach_print": 0, + "value_changed": "description1", + "message": "Description changed", + "recipients": [ + { "email_by_document_field": "owner" } + ] + }).insert() + + event = frappe.new_doc("Event") + event.subject = "test-2", + event.event_type = "Private" + event.starts_on = "2014-06-06 12:00:00" + event.insert() + event.subject = "test 1" + event.save() + + # verify that email_alert is disabled + email_alert.reload() + self.assertEqual(email_alert.enabled, 0) + email_alert.delete() + event.delete() + + def test_date_changed(self): event = frappe.new_doc("Event") event.subject = "test", diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 1a5f6d688e..723c602496 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals from werkzeug.exceptions import NotFound from MySQLdb import ProgrammingError as SQLError, Error +from MySQLdb import OperationalError as DatabaseOperationalError class ValidationError(Exception): diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index d8384eee09..72a9a77e3c 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -25,16 +25,21 @@ def make_mapped_doc(method, source_name, selected_children=None): return method(source_name) +@frappe.whitelist() +def map_docs(method, source_names, target_doc): + '''Returns the mapped document calling the given mapper method + with each of the given source docs on the target doc''' + method = frappe.get_attr(method) + if method not in frappe.whitelisted: + raise frappe.PermissionError + + for src in json.loads(source_names): + target_doc = method(src, target_doc) + return target_doc def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, postprocess=None, ignore_permissions=False, ignore_child_tables=False): - source_doc = frappe.get_doc(from_doctype, from_docname) - - if not ignore_permissions: - if not source_doc.has_permission("read"): - source_doc.raise_no_permission_to("read") - # main if not target_doc: target_doc = frappe.new_doc(table_maps[from_doctype]["doctype"]) @@ -44,6 +49,12 @@ def get_mapped_doc(from_doctype, from_docname, table_maps, target_doc=None, if not ignore_permissions and not target_doc.has_permission("create"): target_doc.raise_no_permission_to("create") + source_doc = frappe.get_doc(from_doctype, from_docname) + + if not ignore_permissions: + if not source_doc.has_permission("read"): + source_doc.raise_no_permission_to("read") + map_doc(source_doc, target_doc, table_maps[source_doc.doctype]) row_exists_for_parentfield = {} diff --git a/frappe/model/naming.py b/frappe/model/naming.py index f6a947cf4b..dbe236f09e 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -94,12 +94,15 @@ def make_autoname(key='', doctype='', doc=''): elif not "." in key: frappe.throw(_("Invalid naming series (. missing)") + (_(" for {0}").format(doctype) if doctype else "")) + parts = key.split('.') + n = parse_naming_series(parts, doctype, doc) + return n + +def parse_naming_series(parts, doctype= '', doc = ''): n = '' - l = key.split('.') series_set = False today = now_datetime() - - for e in l: + for e in parts: part = '' if e.startswith('#'): if not series_set: @@ -120,6 +123,7 @@ def make_autoname(key='', doctype='', doc=''): if isinstance(part, basestring): n+=part + return n def getseries(key, digits, doctype=''): diff --git a/frappe/patches.txt b/frappe/patches.txt index 285c96f4aa..1b2dab39a2 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -12,7 +12,7 @@ frappe.patches.v8_0.drop_in_dialog execute:frappe.reload_doc('core', 'doctype', 'docfield', force=True) #2017-03-03 execute:frappe.reload_doc('core', 'doctype', 'docperm') #2017-03-03 frappe.patches.v8_0.drop_is_custom_from_docperm -frappe.patches.v8_0.update_records_in_global_search +frappe.patches.v8_0.update_records_in_global_search #11-05-2017 frappe.patches.v8_0.update_published_in_global_search execute:frappe.reload_doc('core', 'doctype', 'custom_docperm') execute:frappe.reload_doc('core', 'doctype', 'deleted_document') diff --git a/frappe/patches/v8_0/update_records_in_global_search.py b/frappe/patches/v8_0/update_records_in_global_search.py index 397128c64f..8ee8f36de8 100644 --- a/frappe/patches/v8_0/update_records_in_global_search.py +++ b/frappe/patches/v8_0/update_records_in_global_search.py @@ -1,5 +1,7 @@ +import frappe from frappe.utils.global_search import get_doctypes_with_global_search, rebuild_for_doctype def execute(): + frappe.cache().delete_value('doctypes_with_global_search') for doctype in get_doctypes_with_global_search(with_child_tables=False): rebuild_for_doctype(doctype) diff --git a/frappe/permissions.py b/frappe/permissions.py index 2455f8428a..22342d9811 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -330,7 +330,8 @@ def get_all_perms(role): '''Returns valid permissions for a given role''' perms = frappe.get_all('DocPerm', fields='*', filters=dict(role=role)) custom_perms = frappe.get_all('Custom DocPerm', fields='*', filters=dict(role=role)) - doctypes_with_custom_perms = list(set(p.parent for p in custom_perms)) + doctypes_with_custom_perms = frappe.db.sql_list("""select distinct parent + from `tabCustom DocPerm`""") for p in perms: if p.parent not in doctypes_with_custom_perms: diff --git a/frappe/public/build.json b/frappe/public/build.json index 11bd5ea34e..598b964574 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -30,6 +30,7 @@ "public/js/frappe/ui/field_group.js", "public/js/frappe/form/control.js", "public/js/frappe/form/link_selector.js", + "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js" ], "css/desk.min.css": [ @@ -104,6 +105,7 @@ "public/js/frappe/ui/field_group.js", "public/js/frappe/form/control.js", "public/js/frappe/form/link_selector.js", + "public/js/frappe/form/multi_select_dialog.js", "public/js/frappe/ui/dialog.js", "public/js/frappe/ui/app_icon.js", diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css index 76d352a6b9..302fe38647 100644 --- a/frappe/public/css/desk.css +++ b/frappe/public/css/desk.css @@ -978,6 +978,13 @@ input[type="checkbox"]:checked:before { font-size: 13px; color: #3b99fc; } +.multiselect-empty-state { + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} @-moz-document url-prefix() { input[type="checkbox"] { visibility: visible; diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index a296762426..54a9c44b96 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -447,8 +447,9 @@ frappe.ui.form.Timeline = Class.extend({ var parts = [], count = 0; data.row_changed.every(function(row) { row[3].every(function(p) { - var df = frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, - p[0], me.frm.docname); + var df = me.frm.fields_dict[row[0]] && + frappe.meta.get_docfield(me.frm.fields_dict[row[0]].grid.doctype, + p[0], me.frm.docname); if(df && !df.hidden) { field_display_status = frappe.perm.get_field_display_status(df, diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js new file mode 100644 index 0000000000..d3548c19d6 --- /dev/null +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -0,0 +1,212 @@ +// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// MIT License. See license.txt + +frappe.ui.form.MultiSelectDialog = Class.extend({ + init: function(opts) { + /* Options: doctype, target, setters, get_query, action */ + $.extend(this, opts); + + var me = this; + if(this.doctype!="[Select]") { + frappe.model.with_doctype(this.doctype, function(r) { + me.make(); + }); + } else { + this.make(); + } + }, + make: function() { + let me = this; + + let fields = []; + let count = 0; + if(!this.date_field) { + this.date_field = "transaction_date"; + } + Object.keys(this.setters).forEach(function(setter) { + fields.push({ + fieldtype: me.target.fields_dict[setter].df.fieldtype, + label: me.target.fields_dict[setter].df.label, + fieldname: setter, + options: me.target.fields_dict[setter].df.options, + default: me.setters[setter] + }); + if (count++ < Object.keys(me.setters).length - 1) { + fields.push({fieldtype: "Column Break"}); + } + }); + + fields = fields.concat([ + { fieldtype: "Section Break" }, + { fieldtype: "HTML", fieldname: "results_area" }, + { fieldtype: "Button", fieldname: "make_new", label: __("Make a new " + me.doctype) } + ]); + + let doctype_plural = !this.doctype.endsWith('y') ? this.doctype + 's' + : this.doctype.slice(0, -1) + 'ies'; + + this.dialog = new frappe.ui.Dialog({ + title: __("Select {0}", [(this.doctype=='[Select]') ? __("value") : __(doctype_plural)]), + fields: fields, + primary_action_label: __("Get Items"), + primary_action: function() { + me.action(me.get_checked_values(), me.args); + } + }); + + this.$parent = $(this.dialog.body); + this.$wrapper = this.dialog.fields_dict.results_area.$wrapper.append(`
`); + this.$results = this.$wrapper.find('.results'); + this.$make_new_btn = this.dialog.fields_dict.make_new.$wrapper; + + this.$placeholder = $(`No ${this.doctype} found
+ + +