diff --git a/frappe/__init__.py b/frappe/__init__.py index 8142f8910e..6e3f06807f 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.26' +__version__ = '8.0.27' __title__ = "Frappe Framework" local = Local() diff --git a/frappe/app.py b/frappe/app.py index 5feecb7823..4536bdaf51 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -11,7 +11,6 @@ from werkzeug.local import LocalManager from werkzeug.exceptions import HTTPException, NotFound from werkzeug.contrib.profiler import ProfilerMiddleware from werkzeug.wsgi import SharedDataMiddleware -from werkzeug.serving import run_with_reloader import frappe import frappe.handler @@ -20,7 +19,7 @@ import frappe.api import frappe.async import frappe.utils.response import frappe.website.render -from frappe.utils import get_site_name, get_site_path +from frappe.utils import get_site_name from frappe.middlewares import StaticDataMiddleware from frappe.utils.error import make_error_snapshot from frappe.core.doctype.communication.comment import update_comments_in_parent_after_request @@ -222,5 +221,9 @@ def serve(port=8000, profile=False, site=None, sites_path='.'): 'SERVER_NAME': 'localhost:8000' } - run_simple('0.0.0.0', int(port), application, use_reloader=True, - use_debugger=True, use_evalex=True, threaded=True) + in_test_env = os.environ.get('CI') + run_simple('0.0.0.0', int(port), application, + use_reloader=not in_test_env, + use_debugger=not in_test_env, + use_evalex=not in_test_env, + threaded=True) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e5379b04f9..bbbd30fbca 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals, absolute_import import click import hashlib, os, sys import frappe +from _mysql_exceptions import ProgrammingError from frappe.commands import pass_context, get_site from frappe.commands.scheduler import _is_scheduler_enabled from frappe.limits import update_limits, get_limits @@ -323,7 +324,8 @@ def uninstall(context, app, dry_run=False, yes=False): @click.option('--root-login', default='root') @click.option('--root-password') @click.option('--archived-sites-path') -def drop_site(site, root_login='root', root_password=None, archived_sites_path=None): +@click.option('--force', help='Force drop-site even if an error is encountered', is_flag=True, default=False) +def drop_site(site, root_login='root', root_password=None, archived_sites_path=None, force=False): "Remove site from database and filesystem" from frappe.installer import get_root_connection from frappe.model.db_schema import DbManager @@ -331,7 +333,22 @@ def drop_site(site, root_login='root', root_password=None, archived_sites_path=N frappe.init(site=site) frappe.connect() - scheduled_backup(ignore_files=False, force=True) + + try: + scheduled_backup(ignore_files=False, force=True) + except ProgrammingError as err: + if err[0] == 1146: + if force: + pass + else: + click.echo("="*80) + click.echo("Error: The operation has stopped because backup of {s}'s database failed.".format(s=site)) + click.echo("Reason: {reason}{sep}".format(reason=err[1], sep="\n")) + click.echo("Fix the issue and try again.") + click.echo( + "Hint: Use 'bench drop-site {s} --force' to force the removal of {s}".format(sep="\n", tab="\t", s=site) + ) + sys.exit(1) db_name = frappe.local.conf.db_name frappe.local.db = get_root_connection(root_login, root_password) diff --git a/frappe/core/page/background_jobs/background_jobs.html b/frappe/core/page/background_jobs/background_jobs.html index 793dadfa28..07b5c16451 100644 --- a/frappe/core/page/background_jobs/background_jobs.html +++ b/frappe/core/page/background_jobs/background_jobs.html @@ -28,9 +28,10 @@

- Started + Started Queued - Failed + Failed + Finished

{% else %}

No pending or current jobs for this site

diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 368c6550e9..7fd681652a 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -11,7 +11,8 @@ from frappe.utils import format_datetime, cint colors = { 'queued': 'orange', 'failed': 'red', - 'started': 'green' + 'started': 'blue', + 'finished': 'green' } @frappe.whitelist() diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index c6746831be..8280f6c162 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -66,10 +66,7 @@ def add(args=None): notify_assignment(d.assigned_by, d.owner, d.reference_type, d.reference_name, action='ASSIGN',\ description=args.get("description"), notify=args.get('notify')) - if not args.get("bulk_assign"): - return get(args) - else: - return {} + return get(args) @frappe.whitelist() def add_multiple(args=None): diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 2f0c5c19f9..a6238865ff 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -40,7 +40,7 @@ class AutoEmailReport(Document): count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) - + def validate_report_format(self): """ check if user has select correct report format """ valid_report_formats = ["HTML", "XLS", "CSV"] @@ -59,6 +59,11 @@ class AutoEmailReport(Document): columns, data = report.get_data(limit=self.no_of_rows or 100, user = self.user, filters = self.filters, as_dict=True) + # add serial numbers + columns.insert(0, frappe._dict(fieldname='idx', label='', width='30px')) + for i in range(len(data)): + data[i]['idx'] = i+1 + if len(data)==0 and self.send_if_data: return None @@ -77,7 +82,7 @@ class AutoEmailReport(Document): def get_html_table(self, columns, data): return frappe.render_template('frappe/templates/includes/print_table.html', { 'columns': columns, - 'data': data[1:] + 'data': data }) def get_csv(self, columns, data): @@ -96,7 +101,7 @@ class AutoEmailReport(Document): def send(self): if self.filter_meta and not self.filters: frappe.throw(_("Please set filters value in Report Filter table.")) - + data = self.get_report_content() if not data: return @@ -135,7 +140,7 @@ class AutoEmailReport(Document): def get_report_footer(self): return """

- View report in your browser: + View report in your browser: {{report_name}}

Edit Auto Email Report Settings: {{edit_report_settings}}

""" diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index b330b1570d..8f56f2a435 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -61,7 +61,7 @@ class EmailAccount(Document): if (not self.awaiting_password and not frappe.local.flags.in_install and not frappe.local.flags.in_patch): - if self.password: + if self.password or self.smtp_server in ('127.0.0.1', 'localhost'): if self.enable_incoming: self.get_incoming_server() self.no_failed = 0 @@ -331,7 +331,7 @@ class EmailAccount(Document): raise SentEmailInInbox if email.message_id: - names = frappe.db.sql("""select distinct name from tabCommunication + names = frappe.db.sql("""select distinct name from tabCommunication where message_id='{message_id}' order by creation desc limit 1""".format( message_id=email.message_id @@ -483,7 +483,7 @@ class EmailAccount(Document): parent = frappe.new_doc(self.append_to) if self.subject_field: - parent.set(self.subject_field, frappe.as_unicode(email.subject)) + parent.set(self.subject_field, frappe.as_unicode(email.subject)[:140]) if self.sender_field: parent.set(self.sender_field, frappe.as_unicode(email.from_email)) @@ -591,7 +591,7 @@ class EmailAccount(Document): if not self.use_imap: return - flags = frappe.db.sql("""select name, communication, uid, action from + flags = frappe.db.sql("""select name, communication, uid, action from `tabEmail Flag Queue` where is_completed=0 and email_account='{email_account}' """.format(email_account=self.name), as_dict=True) @@ -614,7 +614,7 @@ class EmailAccount(Document): self.set_communication_seen_status(docnames, seen=0) docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ]) - frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1 + frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1 where name in ({docnames})""".format(docnames=docnames)) def set_communication_seen_status(self, docnames, seen=0): @@ -622,7 +622,7 @@ class EmailAccount(Document): if not docnames: return - frappe.db.sql(""" update `tabCommunication` set seen={seen} + frappe.db.sql(""" update `tabCommunication` set seen={seen} where name in ({docnames})""".format(docnames=docnames, seen=seen)) @frappe.whitelist() @@ -716,4 +716,4 @@ def get_max_email_uid(email_account): return 1 else: max_uid = int(result[0].get("uid", 0)) + 1 - return max_uid \ No newline at end of file + return max_uid diff --git a/frappe/email/receive.py b/frappe/email/receive.py index d64eecdf9a..750ad865a9 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -112,6 +112,10 @@ class EmailServer: self.uid_reindexed = False uid_list = email_list = self.get_new_mails() + + if not email_list: + return + num = num_copy = len(email_list) # WARNING: Hard coded max no. of messages to be popped @@ -166,11 +170,13 @@ class EmailServer: def get_new_mails(self): """Return list of new mails""" if cint(self.settings.use_imap): + email_list = [] self.check_imap_uidvalidity() self.imap.select("Inbox", readonly=True) response, message = self.imap.uid('search', None, self.settings.email_sync_rule) - email_list = message[0].split() + if message[0]: + email_list = message[0].split() else: email_list = self.pop.list()[1] diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 8daedc567c..4e58beeebc 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -59,7 +59,10 @@ def get_outgoing_email_account(raise_exception_not_set=True, append_to=None): if email_account: if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False): - email_account.password = email_account.get_password() + raise_exception = True + if email_account.smtp_server in ['localhost','127.0.0.1']: + raise_exception = False + email_account.password = email_account.get_password(raise_exception=raise_exception) email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id"))) frappe.local.outgoing_email_account[append_to or "default"] = email_account diff --git a/frappe/handler.py b/frappe/handler.py index 70bdb68cce..4845b4d725 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -10,13 +10,23 @@ import frappe.sessions import frappe.utils.file_manager import frappe.desk.form.run_method from frappe.utils.response import build_response +from werkzeug.wrappers import Response def handle(): """handle request""" cmd = frappe.local.form_dict.cmd + data = None if cmd!='login': - execute_cmd(cmd) + data = execute_cmd(cmd) + + if data: + if isinstance(data, Response): + # method returns a response object, pass it on + return data + + # add the response to `message` label + frappe.response['message'] = data return build_response("json") @@ -39,11 +49,8 @@ def execute_cmd(cmd, from_async=False): is_whitelisted(method) - ret = frappe.call(method, **frappe.form_dict) + return frappe.call(method, **frappe.form_dict) - # returns with a message - if ret: - frappe.response['message'] = ret def is_whitelisted(method): # check if whitelisted diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index e0140c5d53..584fb5bbc3 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -73,7 +73,7 @@ class DatabaseQuery(object): self.user = user or frappe.session.user self.update = update self.user_settings_fields = copy.deepcopy(self.fields) - # self.debug = True + #self.debug = True if user_settings: self.user_settings = json.loads(user_settings) @@ -300,8 +300,8 @@ class DatabaseQuery(object): if f.operator.lower() == 'between' and \ (f.fieldname in ('creation', 'modified') or (df and (df.fieldtype=="Date" or df.fieldtype=="Datetime"))): value = "'%s' AND '%s'" % ( - get_datetime(f.value[0]).strftime("%Y-%m-%d %H:%M:%S.%f"), - add_to_date(get_datetime(f.value[1]),days=1).strftime("%Y-%m-%d %H:%M:%S.%f")) + add_to_date(get_datetime(f.value[0]),days=-1).strftime("%Y-%m-%d %H:%M:%S.%f"), + get_datetime(f.value[1]).strftime("%Y-%m-%d %H:%M:%S.%f")) fallback = "'0000-00-00 00:00:00'" elif df and df.fieldtype=="Date": diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index d68c60a2b1..0932ffa623 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -3,9 +3,24 @@ import frappe +# select doctypes that are accessed by the user (not read_only) first, so that the +# the validation message shows the user-facing doctype first. +# For example Journal Entry should be validated before GL Entry (which is an internal doctype) + dynamic_link_queries = [ - """select parent, fieldname, options from tabDocField where fieldtype='Dynamic Link'""", - """select dt as parent, fieldname, options from `tabCustom Field` where fieldtype='Dynamic Link'""", + """select parent, + (select read_only from `tabDocType` where name=tabDocField.parent) as read_only, + fieldname, options + from tabDocField + where fieldtype='Dynamic Link' + order by read_only""", + + """select dt as parent, + (select read_only from `tabDocType` where name=`tabCustom Field`.dt) as read_only, + fieldname, options + from `tabCustom Field` + where fieldtype='Dynamic Link' + order by read_only""", ] def get_dynamic_link_map(for_delete=False): @@ -29,7 +44,6 @@ def get_dynamic_link_map(for_delete=False): dynamic_link_map.setdefault(doctype, []).append(df) frappe.local.dynamic_link_map = dynamic_link_map - return frappe.local.dynamic_link_map def get_dynamic_links(): diff --git a/frappe/patches/v8_0/update_published_in_global_search.py b/frappe/patches/v8_0/update_published_in_global_search.py index 830f01cd13..0a6595319a 100644 --- a/frappe/patches/v8_0/update_published_in_global_search.py +++ b/frappe/patches/v8_0/update_published_in_global_search.py @@ -1,6 +1,11 @@ +import frappe + def execute(): from frappe.website.router import get_doctypes_with_web_view from frappe.utils.global_search import rebuild_for_doctype for doctype in get_doctypes_with_web_view(): - rebuild_for_doctype(doctype) + try: + rebuild_for_doctype(doctype) + except frappe.DoesNotExistError: + pass diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index 735ddec442..24bafa962d 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -497,6 +497,10 @@ h6.uppercase, } .frappe-control pre { white-space: pre-wrap; + background-color: inherit; + border: none; + padding: 0px; + margin: 0px; } .hide-control { display: none !important; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 6de6d318b3..2f00c2ea63 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -328,6 +328,7 @@ frappe.Application = Class.extend({ if(!frappe.app.session_expired_dialog) { var dialog = new frappe.ui.Dialog({ title: __('Session Expired'), + keep_open: true, fields: [ { fieldtype:'Password', fieldname:'password', label: __('Please Enter Your Password to Continue') }, @@ -369,7 +370,7 @@ frappe.Application = Class.extend({ // add backdrop $('.modal-backdrop').css({ 'opacity': 1, - 'background-color': '#EBEFF2' + 'background-color': '#4B4C9D' }); } }, diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index f7a986d6f4..0345a51024 100755 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -1343,10 +1343,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ }; }, filter: function(item, input) { - var d = this.get_item(item.value); - return Awesomplete.FILTER_CONTAINS(d.value, '__link_option') || - Awesomplete.FILTER_CONTAINS(d.value, input) || - Awesomplete.FILTER_CONTAINS(d.description, input); + return true; }, item: function (item, input) { d = this.get_item(item.value); diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 8b279b4ded..8fa5a526ae 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -333,7 +333,7 @@ frappe.ui.form.Dashboard = Class.extend({ if(!this.heatmap) { this.heatmap = new CalHeatMap(); this.heatmap.init({ - itemSelector: "#heatmap-" + this.frm.doctype, + itemSelector: "#heatmap-" + frappe.model.scrub(this.frm.doctype), domain: "month", subDomain: "day", start: moment().subtract(1, 'year').add(1, 'month').toDate(), diff --git a/frappe/public/js/frappe/form/footer/assign_to.js b/frappe/public/js/frappe/form/footer/assign_to.js index 11a7f4df98..05259e541f 100644 --- a/frappe/public/js/frappe/form/footer/assign_to.js +++ b/frappe/public/js/frappe/form/footer/assign_to.js @@ -80,7 +80,7 @@ frappe.ui.form.AssignTo = Class.extend({ add: function() { var me = this; - if(this.frm.doc.__unsaved == 1) { + if(this.frm.is_new()) { frappe.throw(__("Please save the document before assignment")); return; } @@ -93,7 +93,6 @@ frappe.ui.form.AssignTo = Class.extend({ docname: me.frm.docname, callback: function(r) { me.render(r.message); - me.frm.reload_doc(); } }); } @@ -108,7 +107,7 @@ frappe.ui.form.AssignTo = Class.extend({ remove: function(owner) { var me = this; - if(this.frm.doc.__unsaved == 1) { + if(this.frm.is_new()) { frappe.throw(__("Please save the document before removing assignment")); return; } @@ -122,7 +121,6 @@ frappe.ui.form.AssignTo = Class.extend({ }, callback:function(r,rt) { me.render(r.message); - me.frm.reload_doc(); } }); } diff --git a/frappe/public/js/frappe/form/templates/form_dashboard.html b/frappe/public/js/frappe/form/templates/form_dashboard.html index 2c6e5b4342..bf23ff4082 100644 --- a/frappe/public/js/frappe/form/templates/form_dashboard.html +++ b/frappe/public/js/frappe/form/templates/form_dashboard.html @@ -4,7 +4,7 @@ diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index e59ce7024f..99e51fcb35 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -153,8 +153,9 @@ $(window).on('hashchange', function() { return; // hide open dialog - if(cur_dialog && cur_dialog.hide_on_page_refresh) + if(cur_dialog && cur_dialog.hide_on_page_refresh) { cur_dialog.hide(); + } frappe.route(); diff --git a/frappe/public/js/frappe/socketio_client.js b/frappe/public/js/frappe/socketio_client.js index 29dded37c7..8c7d206127 100644 --- a/frappe/public/js/frappe/socketio_client.js +++ b/frappe/public/js/frappe/socketio_client.js @@ -60,6 +60,12 @@ frappe.socket = { if (frappe.flags.doc_subscribe) { return; } + + frappe.flags.doc_subscribe = true; + + // throttle to 1 per sec + setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000); + if (frm.is_new()) { return; } @@ -72,11 +78,6 @@ frappe.socket = { } } - frappe.flags.doc_subscribe = true; - - // throttle to 1 per sec - setTimeout(function() { frappe.flags.doc_subscribe = false }, 1000); - frappe.socket.doc_subscribe(frm.doctype, frm.docname); }); diff --git a/frappe/public/js/frappe/ui/filters/filter_dashboard_head.html b/frappe/public/js/frappe/ui/filters/filter_dashboard_head.html index b4c61949ce..b2008394be 100644 --- a/frappe/public/js/frappe/ui/filters/filter_dashboard_head.html +++ b/frappe/public/js/frappe/ui/filters/filter_dashboard_head.html @@ -1,6 +1,6 @@
-
{{ label }}
+
{{ label }}
{% if (type!=="Date" && type!=="Datetime" && type!=="DateRange") { %}