diff --git a/frappe/__init__.py b/frappe/__init__.py index d3d9c31d43..743533c0ac 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -15,7 +15,7 @@ from past.builtins import cmp from .exceptions import * from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template -__version__ = '10.1.6' +__version__ = '10.1.10' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 00b9918933..8acececa03 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -88,7 +88,7 @@ def make(doctype=None, name=None, content=None, subject=None, sent_or_received = frappe.db.commit() if cint(send_email): - frappe.flags.print_letterhead = print_letterhead + frappe.flags.print_letterhead = cint(print_letterhead) comm.send(print_html, print_format, attachments, send_me_a_copy=send_me_a_copy) return { diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 98b657d71c..ca1fc6cd3f 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -29,7 +29,7 @@ def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data= select_columns=select_columns, file_type=file_type, template=template, filters=filters) exporter.build_response() -class DataExporter(): +class DataExporter: def __init__(self, doctype=None, parent_doctype=None, all_doctypes=True, with_data=False, select_columns=None, file_type='CSV', template=False, filters=None): self.doctype = doctype @@ -98,10 +98,9 @@ class DataExporter(): self.add_data() if self.with_data and not self.data: frappe.respond_as_web_page(_('No Data'), _('There is no data to be exported'), indicator_color='orange') - return if self.file_type == 'Excel': - return self.build_response_as_excel() + self.build_response_as_excel() else: # write out response as a type csv frappe.response['result'] = cstr(self.writer.getvalue()) @@ -323,7 +322,7 @@ class DataExporter(): def build_response_as_excel(self): filename = frappe.generate_hash("", 10) with open(filename, 'wb') as f: - f.write(cstr(self.writer.getvalue()).encode("utf-8")) + f.write(cstr(self.writer.getvalue()).encode('utf-8')) f = open(filename) reader = csv.reader(f) diff --git a/frappe/core/doctype/transaction_log/test_transaction_log.py b/frappe/core/doctype/transaction_log/test_transaction_log.py index 43327b55f8..164a683c38 100644 --- a/frappe/core/doctype/transaction_log/test_transaction_log.py +++ b/frappe/core/doctype/transaction_log/test_transaction_log.py @@ -34,6 +34,9 @@ class TestTransactionLog(unittest.TestCase): sha = hashlib.sha256() - sha.update(str(third_log.transaction_hash) + str(second_log.chaining_hash)) + sha.update( + frappe.safe_encode(str(third_log.transaction_hash)) + + frappe.safe_encode(str(second_log.chaining_hash)) + ) self.assertEqual(sha.hexdigest(), third_log.chaining_hash) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 3a620dae44..8f528edcec 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -68,6 +68,7 @@ class User(Document): self.validate_user_email_inbox() ask_pass_update() self.validate_roles() + self.validate_user_image() if self.language == "Loading...": self.language = None @@ -81,6 +82,10 @@ class User(Document): self.set('roles', []) self.append_roles(*[role.role for role in role_profile.roles]) + def validate_user_image(self): + if self.user_image and len(self.user_image) > 2000: + frappe.throw(_("Not a valid User Image.")) + def on_update(self): # clear new password self.validate_user_limit() diff --git a/frappe/database.py b/frappe/database.py index 07ff5cb32f..939a82fea7 100644 --- a/frappe/database.py +++ b/frappe/database.py @@ -17,6 +17,7 @@ from frappe import _ from frappe.model.utils.link_count import flush_local_link_count from frappe.utils.background_jobs import execute_job, get_queue from frappe import as_unicode +import six # imports - compatibility imports from six import ( @@ -81,10 +82,14 @@ class Database: conversions.update({ FIELD_TYPE.NEWDECIMAL: float, FIELD_TYPE.DATETIME: get_datetime, - TimeDelta: conversions[binary_type], UnicodeWithAttrs: conversions[text_type] }) + if six.PY2: + conversions.update({ + TimeDelta: conversions[binary_type] + }) + if usessl: self._conn = pymysql.connect(self.host, self.user or '', self.password or '', charset='utf8mb4', use_unicode = True, ssl=self.ssl, conv = conversions, local_infile = self.local_infile) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js index 2e3c707a5e..c264d12dec 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.js @@ -3,6 +3,7 @@ frappe.ui.form.on('Dropbox Settings', { refresh: function(frm) { + frm.toggle_display(["app_access_key", "app_secret_key"], !(frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)); frm.clear_custom_buttons(); frm.events.take_backup(frm); }, @@ -19,7 +20,7 @@ frappe.ui.form.on('Dropbox Settings', { } }) } - else if (frm.doc.dropbox_setup_via_site_config) { + else if (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config) { frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_redirect_url", freeze: true, @@ -36,7 +37,7 @@ frappe.ui.form.on('Dropbox Settings', { }, take_backup: function(frm) { - if ((frm.doc.app_access_key && frm.doc.app_secret_key) || frm.doc.dropbox_setup_via_site_config){ + if ((frm.doc.app_access_key && frm.doc.app_secret_key) || (frm.doc.__onload && frm.doc.__onload.dropbox_setup_via_site_config)){ frm.add_custom_button(__("Take Backup Now"), function(frm){ frappe.call({ method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json index cbe1fe8b1a..a8ab581bf7 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.json @@ -54,7 +54,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Send Notifications To", "length": 0, @@ -84,7 +84,7 @@ "ignore_xss_filter": 0, "in_filter": 0, "in_global_search": 0, - "in_list_view": 0, + "in_list_view": 1, "in_standard_filter": 0, "label": "Backup Frequency", "length": 0, @@ -108,7 +108,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:!doc.dropbox_setup_via_site_config", + "depends_on": "", "fieldname": "app_access_key", "fieldtype": "Data", "hidden": 0, @@ -139,7 +139,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "depends_on": "eval:!doc.dropbox_setup_via_site_config", + "depends_on": "", "fieldname": "app_secret_key", "fieldtype": "Password", "hidden": 0, @@ -283,36 +283,6 @@ "search_index": 0, "set_only_once": 0, "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "dropbox_setup_via_site_config", - "fieldtype": "Check", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Dropbox Setup via Site Config", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 } ], "has_web_view": 0, @@ -325,7 +295,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-06-20 15:45:33.683827", + "modified": "2018-03-22 16:02:00.597029", "modified_by": "Administrator", "module": "Integrations", "name": "Dropbox Settings", diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 35027b90c9..283546320f 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -20,7 +20,8 @@ ignore_list = [".DS_Store"] class DropboxSettings(Document): def onload(self): if not self.app_access_key and frappe.conf.dropbox_access_key: - self.dropbox_setup_via_site_config = 1 + self.set_onload("dropbox_setup_via_site_config", 1) + @frappe.whitelist() def take_backup(): @@ -171,7 +172,7 @@ def upload_file_to_dropbox(filename, folder, dropbox_client): cursor.offset = f.tell() except dropbox.exceptions.ApiError as e: if isinstance(e.error, dropbox.files.UploadError): - error = "File Path: {path}\n".foramt(path=path) + error = "File Path: {path}\n".format(path=path) error += frappe.get_traceback() frappe.log_error(error) else: @@ -201,7 +202,7 @@ def get_dropbox_settings(redirect_uri=False): if redirect_uri: app_details.update({ - 'rediret_uri': get_request_site_address(True) \ + 'redirect_uri': get_request_site_address(True) \ + '/api/method/frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish' \ if settings.app_secret_key else frappe.conf.dropbox_broker_site\ + '/api/method/dropbox_erpnext_broker.www.setup_dropbox.generate_dropbox_access_token', @@ -233,7 +234,7 @@ def get_dropbox_authorize_url(): dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( app_details["app_key"], app_details["app_secret"], - app_details["rediret_uri"], + app_details["redirect_uri"], {}, "dropbox-auth-csrf-token" ) @@ -254,7 +255,7 @@ def dropbox_auth_finish(return_access_token=False): dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( app_details["app_key"], app_details["app_secret"], - app_details["rediret_uri"], + app_details["redirect_uri"], { 'dropbox-auth-csrf-token': callback.state }, diff --git a/frappe/integrations/doctype/stripe_settings/stripe_settings.py b/frappe/integrations/doctype/stripe_settings/stripe_settings.py index c96b508743..62d90cbe53 100644 --- a/frappe/integrations/doctype/stripe_settings/stripe_settings.py +++ b/frappe/integrations/doctype/stripe_settings/stripe_settings.py @@ -23,12 +23,18 @@ class StripeSettings(Document): "XAF", "XOF", "XPF", "YER", "ZAR" ] + currency_wise_minimum_charge_amount = { + 'JPY': 50, 'MXN': 10, 'DKK': 2.50, 'HKD': 4.00, 'NOK': 3.00, 'SEK': 3.00, + 'USD': 0.50, 'AUD': 0.50, 'BRL': 0.50, 'CAD': 0.50, 'CHF': 0.50, 'EUR': 0.50, + 'GBP': 0.30, 'NZD': 0.50, 'SGD': 0.50 + } + def validate(self): create_payment_gateway('Stripe') call_hook_method('payment_gateway_enabled', gateway='Stripe') if not self.flags.ignore_mandatory: self.validate_stripe_credentails() - + def validate_stripe_credentails(self): if self.publishable_key and self.secret_key: header = {"Authorization": "Bearer {0}".format(self.get_password(fieldname="secret_key", raise_exception=False))} @@ -36,11 +42,17 @@ class StripeSettings(Document): make_get_request(url="https://api.stripe.com/v1/charges", headers=header) except Exception: frappe.throw(_("Seems Publishable Key or Secret Key is wrong !!!")) - + def validate_transaction_currency(self, currency): if currency not in self.supported_currencies: frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency)) + def validate_minimum_transaction_amount(self, currency, amount): + if currency in self.currency_wise_minimum_charge_amount: + if flt(amount) < self.currency_wise_minimum_charge_amount.get(currency, 0.0): + frappe.throw(_("For currency {0}, the minimum transaction amount should be {1}").format(currency, + self.currency_wise_minimum_charge_amount.get(currency, 0.0))) + def get_payment_url(self, **kwargs): return get_url("./integrations/stripe_checkout?{0}".format(urlencode(kwargs))) diff --git a/frappe/model/naming.py b/frappe/model/naming.py index f6f7fd57a2..70eb44abcd 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -47,7 +47,7 @@ def set_new_name(doc): if autoname.startswith("naming_series:"): set_name_by_naming_series(doc) elif "#" in autoname: - doc.name = make_autoname(autoname) + doc.name = make_autoname(autoname, doc=doc) elif autoname.lower()=='prompt': # set from __newname in save.py if not doc.name: diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index 584c441db8..be9356175f 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -33,12 +33,19 @@ frappe.dom = { document.getElementsByTagName('head')[0].appendChild(el); }, remove_script_and_style: function(txt) { + const evil_tags = ["script", "style", "noscript", "title", "meta", "base", "head"]; + const regex = new RegExp(evil_tags.map(tag => `<${tag}>.*<\\/${tag}>`).join('|')); + if (!regex.test(txt)) { + // no evil tags found, skip the DOM method entirely! + return txt; + } + var div = document.createElement('div'); div.innerHTML = txt; var found = false; - ["script", "style", "noscript", "title", "meta", "base", "head"].forEach(function(e, i) { + evil_tags.forEach(function(e) { var elements = div.getElementsByTagName(e); - var i = elements.length; + i = elements.length; while (i--) { found = true; elements[i].parentNode.removeChild(elements[i]); diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js index 39131e8406..23a8b41129 100644 --- a/frappe/public/js/frappe/form/save.js +++ b/frappe/public/js/frappe/form/save.js @@ -19,6 +19,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { var save = function () { remove_empty_rows(); + $(frm.wrapper).addClass('validated-form'); if (check_mandatory()) { _call({ @@ -127,9 +128,8 @@ frappe.ui.form.save = function (frm, action, callback, btn) { if (df.reqd && !frappe.model.has_value(doc.doctype, doc.name, df.fieldname)) { has_errors = true; error_fields[error_fields.length] = __(df.label); - // scroll to field - if (!me.scroll_set) { + if (!frm.scroll_set) { scroll_to(doc.parentfield || df.fieldname); } @@ -141,6 +141,7 @@ frappe.ui.form.save = function (frm, action, callback, btn) { } }); + if (error_fields.length) { if (doc.parenttype) { var message = __('Mandatory fields required in table {0}, Row {1}', diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index a924522052..d842885bac 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -225,7 +225,7 @@ frappe.views.BaseList = class BaseList { } setup_result_area() { - this.$result = $(`
`).hide(); + this.$result = $(`
`); this.$frappe_list.append(this.$result); } @@ -531,19 +531,6 @@ class FilterArea { } make_standard_filters() { - $( - `
- -
` - ) - .css({ - height: '30px', - width: '20px', - marginRight: '-2px', - marginLeft: '10px' - }) - .prependTo(this.standard_filters_wrapper); - let fields = [ { fieldtype: 'Data', @@ -602,6 +589,21 @@ class FilterArea { } fields.map(df => this.list_view.page.add_field(df)); + + // search icon in name filter + $('') + .appendTo(this.list_view.page.fields_dict.name.$wrapper) + .css({ + 'position': 'absolute', + 'z-index': '1', + 'right': '7px', + 'top': '9px', + 'font-size': '90%' + }); + + this.list_view.page.fields_dict.name.$wrapper + .find('.form-control') + .css('padding-right', '2em'); } get_standard_filters() { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 2e017e80da..ed70c71d5d 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -145,6 +145,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { setup_view() { this.setup_columns(); + this.render_header(); + this.render_skeleton(); this.setup_events(); this.settings.onload && this.settings.onload(this); } @@ -233,10 +235,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
`; } - freeze(show) { - this.$result.find('.list-header-meta').html(__('Refreshing') + '...'); - this.$result.find('.checkbox-actions').toggle(show); - this.$result.find('.list-header-subject').toggle(!show); + freeze() { + this.$result.find('.list-count').html(`${__('Refreshing')}...`); } get_args() { @@ -264,6 +264,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } } + render_header() { + if (this.$result.find('.list-row-head').length === 0) { + // append header once + this.$result.prepend(this.get_header_html()); + } + } + + render_skeleton() { + const $row = this.get_list_row_html_skeleton('
'); + this.$result.append($row.repeat(3)); + } + before_render() { this.settings.before_render && this.settings.before_render(); frappe.model.user_settings.save(this.doctype, 'last_view', this.view_name); @@ -274,12 +286,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } render() { + this.$result.find('.list-row-container').remove(); if (this.data.length > 0) { - this.$result.find('.list-row-container').remove(); - if (this.$result.find('.list-row-head').length === 0) { - // append header once - this.$result.prepend(this.get_header_html()); - } // append rows this.$result.append( this.data.map(doc => this.get_list_row_html(doc)).join('') diff --git a/frappe/public/js/frappe/model/sync.js b/frappe/public/js/frappe/model/sync.js index 7e6f377017..9be70f9a15 100644 --- a/frappe/public/js/frappe/model/sync.js +++ b/frappe/public/js/frappe/model/sync.js @@ -19,7 +19,12 @@ $.extend(frappe.model, { for(var i=0, l=r.docs.length; i doc[fieldname].length) { + for (let i = doc[fieldname].length; i < local_doc[fieldname].length; i++) { + + // clear from local + let d = local_doc[fieldname][i]; + if (locals[d.doctype] && locals[d.doctype][d.name]) { + delete locals[d.doctype][d.name]; + } + } + local_doc[fieldname].length = doc[fieldname].length; + + + } + } else { + // literal + local_doc[fieldname] = doc[fieldname]; + } + } } }); diff --git a/frappe/public/js/frappe/upload.js b/frappe/public/js/frappe/upload.js index 3eb203c5b3..7704b6fd65 100644 --- a/frappe/public/js/frappe/upload.js +++ b/frappe/public/js/frappe/upload.js @@ -313,26 +313,38 @@ frappe.upload = { if (opts.no_socketio || frappe.flags.no_socketio || file_not_big_enough) { upload_with_filedata(); return; + } else { + args.file_size = fileobj.size; + frappe.call({ + method: 'frappe.utils.file_manager.validate_filename', + args: {"filename": args.filename}, + callback: function(r) { + args.filename = r.message; + upload_through_socketio(); + } + }); } - frappe.socketio.uploader.start({ - file: fileobj, - filename: args.filename, - is_private: args.is_private, - fallback: () => { - // if fails, use old filereader - upload_with_filedata(); - }, - callback: (data) => { - args.file_url = data.file_url; - frappe.upload._upload_file(fileobj, args, opts); - }, - on_progress: (percent_complete) => { - let increment = (flt(percent_complete) / frappe.upload.total_files); - frappe.show_progress(__('Uploading'), - start_complete + increment); - } - }); + var upload_through_socketio = function() { + frappe.socketio.uploader.start({ + file: fileobj, + filename: args.filename, + is_private: args.is_private, + fallback: () => { + // if fails, use old filereader + upload_with_filedata(); + }, + callback: (data) => { + args.file_url = data.file_url; + frappe.upload._upload_file(fileobj, args, opts); + }, + on_progress: (percent_complete) => { + let increment = (flt(percent_complete) / frappe.upload.total_files); + frappe.show_progress(__('Uploading'), + start_complete + increment); + } + }); + } }, upload_to_server: function(file, args, opts) { diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 06d61c4373..bbbd42aee9 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -555,10 +555,10 @@ frappe.views.CommunicationComposer = Class.extend({ is_print_letterhead_checked: function() { if (this.frm && $(this.frm.wrapper).find('.form-print-wrapper').is(':visible')){ - return $(this.frm.wrapper).find('.print-letterhead').prop('checked'); + return $(this.frm.wrapper).find('.print-letterhead').prop('checked') ? 1 : 0; } else { return (frappe.model.get_doc(":Print Settings", "Print Settings") || - { with_letterhead: 1 }).with_letterhead ? true : false; + { with_letterhead: 1 }).with_letterhead ? 1 : 0; } }, diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html index 4ec8184e77..c559a234af 100644 --- a/frappe/public/js/frappe/views/reports/print_grid.html +++ b/frappe/public/js/frappe/views/reports/print_grid.html @@ -3,6 +3,10 @@

{{ __(title) }}


{% endif %} +{% if subtitle %} +{{ subtitle }} +
+{% endif %} @@ -24,7 +28,7 @@ {% for col in columns %} {% if col.name && col._id !== "_check" %} - {% var value = col.fieldname ? row[col.fieldname] : row[col.field]; %} + {% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %}
{{ col.formatter diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index d9789abac7..626de0c5e1 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -2,10 +2,10 @@ // MIT License. See license.txt import DataTable from 'frappe-datatable'; -frappe.provide("frappe.views"); -frappe.provide("frappe.query_reports"); +frappe.provide('frappe.views'); +frappe.provide('frappe.query_reports'); -frappe.standard_pages["query-report"] = function() { +frappe.standard_pages['query-report'] = function() { var wrapper = frappe.container.add_page('query-report'); frappe.ui.make_app_page({ @@ -18,7 +18,7 @@ frappe.standard_pages["query-report"] = function() { parent: wrapper, }); - $(wrapper).bind("show", function() { + $(wrapper).bind('show', function() { frappe.query_report.show(); }); }; @@ -86,7 +86,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { frappe.run_serially([ () => this.get_report_doc(), () => this.get_report_settings(), - () => this.report_settings.onload && this.report_settings.onload(this), () => this.setup_page_head(), () => this.refresh_report() ]); @@ -98,6 +97,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return frappe.run_serially([ () => this.setup_filters(), () => this.set_route_filters(), + () => this.report_settings.onload && this.report_settings.onload(this), () => this.get_user_settings(), () => this.refresh(), () => this.save_user_settings() @@ -208,8 +208,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.toggle_message(true); const filters = this.get_filter_values(true); return new Promise(resolve => frappe.call({ - method: "frappe.desk.query_report.run", - type: "GET", + method: 'frappe.desk.query_report.run', + type: 'GET', args: { report_name: this.report_name, filters: filters @@ -230,22 +230,21 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } render_report(data) { - this._data = data.result; - this._columns = this.prepare_columns(data.columns); - this.is_tree_report = this._data.some(d => 'indent' in d); - - const columns = this.get_columns_for_datatable(); + this.columns = this.prepare_columns(data.columns); + this.data = this.prepare_data(data.result); + this.tree_report = this.data.some(d => 'indent' in d); + const columns = this.get_visible_columns(); if (this.datatable) { - this.datatable.refresh(this._data, columns); + this.datatable.refresh(this.data, columns); return; } this.datatable = new DataTable(this.$report[0], { columns: columns, - data: this._data, + data: this.data, inlineFilters: true, - treeView: this.is_tree_report, + treeView: this.tree_report, layout: 'fixed', events: { onRemoveColumn: () => this.save_user_settings(), @@ -280,7 +279,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { save_user_settings(clear_settings = false) { if (clear_settings) { - return frappe.model.user_settings.remove(this.report_name, 'column_order'); + return frappe.model.user_settings.save(this.report_name, 'column_order', []); } if (!this.datatable) return; const column_order = this.datatable.datamanager.getColumns(true).map(col => col.id); @@ -298,48 +297,58 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { [fieldtype, options] = fieldtype.split('/'); } - return { + column = { label, fieldname: label, fieldtype, width, options }; + } else { + column = { + label: column, + fieldname: column, + fieldtype: 'Data' + }; } - - return { - label: column, - fieldname: column, - fieldtype: 'Data' - }; } - return column; + return Object.assign(column, { + id: column.fieldname, + name: column.label, + width: parseInt(column.width) || null, + editable: false, + format: (value, row, column, data) => + frappe.format(value || '', column, + {for_print: false, always_show_decimals: true}, data) + }); }); } - get_columns_for_datatable() { - const columns = this._columns.map(df => { - return { - id: df.fieldname, - name: df.label, - width: df.width || null, - editable: false, - format: (value, row, column, data) => - frappe.format(value || '', df, - {for_print: false, always_show_decimals: true}, data) - }; + prepare_data(data) { + return data.map(row => { + let row_obj = {}; + if (Array.isArray(row)) { + this.columns.forEach((column, i) => { + row_obj[column.id] = row[i] || null; + }); + + return row_obj; + } + return row; }); + } - return columns; + get_visible_columns() { + // return columns according to user_settings - // if (this.user_settings.column_order && this.user_settings.column_order.length > 0) { - // return this.user_settings.column_order - // .map(id => columns.find(col => col.id === id)) - // .filter(Boolean); - // } else { - // return columns; - // } + if (this.user_settings.column_order && this.user_settings.column_order.length > 0) { + return this.user_settings.column_order + .map(id => this.columns.find(col => col.id === id)) + .filter(Boolean); + } else { + return this.columns; + } } get_filter_values(raise) { @@ -348,8 +357,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { if (raise && missing_mandatory.length > 0) { // this.chart_area.hide(); - // this.wrapper.find(".waiting-area").empty().toggle(false); - // this.$no_result.html(__("Please set filters")).show(); + // this.wrapper.find('.waiting-area').empty().toggle(false); + // this.$no_result.html(__('Please set filters')).show(); if (raise) { frappe.throw(__('Filter missing: {0}', [missing_mandatory.map(f => f.df.label).join(', ')])); } @@ -381,14 +390,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } print_report(print_settings) { - const columns = this.get_columns_for_print(); + const custom_format = this.report_settings.html_format || null; + const filters_html = this.get_filters_html_for_print(); + frappe.render_grid({ - template: this.report_settings.html_format || null, + template: custom_format, title: __(this.report_name), + subtitle: filters_html, print_settings: print_settings, filters: this.get_filter_values(), - data: this.get_data_for_print(), - columns: columns, + data: custom_format ? this.data : this.get_data_for_print(), + columns: custom_format ? this.columns: this.get_visible_columns(), report: this }); } @@ -396,49 +408,48 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { pdf_report(print_settings) { const base_url = frappe.urllib.get_base_url(); const print_css = frappe.boot.print_css; - const landscape = print_settings.orientation == "Landscape"; - const columns = this.columns; + const landscape = print_settings.orientation == 'Landscape'; - let html; - if (this.report_settings.html_format) { - const content = frappe.render(this.report_settings.html_format, { - data: this.get_data_for_print(), - filters: this.get_filter_values(), - report: this, - }); + const custom_format = this.report_settings.html_format || null; + const columns = custom_format ? this.columns : this.get_visible_columns(); + const data = custom_format ? this.data : this.get_data_for_print(); + const applied_filters = this.get_filter_values(); - //Render Report in HTML - html = frappe.render_template("print_template", { - title:__(this.report_name), - content: content, - base_url: base_url, - print_css: print_css, - print_settings: print_settings, - landscape: landscape, - columns: columns - }); - } else { - const content = frappe.render_template("print_grid", { - title: __(this.report_name), - data: this.get_data_for_print(), - columns: columns - }); + const filters_html = this.get_filters_html_for_print(); + const content = frappe.render_template(custom_format || 'print_grid', { + title: __(this.report_name), + subtitle: filters_html, + filters: applied_filters, + data: data, + columns: columns, + report: this + }); - //Render Report in HTML - html = frappe.render_template("print_template", { - content: content, - title: __(this.report_name), - base_url: base_url, - print_css: print_css, - print_settings: print_settings, - landscape: landscape, - columns: columns - }); - } + // Render Report in HTML + const html = frappe.render_template('print_template', { + title: __(this.report_name), + content: content, + base_url: base_url, + print_css: print_css, + print_settings: print_settings, + landscape: landscape, + columns: columns + }); frappe.render_pdf(html, print_settings); } + get_filters_html_for_print() { + const applied_filters = this.get_filter_values(); + return Object.keys(applied_filters) + .map(filter_name => { + const label = frappe.query_report_filters_by_name[filter_name].df.label; + const value = applied_filters[filter_name]; + return `
${__(label)}: ${value}
`; + }) + .join(''); + } + export_report() { if (this.export_dialog) { this.export_dialog.clear(); @@ -455,8 +466,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { reqd: 1 }, ({ file_format }) => { if (file_format === 'CSV') { - const column_row = this._columns.map(col => col.label); - const data = this.get_data_for_print(); + const column_row = this.columns.map(col => col.label); + const data = this.get_data_for_csv(); const out = [column_row].concat(data); frappe.tools.downloadify(out, null, this.report_name); @@ -473,17 +484,22 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { open_url_post(frappe.request.url, args); } - }, __("Export Report: "+ this.report_name), __("Download")); + }, __('Export Report: '+ this.report_name), __('Download')); } - get_data_for_print() { + get_data_for_csv() { const indices = this.datatable.datamanager.getFilteredRowIndices(); const out = indices.map(i => this.datatable.datamanager.getRow(i).map(c => c.content)); return out.map(row => row.slice(1)); } + get_data_for_print() { + const indices = this.datatable.datamanager.getFilteredRowIndices(); + return indices.map(i => this.data[i]); + } + get_columns_for_print() { - return this._columns || []; + return this.columns || []; } get_menu_items() { @@ -495,7 +511,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }, { label: __('Edit'), - action: () => frappe.set_route("Form", "Report", this.report_name), + action: () => frappe.set_route('Form', 'Report', this.report_name), condition: () => frappe.user.is_report_manager(), standard: true }, @@ -536,7 +552,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { { label: __('User Permissions'), action: () => frappe.set_route('List', 'User Permission', { - doctype: "Report", + doctype: 'Report', name: this.report_name }), condition: () => frappe.model.can_set_user_permissions('Report'), @@ -557,7 +573,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { setup_page_head() { super.setup_page_head(); - this.page.set_title_sub(``); + this.page.set_title_sub(``); } setup_report_wrapper() { @@ -568,7 +584,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } message_div(message) { - return `
+ return `
${message}
`; } @@ -596,12 +612,4 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { get get_values() { return this.get_filter_values; } - - get data() { - return this._data; - } - - get columns() { - return this._columns; - } -}; \ No newline at end of file +}; diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index 4cd5100327..998681d18c 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -197,12 +197,14 @@ _f.Frm.prototype.watch_model_updates = function() { frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { // set input if(doc.name===me.docname) { - if ((value==='' || value===null) && !doc[value]) { + if ((value==='' || value===null) && !doc[fieldname]) { // both the incoming and outgoing values are falsy // the texteditor, summernote, changes nulls to empty strings on render, // so ignore those changes } else { - me.dirty(); + if (value != doc[fieldname]) { + me.dirty(); + } } me.fields_dict[fieldname] && me.fields_dict[fieldname].refresh(fieldname); @@ -764,53 +766,60 @@ _f.Frm.prototype._save = function(save_action, callback, btn, on_error, resolve) _f.Frm.prototype.savesubmit = function(btn, callback, on_error) { var me = this; + let handle_fail = () => { + $(btn).prop('disabled', false); + if (on_error) { + on_error(); + } + } + return new Promise(resolve => { this.validate_form_action("Submit"); frappe.confirm(__("Permanently Submit {0}?", [this.docname]), function() { frappe.validated = true; me.script_manager.trigger("before_submit").then(function() { if(!frappe.validated) { - if(on_error) { - on_error(); - } + handle_fail(); return; } me.save('Submit', function(r) { if(r.exc) { - if (on_error) { - on_error(); - } + handle_fail(); } else { frappe.utils.play_sound("submit"); callback && callback(); me.script_manager.trigger("on_submit") .then(() => resolve(me)); } - }, btn, on_error, resolve); + }, btn, () => handle_fail(), resolve); }); - }, on_error); + }, () => handle_fail() ); }); }; _f.Frm.prototype.savecancel = function(btn, callback, on_error) { var me = this; + + let handle_fail = () => { + $(btn).prop('disabled', false); + if (on_error) { + on_error(); + } + } + this.validate_form_action('Cancel'); frappe.confirm(__("Permanently Cancel {0}?", [this.docname]), function() { frappe.validated = true; me.script_manager.trigger("before_cancel").then(function() { if(!frappe.validated) { - if(on_error) { - on_error(); - } + handle_fail(); return; } var after_cancel = function(r) { if(r.exc) { - if (on_error) { - on_error(); - } + handle_fail(); } else { frappe.utils.play_sound("cancel"); me.refresh(); @@ -820,7 +829,7 @@ _f.Frm.prototype.savecancel = function(btn, callback, on_error) { }; frappe.ui.form.save(me, "cancel", after_cancel, btn); }); - }, on_error); + }, () => handle_fail()); }; // delete the record diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index b424ec88b0..f478263bf7 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -56,19 +56,21 @@ login.bind_events = function() { return false; }); - $(".btn-ldap-login").on("click", function(){ - var args = {}; - args.cmd = "{{ ldap_settings.method }}"; - args.usr = ($("#login_email").val() || "").trim(); - args.pwd = $("#login_password").val(); - args.device = "desktop"; - if(!args.usr || !args.pwd) { - login.set_indicator("{{ _("Both login and password required") }}", 'red'); + {% if ldap_settings %} + $(".btn-ldap-login").on("click", function(){ + var args = {}; + args.cmd = "{{ ldap_settings.method }}"; + args.usr = ($("#login_email").val() || "").trim(); + args.pwd = $("#login_password").val(); + args.device = "desktop"; + if(!args.usr || !args.pwd) { + login.set_indicator("{{ _("Both login and password required") }}", 'red'); + return false; + } + login.call(args); return false; - } - login.call(args); - return false; - }); + }); + {% endif %} } diff --git a/frappe/tests/ui/test_oauth20.py b/frappe/tests/ui/test_oauth20.py index d21aba2efd..cbd9ff5662 100644 --- a/frappe/tests/ui/test_oauth20.py +++ b/frappe/tests/ui/test_oauth20.py @@ -21,6 +21,7 @@ class TestOAuth20(unittest.TestCase): frappe_login_key = frappe.new_doc("Social Login Key") frappe_login_key.get_social_login_provider("Frappe", initialize=True) frappe_login_key.base_url = "http://localhost:8000" + frappe_login_key.enable_social_login = 0 frappe_login_key.save() def test_invalid_login(self): @@ -88,6 +89,26 @@ class TestOAuth20(unittest.TestCase): # Check revoked token self.assertFalse(check_valid_openid_response(bearer_token.get("access_token"))) + def test_resource_owner_password_credentials_grant(self): + # Set payload + payload = "grant_type=password" + payload += "&username=test@example.com" + payload += "&password=Eastern_43A1W" + payload += "&client_id=" + self.client_id + payload += "&scope=openid%20all" + + headers = {'content-type':'application/x-www-form-urlencoded'} + + # Request for bearer token + token_response = requests.post( frappe.get_site_config().host_name + + "/api/method/frappe.integrations.oauth2.get_token", data=payload, headers=headers) + + # Parse bearer token json + bearer_token = token_response.json() + + # Check token for valid response + self.assertTrue(check_valid_openid_response(bearer_token.get("access_token"))) + def test_login_using_implicit_token(self): oauth_client = frappe.get_doc("OAuth Client", self.client_id) diff --git a/frappe/utils/error.py b/frappe/utils/error.py index 7c4d13ac1f..2c66bd85e4 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -16,6 +16,7 @@ import cgitb import types import datetime import json +import six def make_error_snapshot(exception): if frappe.conf.disable_error_snapshot: @@ -49,7 +50,7 @@ def get_snapshot(exception, context=10): """ etype, evalue, etb = sys.exc_info() - if isinstance(etype, types.ClassType): + if isinstance(etype, six.class_types): etype = etype.__name__ # creates a snapshot dict with some basic information diff --git a/frappe/utils/file_manager.py b/frappe/utils/file_manager.py index 40b612174a..4949b7dcfd 100644 --- a/frappe/utils/file_manager.py +++ b/frappe/utils/file_manager.py @@ -88,6 +88,7 @@ def save_url(file_url, filename, dt, dn, folder, is_private, df=None): # return None, None file_url = unquote(file_url) + file_size = frappe.form_dict.file_size f = frappe.get_doc({ "doctype": "File", @@ -97,6 +98,7 @@ def save_url(file_url, filename, dt, dn, folder, is_private, df=None): "attached_to_name": dn, "attached_to_field": df, "folder": folder, + "file_size": file_size, "is_private": is_private }) f.flags.ignore_permissions = True @@ -416,3 +418,9 @@ def get_random_filename(extn=None, content_type=None): extn = mimetypes.guess_extension(content_type) return random_string(7) + (extn or "") + +@frappe.whitelist() +def validate_filename(filename): + hash_ = get_content_hash(filename) + fname = get_file_name(filename, hash_[-6:]) + return fname diff --git a/frappe/www/login.html b/frappe/www/login.html index cd85d3086f..55fa50c740 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -16,7 +16,8 @@
@@ -26,7 +27,7 @@ - {% if ldap_settings.enabled %} + {% if ldap_settings and ldap_settings.enabled %} {% endif %} diff --git a/frappe/www/printview.html b/frappe/www/printview.html index 6136426593..ecefab57ad 100644 --- a/frappe/www/printview.html +++ b/frappe/www/printview.html @@ -9,6 +9,9 @@ href="/assets/frappe/css/bootstrap.css"> + {%- if has_rtl -%} + + {%- endif -%} diff --git a/frappe/www/printview.py b/frappe/www/printview.py index c0fff90a90..7a40b96c49 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -41,7 +41,8 @@ def get_context(context): no_letterhead=frappe.form_dict.no_letterhead), "css": get_print_style(frappe.form_dict.style, print_format), "comment": frappe.session.user, - "title": doc.get(meta.title_field) if meta.title_field else doc.name + "title": doc.get(meta.title_field) if meta.title_field else doc.name, + "has_rtl": True if frappe.local.lang in ["ar", "he", "fa"] else False } def get_print_format_doc(print_format_name, meta): diff --git a/frappe/www/search.py b/frappe/www/search.py index baf2be4e63..c55970c1fc 100644 --- a/frappe/www/search.py +++ b/frappe/www/search.py @@ -4,13 +4,14 @@ from frappe.utils.global_search import web_search from html2text import html2text from frappe import _ from jinja2 import utils +from frappe.utils import sanitize_html def get_context(context): context.no_cache = 1 if frappe.form_dict.q: - frappe.form_dict.q = str(utils.escape(frappe.form_dict.q)) - context.title = _('Search Results for "{0}"').format(frappe.form_dict.q) - context.update(get_search_results(frappe.form_dict.q)) + query = str(utils.escape(sanitize_html(frappe.form_dict.q))) + context.title = _('Search Results for "{0}"').format(query) + context.update(get_search_results(query)) else: context.title = _('Search') diff --git a/socketio.js b/socketio.js index 0764ef131b..6301535fc9 100644 --- a/socketio.js +++ b/socketio.js @@ -63,6 +63,7 @@ io.on('connection', function (socket) { socket.join(room); } }); + socket.on("frappe.chat.message:typing", function (data) { const user = data.user; const room = get_chat_room(socket, data.room); @@ -92,9 +93,7 @@ io.on('connection', function (socket) { socket.join(get_site_room(socket)); } }); - - - + socket.on('disconnect', function () { delete socket.files; })