Selaa lähdekoodia

Merge branch 'staging'

version-14
mbauskar 8 vuotta sitten
vanhempi
commit
27c58f6aea
80 muutettua tiedostoa jossa 2085 lisäystä ja 171 poistoa
  1. +1
    -1
      frappe/__init__.py
  2. +20
    -6
      frappe/auth.py
  3. +15
    -11
      frappe/build.js
  4. +2
    -0
      frappe/change_log/v8/v8_8_0.md
  5. +5
    -0
      frappe/client.py
  6. +1
    -0
      frappe/contacts/doctype/address/address.js
  7. +2
    -2
      frappe/core/doctype/doctype/boilerplate/test_controller.js
  8. +32
    -1
      frappe/core/doctype/role/role.json
  9. +1
    -0
      frappe/core/doctype/sms_parameter/README.md
  10. +1
    -0
      frappe/core/doctype/sms_parameter/__init__.py
  11. +98
    -0
      frappe/core/doctype/sms_parameter/sms_parameter.json
  12. +10
    -0
      frappe/core/doctype/sms_parameter/sms_parameter.py
  13. +1
    -0
      frappe/core/doctype/sms_settings/README.md
  14. +1
    -0
      frappe/core/doctype/sms_settings/__init__.py
  15. +0
    -0
      frappe/core/doctype/sms_settings/sms_settings.js
  16. +267
    -0
      frappe/core/doctype/sms_settings/sms_settings.json
  17. +117
    -0
      frappe/core/doctype/sms_settings/sms_settings.py
  18. +23
    -0
      frappe/core/doctype/sms_settings/test_sms_settings.js
  19. +160
    -1
      frappe/core/doctype/system_settings/system_settings.json
  20. +7
    -0
      frappe/core/doctype/system_settings/system_settings.py
  21. +1
    -0
      frappe/core/doctype/test_runner/test_runner.js
  22. +10
    -0
      frappe/core/doctype/user/user.js
  23. +1
    -1
      frappe/core/doctype/user/user.json
  24. +85
    -4
      frappe/core/doctype/user/user.py
  25. +2
    -1
      frappe/core/doctype/version/test_version.py
  26. +1
    -1
      frappe/custom/doctype/customize_form/test_customize_form.js
  27. +5
    -5
      frappe/desk/doctype/event/event.json
  28. +42
    -0
      frappe/desk/doctype/event/test_event.js
  29. +3
    -3
      frappe/desk/doctype/note/note.js
  30. +1
    -1
      frappe/desk/page/setup_wizard/setup_wizard.js
  31. +7
    -0
      frappe/desk/page/setup_wizard/setup_wizard.py
  32. +5
    -2
      frappe/desk/query_report.py
  33. +5
    -0
      frappe/email/doctype/newsletter/newsletter.py
  34. +3
    -0
      frappe/exceptions.py
  35. +4
    -1
      frappe/hooks.py
  36. +1
    -0
      frappe/public/build.json
  37. +0
    -1
      frappe/public/css/calendar.css
  38. +1
    -1
      frappe/public/css/email.css
  39. +22
    -5
      frappe/public/css/form.css
  40. +19
    -0
      frappe/public/css/list.css
  41. +14
    -2
      frappe/public/css/mobile.css
  42. +1
    -1
      frappe/public/css/page.css
  43. +55
    -20
      frappe/public/css/website.css
  44. +18
    -0
      frappe/public/js/frappe/desk.js
  45. +5
    -0
      frappe/public/js/frappe/form/control.js
  46. +6
    -4
      frappe/public/js/frappe/form/footer/timeline.js
  47. +1
    -1
      frappe/public/js/frappe/form/footer/timeline_item.html
  48. +6
    -3
      frappe/public/js/frappe/form/print.js
  49. +11
    -4
      frappe/public/js/frappe/ui/base_list.js
  50. +121
    -0
      frappe/public/js/frappe/ui/colors.js
  51. +6
    -1
      frappe/public/js/frappe/ui/page.js
  52. +21
    -17
      frappe/public/js/frappe/views/calendar/calendar.js
  53. +7
    -8
      frappe/public/js/frappe/views/communication.js
  54. +9
    -3
      frappe/public/js/frappe/views/reports/query_report.js
  55. +2
    -1
      frappe/public/js/legacy/form.js
  56. +1
    -2
      frappe/public/less/calendar.less
  57. +1
    -1
      frappe/public/less/email.less
  58. +22
    -5
      frappe/public/less/form.less
  59. +21
    -2
      frappe/public/less/list.less
  60. +14
    -2
      frappe/public/less/mobile.less
  61. +1
    -1
      frappe/public/less/page.less
  62. +59
    -20
      frappe/public/less/website.less
  63. +1
    -1
      frappe/templates/includes/list/list.html
  64. +99
    -2
      frappe/templates/includes/login/login.js
  65. +3
    -4
      frappe/templates/web.html
  66. +132
    -0
      frappe/tests/test_twofactor.py
  67. +2
    -0
      frappe/tests/ui/test_test_runner.py
  68. +2
    -1
      frappe/tests/ui/tests.txt
  69. +369
    -0
      frappe/twofactor.py
  70. +1
    -1
      frappe/utils/redis_wrapper.py
  71. +0
    -1
      frappe/website/router.py
  72. +5
    -1
      frappe/website/utils.py
  73. +8
    -3
      frappe/www/desk.html
  74. +5
    -5
      frappe/www/desk.py
  75. +4
    -4
      frappe/www/login.html
  76. +0
    -1
      frappe/www/login.py
  77. +27
    -0
      frappe/www/qrcode.html
  78. +37
    -0
      frappe/www/qrcode.py
  79. +2
    -1
      package.json
  80. +4
    -0
      requirements.txt

+ 1
- 1
frappe/__init__.py Näytä tiedosto

@@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json
from .exceptions import * from .exceptions import *
from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template from .utils.jinja import get_jenv, get_template, render_template, get_email_from_template


__version__ = '8.7.11'
__version__ = '8.8.0'
__title__ = "Frappe Framework" __title__ = "Frappe Framework"


local = Local() local = Local()


+ 20
- 6
frappe/auth.py Näytä tiedosto

@@ -16,9 +16,14 @@ from frappe.modules.patch_handler import check_session_stopped
from frappe.translate import get_lang_code from frappe.translate import get_lang_code
from frappe.utils.password import check_password from frappe.utils.password import check_password
from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log from frappe.core.doctype.authentication_log.authentication_log import add_authentication_log
from frappe.utils.background_jobs import enqueue
from twofactor import (should_run_2fa, authenticate_for_2factor,
confirm_otp_token, get_cached_user_pass)


from six.moves.urllib.parse import quote from six.moves.urllib.parse import quote


import pyotp, base64, os

class HTTPRequest: class HTTPRequest:
def __init__(self): def __init__(self):
# Get Environment variables # Get Environment variables
@@ -62,6 +67,7 @@ class HTTPRequest:


def validate_csrf_token(self): def validate_csrf_token(self):
if frappe.local.request and frappe.local.request.method=="POST": if frappe.local.request and frappe.local.request.method=="POST":
if not frappe.local.session: return
if not frappe.local.session.data.csrf_token \ if not frappe.local.session.data.csrf_token \
or frappe.local.session.data.device=="mobile" \ or frappe.local.session.data.device=="mobile" \
or frappe.conf.get('ignore_csrf', None): or frappe.conf.get('ignore_csrf', None):
@@ -88,7 +94,7 @@ class HTTPRequest:
def connect(self, ac_name = None): def connect(self, ac_name = None):
"""connect to db, from ac_name or db_name""" """connect to db, from ac_name or db_name"""
frappe.local.db = frappe.database.Database(user = self.get_db_name(), \ frappe.local.db = frappe.database.Database(user = self.get_db_name(), \
password = getattr(conf,'db_password', ''))
password = getattr(conf, 'db_password', ''))


class LoginManager: class LoginManager:
def __init__(self): def __init__(self):
@@ -98,7 +104,7 @@ class LoginManager:
self.user_type = None self.user_type = None


if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login": if frappe.local.form_dict.get('cmd')=='login' or frappe.local.request.path=="/api/method/login":
self.login()
if self.login()==False: return
self.resume = False self.resume = False


# run login triggers # run login triggers
@@ -116,7 +122,12 @@ class LoginManager:
def login(self): def login(self):
# clear cache # clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr')) frappe.clear_cache(user = frappe.form_dict.get('usr'))
self.authenticate()
user, pwd = get_cached_user_pass()
self.authenticate(user=user, pwd=pwd)
if should_run_2fa(self.user):
authenticate_for_2factor(self.user)
if not confirm_otp_token(self):
return False
self.post_login() self.post_login()


def post_login(self): def post_login(self):
@@ -183,7 +194,7 @@ class LoginManager:
if not (user and pwd): if not (user and pwd):
user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd') user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('pwd')
if not (user and pwd): if not (user and pwd):
self.fail('Incomplete login details', user=user)
self.fail(_('Incomplete login details'), user=user)


if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")): if cint(frappe.db.get_value("System Settings", "System Settings", "allow_login_using_mobile_number")):
user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user
@@ -205,7 +216,9 @@ class LoginManager:
except frappe.AuthenticationError: except frappe.AuthenticationError:
self.fail('Incorrect password', user=user) self.fail('Incorrect password', user=user)


def fail(self, message, user="NA"):
def fail(self, message, user=None):
if not user:
user = _('Unknown User')
frappe.local.response['message'] = message frappe.local.response['message'] = message
add_authentication_log(message, user, status="Failed") add_authentication_log(message, user, status="Failed")
frappe.db.commit() frappe.db.commit()
@@ -302,6 +315,7 @@ class CookieManager:
for key in set(self.to_delete): for key in set(self.to_delete):
response.set_cookie(key, "", expires=expires) response.set_cookie(key, "", expires=expires)



@frappe.whitelist() @frappe.whitelist()
def get_logged_user(): def get_logged_user():
return frappe.session.user return frappe.session.user
@@ -317,4 +331,4 @@ def get_website_user_home_page(user):
home_page = frappe.get_attr(home_page_method[-1])(user) home_page = frappe.get_attr(home_page_method[-1])(user)
return '/' + home_page.strip('/') return '/' + home_page.strip('/')
else: else:
return '/me'
return '/me'

+ 15
- 11
frappe/build.js Näytä tiedosto

@@ -10,6 +10,7 @@ const path_join = path.resolve;
const app = require('express')(); const app = require('express')();
const http = require('http').Server(app); const http = require('http').Server(app);
const io = require('socket.io')(http); const io = require('socket.io')(http);
const touch = require("touch");


// basic setup // basic setup
const sites_path = path_join(__dirname, '..', '..', '..', 'sites'); const sites_path = path_join(__dirname, '..', '..', '..', 'sites');
@@ -42,6 +43,7 @@ function build(minify) {
for (const output_path in build_map) { for (const output_path in build_map) {
pack(output_path, build_map[output_path], minify); pack(output_path, build_map[output_path], minify);
} }
touch(path_join(sites_path, '.build'), {force:true});
} }


let socket_connection = false; let socket_connection = false;
@@ -228,7 +230,7 @@ function watch_less(ondirty) {
const less_paths = app_paths.map(path => path_join(path, 'public', 'less')); const less_paths = app_paths.map(path => path_join(path, 'public', 'less'));


const to_watch = filter_valid_paths(less_paths); const to_watch = filter_valid_paths(less_paths);
chokidar.watch(to_watch).on('change', (filename, stats) => {
chokidar.watch(to_watch).on('change', (filename) => {
console.log(filename, 'dirty'); console.log(filename, 'dirty');
var last_index = filename.lastIndexOf('/'); var last_index = filename.lastIndexOf('/');
const less_path = filename.slice(0, last_index); const less_path = filename.slice(0, last_index);
@@ -236,17 +238,18 @@ function watch_less(ondirty) {
filename = filename.split('/').pop(); filename = filename.split('/').pop();


compile_less_file(filename, less_path, public_path) compile_less_file(filename, less_path, public_path)
.then(css_file_path => {
// build the target css file for which this css file is input
for (const target in build_map) {
const sources = build_map[target];
if (sources.includes(css_file_path)) {
pack(target, sources);
ondirty && ondirty(target);
break;
.then(css_file_path => {
// build the target css file for which this css file is input
for (const target in build_map) {
const sources = build_map[target];
if (sources.includes(css_file_path)) {
pack(target, sources);
ondirty && ondirty(target);
break;
}
} }
}
})
});
touch(path_join(sites_path, '.build'), {force:true});
}); });
} }


@@ -265,6 +268,7 @@ function watch_js(ondirty) {
// break; // break;
} }
} }
touch(path_join(sites_path, '.build'), {force:true});
}); });
} }




+ 2
- 0
frappe/change_log/v8/v8_8_0.md Näytä tiedosto

@@ -0,0 +1,2 @@
### Two Factor Authentication
- Now you can authenticate user with two factor authentication. You can enable the Two Factor Authentication from System Settings.

+ 5
- 0
frappe/client.py Näytä tiedosto

@@ -296,3 +296,8 @@ def get_js(items):
out.append(code) out.append(code)


return out return out

@frappe.whitelist(allow_guest=True)
def get_time_zone():
'''Returns default time zone'''
return {"time_zone": frappe.defaults.get_defaults().get("time_zone")}

+ 1
- 0
frappe/contacts/doctype/address/address.js Näytä tiedosto

