Merge branch 'hotfix'

This commit is contained in:
Frappe Bot 2019-02-15 10:17:14 +00:00
commit f7da1a7124
24 changed files with 201 additions and 75 deletions

View file

@ -16,6 +16,7 @@ from faker import Faker
# public
from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
from .utils.error import get_frame_locals
# Hamless for Python 3
# For Python 2 set default encoding to utf-8
@ -23,7 +24,7 @@ if sys.version[0] == '2':
reload(sys)
sys.setdefaultencoding("utf-8")
__version__ = '11.1.5'
__version__ = '11.1.6'
__title__ = "Frappe Framework"
local = Local()
@ -273,7 +274,7 @@ def errprint(msg):
if not request or (not "cmd" in local.form_dict) or conf.developer_mode:
print(msg.encode('utf-8'))
error_log.append(msg)
error_log.append({"exc": msg, "locals": get_frame_locals()})
def log(msg):
"""Add to `debug_log`.

View file

@ -307,7 +307,8 @@ def set_incoming_outgoing_accounts(doc):
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"append_to": doc.reference_doctype, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name"], as_dict=True)
["email_id", "always_use_account_email_id_as_sender", "name",
"always_use_account_name_as_sender_name"], as_dict=True)
if not doc.incoming_email_account:
doc.incoming_email_account = frappe.db.get_value("Email Account",
@ -317,12 +318,14 @@ def set_incoming_outgoing_accounts(doc):
# if from address is not the default email account
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"email_id": doc.sender, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"], as_dict=True) or frappe._dict()
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"], as_dict=True) or frappe._dict()
if not doc.outgoing_email_account:
doc.outgoing_email_account = frappe.db.get_value("Email Account",
{"default_outgoing": 1, "enable_outgoing": 1},
["email_id", "always_use_account_email_id_as_sender", "name", "send_unsubscribe_message"],as_dict=True) or frappe._dict()
["email_id", "always_use_account_email_id_as_sender", "name",
"send_unsubscribe_message", "always_use_account_name_as_sender_name"],as_dict=True) or frappe._dict()
if doc.sent_or_received == "Sent":
doc.db_set("email_account", doc.outgoing_email_account.name)

View file

@ -56,7 +56,6 @@ class DocType(Document):
self.permissions = []
self.scrub_field_names()
self.scrub_options_in_select()
self.set_default_in_list_view()
self.set_default_translatable()
self.validate_series()
@ -195,17 +194,6 @@ class DocType(Document):
# unique is automatically an index
if d.unique: d.search_index = 0
def scrub_options_in_select(self):
"""Strip options for whitespaces"""
for field in self.fields:
if field.fieldtype == "Select" and field.options is not None:
options_list = []
for i, option in enumerate(field.options.split("\n")):
_option = option.strip()
if i==0 or _option:
options_list.append(_option)
field.options = '\n'.join(options_list)
def validate_series(self, autoname=None, name=None):
"""Validate if `autoname` property is correctly set."""
if not autoname: autoname = self.autoname
@ -693,6 +681,21 @@ def validate_fields(meta):
re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
def scrub_options_in_select(field):
"""Strip options for whitespaces"""
if field.fieldtype == "Select" and field.options is not None:
options_list = []
for i, option in enumerate(field.options.split("\n")):
_option = option.strip()
if i==0 or _option:
options_list.append(_option)
field.options = '\n'.join(options_list)
def scrub_fetch_from(field):
if hasattr(field, 'fetch_from') and getattr(field, 'fetch_from'):
field.fetch_from = field.fetch_from.strip('\n').strip()
fields = meta.get("fields")
fieldname_list = [d.fieldname for d in fields]
@ -720,6 +723,8 @@ def validate_fields(meta):
check_illegal_default(d)
check_unique_and_text(d)
check_illegal_depends_on_conditions(d)
scrub_options_in_select(d)
scrub_fetch_from(d)
check_fold(fields)
check_search_fields(meta, fields)

View file

@ -3,11 +3,22 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe, json
import frappe, json, re
from frappe import _
from frappe.model.document import Document
class Language(Document):
pass
def validate(self):
validate_with_regex(self.language_code, "Language Code")
def before_rename(self, old, new, merge=False):
validate_with_regex(new, "Name")
def validate_with_regex(name, label):
pattern = re.compile("^[a-zA-Z]+[-_]*[a-zA-Z]+$")
if not pattern.match(name):
frappe.throw(_("""{0} must begin and end with a letter and can only contain letters,
hyphen or underscore.""").format(label))
def export_languages_json():
'''Export list of all languages'''

View file

@ -41,7 +41,7 @@
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"search_index": 1,
"set_only_once": 0,
"translatable": 0,
"unique": 0
@ -222,7 +222,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-12 16:26:12.362352",
"modified": "2019-02-13 22:58:27.428741",
"modified_by": "Administrator",
"module": "Core",
"name": "User Permission",

View file

@ -967,9 +967,14 @@ class Database:
def get_descendants(self, doctype, name):
'''Return descendants of the current record'''
lft, rgt = self.get_value(doctype, name, ('lft', 'rgt'))
return self.sql_list('''select name from `tab{doctype}`
where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt))
node_location_indexes = self.get_value(doctype, name, ('lft', 'rgt'))
if node_location_indexes:
lft, rgt = node_location_indexes
return self.sql_list('''select name from `tab{doctype}`
where lft > {lft} and rgt < {rgt}'''.format(doctype=doctype, lft=lft, rgt=rgt))
else:
# when document does not exist
return []
def enqueue_jobs_after_commit():
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:

View file

@ -191,29 +191,33 @@ def run(report_name, filters=None, user=None):
def get_prepared_report_result(report, filters, dn="", user=None):
latest_report_data = {}
# Only look for completed prepared reports with given filters.
doc_list = frappe.get_all("Prepared Report",
filters={"status": "Completed", "report_name": report.name, "filters": filters, "owner": user})
doc = None
if len(doc_list):
if dn:
# Get specified dn
doc = frappe.get_doc("Prepared Report", dn)
else:
if dn:
# Get specified dn
doc = frappe.get_doc("Prepared Report", dn)
else:
# Only look for completed prepared reports with given filters.
doc_list = frappe.get_all("Prepared Report", filters={"status": "Completed", "filters": json.dumps(filters), "owner": user})
if doc_list:
# Get latest
doc = frappe.get_doc("Prepared Report", doc_list[0])
# Prepared Report data is stored in a GZip compressed JSON file
attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name")
compressed_content = get_file(attached_file_name)[1]
uncompressed_content = gzip_decompress(compressed_content)
data = json.loads(uncompressed_content)
if data:
latest_report_data = {
"columns": json.loads(doc.columns) if doc.columns else data[0],
"result": data
}
if doc:
try:
# Prepared Report data is stored in a GZip compressed JSON file
attached_file_name = frappe.db.get_value("File", {"attached_to_doctype": doc.doctype, "attached_to_name":doc.name}, "name")
compressed_content = get_file(attached_file_name)[1]
uncompressed_content = gzip_decompress(compressed_content)
data = json.loads(uncompressed_content)
if data:
latest_report_data = {
"columns": json.loads(doc.columns) if doc.columns else data[0],
"result": data
}
except Exception:
frappe.delete_doc("Prepared Report", doc.name)
frappe.db.commit()
doc = None
latest_report_data.update({
"prepared_report": True,

View file

@ -1,5 +1,6 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
@ -1063,6 +1064,40 @@
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "enable_outgoing",
"description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.",
"fieldname": "always_use_account_name_as_sender_name",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Always use Account's Name as Sender's Name",
"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,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
@ -1563,7 +1598,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-01-30 11:02:41.011412",
"modified": "2019-02-12 17:09:50.653403",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",

View file

@ -174,6 +174,7 @@ class EMail:
self.reply_to = validate_email_add(strip(self.reply_to) or self.sender, True)
self.replace_sender()
self.replace_sender_name()
self.recipients = [strip(r) for r in self.recipients]
self.cc = [strip(r) for r in self.cc]
@ -188,6 +189,12 @@ class EMail:
sender_name, sender_email = parse_addr(self.sender)
self.sender = email.utils.formataddr((str(Header(sender_name or self.email_account.name, 'utf-8')), self.email_account.email_id))
def replace_sender_name(self):
if cint(self.email_account.always_use_account_name_as_sender_name):
self.set_header('X-Original-From', self.sender)
sender_name, sender_email = parse_addr(self.sender)
self.sender = email.utils.formataddr((str(Header(self.email_account.name, 'utf-8')), sender_email))
def set_message_id(self, message_id, is_notification=False):
if message_id:
self.msg_root["Message-Id"] = '<' + message_id + '>'

View file

@ -109,7 +109,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True):
"mail_password": "Super.Secret.Password",
"auto_email_id": "emails@example.com",
"email_sender_name": "Example Notifications",
"always_use_account_email_id_as_sender": 0
"always_use_account_email_id_as_sender": 0,
"always_use_account_name_as_sender_name": 0
}
'''
email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1})
@ -128,7 +129,8 @@ def get_default_outgoing_email_account(raise_exception_not_set=True):
"login_id": frappe.conf.get("mail_login"),
"email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com',
"password": frappe.conf.get("mail_password"),
"always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0)
"always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0),
"always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0)
})
email_account.from_site_config = True
email_account.name = frappe.conf.get("email_sender_name") or "Frappe"
@ -182,6 +184,7 @@ class SMTPServer:
self.use_tls = self.email_account.use_tls
self.sender = self.email_account.email_id
self.always_use_account_email_id_as_sender = cint(self.email_account.get("always_use_account_email_id_as_sender"))
self.always_use_account_name_as_sender_name = cint(self.email_account.get("always_use_account_name_as_sender_name"))
@property
def sess(self):

View file

@ -658,13 +658,15 @@ Object.assign(frappe.utils, {
return route;
},
report_total_accumulator: function(column, values, type) {
if (column.fieldtype == "Percent" || type === "mean") {
return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content / values.length;
} else if (frappe.model.is_numeric_field(column.fieldtype)) {
return values.reduce((a, b) => ({content: a.content + flt(b.content)})).content;
report_column_total: function(values, column, type) {
if (column.column.fieldtype == "Percent" || type === "mean") {
return values.reduce((a, b) => a + flt(b)) / values.length;
} else if (column.column.fieldtype == "Int") {
return values.reduce((a, b) => a + cint(b));
} else if (frappe.model.is_numeric_field(column.column.fieldtype)) {
return values.reduce((a, b) => a + flt(b));
} else {
return false;
return null;
}
}
});

View file

@ -187,7 +187,7 @@ frappe.request.call = function(opts) {
type: opts.type,
dataType: opts.dataType || 'json',
async: opts.async,
headers: {
headers: {
"X-Frappe-CSRF-Token": frappe.csrf_token,
"Accept": "application/json"
},
@ -374,9 +374,12 @@ frappe.request.report_error = function(xhr, request_opts) {
var data = JSON.parse(xhr.responseText);
if (data.exc) {
var exc = (JSON.parse(data.exc) || []).join("\n");
var locals = (JSON.parse(data.locals) || []).join("\n");
delete data.exc;
delete data.locals;
} else {
var exc = "";
locals = "";
}
if (exc) {
@ -408,6 +411,9 @@ frappe.request.report_error = function(xhr, request_opts) {
'<h5>Error Report</h5>',
'<pre>' + exc + '</pre>',
'<hr>',
'<h5>Locals</h5>',
'<pre>' + locals + '</pre>',
'<hr>',
'<h5>Request Data</h5>',
'<pre>' + JSON.stringify(request_opts, null, "\t") + '</pre>',
'<hr>',

View file

@ -35,7 +35,6 @@ frappe.ui.app_icon = {
}
});
icon = '<object class="app-icon-svg">'+ icon+'</object>';
return icon;
} else {
icon = '<i class="'+ icon+'" title="' + module._label + '" style="'+ icon_style + '"></i>';
}

View file

@ -424,7 +424,7 @@ frappe.views.GridReport = Class.extend({
}
},
hooks: {
totalAccumulator: frappe.utils.report_total_accumulator
columnTotal: frappe.utils.report_column_total
}
});

View file

@ -436,7 +436,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
cellHeight: 33,
showTotalRow: this.raw_data.add_total_row,
hooks: {
totalAccumulator: frappe.utils.report_total_accumulator
columnTotal: frappe.utils.report_column_total
}
};

View file

@ -230,7 +230,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
}
},
hooks: {
totalAccumulator: frappe.utils.report_total_accumulator
columnTotal: frappe.utils.report_column_total
},
headerDropdown: [{
label: __('Add Column'),

View file

@ -100,17 +100,20 @@ frappe.views.TreeView = Class.extend({
filter.default = frappe.route_options[filter.fieldname]
}
filter.change = function() {
var val = this.get_value();
me.args[filter.fieldname] = val;
if (val) {
me.root_label = val;
me.page.set_title(val);
} else {
me.root_label = me.opts.root_label;
me.set_title();
if(!filter.disable_onchange) {
filter.change = function() {
filter.on_change && filter.on_change();
var val = this.get_value();
me.args[filter.fieldname] = val;
if (val) {
me.root_label = val;
me.page.set_title(val);
} else {
me.root_label = me.opts.root_label;
me.set_title();
}
me.make_tree();
}
me.make_tree();
}
me.page.add_field(filter);
@ -153,6 +156,12 @@ frappe.views.TreeView = Class.extend({
});
cur_tree = this.tree;
this.post_render();
},
post_render: function() {
var me = this;
me.opts.post_render && me.opts.post_render(me);
},
select_node: function(node) {
@ -219,6 +228,9 @@ frappe.views.TreeView = Class.extend({
]
if(this.opts.toolbar && this.opts.extend_toolbar) {
toolbar = toolbar.filter(btn => {
return !me.opts.toolbar.find(d => d["label"]==btn["label"]);
});
return toolbar.concat(this.opts.toolbar)
} else if (this.opts.toolbar && !this.opts.extend_toolbar) {
return this.opts.toolbar

View file

@ -35,6 +35,7 @@
.dt-scrollable {
max-height: calc(100vh - 250px);
min-height: 100px;
}
table td.dt-cell {

View file

@ -199,3 +199,14 @@ def clear_old_snapshots():
def get_error_snapshot_path():
return frappe.get_site_path('error-snapshots')
def get_frame_locals():
traceback = sys.exc_info()[2]
if traceback:
frames = inspect.getinnerframes(traceback, context=0)
_locals = ['Locals (most recent call last):']
for frame, filename, lineno, function, __, __ in frames:
if '/apps/' in filename:
_locals.append('File "{}", line {}, in {}\n{}'.format(filename, lineno, function, json.dumps(frame.f_locals, default=str, indent=4)))
return '\n'.join(_locals)

View file

@ -256,9 +256,20 @@ def get_root_of(doctype):
and t1.rgt > t1.lft""".format(doctype, doctype))
return result[0][0] if result else None
def get_ancestors_of(doctype, name):
def get_ancestors_of(doctype, name, order_by="lft desc", limit=None):
"""Get ancestor elements of a DocType with a tree structure"""
lft, rgt = frappe.db.get_value(doctype, name, ["lft", "rgt"])
result = frappe.db.sql_list("""select name from `tab{0}`
where lft<%s and rgt>%s order by lft desc""".format(doctype), (lft, rgt))
result = [d["name"] for d in frappe.db.get_list(doctype, {"lft": ["<", lft], "rgt": [">", rgt]},
"name", order_by=order_by, limit_page_length=limit)]
return result or []
def get_descendants_of(doctype, name, order_by="lft desc", limit=None):
'''Return descendants of the current record'''
lft, rgt = frappe.db.get_value(doctype, name, ['lft', 'rgt'])
result = [d["name"] for d in frappe.db.get_list(doctype, {"lft": [">", lft], "rgt": ["<", rgt]},
"name", order_by=order_by, limit_page_length=limit)]
return result or []

