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 @@