diff --git a/frappe/__init__.py b/frappe/__init__.py
index cc89e88ca7..e8d37e929a 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__ = '7.2.28'
+__version__ = '7.2.29'
__title__ = "Frappe Framework"
local = Local()
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 556e574f05..d835331be8 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -235,8 +235,17 @@ class User(Document):
link = self.reset_password()
- self.send_login_mail(_("Verify Your Account"), "templates/emails/new_user.html",
- {"link": link, "site_url": get_url()})
+ app_title = [t for t in frappe.get_hooks('app_title') if t != 'Frappe Framework']
+ if app_title:
+ subject = _("Welcome to {0}").format(app_title[0])
+ else:
+ subject = _("Complete Registration")
+
+ self.send_login_mail(subject, "templates/emails/new_user.html",
+ dict(
+ link=link,
+ site_url=get_url(),
+ ))
def send_login_mail(self, subject, template, add_args, now=None):
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 42f31e78da..0bb6ac0a0b 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -112,7 +112,7 @@ def get_std_fields_list(meta, key):
def build_for_autosuggest(res):
results = []
for r in res:
- out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r)[1:])}
+ out = {"value": r[0], "description": ", ".join(unique(cstr(d) for d in r if d)[1:])}
results.append(out)
return results
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 5193e8faa7..f6b940b468 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -281,7 +281,7 @@ class DatabaseQuery(object):
can_be_null = True
# prepare in condition
- if f.operator in ('in', 'not in'):
+ if f.operator.lower() in ('in', 'not in'):
values = f.value
if not isinstance(values, (list, tuple)):
values = values.split(",")
@@ -296,7 +296,7 @@ class DatabaseQuery(object):
if df and df.fieldtype in ("Check", "Float", "Int", "Currency", "Percent"):
can_be_null = False
- if f.operator=='Between' and \
+ 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"),
@@ -315,12 +315,12 @@ class DatabaseQuery(object):
value = get_time(f.value).strftime("%H:%M:%S.%f")
fallback = "'00:00:00'"
- elif f.operator in ("like", "not like") or (isinstance(f.value, basestring) and
+ elif f.operator.lower() in ("like", "not like") or (isinstance(f.value, basestring) and
(not df or df.fieldtype not in ["Float", "Int", "Currency", "Percent", "Check"])):
value = "" if f.value==None else f.value
fallback = '""'
- if f.operator in ("like", "not like") and isinstance(value, basestring):
+ if f.operator.lower() in ("like", "not like") and isinstance(value, basestring):
# because "like" uses backslash (\) for escaping
value = value.replace("\\", "\\\\").replace("%", "%%")
@@ -329,12 +329,12 @@ class DatabaseQuery(object):
fallback = 0
# put it inside double quotes
- if isinstance(value, basestring) and not f.operator=='Between':
+ if isinstance(value, basestring) and not f.operator.lower() == 'between':
value = '"{0}"'.format(frappe.db.escape(value, percent=False))
if (self.ignore_ifnull
or not can_be_null
- or (f.value and f.operator in ('=', 'like'))
+ or (f.value and f.operator.lower() in ('=', 'like'))
or 'ifnull(' in column_name.lower()):
condition = '{column_name} {operator} {value}'.format(
column_name=column_name, operator=f.operator,
diff --git a/frappe/public/css/common.css b/frappe/public/css/common.css
index 781a78d4d2..0a1c5c09f2 100644
--- a/frappe/public/css/common.css
+++ b/frappe/public/css/common.css
@@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
-.msgprint {
- margin: 15px 0px;
-}
.msgprint pre {
text-align: left;
}
diff --git a/frappe/public/css/desk.css b/frappe/public/css/desk.css
index cd95c71021..5cfafebfa2 100644
--- a/frappe/public/css/desk.css
+++ b/frappe/public/css/desk.css
@@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
-.msgprint {
- margin: 15px 0px;
-}
.msgprint pre {
text-align: left;
}
@@ -503,6 +500,7 @@ fieldset[disabled] .form-control {
color: #fff;
border-radius: 10px;
cursor: pointer;
+ margin-right: 10px;
}
/* on small screens, show only icons on top */
@media (max-width: 767px) {
diff --git a/frappe/public/css/form_grid.css b/frappe/public/css/form_grid.css
index 18a7483b43..bac25d1d19 100644
--- a/frappe/public/css/form_grid.css
+++ b/frappe/public/css/form_grid.css
@@ -3,6 +3,9 @@
border: 1px solid #d1d8dd;
border-radius: 3px;
}
+.form-grid.error {
+ border-color: #ff5858;
+}
.grid-heading-row {
border-bottom: 1px solid #d1d8dd;
background-color: #F7FAFC;
@@ -55,7 +58,13 @@
padding: 10px 15px;
max-height: 200px;
border-right: 1px solid #d1d8dd;
- margin-bottom: -1px;
+}
+.grid-static-col.bold {
+ font-weight: bold;
+ background-color: #fffdf4;
+}
+.validated-form .grid-static-col.error {
+ background-color: #FFDCDC;
}
.grid-static-col input[type="checkbox"] {
margin-left: -16px !important;
diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css
index 5736704e5d..7a667e0c22 100644
--- a/frappe/public/css/website.css
+++ b/frappe/public/css/website.css
@@ -112,9 +112,6 @@ a.badge-hover:focus .badge,
a.badge-hover:active .badge {
background-color: #D8DFE5;
}
-.msgprint {
- margin: 15px 0px;
-}
.msgprint pre {
text-align: left;
}
diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js
index 1141282f40..3ec2d65c63 100644
--- a/frappe/public/js/frappe/form/control.js
+++ b/frappe/public/js/frappe/form/control.js
@@ -401,10 +401,10 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
},
set_bold: function() {
if(this.$input) {
- this.$input.toggleClass("bold", !!this.df.bold);
+ this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
if(this.disp_area) {
- $(this.disp_area).toggleClass("bold", !!this.df.bold);
+ $(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd));
}
}
});
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index 90da34a220..31d11a4d08 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -53,6 +53,8 @@ frappe.ui.form.Grid = Class.extend({
.appendTo(this.parent)
.attr("data-fieldname", this.df.fieldname);
+ this.form_grid = this.wrapper.find('.form-grid');
+
this.wrapper.find(".grid-add-row").click(function() {
me.add_new_row(null, null, true);
me.set_focus_on_row();
@@ -206,6 +208,9 @@ frappe.ui.form.Grid = Class.extend({
frappe.utils.scroll_to(_scroll_y);
}
+ // red if mandatory
+ this.form_grid.toggleClass('error', !!(this.df.reqd && !(data && data.length)));
+
this.refresh_remove_rows_button();
},
setup_toolbar: function() {
@@ -821,6 +826,15 @@ frappe.ui.form.GridRow = Class.extend({
this.refresh_field(df.fieldname, txt);
}
+ // background color for cellz
+ if(this.doc) {
+ if(df.reqd && !txt) {
+ column.addClass('error');
+ }
+ if (df.reqd || df.bold) {
+ column.addClass('bold');
+ }
+ }
}
},
@@ -1085,8 +1099,9 @@ frappe.ui.form.GridRow = Class.extend({
}
},
refresh_field: function(fieldname, txt) {
+ var df = this.grid.get_docfield(fieldname);
if(txt===undefined) {
- var txt = frappe.format(this.doc[fieldname], this.grid.get_docfield(fieldname),
+ var txt = frappe.format(this.doc[fieldname], df,
null, this.frm.doc);
}
@@ -1094,6 +1109,9 @@ frappe.ui.form.GridRow = Class.extend({
var column = this.columns[fieldname];
if(column) {
column.static_area.html(txt || "");
+ if(df.reqd) {
+ column.toggleClass('error', !!(txt===null || txt===''));
+ }
}
// reset field value
diff --git a/frappe/public/js/frappe/form/save.js b/frappe/public/js/frappe/form/save.js
index 3a935ac71b..70a3173b05 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() {
check_name(function() {
+ $(frm.wrapper).addClass('validated-form');
if(check_mandatory()) {
_call({
method: "frappe.desk.form.save.savedocs",
@@ -132,10 +133,21 @@ frappe.ui.form.save = function(frm, action, callback, btn) {
}
});
- if(error_fields.length)
- msgprint(__('Mandatory fields required in {0}', [(doc.parenttype
- ? (__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label) + ' ('+ __("Table") + ')')
- : __(doc.doctype))]) + '
{{_("Dear")}} {{ first_name }}{% if last_name %} {{ last_name}}{% endif %},
{{_("A new account has been created for you at {0}").format(site_url)}}.
{{_("Your login id is")}}: {{ user }}
{{_("Click on the link below to complete your registration and set a new password")}}.
-{{_("Complete Registration")}}
+ ++ {{ _("Complete Registration") }} +
+ +{% if user_fullname != "Administrator" %}{{_("Thank you")}},
{{ user_fullname }}
{{_("You can also copy-paste this link in your browser")}} {{ link }}
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index e41d933544..b3ec180842 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -6,9 +6,8 @@ from __future__ import unicode_literals from werkzeug.test import Client import os, re, urllib, sys, json, md5, requests, traceback -import bleach, bleach_whitelist -from html5lib.sanitizer import HTMLSanitizer from markdown2 import markdown as _markdown +from .html_utils import sanitize_html import frappe from frappe.utils.identicon import Identicon @@ -441,51 +440,6 @@ def watch(path, handler=None, debug=True): observer.stop() observer.join() -def sanitize_html(html, linkify=False): - """ - Sanitize HTML tags, attributes and style to prevent XSS attacks - Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults - - Does not sanitize JSON, as it could lead to future problems - """ - if not isinstance(html, basestring): - return html - - elif is_json(html): - return html - - tags = (HTMLSanitizer.acceptable_elements + HTMLSanitizer.svg_elements - + ["html", "head", "meta", "link", "body", "iframe", "style", "o:p"]) - attributes = {"*": HTMLSanitizer.acceptable_attributes, "svg": HTMLSanitizer.svg_attributes} - styles = bleach_whitelist.all_styles - strip_comments = False - - # retuns html with escaped tags, escaped orphan >, <, etc. - escaped_html = bleach.clean(html, tags=tags, attributes=attributes, styles=styles, strip_comments=strip_comments) - - if linkify: - # based on bleach.clean - class s(bleach.BleachSanitizer): - allowed_elements = tags - allowed_attributes = attributes - allowed_css_properties = styles - strip_disallowed_elements = False - strip_html_comments = strip_comments - - escaped_html = bleach.linkify(escaped_html, tokenizer=s) - - return escaped_html - -def is_json(text): - try: - json.loads(text) - - except ValueError: - return False - - else: - return True - def markdown(text, sanitize=True, linkify=True): html = _markdown(text) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index cef9fcad5e..7f2b2176d1 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -714,8 +714,8 @@ def get_filter(doctype, f): # if operator is missing f.operator = "=" - valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "Between") - if f.operator not in valid_operators: + valid_operators = ("=", "!=", ">", "<", ">=", "<=", "like", "not like", "in", "not in", "between") + if f.operator.lower() not in valid_operators: frappe.throw("Operator must be one of {0}".format(", ".join(valid_operators))) diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py new file mode 100644 index 0000000000..9824a64106 --- /dev/null +++ b/frappe/utils/html_utils.py @@ -0,0 +1,131 @@ +import json +import bleach, bleach_whitelist + +def sanitize_html(html, linkify=False): + """ + Sanitize HTML tags, attributes and style to prevent XSS attacks + Based on bleach clean, bleach whitelist and HTML5lib's Sanitizer defaults + + Does not sanitize JSON, as it could lead to future problems + """ + if not isinstance(html, basestring): + return html + + elif is_json(html): + return html + + tags = (acceptable_elements + svg_elements + mathml_elements + + ["html", "head", "meta", "link", "body", "iframe", "style", "o:p"]) + attributes = {"*": acceptable_attributes, 'svg': svg_attributes} + styles = bleach_whitelist.all_styles + strip_comments = False + + # retuns html with escaped tags, escaped orphan >, <, etc. + escaped_html = bleach.clean(html, tags=tags, attributes=attributes, styles=styles, strip_comments=strip_comments) + + if linkify: + escaped_html = bleach.linkify(escaped_html, callbacks=[]) + + return escaped_html + + +def is_json(text): + try: + json.loads(text) + except ValueError: + return False + else: + return True + +# adapted from https://raw.githubusercontent.com/html5lib/html5lib-python/4aa79f113e7486c7ec5d15a6e1777bfe546d3259/html5lib/sanitizer.py + +acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'form', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + +mathml_elements = ['maction', 'math', 'merror', 'mfrac', 'mi', + 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', + 'mprescripts', 'mroot', 'mrow', 'mspace', 'msqrt', 'mstyle', 'msub', + 'msubsup', 'msup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', + 'munderover', 'none'] + +svg_elements = ['a', 'animate', 'animateColor', 'animateMotion', + 'animateTransform', 'clipPath', 'circle', 'defs', 'desc', 'ellipse', + 'font-face', 'font-face-name', 'font-face-src', 'g', 'glyph', 'hkern', + 'linearGradient', 'line', 'marker', 'metadata', 'missing-glyph', + 'mpath', 'path', 'polygon', 'polyline', 'radialGradient', 'rect', + 'set', 'stop', 'svg', 'switch', 'text', 'title', 'tspan', 'use'] + +acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', + 'action', 'align', 'alt', 'autocomplete', 'autofocus', 'axis', + 'background', 'balance', 'bgcolor', 'bgproperties', 'border', + 'bordercolor', 'bordercolordark', 'bordercolorlight', 'bottompadding', + 'cellpadding', 'cellspacing', 'ch', 'challenge', 'char', 'charoff', + 'choff', 'charset', 'checked', 'cite', 'class', 'clear', 'color', + 'cols', 'colspan', 'compact', 'contenteditable', 'controls', 'coords', + 'data', 'datafld', 'datapagesize', 'datasrc', 'datetime', 'default', + 'delay', 'dir', 'disabled', 'draggable', 'dynsrc', 'enctype', 'end', + 'face', 'for', 'form', 'frame', 'galleryimg', 'gutter', 'headers', + 'height', 'hidefocus', 'hidden', 'high', 'href', 'hreflang', 'hspace', + 'icon', 'id', 'inputmode', 'ismap', 'keytype', 'label', 'leftspacing', + 'lang', 'list', 'longdesc', 'loop', 'loopcount', 'loopend', + 'loopstart', 'low', 'lowsrc', 'max', 'maxlength', 'media', 'method', + 'min', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'open', + 'optimum', 'pattern', 'ping', 'point-size', 'poster', 'pqg', 'preload', + 'prompt', 'radiogroup', 'readonly', 'rel', 'repeat-max', 'repeat-min', + 'replace', 'required', 'rev', 'rightspacing', 'rows', 'rowspan', + 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', + 'step', 'style', 'summary', 'suppress', 'tabindex', 'target', + 'template', 'title', 'toppadding', 'type', 'unselectable', 'usemap', + 'urn', 'valign', 'value', 'variable', 'volume', 'vspace', 'vrml', + 'width', 'wrap', 'xml:lang'] + +mathml_attributes = ['actiontype', 'align', 'columnalign', 'columnalign', + 'columnalign', 'columnlines', 'columnspacing', 'columnspan', 'depth', + 'display', 'displaystyle', 'equalcolumns', 'equalrows', 'fence', + 'fontstyle', 'fontweight', 'frame', 'height', 'linethickness', 'lspace', + 'mathbackground', 'mathcolor', 'mathvariant', 'mathvariant', 'maxsize', + 'minsize', 'other', 'rowalign', 'rowalign', 'rowalign', 'rowlines', + 'rowspacing', 'rowspan', 'rspace', 'scriptlevel', 'selection', + 'separator', 'stretchy', 'width', 'width', 'xlink:href', 'xlink:show', + 'xlink:type', 'xmlns', 'xmlns:xlink'] + +svg_attributes = ['accent-height', 'accumulate', 'additive', 'alphabetic', + 'arabic-form', 'ascent', 'attributeName', 'attributeType', + 'baseProfile', 'bbox', 'begin', 'by', 'calcMode', 'cap-height', + 'class', 'clip-path', 'color', 'color-rendering', 'content', 'cx', + 'cy', 'd', 'dx', 'dy', 'descent', 'display', 'dur', 'end', 'fill', + 'fill-opacity', 'fill-rule', 'font-family', 'font-size', + 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'from', + 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'gradientUnits', 'hanging', + 'height', 'horiz-adv-x', 'horiz-origin-x', 'id', 'ideographic', 'k', + 'keyPoints', 'keySplines', 'keyTimes', 'lang', 'marker-end', + 'marker-mid', 'marker-start', 'markerHeight', 'markerUnits', + 'markerWidth', 'mathematical', 'max', 'min', 'name', 'offset', + 'opacity', 'orient', 'origin', 'overline-position', + 'overline-thickness', 'panose-1', 'path', 'pathLength', 'points', + 'preserveAspectRatio', 'r', 'refX', 'refY', 'repeatCount', + 'repeatDur', 'requiredExtensions', 'requiredFeatures', 'restart', + 'rotate', 'rx', 'ry', 'slope', 'stemh', 'stemv', 'stop-color', + 'stop-opacity', 'strikethrough-position', 'strikethrough-thickness', + 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', + 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', + 'stroke-width', 'systemLanguage', 'target', 'text-anchor', 'to', + 'transform', 'type', 'u1', 'u2', 'underline-position', + 'underline-thickness', 'unicode', 'unicode-range', 'units-per-em', + 'values', 'version', 'viewBox', 'visibility', 'width', 'widths', 'x', + 'x-height', 'x1', 'x2', 'xlink:actuate', 'xlink:arcrole', + 'xlink:href', 'xlink:role', 'xlink:show', 'xlink:title', 'xlink:type', + 'xml:base', 'xml:lang', 'xml:space', 'xmlns', 'xmlns:xlink', 'y', + 'y1', 'y2', 'zoomAndPan'] + diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 896e7d8844..b9dd3eb371 100755 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -261,6 +261,7 @@ def add_system_manager(email, first_name=None, last_name=None, send_welcome_emai "user_type": "System User", "send_welcome_email": 1 if send_welcome_email else 0 }) + user.insert() # add roles