View file

@ -105,8 +105,8 @@ def make_logs(response = None):
response = frappe.local.response
if frappe.error_log:
# frappe.response['exc'] = json.dumps("\n".join([cstr(d) for d in frappe.error_log]))
response['exc'] = json.dumps([frappe.utils.cstr(d) for d in frappe.local.error_log])
response['exc'] = json.dumps([frappe.utils.cstr(d["exc"]) for d in frappe.local.error_log])
response['locals'] = json.dumps([frappe.utils.cstr(d["locals"]) for d in frappe.local.error_log])
if frappe.local.message_log:
response['_server_messages'] = json.dumps([frappe.utils.cstr(d) for

View file

@ -85,4 +85,9 @@ def get_desk_assets(build_version):
}
def get_build_version():
return str(os.path.getmtime(os.path.join(frappe.local.sites_path, '.build')))
try:
return str(os.path.getmtime(os.path.join(frappe.local.sites_path, '.build')))
except OSError:
# .build can sometimes not exist
# this is not a major problem so send fallback
return frappe.utils.random_string(8)

View file

@ -326,7 +326,7 @@ def is_visible(df, doc):
if df.fieldname in doc.hide_in_print_layout:
return False
if df.permlevel or 0 > 0 and not doc.has_permlevel_access_to(df.fieldname, df):
if (df.permlevel or 0) > 0 and not doc.has_permlevel_access_to(df.fieldname, df):
return False
return not doc.is_print_hide(df.fieldname, df)

View file

@ -78,7 +78,12 @@ frappe.ready(function() {
.removeClass().addClass('indicator green')
.html("{{ _('Password Updated') }}");
if(r.message) {
frappe.msgprint("{{ _("Password Updated") }}");
frappe.msgprint({
message: "{{ _("Password Updated") }}",
// password is updated successfully
// clear any server message
clear: true
});
setTimeout(function() {
window.location.href = r.message;
}, 2000);