@@ -22,6 +22,7 @@ frappe.ui.form.on("Address", {
} }
} }
}); });
frm.refresh_field("links");
}, },
validate: function(frm) { validate: function(frm) {
// clear linked customer / supplier / sales partner on saving... // clear linked customer / supplier / sales partner on saving...


+ 2
- 2
frappe/core/doctype/doctype/boilerplate/test_controller.js Näytä tiedosto

@@ -8,9 +8,9 @@ QUnit.test("test: {doctype}", function (assert) {{
// number of asserts // number of asserts
assert.expect(1); assert.expect(1);


frappe.run_serially('{doctype}', [
frappe.run_serially([
// insert a new {doctype} // insert a new {doctype}
() => frappe.tests.make([
() => frappe.tests.make('{doctype}', [
// values to be set // values to be set
{{key: 'value'}} {{key: 'value'}}
]), ]),


+ 32
- 1
frappe/core/doctype/role/role.json Näytä tiedosto

@@ -105,6 +105,37 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "two_factor_auth",
"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": "Two Factor Authentication",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -148,7 +179,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-05-04 11:03:41.533058",
"modified": "2017-07-06 12:42:57.097914",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "Role", "name": "Role",


+ 1
- 0
frappe/core/doctype/sms_parameter/README.md Näytä tiedosto

@@ -0,0 +1 @@
SMS query parameter for SMS Settings.

+ 1
- 0
frappe/core/doctype/sms_parameter/__init__.py Näytä tiedosto

@@ -0,0 +1 @@
from __future__ import unicode_literals

+ 98
- 0
frappe/core/doctype/sms_parameter/sms_parameter.json Näytä tiedosto

@@ -0,0 +1,98 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:58",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 1,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "parameter",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Parameter",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150px"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "value",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Value",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "150px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "150px"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2017-07-22 22:52:53.309396",
"modified_by": "chude.osiegbu@manqala.com",
"module": "Core",
"name": "SMS Parameter",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
}

+ 10
- 0
frappe/core/doctype/sms_parameter/sms_parameter.py Näytä tiedosto

@@ -0,0 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals
import frappe

from frappe.model.document import Document

class SMSParameter(Document):
pass

+ 1
- 0
frappe/core/doctype/sms_settings/README.md Näytä tiedosto

@@ -0,0 +1 @@
Settings for automatically sending SMS from the system.

+ 1
- 0
frappe/core/doctype/sms_settings/__init__.py Näytä tiedosto

@@ -0,0 +1 @@
from __future__ import unicode_literals

+ 0
- 0
frappe/core/doctype/sms_settings/sms_settings.js Näytä tiedosto


+ 267
- 0
frappe/core/doctype/sms_settings/sms_settings.json Näytä tiedosto

@@ -0,0 +1,267 @@
{
"allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-01-10 16:34:24",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 0,
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break0",
"fieldtype": "Column Break",
"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,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Eg. smsgateway.com/api/send_sms.cgi",
"fieldname": "sms_gateway_url",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "SMS Gateway URL",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Enter url parameter for message",
"fieldname": "message_parameter",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Message Parameter",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Enter url parameter for receiver nos",
"fieldname": "receiver_parameter",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Receiver Parameter",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sms_sender_name",
"fieldtype": "Data",
"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": "SMS Sender 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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "static_parameters_section",
"fieldtype": "Column Break",
"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,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"unique": 0,
"width": "50%"
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)",
"fieldname": "parameters",
"fieldtype": "Table",
"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": "Static Parameters",
"length": 0,
"no_copy": 0,
"options": "SMS Parameter",
"permlevel": 0,
"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,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-cog",
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2017-07-22 22:52:16.066981",
"modified_by": "chude.osiegbu@manqala.com",
"module": "Core",
"name": "SMS Settings",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1,
"report": 0,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 0,
"track_seen": 0
}

+ 117
- 0
frappe/core/doctype/sms_settings/sms_settings.py Näytä tiedosto

@@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt

from __future__ import unicode_literals
import frappe

from frappe import _, throw, msgprint
from frappe.utils import nowdate

from frappe.model.document import Document

class SMSSettings(Document):
pass

def validate_receiver_nos(receiver_list):
validated_receiver_list = []
for d in receiver_list:
# remove invalid character
for x in [' ', '+', '-', '(', ')']:
d = d.replace(x, '')

validated_receiver_list.append(d)

if not validated_receiver_list:
throw(_("Please enter valid mobile nos"))

return validated_receiver_list


def get_sender_name():
"returns name as SMS sender"
sender_name = frappe.db.get_single_value('SMS Settings', 'sms_sender_name') or \
'ERPNXT'
if len(sender_name) > 6 and \
frappe.db.get_default("country") == "India":
throw("""As per TRAI rule, sender name must be exactly 6 characters.
Kindly change sender name in Setup --> Global Defaults.
Note: Hyphen, space, numeric digit, special characters are not allowed.""")
return sender_name

@frappe.whitelist()
def get_contact_number(contact_name, ref_doctype, ref_name):
"returns mobile number of the contact"
number = frappe.db.sql("""select mobile_no, phone from tabContact
where name=%s
and exists(
select name from `tabDynamic Link` where link_doctype=%s and link_name=%s
)
""", (contact_name, ref_doctype, ref_name))
return number and (number[0][0] or number[0][1]) or ''

@frappe.whitelist()
def send_sms(receiver_list, msg, sender_name = '', success_msg = True):

import json
if isinstance(receiver_list, basestring):
receiver_list = json.loads(receiver_list)
if not isinstance(receiver_list, list):
receiver_list = [receiver_list]

receiver_list = validate_receiver_nos(receiver_list)

arg = {
'receiver_list' : receiver_list,
'message' : unicode(msg).encode('utf-8'),
'sender_name' : sender_name or get_sender_name(),
'success_msg' : success_msg
}

if frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
send_via_gateway(arg)
else:
msgprint(_("Please Update SMS Settings"))

def send_via_gateway(arg):
ss = frappe.get_doc('SMS Settings', 'SMS Settings')
args = {ss.message_parameter: arg.get('message')}
for d in ss.get("parameters"):
args[d.parameter] = d.value

success_list = []
for d in arg.get('receiver_list'):
args[ss.receiver_parameter] = d
status = send_request(ss.sms_gateway_url, args)

if 200 <= status < 300:
success_list.append(d)

if len(success_list) > 0:
args.update(arg)
create_sms_log(args, success_list)
if arg.get('success_msg'):
frappe.msgprint(_("SMS sent to following numbers: {0}").format("\n" + "\n".join(success_list)))


def send_request(gateway_url, params):
import requests
response = requests.get(gateway_url, params = params, headers={'Accept': "text/plain, text/html, */*"})
response.raise_for_status()
return response.status_code


# Create SMS Log
# =========================================================
def create_sms_log(args, sent_to):
sl = frappe.new_doc('SMS Log')
sl.sender_name = args['sender_name']
sl.sent_on = nowdate()
sl.message = args['message'].decode('utf-8')
sl.no_of_requested_sms = len(args['receiver_list'])
sl.requested_numbers = "\n".join(args['receiver_list'])
sl.no_of_sent_sms = len(sent_to)
sl.sent_to = "\n".join(sent_to)
sl.flags.ignore_permissions = True
sl.save()

+ 23
- 0
frappe/core/doctype/sms_settings/test_sms_settings.js Näytä tiedosto

@@ -0,0 +1,23 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line

QUnit.test("test: SMS Settings", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(1);

frappe.run_serially('SMS Settings', [
// insert a new SMS Settings
() => frappe.tests.make([
// values to be set
{key: 'value'}
]),
() => {
assert.equal(cur_frm.doc.key, 'value');
},
() => done()
]);

});

+ 160
- 1
frappe/core/doctype/system_settings/system_settings.json Näytä tiedosto

@@ -895,6 +895,165 @@
"set_only_once": 0, "set_only_once": 0,
"unique": 0 "unique": 0
}, },
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1,
"columns": 0,
"fieldname": "two_factor_authentication",
"fieldtype": "Section Break",
"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": "Two Factor Authentication",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enable_two_factor_auth",
"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": "Enable Two Factor Auth",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "OTP App",
"depends_on": "",
"description": "Choose authentication method to be used by all users",
"fieldname": "two_factor_method",
"fieldtype": "Select",
"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": "Two Factor Authentication method",
"length": 0,
"no_copy": 0,
"options": "OTP App\nSMS\nEmail",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.two_factor_method == \"OTP App\"",
"description": "Time in seconds to retain QR code image on server. Min:<strong>240</strong>",
"fieldname": "lifespan_qrcode_image",
"fieldtype": "Int",
"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": "Expiry time of QR Code Image Page",
"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,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Frappe Framework",
"depends_on": "enable_two_factor_auth",
"fieldname": "otp_issuer_name",
"fieldtype": "Data",
"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": "OTP Issuer Name",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "allow_bulk_edit": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -1027,7 +1186,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-07-20 22:57:56.466867",
"modified": "2017-08-07 23:29:18.858797",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "System Settings", "name": "System Settings",


+ 7
- 0
frappe/core/doctype/system_settings/system_settings.py Näytä tiedosto

@@ -9,6 +9,7 @@ from frappe.model import no_value_fields
from frappe.translate import set_default_language from frappe.translate import set_default_language
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.momentjs import get_all_timezones from frappe.utils.momentjs import get_all_timezones
from frappe.twofactor import toggle_two_factor_auth


class SystemSettings(Document): class SystemSettings(Document):
def validate(self): def validate(self):
@@ -25,6 +26,12 @@ class SystemSettings(Document):
if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])): if len(parts)!=2 or not (cint(parts[0]) or cint(parts[1])):
frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm")) frappe.throw(_("Session Expiry must be in format {0}").format("hh:mm"))


if self.enable_two_factor_auth:
if self.two_factor_method=='SMS':
if not frappe.db.get_value('SMS Settings', None, 'sms_gateway_url'):
frappe.throw(_('Please setup SMS before setting it as an authentication method, via SMS Settings'))
toggle_two_factor_auth(True, roles=['All'])

def on_update(self): def on_update(self):
for df in self.meta.get("fields"): for df in self.meta.get("fields"):
if df.fieldtype not in no_value_fields: if df.fieldtype not in no_value_fields:


+ 1
- 0
frappe/core/doctype/test_runner/test_runner.js Näytä tiedosto

@@ -23,6 +23,7 @@ frappe.ui.form.on('Test Runner', {


}, },
run_tests: function(frm, files) { run_tests: function(frm, files) {
frappe.flags.in_test = true;
let require_list = [ let require_list = [
"assets/frappe/js/lib/jquery/qunit.js", "assets/frappe/js/lib/jquery/qunit.js",
"assets/frappe/js/lib/jquery/qunit.css" "assets/frappe/js/lib/jquery/qunit.css"


+ 10
- 0
frappe/core/doctype/user/user.js Näytä tiedosto

@@ -78,6 +78,15 @@ frappe.ui.form.on('User', {
}) })
}) })


frm.add_custom_button(__("Reset OTP Secret"), function() {
frappe.call({
method: "frappe.core.doctype.user.user.reset_otp_secret",
args: {
"user": frm.doc.name
}
})
})

frm.trigger('enabled'); frm.trigger('enabled');


