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))]) + '
"); + if(error_fields.length) { + if(doc.parenttype) { + var message = __('Mandatory fields required in table {0}, Row {1}', + [__(frappe.meta.docfield_map[doc.parenttype][doc.parentfield].label).bold(), doc.idx]); + } else { + var message = __('Mandatory fields required in {0}', [__(doc.doctype)]); + + } + message = message + '

"; + msgprint({ + message: message, + indicator: 'red', + title: __('Missing Fields') + }); + } }); return !has_errors; diff --git a/frappe/public/js/frappe/form/templates/form_links.html b/frappe/public/js/frappe/form/templates/form_links.html index afb4a87b2b..708fc3d485 100644 --- a/frappe/public/js/frappe/form/templates/form_links.html +++ b/frappe/public/js/frappe/form/templates/form_links.html @@ -13,8 +13,9 @@ {% if !internal_links[doctype] %} - + {% endif %} {% } %} diff --git a/frappe/public/js/legacy/form.js b/frappe/public/js/legacy/form.js index e7c60f1540..75fcebc9ed 100644 --- a/frappe/public/js/legacy/form.js +++ b/frappe/public/js/legacy/form.js @@ -457,6 +457,7 @@ _f.Frm.prototype.refresh = function(docname) { } if(is_a_different_doc) { + $(this.wrapper).removeClass('validated-form') if(this.show_print_first && this.doc.docstatus===1) { // show print view this.print_doc(); diff --git a/frappe/public/less/common.less b/frappe/public/less/common.less index 3328d10ef0..490c718194 100644 --- a/frappe/public/less/common.less +++ b/frappe/public/less/common.less @@ -135,7 +135,7 @@ a.badge-hover& { } .msgprint { - margin: 15px 0px; + // margin: 15px 0px; // text-align: center; pre { diff --git a/frappe/public/less/desk.less b/frappe/public/less/desk.less index 644a813256..f485c7268c 100644 --- a/frappe/public/less/desk.less +++ b/frappe/public/less/desk.less @@ -324,6 +324,7 @@ textarea.form-control { color:#fff; border-radius:10px; cursor: pointer; + margin-right: 10px; } /* on small screens, show only icons on top */ diff --git a/frappe/public/less/form_grid.less b/frappe/public/less/form_grid.less index 06abb0294d..3cf3668b92 100644 --- a/frappe/public/less/form_grid.less +++ b/frappe/public/less/form_grid.less @@ -6,6 +6,10 @@ border-radius: 3px; } +.form-grid.error { + border-color: @indicator-red; +} + .grid-heading-row { border-bottom: 1px solid @border-color; background-color: @panel-bg; @@ -69,7 +73,15 @@ padding: 10px 15px; max-height: 200px; border-right: 1px solid @border-color; - margin-bottom: -1px; +} + +.grid-static-col.bold { + font-weight: bold; + background-color: @extra-light-yellow; +} + +.validated-form .grid-static-col.error { + background-color: @label-danger-bg; } .grid-static-col { diff --git a/frappe/templates/emails/new_user.html b/frappe/templates/emails/new_user.html index 9eae7c5ee1..9386a24b5d 100644 --- a/frappe/templates/emails/new_user.html +++ b/frappe/templates/emails/new_user.html @@ -1,12 +1,17 @@ -

{{ title }}

+{% if title %}

{{ title }}

{% endif %}

{{_("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 }}

- +{% endif %}

{{_("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