frm.roles_editor && frm.roles_editor.show(); frm.roles_editor && frm.roles_editor.show();
@@ -111,6 +120,7 @@ frappe.ui.form.on('User', {
} }
cur_frm.dirty(); cur_frm.dirty();
} }

}, },
validate: function(frm) { validate: function(frm) {
if(frm.roles_editor) { if(frm.roles_editor) {


+ 1
- 1
frappe/core/doctype/user/user.json Näytä tiedosto

@@ -1971,7 +1971,7 @@
"istable": 0, "istable": 0,
"max_attachments": 5, "max_attachments": 5,
"menu_index": 0, "menu_index": 0,
"modified": "2017-07-12 19:24:00.824902",
"modified": "2017-07-07 17:18:14.047969",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Core", "module": "Core",
"name": "User", "name": "User",


+ 85
- 4
frappe/core/doctype/user/user.py Näytä tiedosto

@@ -14,6 +14,7 @@ import frappe.share
import re import re
from frappe.limits import get_limits from frappe.limits import get_limits
from frappe.website.utils import is_signup_enabled from frappe.website.utils import is_signup_enabled
from frappe.utils.background_jobs import enqueue


STANDARD_USERS = ("Guest", "Administrator") STANDARD_USERS = ("Guest", "Administrator")


@@ -586,8 +587,8 @@ def get_email_awaiting(user):
return waiting return waiting
else: else:
frappe.db.sql("""update `tabUser Email` frappe.db.sql("""update `tabUser Email`
set awaiting_password =0
where parent = %(user)s""",{"user":user})
set awaiting_password =0
where parent = %(user)s""",{"user":user})
return False return False


@frappe.whitelist(allow_guest=False) @frappe.whitelist(allow_guest=False)
@@ -675,7 +676,7 @@ def ask_pass_update():
from frappe.utils import set_default from frappe.utils import set_default


users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email` users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
WHERE awaiting_password = 1""", as_dict=True)
WHERE awaiting_password = 1""", as_dict=True)


password_list = [ user.get("user") for user in users ] password_list = [ user.get("user") for user in users ]
set_default("email_user_password", u','.join(password_list)) set_default("email_user_password", u','.join(password_list))
@@ -888,4 +889,84 @@ def handle_password_test_fail(result):
def update_gravatar(name): def update_gravatar(name):
gravatar = has_gravatar(name) gravatar = has_gravatar(name)
if gravatar: if gravatar:
frappe.db.set_value('User', name, 'user_image', gravatar)
frappe.db.set_value('User', name, 'user_image', gravatar)

@frappe.whitelist(allow_guest=True)
def send_token_via_sms(tmp_id,phone_no=None,user=None):
try:
from frappe.core.doctype.sms_settings.sms_settings import send_request
except:
return False

if not frappe.cache().ttl(tmp_id + '_token'):
return False
ss = frappe.get_doc('SMS Settings', 'SMS Settings')
if not ss.sms_gateway_url:
return False

token = frappe.cache().get(tmp_id + '_token')
args = {ss.message_parameter: 'verification code is {}'.format(token)}

for d in ss.get("parameters"):
args[d.parameter] = d.value

if user:
user_phone = frappe.db.get_value('User', user, ['phone','mobile_no'], as_dict=1)
usr_phone = user_phone.mobile_no or user_phone.phone
if not usr_phone:
return False
else:
if phone_no:
usr_phone = phone_no
else:
return False

args[ss.receiver_parameter] = usr_phone
status = send_request(ss.sms_gateway_url, args)

if 200 <= status < 300:
frappe.cache().delete(tmp_id + '_token')
return True
else:
return False

@frappe.whitelist(allow_guest=True)
def send_token_via_email(tmp_id,token=None):
import pyotp

user = frappe.cache().get(tmp_id + '_user')
count = token or frappe.cache().get(tmp_id + '_token')

if ((not user) or (user == 'None') or (not count)):
return False
user_email = frappe.db.get_value('User',user, 'email')
if not user_email:
return False

otpsecret = frappe.cache().get(tmp_id + '_otp_secret')
hotp = pyotp.HOTP(otpsecret)

frappe.sendmail(
recipients=user_email, sender=None, subject='Verification Code',
message='<p>Your verification code is {0}</p>'.format(hotp.at(int(count))),
delayed=False, retry=3)

return True
@frappe.whitelist(allow_guest=True)
def reset_otp_secret(user):
otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
user_email = frappe.db.get_value('User',user, 'email')
if frappe.session.user in ["Administrator", user] :
frappe.defaults.clear_default(user + '_otplogin')
frappe.defaults.clear_default(user + '_otpsecret')
email_args = {
'recipients':user_email, 'sender':None, 'subject':'OTP Secret Reset - {}'.format(otp_issuer or "Frappe Framework"),
'message':'<p>Your OTP secret on {} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.</p>'.format(otp_issuer or "Frappe Framework"),
'delayed':False,
'retry':3
}
enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **email_args)
return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
else:
return frappe.throw(_("OTP secret can only be reset by the Administrator."))

+ 2
- 1
frappe/core/doctype/version/test_version.py Näytä tiedosto

@@ -14,12 +14,13 @@ class TestVersion(unittest.TestCase):
new_doc = copy.deepcopy(old_doc) new_doc = copy.deepcopy(old_doc)


old_doc.color = None old_doc.color = None
new_doc.color = '#fafafa'


diff = get_diff(old_doc, new_doc)['changed'] diff = get_diff(old_doc, new_doc)['changed']


self.assertEquals(get_fieldnames(diff)[0], 'color') self.assertEquals(get_fieldnames(diff)[0], 'color')
self.assertTrue(get_old_values(diff)[0] is None) self.assertTrue(get_old_values(diff)[0] is None)
self.assertEquals(get_new_values(diff)[0], 'blue')
self.assertEquals(get_new_values(diff)[0], '#fafafa')


new_doc.starts_on = "2017-07-20" new_doc.starts_on = "2017-07-20"




+ 1
- 1
frappe/custom/doctype/customize_form/test_customize_form.js Näytä tiedosto

@@ -7,7 +7,7 @@ QUnit.test("test customize form", function(assert) {
let done = assert.async(); let done = assert.async();
frappe.run_serially([ frappe.run_serially([
() => frappe.set_route('Form', 'Customize Form'), () => frappe.set_route('Form', 'Customize Form'),
() => frappe.timeout(2),
() => frappe.timeout(1),
() => cur_frm.set_value('doc_type', 'ToDo'), () => cur_frm.set_value('doc_type', 'ToDo'),
() => frappe.timeout(2), () => frappe.timeout(2),
() => { () => {


+ 5
- 5
frappe/desk/doctype/event/event.json Näytä tiedosto

@@ -312,9 +312,9 @@
"bold": 0, "bold": 0,
"collapsible": 0, "collapsible": 0,
"columns": 0, "columns": 0,
"default": "blue",
"default": "",
"fieldname": "color", "fieldname": "color",
"fieldtype": "Select",
"fieldtype": "Color",
"hidden": 0, "hidden": 0,
"ignore_user_permissions": 0, "ignore_user_permissions": 0,
"ignore_xss_filter": 0, "ignore_xss_filter": 0,
@@ -325,7 +325,7 @@
"label": "Color", "label": "Color",
"length": 0, "length": 0,
"no_copy": 0, "no_copy": 0,
"options": "red\ngreen\nblue\nyellow\nskyblue\norange",
"options": "",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
@@ -895,8 +895,8 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2017-07-06 12:37:44.036819",
"modified_by": "Administrator",
"modified": "2017-08-03 16:34:54.657796",
"modified_by": "faris@erpnext.com",
"module": "Desk", "module": "Desk",
"name": "Event", "name": "Event",
"owner": "Administrator", "owner": "Administrator",


+ 42
- 0
frappe/desk/doctype/event/test_event.js Näytä tiedosto

@@ -0,0 +1,42 @@

QUnit.test("test: Event", function (assert) {
let done = assert.async();

// number of asserts
assert.expect(4);

const subject = '_Test Event 1';
const datetime = frappe.datetime.now_datetime();
const hex = '#6be273';
const rgb = 'rgb(107, 226, 115)';

frappe.run_serially([
// insert a new Event
() => frappe.tests.make('Event', [
// values to be set
{subject: subject},
{starts_on: datetime},
{color: hex},
{event_type: 'Private'}
]),
() => {
assert.equal(cur_frm.doc.subject, subject, 'Subject correctly set');
assert.equal(cur_frm.doc.starts_on, datetime, 'Date correctly set');
assert.equal(cur_frm.doc.color, hex, 'Color correctly set');

// set filters explicitly for list view
frappe.route_options = {
event_type: 'Private'
};
},
() => frappe.set_route('List', 'Event', 'Calendar'),
() => frappe.timeout(2),
() => {
const bg_color = $(`.result-list:visible .fc-day-grid-event:contains("${subject}")`)
.css('background-color');
assert.equal(bg_color, rgb, 'Event background color is set correctly');
},
() => done()
]);

});

+ 3
- 3
frappe/desk/doctype/note/note.js Näytä tiedosto

@@ -10,7 +10,7 @@ frappe.ui.form.on("Note", {
// toggle edit // toggle edit
frm.add_custom_button("Edit", function() { frm.add_custom_button("Edit", function() {
frm.events.set_editable(frm, !frm.is_note_editable); frm.events.set_editable(frm, !frm.is_note_editable);
})
});
frm.events.set_editable(frm, false); frm.events.set_editable(frm, false);
} }
}, },
@@ -24,12 +24,12 @@ frappe.ui.form.on("Note", {
frm.set_df_property("content", "read_only", editable ? 0: 1); frm.set_df_property("content", "read_only", editable ? 0: 1);


// hide all other fields // hide all other fields
$.each(frm.fields_dict, function(fieldname, field) {
$.each(frm.fields_dict, function(fieldname) {


if(fieldname !== "content") { if(fieldname !== "content") {
frm.set_df_property(fieldname, "hidden", editable ? 0: 1); frm.set_df_property(fieldname, "hidden", editable ? 0: 1);
} }
})
});


// no label, description for content either // no label, description for content either
frm.get_field("content").toggle_label(editable); frm.get_field("content").toggle_label(editable);


+ 1
- 1
frappe/desk/page/setup_wizard/setup_wizard.js Näytä tiedosto

@@ -561,7 +561,7 @@ var frappe_slides = [
} }
} }
}, },
},
}
]; ];


var utils = { var utils = {


+ 7
- 0
frappe/desk/page/setup_wizard/setup_wizard.py Näytä tiedosto

@@ -267,3 +267,10 @@ def email_setup_wizard_exception(traceback, args):


def get_language_code(lang): def get_language_code(lang):
return frappe.db.get_value('Language', {'language_name':lang}) return frappe.db.get_value('Language', {'language_name':lang})


def enable_twofactor_all_roles():
all_role = frappe.get_doc('Role',{'role_name':'All'})
all_role.two_factor_auth = True
all_role.save(ignore_permissions=True)


+ 5
- 2
frappe/desk/query_report.py Näytä tiedosto

@@ -77,7 +77,7 @@ def run(report_name, filters=None, user=None):
frappe.msgprint(_("Must have report permission to access this report."), frappe.msgprint(_("Must have report permission to access this report."),
raise_exception=True) raise_exception=True)


columns, result, message, chart = [], [], None, None
columns, result, message, chart, data_to_be_printed = [], [], None, None, None
if report.report_type=="Query Report": if report.report_type=="Query Report":
if not report.query: if not report.query:
frappe.msgprint(_("Must specify a Query to run"), raise_exception=True) frappe.msgprint(_("Must specify a Query to run"), raise_exception=True)
@@ -99,6 +99,8 @@ def run(report_name, filters=None, user=None):
message = res[2] message = res[2]
if len(res) > 3: if len(res) > 3:
chart = res[3] chart = res[3]
if len(res) > 4:
data_to_be_printed = res[4]


if report.apply_user_permissions and result: if report.apply_user_permissions and result:
result = get_filtered_data(report.ref_doctype, columns, result, user) result = get_filtered_data(report.ref_doctype, columns, result, user)
@@ -110,7 +112,8 @@ def run(report_name, filters=None, user=None):
"result": result, "result": result,
"columns": columns, "columns": columns,
"message": message, "message": message,
"chart": chart
"chart": chart,
"data_to_be_printed": data_to_be_printed
} }






+ 5
- 0
frappe/email/doctype/newsletter/newsletter.py Näytä tiedosto

@@ -14,6 +14,7 @@ from frappe.utils.scheduler import log
from frappe.email.queue import send from frappe.email.queue import send
from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.email.doctype.email_group.email_group import add_subscribers
from frappe.utils import parse_addr from frappe.utils import parse_addr
from frappe.utils import validate_email_add




class Newsletter(Document): class Newsletter(Document):
@@ -23,6 +24,10 @@ class Newsletter(Document):
from `tabEmail Queue` where reference_doctype=%s and reference_name=%s from `tabEmail Queue` where reference_doctype=%s and reference_name=%s
group by status""", (self.doctype, self.name))) or None group by status""", (self.doctype, self.name))) or None


def validate(self):
if self.send_from:
validate_email_add(self.send_from, True)

def test_send(self, doctype="Lead"): def test_send(self, doctype="Lead"):
self.recipients = frappe.utils.split_emails(self.test_email_id) self.recipients = frappe.utils.split_emails(self.test_email_id)
self.queue_all() self.queue_all()


+ 3
- 0
frappe/exceptions.py Näytä tiedosto

@@ -37,6 +37,9 @@ class SessionStopped(Exception):
class UnsupportedMediaType(Exception): class UnsupportedMediaType(Exception):
http_status_code = 415 http_status_code = 415


class RequestToken(Exception):
http_status_code = 200

class Redirect(Exception): class Redirect(Exception):
http_status_code = 301 http_status_code = 301




+ 4
- 1
frappe/hooks.py Näytä tiedosto

@@ -128,7 +128,8 @@ scheduler_events = {
"frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.pull",
"frappe.email.doctype.email_account.email_account.notify_unreplied", "frappe.email.doctype.email_account.email_account.notify_unreplied",
"frappe.oauth.delete_oauth2_data", "frappe.oauth.delete_oauth2_data",
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment"
"frappe.integrations.doctype.razorpay_settings.razorpay_settings.capture_payment",
"frappe.twofactor.delete_all_barcodes_for_users"
], ],
"hourly": [ "hourly": [
"frappe.model.utils.link_count.update_link_count", "frappe.model.utils.link_count.update_link_count",
@@ -189,3 +190,5 @@ bot_parsers = [


setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception" setup_wizard_exception = "frappe.desk.page.setup_wizard.setup_wizard.email_setup_wizard_exception"
before_write_file = "frappe.limits.validate_space_limit" before_write_file = "frappe.limits.validate_space_limit"

otp_methods = ['OTP App','Email','SMS']

+ 1
- 0
frappe/public/build.json Näytä tiedosto

@@ -87,6 +87,7 @@
"public/js/frappe/ui/messages.js", "public/js/frappe/ui/messages.js",
"public/js/frappe/ui/keyboard.js", "public/js/frappe/ui/keyboard.js",
"public/js/frappe/ui/emoji.js", "public/js/frappe/ui/emoji.js",
"public/js/frappe/ui/colors.js",


"public/js/frappe/request.js", "public/js/frappe/request.js",
"public/js/frappe/socketio_client.js", "public/js/frappe/socketio_client.js",


+ 0
- 1
frappe/public/css/calendar.css Näytä tiedosto

@@ -73,7 +73,6 @@ th.fc-day-header {
background: #cfdce5 !important; background: #cfdce5 !important;
} }
.fc-day-grid-event { .fc-day-grid-event {
background-color: rgba(94, 100, 255, 0.2) !important;
border: none !important; border: none !important;
margin: 5px 4px 0 !important; margin: 5px 4px 0 !important;
padding: 1px 5px !important; padding: 1px 5px !important;


+ 1
- 1
frappe/public/css/email.css Näytä tiedosto

@@ -49,7 +49,7 @@ hr {
border-top: none; border-top: none;
} }
.email-footer-container { .email-footer-container {
margin-top: 10px;
margin-top: 30px;
} }
.email-footer-container > div:not(:last-child) { .email-footer-container > div:not(:last-child) {
margin-bottom: 5px; margin-bottom: 5px;


+ 22
- 5
frappe/public/css/form.css Näytä tiedosto

@@ -299,17 +299,32 @@ h6.uppercase,
.timeline-item.user-content .action-btns { .timeline-item.user-content .action-btns {
position: absolute; position: absolute;
right: 0; right: 0;
padding: 5px 15px 2px 5px;
padding: 8px 15px 0 5px;
}
.timeline-item.user-content .action-btns .edit-btn-container {
margin-right: 13px;
} }
.timeline-item.user-content .comment-header { .timeline-item.user-content .comment-header {
background-color: #fafbfc; background-color: #fafbfc;
padding: 10px 15px 10px 13px;
padding: 10px 15px 8px 13px;
margin: 0px; margin: 0px;
color: #8D99A6; color: #8D99A6;
border-bottom: 1px solid #EBEFF2; border-bottom: 1px solid #EBEFF2;
} }
.timeline-item.user-content .comment-header.links-active { .timeline-item.user-content .comment-header.links-active {
padding-right: 60px;
padding-right: 77px;
}
.timeline-item.user-content .comment-header .asset-details {
display: inline-block;
width: 100%;
}
.timeline-item.user-content .comment-header .asset-details .btn-link {
border: 0;
border-radius: 0;
padding: 0;
}
.timeline-item.user-content .comment-header .asset-details .btn-link:hover {
text-decoration: none;
} }
.timeline-item.user-content .comment-header .commented-on-small { .timeline-item.user-content .comment-header .commented-on-small {
display: none; display: none;
@@ -334,7 +349,8 @@ h6.uppercase,
.timeline-item.user-content .close-btn-container .close { .timeline-item.user-content .close-btn-container .close {
color: inherit; color: inherit;
opacity: 1; opacity: 1;
padding: 0 0 0 10px;
padding: 0;
font-size: 18px;
} }
.timeline-item.user-content .edit-btn-container { .timeline-item.user-content .edit-btn-container {
padding: 0; padding: 0;
@@ -409,7 +425,8 @@ h6.uppercase,
top: 5px; top: 5px;
} }
.timeline-item .reply-link { .timeline-item .reply-link {
padding-left: 7px;
margin-left: 15px;
font-size: 12px;
} }
.timeline-head { .timeline-head {
background-color: white; background-color: white;


+ 19
- 0
frappe/public/css/list.css Näytä tiedosto

@@ -183,6 +183,25 @@
.listview-main-section .octicon-heart { .listview-main-section .octicon-heart {
cursor: pointer; cursor: pointer;
} }
.listview-main-section .page-form {
padding-left: 17px;
}
@media (max-width: 991px) {
.listview-main-section .page-form {
padding-left: 25px;
}
}
.listview-main-section .page-form .octicon-search {
float: left;
padding-top: 7px;
margin-left: -4px;
margin-right: -4px;
}
@media (max-width: 991px) {
.listview-main-section .page-form .octicon-search {
margin-left: -12px;
}
}
.like-action.octicon-heart { .like-action.octicon-heart {
color: #ff5858; color: #ff5858;
} }


+ 14
- 2
frappe/public/css/mobile.css Näytä tiedosto

@@ -25,6 +25,9 @@ body {
body[data-route^="Form"] .page-title h1 { body[data-route^="Form"] .page-title h1 {
margin-top: 12px; margin-top: 12px;
} }
body[data-route^="Form"] .page-title h1.editable-title {
padding-right: 80px;
}
body[data-route^="Form"] .page-title .indicator { body[data-route^="Form"] .page-title .indicator {
display: inline-block; display: inline-block;
margin-top: 12px; margin-top: 12px;
@@ -197,7 +200,7 @@ body {
} }
body[data-route^="Form"] .page-title .title-text { body[data-route^="Form"] .page-title .title-text {
font-size: 16px; font-size: 16px;
width: calc(100% - 30px);
width: calc(100% - 90px);
} }
body[data-route^="Form"] .page-title .indicator { body[data-route^="Form"] .page-title .indicator {
float: left; float: left;
@@ -356,7 +359,10 @@ body {
content: none; content: none;
} }
.timeline .timeline-item.user-content .action-btns { .timeline .timeline-item.user-content .action-btns {
padding: 5px 10px 2px 5px;
padding: 7px 10px 2px 5px;
}
.timeline .timeline-item.user-content .action-btns .edit-btn-container {
margin-right: 0;
} }
.timeline .timeline-item.user-content .comment-header { .timeline .timeline-item.user-content .comment-header {
padding: 7px 10px; padding: 7px 10px;
@@ -364,6 +370,12 @@ body {
.timeline .timeline-item.user-content .comment-header .links-active { .timeline .timeline-item.user-content .comment-header .links-active {
padding-right: 10px; padding-right: 10px;
} }
.timeline .timeline-item.user-content .comment-header .reply-link {
margin-left: 0;
}
.timeline .timeline-item.user-content .comment-header .asset-details {
width: calc(100% - 30px);
}
.timeline .timeline-item.user-content .avatar-medium { .timeline .timeline-item.user-content .avatar-medium {
margin-right: 10px; margin-right: 10px;
} }


+ 1
- 1
frappe/public/css/page.css Näytä tiedosto

@@ -44,7 +44,6 @@
vertical-align: middle; vertical-align: middle;
} }
.page-title .title-image { .page-title .title-image {
display: inline-block;
width: 46px; width: 46px;
height: 0; height: 0;
padding: 23px 0; padding: 23px 0;
@@ -56,6 +55,7 @@
text-align: center; text-align: center;
line-height: 0; line-height: 0;
float: left; float: left;
margin-right: 10px;
} }
.editable-title .title-text { .editable-title .title-text {
cursor: pointer; cursor: pointer;


+ 55
- 20
frappe/public/css/website.css Näytä tiedosto

@@ -507,6 +507,7 @@ li {
border-top: 1px solid #EBEFF2; border-top: 1px solid #EBEFF2;
} }
.page_content { .page_content {
padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
} }
.carousel-control .icon { .carousel-control .icon {
@@ -554,6 +555,9 @@ li {
.panel-body { .panel-body {
padding-left: 15px; padding-left: 15px;
} }
.page-head {
margin-bottom: -30px;
}
.page-head h1, .page-head h1,
.page-head h2 { .page-head h2 {
margin-top: 0px; margin-top: 0px;
@@ -588,9 +592,14 @@ fieldset {
width: 100%; width: 100%;
} }
.page-container { .page-container {
padding: 0px;
display: flex;
max-width: 970px; max-width: 970px;
margin: auto;
margin: 0 auto;
}
@media (max-width: 767px) {
.page-container {
flex-direction: column-reverse;
}
} }
.page-max-width { .page-max-width {
max-width: 800px; max-width: 800px;
@@ -603,30 +612,28 @@ fieldset {
.web-sidebar { .web-sidebar {
position: relative; position: relative;
} }
.web-sidebar .sidebar-item {
.web-sidebar .sidebar-item:not(:last-child) {
margin: 0px; margin: 0px;
padding-bottom: 12px; padding-bottom: 12px;
border: none; border: none;
color: #8D99A6; color: #8D99A6;
font-size: 12px;
} }
.web-sidebar .sidebar-item .badge {
.web-sidebar .sidebar-item:not(:last-child) .badge {
font-weight: normal; font-weight: normal;
} }
.web-sidebar .sidebar-item a { .web-sidebar .sidebar-item a {
color: #36414C !important;
color: #8D99A6;
} }
.web-sidebar .sidebar-item a.active { .web-sidebar .sidebar-item a.active {
color: #36414C !important;
font-weight: 500 !important;
}
.web-sidebar .sidebar-items {
margin-bottom: 30px;
color: #36414C;
} }
.web-sidebar .sidebar-items .title { .web-sidebar .sidebar-items .title {
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
} }
.web-sidebar .sidebar-items ul {
margin-bottom: 0;
}
.page-footer { .page-footer {
padding: 15px 0px; padding: 15px 0px;
border-top: 1px solid #EBEFF2; border-top: 1px solid #EBEFF2;
@@ -712,11 +719,6 @@ textarea {
.sidebar-navbar-items a:visited { .sidebar-navbar-items a:visited {
border-bottom: 0px; border-bottom: 0px;
} }
@media (max-width: 767px) {
.visible-xs {
display: inline-block !important;
}
}
.more-block { .more-block {
padding-bottom: 30px; padding-bottom: 30px;
} }
@@ -790,16 +792,49 @@ a.active {
.btn-next-wrapper { .btn-next-wrapper {
margin-top: 60px; margin-top: 60px;
} }
.sidebar-block,
.sidebar-block {
flex: 1;
font-size: 12px;
border-right: 1px solid #d1d8dd;
padding: 30px;
padding-left: 0px;
}
@media (max-width: 767px) {
.sidebar-block {
font-size: 14px;
border-right: none;
border-top: 1px solid #d1d8dd;
padding-left: 20px;
}
}
.page-content { .page-content {
flex: 6;
}
.page-content h1:first-child {
margin-top: 0;
}
.page-content.with-sidebar {
padding: 30px;
padding-left: 40px;
}
.page-content.without-sidebar {
padding-top: 30px; padding-top: 30px;
padding-bottom: 50px;
} }
.your-account-info { .your-account-info {
margin-top: 30px; margin-top: 30px;
} }
.page-content.with-sidebar {
padding-left: 50px;
@media (max-width: 767px) {
.visible-xs {
display: inline-block !important;
}
.sidebar-block {
width: 100%;
}
.page-content.with-sidebar {
width: 100%;
padding-left: 20px;
padding-right: 20px;
}
} }
@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {
.page-content { .page-content {


+ 18
- 0
frappe/public/js/frappe/desk.js Näytä tiedosto

@@ -45,6 +45,7 @@ frappe.Application = Class.extend({
this.make_nav_bar(); this.make_nav_bar();
this.set_favicon(); this.set_favicon();
this.setup_analytics(); this.setup_analytics();
this.setup_beforeunload();
frappe.ui.keys.setup(); frappe.ui.keys.setup();
this.set_rtl(); this.set_rtl();


@@ -480,6 +481,23 @@ frappe.Application = Class.extend({
} }
}, },


setup_beforeunload: function() {
if (frappe.defaults.get_default('in_selenium')) {
return;
}
window.onbeforeunload = function () {
if (frappe.flags.in_test) return null;
var unsaved_docs = [];
for (doctype in locals) {
for (name in locals[doctype]) {
var doc = locals[doctype][name];
if(doc.__unsaved) { unsaved_docs.push(doc.name); }
}
}
return unsaved_docs.length ? true : null;
};
},

show_notes: function() { show_notes: function() {
var me = this; var me = this;
if(frappe.boot.notes.length) { if(frappe.boot.notes.length) {


+ 5
- 0
frappe/public/js/frappe/form/control.js Näytä tiedosto

@@ -688,6 +688,8 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
}, },
set_formatted_input: function(value) { set_formatted_input: function(value) {
this._super(value); this._super(value);

if(!value) value = '#ffffff';
this.$input.css({ this.$input.css({
"background-color": value "background-color": value
}); });
@@ -721,6 +723,9 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
}); });
}, },
validate: function (value) { validate: function (value) {
if(value === '') {
return '';
}
var is_valid = /^#[0-9A-F]{6}$/i.test(value); var is_valid = /^#[0-9A-F]{6}$/i.test(value);
if(is_valid) { if(is_valid) {
return value; return value;


+ 6
- 4
frappe/public/js/frappe/form/footer/timeline.js Näytä tiedosto

@@ -159,12 +159,12 @@ frappe.ui.form.Timeline = Class.extend({
this.prepare_timeline_item(c); this.prepare_timeline_item(c);
var $timeline_item = $(frappe.render_template("timeline_item", {data:c, frm:this.frm})) var $timeline_item = $(frappe.render_template("timeline_item", {data:c, frm:this.frm}))
.appendTo(me.list) .appendTo(me.list)
.on("click", ".close", function() {
.on("click", ".delete-comment", function() {
var name = $timeline_item.data('name'); var name = $timeline_item.data('name');
me.delete_comment(name); me.delete_comment(name);
return false; return false;
}) })
.on('click', '.edit', function(e) {
.on('click', '.edit-comment', function(e) {
e.preventDefault(); e.preventDefault();
var name = $timeline_item.data('name'); var name = $timeline_item.data('name');


@@ -176,6 +176,7 @@ frappe.ui.form.Timeline = Class.extend({
var content = $timeline_item.find('.timeline-item-content').html(); var content = $timeline_item.find('.timeline-item-content').html();


$edit_btn $edit_btn
.text("Save")
.find('i') .find('i')
.removeClass('octicon-pencil') .removeClass('octicon-pencil')
.addClass('octicon-check'); .addClass('octicon-check');
@@ -232,6 +233,7 @@ frappe.ui.form.Timeline = Class.extend({
new frappe.views.CommunicationComposer({ new frappe.views.CommunicationComposer({
doc: me.frm.doc, doc: me.frm.doc,
txt: "", txt: "",
title: __('Reply'),
frm: me.frm, frm: me.frm,
last_email: last_email last_email: last_email
}); });
@@ -251,11 +253,11 @@ frappe.ui.form.Timeline = Class.extend({
c["edit"] = ""; c["edit"] = "";
if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") { if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") {
if(frappe.model.can_delete("Communication")) { if(frappe.model.can_delete("Communication")) {
c["delete"] = '<a class="close" title="Delete" href="#"><i class="octicon octicon-x"></i></a>';
c["delete"] = '<a class="close delete-comment" title="Delete" href="#"><i class="octicon octicon-x"></i></a>';
} }


if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) { if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) {
c["edit"] = '<a class="edit" title="Edit" href="#"><i class="octicon octicon-pencil"></i></a>';
c["edit"] = '<a class="edit-comment text-muted" title="Edit" href="#">Edit</a>';
} }
} }
c.comment_on_small = comment_when(c.creation, true); c.comment_on_small = comment_when(c.creation, true);


+ 1
- 1
frappe/public/js/frappe/form/footer/timeline_item.html Näytä tiedosto

@@ -91,7 +91,7 @@
{% if (data.communication_medium === "Email" {% if (data.communication_medium === "Email"
&& data.sender !== frappe.session.user_email) { %} && data.sender !== frappe.session.user_email) { %}
<a class="text-muted reply-link pull-right timeline-content-show" <a class="text-muted reply-link pull-right timeline-content-show"
data-name="{%= data.name %}" title="{%= __("Reply") %}"><i class="octicon octicon-mail-reply"></i></a>
data-name="{%= data.name %}" title="{%= __("Reply") %}">{%= __("Reply") %}</a>
{% } %} {% } %}
{% } %} {% } %}
<span class="text-muted commented-on hidden-xs"> <span class="text-muted commented-on hidden-xs">


+ 6
- 3
frappe/public/js/frappe/form/print.js Näytä tiedosto

@@ -132,11 +132,14 @@ frappe.ui.form.PrintPreview = Class.extend({
show_footer: function() { show_footer: function() {
// footer is hidden by default as reqd by pdf generation // footer is hidden by default as reqd by pdf generation
// simple hack to show it in print preview // simple hack to show it in print preview
this.wrapper.find('.print-format').css('position', 'relative');
this.wrapper.find('.page-break').css({
'display': 'flex',
'flex-direction': 'column'
});
this.wrapper.find('#footer-html').attr('style', ` this.wrapper.find('#footer-html').attr('style', `
display: block !important; display: block !important;
position: absolute;
bottom: 0.75in;
order: 1;
margin-top: 20px;
`); `);
}, },
printit: function () { printit: function () {


+ 11
- 4
frappe/public/js/frappe/ui/base_list.js Näytä tiedosto

@@ -197,14 +197,14 @@ frappe.ui.BaseList = Class.extend({
onchange: () => { me.refresh(true); } onchange: () => { me.refresh(true); }
}); });


this.meta.fields.forEach(function(df) {
this.meta.fields.forEach(function(df, i) {
if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) { if(df.in_standard_filter && !frappe.model.no_value_type.includes(df.fieldtype)) {
let options = df.options; let options = df.options;
let condition = '='; let condition = '=';
let fieldtype = df.fieldtype; let fieldtype = df.fieldtype;
if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) { if (['Text', 'Small Text', 'Text Editor', 'Data'].includes(fieldtype)) {
fieldtype = 'Data',
condition = 'like'
fieldtype = 'Data';
condition = 'like';
} }
if(df.fieldtype == "Select" && df.options) { if(df.fieldtype == "Select" && df.options) {
options = df.options.split("\n"); options = df.options.split("\n");
@@ -213,7 +213,7 @@ frappe.ui.BaseList = Class.extend({
options = options.join("\n"); options = options.join("\n");
} }
} }
me.page.add_field({
let f = me.page.add_field({
fieldtype: fieldtype, fieldtype: fieldtype,
label: __(df.label), label: __(df.label),
options: options, options: options,
@@ -221,6 +221,13 @@ frappe.ui.BaseList = Class.extend({
condition: condition, condition: condition,
onchange: () => {me.refresh(true);} onchange: () => {me.refresh(true);}
}); });
filter_count ++;
if (filter_count > 3) {
$(f.wrapper).addClass('hidden-sm').addClass('hidden-xs');
}
if (filter_count > 5) {
return false;
}
} }
}); });
} }


+ 121
- 0
frappe/public/js/frappe/ui/colors.js Näytä tiedosto

@@ -0,0 +1,121 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt

frappe.provide("frappe.ui");

frappe.ui.color_map = {
red: ["#ffc4c4", "#ff8989", "#ff4d4d", "#a83333"],
brown: ["#ffe8cd", "#ffd19c", "#ffb868", "#a87945"],
orange: ["#ffd2c2", "#ffa685", "#ff7846", "#a85b5b"],
peach: ["#ffd7d7", "#ffb1b1", "#ff8989", "#a84f2e"],
yellow: ["#fffacd", "#fff168", "#fff69c", "#a89f45"],
yellowgreen: ["#ebf8cc", "#d9f399", "#c5ec63", "#7b933d"],
green: ["#cef6d1", "#9deca2", "#6be273", "#428b46"],
cyan: ["#d2f8ed", "#a4f3dd", "#77ecca", "#49937e"],
skyblue: ["#d2f1ff", "#a6e4ff", "#78d6ff", "#4f8ea8"],
blue: ["#d2d2ff", "#a3a3ff", "#7575ff", "#4d4da8"],
purple: ["#dac7ff", "#b592ff", "#8e58ff", "#5e3aa8"],
pink: ["#f8d4f8", "#f3aaf0", "#ec7dea", "#934f92"]
};

frappe.ui.color = {
get: function(color_name, shade) {
if(color_name && shade) return this.get_color_shade(color_name, shade);
if(color_name) return this.get_color_shade(color_name, 'default');
return frappe.ui.color_map;
},
get_color: function(color_name) {
const color_names = Object.keys(frappe.ui.color_map);
if(color_names.includes(color_name)) {
return frappe.ui.color_map[color_name];
} else {
throw new RangeError(`${color_name} can be one of ${color_names}`);
}
},
get_color_shade: function(color_name, shade) {
const shades = {
'default': 2,
'light': 1,
'extra-light': 0,
'dark': 3
};

if(Object.keys(shades).includes(shade)) {
return frappe.ui.color_map[color_name][shades[shade]];
} else {
throw new RangeError(`${shade} can be one of ${Object.keys(shades)}`);
}
},
all: function() {
return Object.values(frappe.ui.color_map)
.reduce((acc, curr) => acc.concat(curr) , []);
},
names: function() {
return Object.keys(frappe.ui.color_map);
},
validate: function(color_name) {
if(!color_name) return false;
if(color_name.startsWith('#')) {
return this.all().includes(color_name);
}
return this.names().includes(color_name);
},
get_color_name: function(hex) {
for (const key in frappe.ui.color_map) {
const colors = frappe.ui.color_map[key];
if (colors.includes(hex)) return key;
}
},
get_contrast_color: function(hex) {
if(!this.validate(hex)) {
const brightness = this.brightness(hex);
if(brightness < 128) {
return this.lighten(hex, 0.5);
}
return this.lighten(hex, -0.5);
}

const color_name = this.get_color_name(hex);
const colors = this.get_color(color_name);
const shade_value = colors.indexOf(hex);
if(shade_value <= 1) {
return this.get(color_name, 'dark');
}
return this.get(color_name, 'extra-light');
},

lighten(color, percent) {
// https://stackoverflow.com/a/13542669/5353542
var f = parseInt(color.slice(1), 16),
t = percent < 0 ? 0 : 255,
p = percent < 0 ? percent * -1 : percent,
R = f >> 16,
G = f >> 8 & 0x00FF,
B = f & 0x0000FF;
return "#" +
(0x1000000 +
(Math.round((t - R) * p) + R) *
0x10000 +
(Math.round((t - G) * p) + G) *
0x100 + (Math.round((t - B) * p) + B)
).toString(16).slice(1);
},

hex_to_rgb(hex) {
if(hex.startsWith('#')) {
hex = hex.substring(1);
}
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return {r, g, b};
},

brightness(hex) {
const rgb = this.hex_to_rgb(hex);
// https://www.w3.org/TR/AERT#color-contrast
// 255 - brightest (#fff)
// 0 - darkest (#000)
return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
}
};

+ 6
- 1
frappe/public/js/frappe/ui/page.js Näytä tiedosto

@@ -401,8 +401,13 @@ frappe.ui.Page = Class.extend({
.addClass('col-md-2') .addClass('col-md-2')
.attr("title", __(df.label)).tooltip(); .attr("title", __(df.label)).tooltip();


// html fields in toolbar are only for display
if (df.fieldtype=='HTML') {
return;
}

// hidden fields dont have $input // hidden fields dont have $input
if(!f.$input) f.make_input();
if (!f.$input) f.make_input();


f.$input.addClass("input-sm").attr("placeholder", __(df.label)); f.$input.addClass("input-sm").attr("placeholder", __(df.label));




+ 21
- 17
frappe/public/js/frappe/views/calendar/calendar.js Näytä tiedosto

@@ -100,7 +100,8 @@ frappe.views.Calendar = Class.extend({
color_map: { color_map: {
"danger": "red", "danger": "red",
"success": "green", "success": "green",
"warning": "orange"
"warning": "orange",
"default": "blue"
}, },
get_system_datetime: function(date) { get_system_datetime: function(date) {
date._offset = moment.user_utc_offset; date._offset = moment.user_utc_offset;
@@ -232,25 +233,28 @@ frappe.views.Calendar = Class.extend({
d.end = frappe.datetime.convert_to_user_tz(d.end); d.end = frappe.datetime.convert_to_user_tz(d.end);


me.fix_end_date_for_event_render(d); me.fix_end_date_for_event_render(d);

let color;
if(me.get_css_class) {
color = me.color_map[me.get_css_class(d)];
// if invalid, fallback to blue color
if(!Object.values(me.color_map).includes(color)) {
color = "blue";
}
} else {
// color field can be set in {doctype}_calendar.js
// see event_calendar.js
color = d.color;
}

if(!color) color = "blue";
d.className = "fc-bg-" + color;
me.prepare_colors(d);
return d; return d;
}); });
}, },
prepare_colors: function(d) {
let color, color_name;
if(this.get_css_class) {
color_name = this.color_map[this.get_css_class(d)];
color_name =
frappe.ui.color.validate(color_name) ?
color_name :
'blue';
d.backgroundColor = frappe.ui.color.get(color_name, 'extra-light');
d.textColor = frappe.ui.color.get(color_name, 'dark');
} else {
color = d.color;
if(!color) color = frappe.ui.color.get('blue', 'extra-light');
d.backgroundColor = color;
d.textColor = frappe.ui.color.get_contrast_color(color);
}
return d;
},
update_event: function(event, revertFunc) { update_event: function(event, revertFunc) {
var me = this; var me = this;
frappe.model.remove_from_locals(me.doctype, event.name); frappe.model.remove_from_locals(me.doctype, event.name);


+ 7
- 8
frappe/public/js/frappe/views/communication.js Näytä tiedosto

@@ -12,7 +12,7 @@ frappe.views.CommunicationComposer = Class.extend({
make: function() { make: function() {
var me = this; var me = this;
this.dialog = new frappe.ui.Dialog({ this.dialog = new frappe.ui.Dialog({
title: (this.subject || ""),
title: (this.title || this.subject || __("New Email")),
no_submit_on_enter: true, no_submit_on_enter: true,
fields: this.get_fields(), fields: this.get_fields(),
primary_action_label: __("Send"), primary_action_label: __("Send"),
@@ -49,12 +49,12 @@ frappe.views.CommunicationComposer = Class.extend({
var fields= [ var fields= [
{label:__("To"), fieldtype:"Data", reqd: 0, fieldname:"recipients",length:524288}, {label:__("To"), fieldtype:"Data", reqd: 0, fieldname:"recipients",length:524288},
{fieldtype: "Section Break", collapsible: 1, label: "CC & Standard Reply"}, {fieldtype: "Section Break", collapsible: 1, label: "CC & Standard Reply"},
{label:__("CC"), fieldtype:"Data", fieldname:"cc",length:524288},
{label:__("CC"), fieldtype:"Data", fieldname:"cc", length:524288},
{label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply", {label:__("Standard Reply"), fieldtype:"Link", options:"Standard Reply",
fieldname:"standard_reply"}, fieldname:"standard_reply"},
{fieldtype: "Section Break"}, {fieldtype: "Section Break"},
{label:__("Subject"), fieldtype:"Data", reqd: 1, {label:__("Subject"), fieldtype:"Data", reqd: 1,
fieldname:"subject",length:524288},
fieldname:"subject", length:524288},
{fieldtype: "Section Break"}, {fieldtype: "Section Break"},
{label:__("Message"), fieldtype:"Text Editor", reqd: 1, {label:__("Message"), fieldtype:"Text Editor", reqd: 1,
fieldname:"content"}, fieldname:"content"},
@@ -444,6 +444,7 @@ frappe.views.CommunicationComposer = Class.extend({


send_email: function(btn, form_values, selected_attachments, print_html, print_format) { send_email: function(btn, form_values, selected_attachments, print_html, print_format) {
var me = this; var me = this;
me.dialog.hide();


if((form_values.send_email || form_values.communication_medium === "Email") && !form_values.recipients) { if((form_values.send_email || form_values.communication_medium === "Email") && !form_values.recipients) {
frappe.msgprint(__("Enter Email Recipient(s)")); frappe.msgprint(__("Enter Email Recipient(s)"));
@@ -496,8 +497,6 @@ frappe.views.CommunicationComposer = Class.extend({
[ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) ); [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
} }


me.dialog.hide();

if ((frappe.last_edited_communication[me.doc] || {})[me.key]) { if ((frappe.last_edited_communication[me.doc] || {})[me.key]) {
delete frappe.last_edited_communication[me.doc][me.key]; delete frappe.last_edited_communication[me.doc][me.key];
} }
@@ -506,7 +505,7 @@ frappe.views.CommunicationComposer = Class.extend({
cur_frm.timeline.input && cur_frm.timeline.input.val(""); cur_frm.timeline.input && cur_frm.timeline.input.val("");
cur_frm.reload_doc(); cur_frm.reload_doc();
} }
// try the success callback if it exists // try the success callback if it exists
if (me.success) { if (me.success) {
try { try {
@@ -515,10 +514,10 @@ frappe.views.CommunicationComposer = Class.extend({
console.log(e); console.log(e);
} }
} }
} else { } else {
frappe.msgprint(__("There were errors while sending email. Please try again.")); frappe.msgprint(__("There were errors while sending email. Please try again."));
// try the error callback if it exists // try the error callback if it exists
if (me.error) { if (me.error) {
try { try {


+ 9
- 3
frappe/public/js/frappe/views/reports/query_report.js Näytä tiedosto

@@ -184,12 +184,12 @@ frappe.views.QueryReport = Class.extend({
frappe.msgprint(__("You are not allowed to print this report")); frappe.msgprint(__("You are not allowed to print this report"));
return false; return false;
} }

if(this.html_format) { if(this.html_format) {
var content = frappe.render(this.html_format, { var content = frappe.render(this.html_format, {
data: frappe.slickgrid_tools.get_filtered_items(this.dataView), data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
filters: this.get_values(), filters: this.get_values(),
report: this
report: this,
data_to_be_printed: this.data_to_be_printed
}); });


frappe.render_grid({ frappe.render_grid({
@@ -223,7 +223,8 @@ frappe.views.QueryReport = Class.extend({
var content = frappe.render(this.html_format, { var content = frappe.render(this.html_format, {
data: frappe.slickgrid_tools.get_filtered_items(this.dataView), data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
filters:this.get_values(), filters:this.get_values(),
report:this
report:this,
data_to_be_printed: this.data_to_be_printed
}); });


//Render Report in HTML //Render Report in HTML
@@ -487,6 +488,7 @@ frappe.views.QueryReport = Class.extend({


this.set_message(res.message); this.set_message(res.message);
this.setup_chart(res); this.setup_chart(res);
this.set_print_data(res.data_to_be_printed);


this.toggle_expand_collapse_buttons(this.is_tree_report); this.toggle_expand_collapse_buttons(this.is_tree_report);
}, },
@@ -897,5 +899,9 @@ frappe.views.QueryReport = Class.extend({
if(this.chart && opts.data && opts.data.rows && opts.data.rows.length) { if(this.chart && opts.data && opts.data.rows && opts.data.rows.length) {
this.chart_area.toggle(true); this.chart_area.toggle(true);
} }
},

set_print_data: function(data_to_be_printed) {
this.data_to_be_printed = data_to_be_printed;
} }
}) })

+ 2
- 1
frappe/public/js/legacy/form.js Näytä tiedosto

@@ -335,7 +335,7 @@ _f.Frm.prototype.refresh_header = function(is_a_different_doc) {
! this.is_dirty() && ! this.is_dirty() &&
! this.is_new() && ! this.is_new() &&
this.doc.docstatus===0) { this.doc.docstatus===0) {
this.dashboard.add_comment(__('Submit this document to confirm'), 'alert-warning', true);
this.dashboard.add_comment(__('Submit this document to confirm'), 'orange', true);
} }


this.clear_custom_buttons(); this.clear_custom_buttons();
@@ -459,6 +459,7 @@ _f.Frm.prototype.refresh = function(docname) {
_f.Frm.prototype.show_if_needs_refresh = function() { _f.Frm.prototype.show_if_needs_refresh = function() {
if(this.doc.__needs_refresh) { if(this.doc.__needs_refresh) {
if(this.doc.__unsaved) { if(this.doc.__unsaved) {
this.dashboard.clear_headline();
this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it") this.dashboard.set_headline_alert(__("This form has been modified after you have loaded it")
+ '<a class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">' + '<a class="btn btn-xs btn-primary pull-right" onclick="cur_frm.reload_doc()">'
+ __("Refresh") + '</a>', "alert-warning"); + __("Refresh") + '</a>', "alert-warning");


+ 1
- 2
frappe/public/less/calendar.less Näytä tiedosto

@@ -27,7 +27,7 @@ th.fc-widget-header {


.fc-unthemed .fc-today { .fc-unthemed .fc-today {
background-color: #FFF !important; background-color: #FFF !important;
.fc-day-number { .fc-day-number {
background-color: @brand-primary; background-color: @brand-primary;
min-width: 20px; min-width: 20px;
@@ -90,7 +90,6 @@ th.fc-day-header {
} }


.fc-day-grid-event { .fc-day-grid-event {
background-color: rgba(94, 100, 255, 0.2) !important;
border: none !important; border: none !important;
margin: 5px 4px 0 !important; margin: 5px 4px 0 !important;
padding: 1px 5px !important; padding: 1px 5px !important;


+ 1
- 1
frappe/public/less/email.less Näytä tiedosto

@@ -64,7 +64,7 @@ hr {
} }


.email-footer-container { .email-footer-container {
margin-top: 10px;
margin-top: 30px;


& > div:not(:last-child) { & > div:not(:last-child) {
margin-bottom: 5px; margin-bottom: 5px;


+ 22
- 5
frappe/public/less/form.less Näytä tiedosto

@@ -391,17 +391,32 @@ h6.uppercase, .h6.uppercase {
.action-btns { .action-btns {
position: absolute; position: absolute;
right: 0; right: 0;
padding: 5px 15px 2px 5px;
padding: 8px 15px 0 5px;
.edit-btn-container {
margin-right: 13px;
}
} }


.comment-header { .comment-header {
background-color: @light-bg; background-color: @light-bg;
padding: 10px 15px 10px 13px;
padding: 10px 15px 8px 13px;
margin: 0px; margin: 0px;
color: @text-muted; color: @text-muted;
border-bottom: 1px solid @light-border-color; border-bottom: 1px solid @light-border-color;
&.links-active { &.links-active {
padding-right: 60px;
padding-right: 77px;
}
.asset-details {
display: inline-block;
width: 100%;
.btn-link {
border: 0;
border-radius: 0;
padding: 0;
&:hover {
text-decoration: none;
}
}
} }
.commented-on-small { .commented-on-small {
display: none; display: none;
@@ -434,7 +449,8 @@ h6.uppercase, .h6.uppercase {
.close { .close {
color: inherit; color: inherit;
opacity: 1; opacity: 1;
padding: 0 0 0 10px;
padding: 0;
font-size: 18px;
} }
} }


@@ -530,7 +546,8 @@ h6.uppercase, .h6.uppercase {
} }


.timeline-item .reply-link { .timeline-item .reply-link {
padding-left: 7px;
margin-left: 15px;
font-size: 12px;
} }


.timeline-head { .timeline-head {


+ 21
- 2
frappe/public/less/list.less Näytä tiedosto

@@ -226,8 +226,27 @@
padding: 5px 15px; padding: 5px 15px;
} }


.listview-main-section .octicon-heart {
cursor: pointer;
.listview-main-section {
.octicon-heart {
cursor: pointer;
}
.page-form {
padding-left: 17px;

@media (max-width: @screen-sm) {
padding-left: 25px;
}

.octicon-search {
float: left;
padding-top: 7px;
margin-left: -4px;
margin-right: -4px;
@media (max-width: @screen-sm) {
margin-left: -12px;
}
}
}
} }


.like-action.octicon-heart { .like-action.octicon-heart {


+ 14
- 2
frappe/public/less/mobile.less Näytä tiedosto

@@ -34,6 +34,9 @@ body {
body[data-route^="Form"] { body[data-route^="Form"] {
.page-title h1 { .page-title h1 {
margin-top: 12px; margin-top: 12px;
&.editable-title {
padding-right: 80px;
}
} }


.page-title .indicator { .page-title .indicator {
@@ -230,7 +233,7 @@ body {
.page-title { .page-title {
.title-text { .title-text {
font-size: 16px; font-size: 16px;
width: calc(~"100% - 30px");
width: calc(~"100% - 90px");
} }
.indicator { .indicator {
float: left; float: left;
@@ -432,13 +435,22 @@ body {
} }
} }
.action-btns { .action-btns {
padding: 5px 10px 2px 5px;
padding: 7px 10px 2px 5px;
.edit-btn-container {
margin-right: 0;
}
} }
.comment-header{ .comment-header{
padding: 7px 10px; padding: 7px 10px;
.links-active { .links-active {
padding-right: 10px; padding-right: 10px;
} }
.reply-link {
margin-left: 0;
}
.asset-details {
width: calc(~"100% - 30px")
}
} }
.avatar-medium { .avatar-medium {
margin-right: 10px; margin-right: 10px;


+ 1
- 1
frappe/public/less/page.less Näytä tiedosto

@@ -54,7 +54,6 @@
} }


.title-image { .title-image {
display: inline-block;
width: 46px; width: 46px;
height: 0; height: 0;
padding: 23px 0; padding: 23px 0;
@@ -66,6 +65,7 @@
text-align: center; text-align: center;
line-height: 0; line-height: 0;
float: left; float: left;
margin-right: 10px;
} }
} }




+ 59
- 20
frappe/public/less/website.less Näytä tiedosto

@@ -125,6 +125,7 @@ li {
} }


.page_content { .page_content {
padding-top: 30px;
padding-bottom: 30px; padding-bottom: 30px;
} }


@@ -181,6 +182,7 @@ li {
} }


.page-head { .page-head {
margin-bottom: -30px;
h1, h2 { h1, h2 {
margin-top: 0px; margin-top: 0px;
} }
@@ -221,9 +223,13 @@ fieldset {
} }


.page-container { .page-container {
padding: 0px;
display: flex;
max-width: 970px; max-width: 970px;
margin: auto;
margin: 0 auto;

@media(max-width: @screen-xs) {
flex-direction: column-reverse;
}
} }


.page-max-width { .page-max-width {
@@ -241,12 +247,11 @@ fieldset {
.web-sidebar { .web-sidebar {
position: relative; position: relative;


.sidebar-item {
.sidebar-item:not(:last-child) {
margin: 0px; margin: 0px;
padding-bottom: 12px; padding-bottom: 12px;
border: none; border: none;
color: @text-muted; color: @text-muted;
font-size: 12px;


.badge { .badge {
font-weight: normal; font-weight: normal;
@@ -255,21 +260,22 @@ fieldset {
} }


.sidebar-item a { .sidebar-item a {
color: @text-color !important;
}
color: @text-muted;


.sidebar-item a.active {
color: @text-color !important;
font-weight: 500 !important;
&.active {
color: @text-color;
}
} }


.sidebar-items { .sidebar-items {
// margin-top:30px;
margin-bottom:30px;
.title{ .title{
font-size: 14px; font-size: 14px;
font-weight: bold; font-weight: bold;
} }

ul {
margin-bottom: 0;
}
} }
} }


@@ -378,11 +384,6 @@ textarea {
} }
} }


@media (max-width: 767px) {
.visible-xs {
display: inline-block !important;
}
}


.more-block { .more-block {
padding-bottom: 30px; padding-bottom: 30px;
@@ -477,16 +478,54 @@ a.active {
margin-top: 60px; margin-top: 60px;
} }


.sidebar-block, .page-content {
.sidebar-block {
flex: 1;
font-size: @text-medium;
border-right: 1px solid @border-color;
padding: 30px;
padding-left: 0px;

@media(max-width: @screen-xs) {
font-size: @text-regular;
border-right: none;
border-top: 1px solid @border-color;
padding-left: 20px;
}
}

.page-content {
flex: 6;

h1:first-child {
margin-top: 0;
}
}

.page-content.with-sidebar {
padding: 30px;
padding-left: 40px;
}

.page-content.without-sidebar {
padding-top: 30px; padding-top: 30px;
padding-bottom: 50px;
} }

.your-account-info { .your-account-info {
margin-top: 30px; margin-top: 30px;
} }


.page-content.with-sidebar {
padding-left: 50px;
@media (max-width: 767px) {
.visible-xs {
display: inline-block !important;
}
.sidebar-block {
width: 100%;
}
.page-content.with-sidebar {
width: 100%;
padding-left: 20px;
padding-right: 20px;
}
} }


@media screen and (max-width: 480px) { @media screen and (max-width: 480px) {


+ 1
- 1
frappe/templates/includes/list/list.html Näytä tiedosto

@@ -20,7 +20,7 @@
{% endfor %} {% endfor %}
</div> </div>
<div class="more-block {% if not show_more -%} hide {%- endif %}"> <div class="more-block {% if not show_more -%} hide {%- endif %}">
<button class="btn btn-default btn-more">{{ _("More") }}</button>
<button class="btn btn-default btn-more btn-sm">{{ _("More") }}</button>
</div> </div>
</div> </div>
{%- endif %} {%- endif %}

+ 99
- 2
frappe/templates/includes/login/login.js Näytä tiedosto

@@ -5,11 +5,14 @@ window.disable_signup = {{ disable_signup and "true" or "false" }};


window.login = {}; window.login = {};


window.verify = {};

login.bind_events = function() { login.bind_events = function() {
$(window).on("hashchange", function() { $(window).on("hashchange", function() {
login.route(); login.route();
}); });



$(".form-login").on("submit", function(event) { $(".form-login").on("submit", function(event) {
event.preventDefault(); event.preventDefault();
var args = {}; var args = {};
@@ -92,6 +95,11 @@ login.login = function() {
$(".for-login").toggle(true); $(".for-login").toggle(true);
} }


login.steptwo = function() {
login.reset_sections();
$(".for-login").toggle(true);
}

login.forgot = function() { login.forgot = function() {
login.reset_sections(); login.reset_sections();
$(".for-forgot").toggle(true); $(".for-forgot").toggle(true);
@@ -150,7 +158,7 @@ login.login_handlers = (function() {


var login_handlers = { var login_handlers = {
200: function(data) { 200: function(data) {
if(data.message=="Logged In") {
if(data.message == 'Logged In'){
login.set_indicator("{{ _("Success") }}", 'green'); login.set_indicator("{{ _("Success") }}", 'green');
window.location.href = get_url_arg("redirect-to") || data.home_page; window.location.href = get_url_arg("redirect-to") || data.home_page;
} else if(data.message=="No App") { } else if(data.message=="No App") {
@@ -190,15 +198,31 @@ login.login_handlers = (function() {
} }
//login.set_indicator(__(data.message), 'green'); //login.set_indicator(__(data.message), 'green');
} }

//OTP verification
if(data.verification && data.message != 'Logged In') {
login.set_indicator("{{ _("Success") }}", 'green');

document.cookie = "tmp_id="+data.tmp_id;

if (data.verification.method == 'OTP App'){
continue_otp_app(data.verification.setup, data.verification.qrcode);
} else if (data.verification.method == 'SMS'){
continue_sms(data.verification.setup, data.verification.prompt);
} else if (data.verification.method == 'Email'){
continue_email(data.verification.setup, data.verification.prompt);
}
}
}, },
401: get_error_handler("{{ _("Invalid Login. Try again.") }}"), 401: get_error_handler("{{ _("Invalid Login. Try again.") }}"),
417: get_error_handler("{{ _("Oops! Something went wrong") }}") 417: get_error_handler("{{ _("Oops! Something went wrong") }}")
}; };


return login_handlers; return login_handlers;
})();
} )();


frappe.ready(function() { frappe.ready(function() {

login.bind_events(); login.bind_events();


if (!window.location.hash) { if (!window.location.hash) {
@@ -210,3 +234,76 @@ frappe.ready(function() {
$(".form-signup, .form-forgot").removeClass("hide"); $(".form-signup, .form-forgot").removeClass("hide");
$(document).trigger('login_rendered'); $(document).trigger('login_rendered');
}); });

var verify_token = function(event) {
$(".form-verify").on("submit", function(eventx) {
eventx.preventDefault();
var args = {};
args.cmd = "login";
args.otp = $("#login_token").val();
args.tmp_id = frappe.get_cookie('tmp_id');
if(!args.otp) {
frappe.msgprint('{{ _("Login token required") }}');
return false;
}
login.call(args);
return false;
});
}

var request_otp = function(r){
$('.login-content').empty().append($('<div>').attr({'id':'twofactor_div'}).html(
'<form class="form-verify">\
<div class="page-card-head">\
<span class="indicator blue" data-text="Verification">Verification</span>\
</div>\
<div id="otp_div"></div>\
<input type="text" id="login_token" autocomplete="off" class="form-control" placeholder="Verification Code" required="" autofocus="">\
<button class="btn btn-sm btn-primary btn-block" id="verify_token">Verify</button>\
</form>'));
// add event handler for submit button
verify_token();
}

var continue_otp_app = function(setup, qrcode){
request_otp();
var qrcode_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');

if (setup){
direction = $('<div>').attr('id','qr_info').text('Enter Code displayed in OTP App.');
qrcode_div.append(direction);
$('#otp_div').prepend(qrcode_div);
} else {
direction = $('<div>').attr('id','qr_info').text('OTP setup using OTP App was not completed. Please contact Administrator.');
qrcode_div.append(direction);
$('#otp_div').prepend(qrcode_div);
}
}

var continue_sms = function(setup, prompt){
request_otp();
var sms_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');

if (setup){
sms_div.append(prompt)
$('#otp_div').prepend(sms_div);
} else {
direction = $('<div>').attr('id','qr_info').text(prompt || 'SMS was not sent. Please contact Administrator.');
sms_div.append(direction);
$('#otp_div').prepend(sms_div)
}
}

var continue_email = function(setup, prompt){
request_otp();
var email_div = $('<div class="text-muted" style="padding-bottom: 15px;"></div>');

if (setup){
email_div.append(prompt)
$('#otp_div').prepend(email_div);
} else {
var direction = $('<div>').attr('id','qr_info').text(prompt || 'Verification code email not sent. Please contact Administrator.');
email_div.append(direction);
$('#otp_div').prepend(email_div);
}
}

+ 3
- 4
frappe/templates/web.html Näytä tiedosto

@@ -6,13 +6,12 @@
data-path="{{ pathname }}" data-path="{{ pathname }}"
{%- if page_or_generator=="Generator" %} {%- if page_or_generator=="Generator" %}
data-doctype="{{ doctype }}"{% endif %}> data-doctype="{{ doctype }}"{% endif %}>
<div class="row {% if show_sidebar %}vert-line{% endif %}">
{% if show_sidebar %} {% if show_sidebar %}
<div class="col-sm-3 sidebar-block hidden-xs">
<div class="sidebar-block">
{% include "templates/includes/web_sidebar.html" %} {% include "templates/includes/web_sidebar.html" %}
</div> </div>
{% endif %} {% endif %}
<div class="{% if show_sidebar %}page-content with-sidebar col-sm-9{% else %} page-content col-sm-12 {% endif %}">
<div class="{% if show_sidebar %}page-content with-sidebar{% else %}page-content without-sidebar{% endif %}">
<div class="page-content-wrapper"> <div class="page-content-wrapper">
<div class="row page-head"> <div class="row page-head">
<div class='col-sm-12'> <div class='col-sm-12'>
@@ -48,7 +47,7 @@
{%- block page_content -%}{%- endblock -%} {%- block page_content -%}{%- endblock -%}
</div> </div>
</div> </div>
<!-- sidebar ends -->
</div> </div>
</div>
</div> </div>
{% endblock %} {% endblock %}

+ 132
- 0
frappe/tests/test_twofactor.py Näytä tiedosto

@@ -0,0 +1,132 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
from __future__ import unicode_literals

import unittest, frappe, pyotp
from werkzeug.wrappers import Request
from werkzeug.test import EnvironBuilder
from frappe.auth import HTTPRequest
from frappe.twofactor import (should_run_2fa, authenticate_for_2factor, get_cached_user_pass,
two_factor_is_enabled_for_, confirm_otp_token, get_otpsecret_for_, get_verification_obj,
render_string_template)

import time

class TestTwoFactor(unittest.TestCase):
def setUp(self):
self.http_requests = create_http_request()
self.login_manager = frappe.local.login_manager
self.user = self.login_manager.user

def tearDown(self):
frappe.local.response['verification'] = None
frappe.local.response['tmp_id'] = None
disable_2fa()
frappe.clear_cache(user=self.user)

def test_should_run_2fa(self):
'''Should return true if enabled.'''
toggle_2fa_all_role(state=True)
self.assertTrue(should_run_2fa(self.user))
toggle_2fa_all_role(state=False)
self.assertFalse(should_run_2fa(self.user))

def test_get_cached_user_pass(self):
'''Cached data should not contain user and pass before 2fa.'''
user,pwd = get_cached_user_pass()
self.assertTrue(all([not user, not pwd]))

def test_authenticate_for_2factor(self):
'''Verification obj and tmp_id should be set in frappe.local.'''
authenticate_for_2factor(self.user)
verification_obj = frappe.local.response['verification']
tmp_id = frappe.local.response['tmp_id']
self.assertTrue(verification_obj)
self.assertTrue(tmp_id)
for k in ['_usr','_pwd','_otp_secret']:
self.assertTrue(frappe.cache().get('{0}{1}'.format(tmp_id,k)),
'{} not available'.format(k))

def test_two_factor_is_enabled_for_user(self):
'''Should return true if enabled for user.'''
toggle_2fa_all_role(state=True)
self.assertTrue(two_factor_is_enabled_for_(self.user))
toggle_2fa_all_role(state=False)
self.assertFalse(two_factor_is_enabled_for_(self.user))

def test_get_otpsecret_for_user(self):
'''OTP secret should be set for user.'''
self.assertTrue(get_otpsecret_for_(self.user))
self.assertTrue(frappe.db.get_default(self.user + '_otpsecret'))

def test_confirm_otp_token(self):
'''Ensure otp is confirmed'''
authenticate_for_2factor(self.user)
tmp_id = frappe.local.response['tmp_id']
otp = 'wrongotp'
with self.assertRaises(frappe.AuthenticationError):
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)
otp = get_otp(self.user)
self.assertTrue(confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id))
if frappe.flags.tests_verbose:
print('Sleeping for 30secs to confirm token expires..')
time.sleep(30)
with self.assertRaises(frappe.AuthenticationError):
confirm_otp_token(self.login_manager,otp=otp,tmp_id=tmp_id)

def test_get_verification_obj(self):
'''Confirm verification object is returned.'''
otp_secret = get_otpsecret_for_(self.user)
token = int(pyotp.TOTP(otp_secret).now())
self.assertTrue(get_verification_obj(self.user,token,otp_secret))

def test_render_string_template(self):
'''String template renders as expected with variables.'''
args = {'issuer_name':'Frappe Technologies'}
_str = 'Verification Code from {{issuer_name}}'
_str = render_string_template(_str,args)
self.assertEqual(_str,'Verification Code from Frappe Technologies')


def set_request(**kwargs):
builder = EnvironBuilder(**kwargs)
frappe.local.request = Request(builder.get_environ())

def create_http_request():
'''Get http request object.'''
set_request(method='POST', path='login')
enable_2fa()
frappe.form_dict['usr'] = 'test@erpnext.com'
frappe.form_dict['pwd'] = 'test'
frappe.local.form_dict['cmd'] = 'login'
http_requests = HTTPRequest()
return http_requests

def enable_2fa():
'''Enable Two factor in system settings.'''
system_settings = frappe.get_doc('System Settings')
system_settings.enable_two_factor_auth = 1
system_settings.two_factor_method = 'OTP App'
system_settings.save(ignore_permissions=True)
frappe.db.commit()

def disable_2fa():
system_settings = frappe.get_doc('System Settings')
system_settings.enable_two_factor_auth = 0
system_settings.save(ignore_permissions=True)
frappe.db.commit()

def toggle_2fa_all_role(state=None):
'''Enable or disable 2fa for 'all' role on the system.'''
all_role = frappe.get_doc('Role','All')
if state == None:
state = False if all_role.two_factor_auth == True else False
if state not in [True,False]:return
all_role.two_factor_auth = state
all_role.save(ignore_permissions=True)
frappe.db.commit()

def get_otp(user):
otp_secret = get_otpsecret_for_(user)
otp = pyotp.TOTP(otp_secret)
return otp.now()

+ 2
- 0
frappe/tests/ui/test_test_runner.py Näytä tiedosto

@@ -6,6 +6,7 @@ class TestTestRunner(unittest.TestCase):
def test_test_runner(self): def test_test_runner(self):
driver = TestDriver() driver = TestDriver()
driver.login() driver.login()
frappe.db.set_default('in_selenium', '1')
for test in get_tests(): for test in get_tests():
if test.startswith('#'): if test.startswith('#'):
continue continue
@@ -33,6 +34,7 @@ class TestTestRunner(unittest.TestCase):
print('Checking if passed "{0}"'.format(test)) print('Checking if passed "{0}"'.format(test))
self.assertTrue('Tests Passed' in console) self.assertTrue('Tests Passed' in console)
time.sleep(1) time.sleep(1)
frappe.db.set_default('in_selenium', None)
driver.close() driver.close()


def get_tests(): def get_tests():


+ 2
- 1
frappe/tests/ui/tests.txt Näytä tiedosto

@@ -9,4 +9,5 @@ frappe/tests/ui/test_kanban/test_kanban_filters.js
frappe/tests/ui/test_kanban/test_kanban_column.js frappe/tests/ui/test_kanban/test_kanban_column.js
frappe/core/doctype/report/test_query_report.js frappe/core/doctype/report/test_query_report.js
frappe/tests/ui/test_linked_with.js frappe/tests/ui/test_linked_with.js
frappe/custom/doctype/customize_form/test_customize_form.js
frappe/custom/doctype/customize_form/test_customize_form.js
frappe/desk/doctype/event/test_event.js

+ 369
- 0
frappe/twofactor.py Näytä tiedosto

@@ -0,0 +1,369 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals

import frappe
from frappe import _
import pyotp, os
from frappe.utils.background_jobs import enqueue
from jinja2 import Template
from pyqrcode import create as qrcreate
from StringIO import StringIO
from base64 import b64encode, b32encode
from frappe.utils import get_url, get_datetime, time_diff_in_seconds

class ExpiredLoginException(Exception): pass

def toggle_two_factor_auth(state, roles=[]):
'''Enable or disable 2FA in site_config and roles'''
for role in roles:
role = frappe.get_doc('Role', {'role_name': role})
role.two_factor_auth = state
role.save(ignore_permissions=True)

def two_factor_is_enabled(user=None):
'''Returns True if 2FA is enabled.'''
enabled = int(frappe.db.get_value('System Settings', None, 'enable_two_factor_auth') or 0)
if not user or not enabled:
return enabled
return two_factor_is_enabled_for_(user)

def should_run_2fa(user):
'''Check if 2fa should run.'''
return two_factor_is_enabled(user=user)

def get_cached_user_pass():
'''Get user and password if set.'''
user = pwd = None
tmp_id = frappe.form_dict.get('tmp_id')
if tmp_id:
user = frappe.cache().get(tmp_id+'_usr')
pwd = frappe.cache().get(tmp_id+'_pwd')
return (user, pwd)

def authenticate_for_2factor(user):
'''Authenticate two factor for enabled user before login.'''
if frappe.form_dict.get('otp'):
return
otp_secret = get_otpsecret_for_(user)
token = int(pyotp.TOTP(otp_secret).now())
tmp_id = frappe.generate_hash(length=8)
cache_2fa_data(user, token, otp_secret, tmp_id)
verification_obj = get_verification_obj(user, token, otp_secret)
# Save data in local
frappe.local.response['verification'] = verification_obj
frappe.local.response['tmp_id'] = tmp_id

def cache_2fa_data(user, token, otp_secret, tmp_id):
'''Cache and set expiry for data.'''
pwd = frappe.form_dict.get('pwd')
verification_method = get_verification_method()

# set increased expiry time for SMS and Email
if verification_method in ['SMS', 'Email']:
expiry_time = 300
frappe.cache().set(tmp_id + '_token', token)
frappe.cache().expire(tmp_id + '_token', expiry_time)
else:
expiry_time = 180
for k, v in {'_usr': user, '_pwd': pwd, '_otp_secret': otp_secret}.iteritems():
frappe.cache().set("{0}{1}".format(tmp_id, k), v)
frappe.cache().expire("{0}{1}".format(tmp_id, k), expiry_time)

def two_factor_is_enabled_for_(user):
'''Check if 2factor is enabled for user.'''
if isinstance(user, basestring):
user = frappe.get_doc('User', user)

roles = [frappe.db.escape(d.role) for d in user.roles or []]
roles.append('All')

query = """select name from `tabRole` where two_factor_auth=1
and name in ({0}) limit 1""".format(', '.join('\"{}\"'.format(i) for \
i in roles))
if len(frappe.db.sql(query)) > 0:
return True

return False

def get_otpsecret_for_(user):
'''Set OTP Secret for user even if not set.'''
otp_secret = frappe.db.get_default(user + '_otpsecret')
if not otp_secret:
otp_secret = b32encode(os.urandom(10)).decode('utf-8')
frappe.db.set_default(user + '_otpsecret', otp_secret)
frappe.db.commit()
return otp_secret

def get_verification_method():
return frappe.db.get_value('System Settings', None, 'two_factor_method')

def confirm_otp_token(login_manager, otp=None, tmp_id=None):
'''Confirm otp matches.'''
if not otp:
otp = frappe.form_dict.get('otp')
if not otp:
if two_factor_is_enabled_for_(login_manager.user):
return False
return True
if not tmp_id:
tmp_id = frappe.form_dict.get('tmp_id')
hotp_token = frappe.cache().get(tmp_id + '_token')
otp_secret = frappe.cache().get(tmp_id + '_otp_secret')
if not otp_secret:
raise ExpiredLoginException(_('Login session expired, refresh page to retry'))
hotp = pyotp.HOTP(otp_secret)
if hotp_token:
if hotp.verify(otp, int(hotp_token)):
frappe.cache().delete(tmp_id + '_token')
return True
else:
login_manager.fail(_('Incorrect Verification code'), login_manager.user)

totp = pyotp.TOTP(otp_secret)
if totp.verify(otp):
# show qr code only once
if not frappe.db.get_default(login_manager.user + '_otplogin'):
frappe.db.set_default(login_manager.user + '_otplogin', 1)
delete_qrimage(login_manager.user)
return True
else:
login_manager.fail(_('Incorrect Verification code'), login_manager.user)


def get_verification_obj(user, token, otp_secret):
otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
verification_method = get_verification_method()
verification_obj = None
if verification_method == 'SMS':
verification_obj = process_2fa_for_sms(user, token, otp_secret)
elif verification_method == 'OTP App':
#check if this if the first time that the user is trying to login. If so, send an email
if not frappe.db.get_default(user + '_otplogin'):
verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer, method='OTP App')
else:
verification_obj = process_2fa_for_otp_app(user, otp_secret, otp_issuer)
elif verification_method == 'Email':
verification_obj = process_2fa_for_email(user, token, otp_secret, otp_issuer)
return verification_obj


def process_2fa_for_sms(user, token, otp_secret):
'''Process sms method for 2fa.'''
phone = frappe.db.get_value('User', user, ['phone', 'mobile_no'], as_dict=1)
phone = phone.mobile_no or phone.phone
status = send_token_via_sms(otp_secret, token=token, phone_no=phone)
verification_obj = {
'token_delivery': status,
'prompt': status and 'Enter verification code sent to {}'.format(phone[:4] + '******' + phone[-3:]),
'method': 'SMS',
'setup': status
}
return verification_obj

def process_2fa_for_otp_app(user, otp_secret, otp_issuer):
'''Process OTP App method for 2fa.'''
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
if frappe.db.get_default(user + '_otplogin'):
otp_setup_completed = True
else:
otp_setup_completed = False

verification_obj = {
'totp_uri': totp_uri,
'method': 'OTP App',
'qrcode': get_qr_svg_code(totp_uri),
'setup': otp_setup_completed
}
return verification_obj

def process_2fa_for_email(user, token, otp_secret, otp_issuer, method='Email'):
'''Process Email method for 2fa.'''
subject = None
message = None
status = True
prompt = ''
if method == 'OTP App' and not frappe.db.get_default(user + '_otplogin'):
'''Sending one-time email for OTP App'''
totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer)
qrcode_link = get_link_for_qrcode(user, totp_uri)
message = get_email_body_for_qr_code({'qrcode_link': qrcode_link})
subject = get_email_subject_for_qr_code({'qrcode_link': qrcode_link})
prompt = _('Please check your registered email address for instructions on how to proceed. Do not close this window as you will have to return to it.')
else:
'''Sending email verification'''
prompt = _('Verification code has been sent to your registered email address.')
status = send_token_via_email(user, token, otp_secret, otp_issuer, subject=subject, message=message)
verification_obj = {
'token_delivery': status,
'prompt': status and prompt,
'method': 'Email',
'setup': status
}
return verification_obj

def get_email_subject_for_2fa(kwargs_dict):
'''Get email subject for 2fa.'''
subject_template = _('Login Verification Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
subject = render_string_template(subject_template, kwargs_dict)
return subject

def get_email_body_for_2fa(kwargs_dict):
'''Get email body for 2fa.'''
body_template = 'Enter this code to complete your login:<br><br> <b>{{otp}}</b>'
body = render_string_template(body_template, kwargs_dict)
return body

def get_email_subject_for_qr_code(kwargs_dict):
'''Get QRCode email subject.'''
subject_template = _('One Time Password (OTP) Registration Code from {}').format(frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name'))
subject = render_string_template(subject_template, kwargs_dict)
return subject

def get_email_body_for_qr_code(kwargs_dict):
'''Get QRCode email body.'''
body_template = 'Please click on the following link and follow the instructions on the page.<br><br> {{qrcode_link}}'
body = render_string_template(body_template, kwargs_dict)
return body

def render_string_template(_str, kwargs_dict):
'''Render string with jinja.'''
s = Template(_str)
s = s.render(**kwargs_dict)
return s

def get_link_for_qrcode(user, totp_uri):
'''Get link to temporary page showing QRCode.'''
key = frappe.generate_hash(length=20)
key_user = "{}_user".format(key)
key_uri = "{}_uri".format(key)
lifespan = int(frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image'))
if lifespan<=0:
lifespan = 240
frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan)
frappe.cache().set_value(key_user, user, expires_in_sec=lifespan)
return get_url('/qrcode?k={}'.format(key))

def send_token_via_sms(otpsecret, token=None, phone_no=None):
'''Send token as sms to user.'''
otp_issuer = frappe.db.get_value('System Settings', 'System Settings', 'otp_issuer_name')
try:
from frappe.core.doctype.sms_settings.sms_settings import send_request
except:
return False

if not phone_no:
return False

ss = frappe.get_doc('SMS Settings', 'SMS Settings')
if not ss.sms_gateway_url:
return False

hotp = pyotp.HOTP(otpsecret)
args = {ss.message_parameter: 'Your verification code is {}'.format(hotp.at(int(token))), ss.sms_sender_name: otp_issuer}
for d in ss.get("parameters"):
args[d.parameter] = d.value

args[ss.receiver_parameter] = phone_no

sms_args = {'gateway_url': ss.sms_gateway_url, 'params': args}
enqueue(method=send_request, queue='short', timeout=300, event=None, async=True, job_name=None, now=False, **sms_args)
return True

def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, message=None):
'''Send token to user as email.'''
user_email = frappe.db.get_value('User', user, 'email')
if not user_email:
return False
hotp = pyotp.HOTP(otp_secret)
otp = hotp.at(int(token))
template_args = {'otp': otp, 'otp_issuer': otp_issuer}
if not subject:
subject = get_email_subject_for_2fa(template_args)
if not message:
message = get_email_body_for_2fa(template_args)

email_args = {
'recipients': user_email,
'sender': None,
'subject': subject,
'message': message,
'header': [_('Verfication Code'), 'blue'],
'delayed': False,
'retry':3
}

enqueue(method=frappe.sendmail, queue='short',
timeout=300, event=None, async=True, job_name=None, now=False, **email_args)
return True

def get_qr_svg_code(totp_uri):
'''Get SVG code to display Qrcode for OTP.'''
url = qrcreate(totp_uri)
svg = ''
stream = StringIO()
try:
url.svg(stream, scale=4, background="#eee", module_color="#222")
svg = stream.getvalue().replace('\n', '')
svg = b64encode(bytes(svg))
finally:
stream.close()
return svg

def qrcode_as_png(user, totp_uri):
'''Save temporary Qrcode to server.'''
from frappe.utils.file_manager import save_file
folder = create_barcode_folder()
png_file_name = '{}.png'.format(frappe.generate_hash(length=20))
file_obj = save_file(png_file_name, png_file_name, 'User', user, folder=folder)
frappe.db.commit()
file_url = get_url(file_obj.file_url)
file_path = os.path.join(frappe.get_site_path('public', 'files'), file_obj.file_name)
url = qrcreate(totp_uri)
with open(file_path, 'w') as png_file:
url.png(png_file, scale=8, module_color=[0, 0, 0, 180], background=[0xff, 0xff, 0xcc])
return file_url

def create_barcode_folder():
'''Get Barcodes folder.'''
folder_name = 'Barcodes'
folder = frappe.db.exists('File', {'file_name': folder_name})
if folder:
return folder
folder = frappe.get_doc({
'doctype': 'File',
'file_name': folder_name,
'is_folder':1,
'folder': 'Home'
})
folder.insert(ignore_permissions=True)
return folder.name

def delete_qrimage(user, check_expiry=False):
'''Delete Qrimage when user logs in.'''
user_barcodes = frappe.get_all('File', {'attached_to_doctype': 'User',
'attached_to_name': user, 'folder': 'Home/Barcodes'})
for barcode in user_barcodes:
if check_expiry and not should_remove_barcode_image(barcode): continue
barcode = frappe.get_doc('File', barcode.name)
frappe.delete_doc('File', barcode.name, ignore_permissions=True)

def delete_all_barcodes_for_users():
'''Task to delete all barcodes for user.'''
users = frappe.get_all('User', {'enabled':1})
for user in users:
delete_qrimage(user.name, check_expiry=True)

def should_remove_barcode_image(barcode):
'''Check if it's time to delete barcode image from server. '''
if isinstance(barcode, basestring):
barcode = frappe.get_doc('File', barcode)
lifespan = frappe.db.get_value('System Settings', 'System Settings', 'lifespan_qrcode_image')
if time_diff_in_seconds(get_datetime(), barcode.creation) > int(lifespan):
return True
return False

def disable():
frappe.db.set_value('System Settings', None, 'enable_two_factor_auth', 0)


+ 1
- 1
frappe/utils/redis_wrapper.py Näytä tiedosto

@@ -3,7 +3,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals


import redis, frappe, re import redis, frappe, re
import cPickle as pickle
from six.moves import cPickle as pickle
from frappe.utils import cstr from frappe.utils import cstr
from six import iteritems from six import iteritems




+ 0
- 1
frappe/website/router.py Näytä tiedosto

@@ -35,7 +35,6 @@ def get_page_context(path):
page_context = make_page_context(path) page_context = make_page_context(path)
if can_cache(page_context.no_cache): if can_cache(page_context.no_cache):
page_context_cache[frappe.local.lang] = page_context page_context_cache[frappe.local.lang] = page_context

frappe.cache().hset("page_context", path, page_context_cache) frappe.cache().hset("page_context", path, page_context_cache)


return page_context return page_context


+ 5
- 1
frappe/website/utils.py Näytä tiedosto

@@ -24,7 +24,11 @@ def find_first_image(html):
return None return None


def can_cache(no_cache=False): def can_cache(no_cache=False):
return not (frappe.conf.disable_website_cache or getattr(frappe.local, "no_cache", False) or no_cache)
if frappe.conf.disable_website_cache or frappe.conf.developer_mode:
return False
if getattr(frappe.local, "no_cache", False):
return False
return not no_cache


def get_comment_list(doctype, name): def get_comment_list(doctype, name):
return frappe.db.sql("""select return frappe.db.sql("""select


+ 8
- 3
frappe/www/desk.html Näytä tiedosto

@@ -1,6 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<head> <head>
<meta charset="utf-8">
<!-- Chrome, Firefox OS and Opera -->
<meta name="theme-color" content="#7575ff">
<!-- Windows Phone -->
<meta name="msapplication-navbutton-color" content="#7575ff">
<!-- iOS Safari -->
<meta name="apple-mobile-web-app-status-bar-style" content="#7575ff"> <meta charset="utf-8">
<meta content="text/html;charset=utf-8" http-equiv="Content-Type"> <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
<meta content="utf-8" http-equiv="encoding"> <meta content="utf-8" http-equiv="encoding">
<meta name="author" content=""> <meta name="author" content="">
@@ -15,7 +20,7 @@
<link rel="icon" <link rel="icon"
href="{{ favicon or "/assets/frappe/images/favicon.png" }}" type="image/x-icon"> href="{{ favicon or "/assets/frappe/images/favicon.png" }}" type="image/x-icon">
{% for include in include_css -%} {% for include in include_css -%}
<link type="text/css" rel="stylesheet" href="{{ include }}">
<link type="text/css" rel="stylesheet" href="{{ include }}?ver={{ build_version }}">
{%- endfor -%} {%- endfor -%}
</head> </head>
<body> <body>
@@ -50,7 +55,7 @@
</script> </script>


{% for include in include_js %} {% for include in include_js %}
<script type="text/javascript" src="{{ include }}"></script>
<script type="text/javascript" src="{{ include }}?ver={{ build_version }}"></script>
{% endfor %} {% endfor %}
{% include "templates/includes/app_analytics/google_analytics.html" %} {% include "templates/includes/app_analytics/google_analytics.html" %}
{% include "templates/includes/app_analytics/mixpanel_analytics.html" %} {% include "templates/includes/app_analytics/mixpanel_analytics.html" %}


+ 5
- 5
frappe/www/desk.py Näytä tiedosto

@@ -35,7 +35,8 @@ def get_context(context):
# remove script tags from boot # remove script tags from boot
boot_json = re.sub("\<script\>[^<]*\</script\>", "", boot_json) boot_json = re.sub("\<script\>[^<]*\</script\>", "", boot_json)


return {
context.update({
"no_cache": 1,
"build_version": get_build_version(), "build_version": get_build_version(),
"include_js": hooks["app_include_js"], "include_js": hooks["app_include_js"],
"include_css": hooks["app_include_css"], "include_css": hooks["app_include_css"],
@@ -46,7 +47,7 @@ def get_context(context):
(boot.user.background_image or boot.default_background_image) or None), (boot.user.background_image or boot.default_background_image) or None),
"google_analytics_id": frappe.conf.get("google_analytics_id"), "google_analytics_id": frappe.conf.get("google_analytics_id"),
"mixpanel_id": frappe.conf.get("mixpanel_id") "mixpanel_id": frappe.conf.get("mixpanel_id")
}
})


@frappe.whitelist() @frappe.whitelist()
def get_desk_assets(build_version): def get_desk_assets(build_version):
@@ -64,7 +65,7 @@ def get_desk_assets(build_version):
try: try:
with open(os.path.join(frappe.local.sites_path, path) ,"r") as f: with open(os.path.join(frappe.local.sites_path, path) ,"r") as f:
assets[0]["data"] = assets[0]["data"] + "\n" + text_type(f.read(), "utf-8") assets[0]["data"] = assets[0]["data"] + "\n" + text_type(f.read(), "utf-8")
except IOError as e:
except IOError:
pass pass


for path in data["include_css"]: for path in data["include_css"]:
@@ -78,5 +79,4 @@ def get_desk_assets(build_version):
} }


def get_build_version(): def get_build_version():
return str(os.path.getmtime(os.path.join(frappe.local.sites_path, "assets", "js",
"desk.min.js")))
return str(os.path.getmtime(os.path.join(frappe.local.sites_path, '.build')))

+ 4
- 4
frappe/www/login.html Näytä tiedosto

@@ -9,16 +9,16 @@
{% block page_content %} {% block page_content %}
<!-- {{ for_test }} --> <!-- {{ for_test }} -->
<section class='for-login'> <section class='for-login'>
<div class="login-content page-card" style="margin-top: 20px;">
<div class="login-content page-card" style="margin-top: 30px;">
<form class="form-signin form-login" role="form"> <form class="form-signin form-login" role="form">
<div class="page-card-head"> <div class="page-card-head">
<span class="indicator blue" data-text="{{ _("Sign In") }}"></span> <span class="indicator blue" data-text="{{ _("Sign In") }}"></span>
</div> </div>


<input type="text" id="login_email" <input type="text" id="login_email"
class="form-control" placeholder="{{
_('Email address or Mobile number')
if frappe.utils.cint(frappe.db.get_value('System Settings', 'System Settings', 'allow_login_using_mobile_number'))
class="form-control" placeholder="{{
_('Email address or Mobile number')
if frappe.utils.cint(frappe.db.get_value('System Settings', 'System Settings', 'allow_login_using_mobile_number'))
else _('Email address') }}" else _('Email address') }}"
required autofocus> required autofocus>




+ 0
- 1
frappe/www/login.py Näytä tiedosto

@@ -68,4 +68,3 @@ def login_via_token(login_token):
frappe.local.login_manager = LoginManager() frappe.local.login_manager = LoginManager()


redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User") redirect_post_login(desk_user = frappe.db.get_value("User", frappe.session.user, "user_type")=="System User")


+ 27
- 0
frappe/www/qrcode.html Näytä tiedosto

@@ -0,0 +1,27 @@
{% extends "templates/web.html" %}

{% block title %}{{ _("QR Code") }}{% endblock %}

{% block page_content %}
<h1>{{ _("QR Code for Login Verification") }}</h1>
<div class='row'>
<div class='col-sm-6'>
<p>{{ _("Hi {0}").format(qr_code_user.first_name) }},</p>

<p>{{ _("Steps to verify your login") }}:</p>
<ol>
<li> {{ _("Open your authentication app on your mobile phone.") }}
<li> {{ _("Scan the QR Code and enter the resulting code displayed.") }}
<li> {{ _("Return to the Verification screen and enter the code displayed by your authentication app") }}
</ol>
</p>
<br>
<p class='text-muted small'>{{ _("Authentication Apps you can use are: ") }}
Google Authenticator, Lastpass Authenticator, Authy and Duo Mobile.
</p>
</div>
<div class='col-sm-6' style='padding-top: 15px;'>
<img src="data:image/svg+xml;base64,{{qrcode_svg}}">
</div>
</div>
{% endblock %}

+ 37
- 0
frappe/www/qrcode.py Näytä tiedosto

@@ -0,0 +1,37 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt

from __future__ import unicode_literals

import frappe
from frappe import _
from urlparse import parse_qs
from frappe.twofactor import get_qr_svg_code

def get_context(context):
context.no_cache = 1
context.qr_code_user,context.qrcode_svg = get_user_svg_from_cache()

def get_query_key():
'''Return query string arg.'''
query_string = frappe.local.request.query_string
query = parse_qs(query_string)
if not 'k' in query.keys():
frappe.throw(_('Not Permitted'),frappe.PermissionError)
query = (query['k'][0]).strip()
if False in [i.isalpha() or i.isdigit() for i in query]:
frappe.throw(_('Not Permitted'),frappe.PermissionError)
return query

def get_user_svg_from_cache():
'''Get User and SVG code from cache.'''
key = get_query_key()
totp_uri = frappe.cache().get_value("{}_uri".format(key))
user = frappe.cache().get_value("{}_user".format(key))
if not totp_uri or not user:
frappe.throw(_('Page has expired!'),frappe.PermissionError)
if not frappe.db.exists('User',user):
frappe.throw(_('Not Permitted'), frappe.PermissionError)
user = frappe.get_doc('User',user)
svg = get_qr_svg_code(totp_uri)
return (user,svg)

+ 2
- 1
package.json Näytä tiedosto

@@ -27,6 +27,7 @@
"nightwatch": "^0.9.16", "nightwatch": "^0.9.16",
"redis": "^2.7.1", "redis": "^2.7.1",
"socket.io": "^2.0.1", "socket.io": "^2.0.1",
"superagent": "^3.5.2"
"superagent": "^3.5.2",
"touch": "^3.1.0"
} }
} }

+ 4
- 0
requirements.txt Näytä tiedosto

@@ -41,4 +41,8 @@ oauthlib
PyJWT PyJWT
pypdf pypdf
openpyxl openpyxl
pyotp
pyqrcode
pypng
premailer premailer


Ladataan…
Peruuta
Tallenna