@@ -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() | ||||
@@ -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' |
@@ -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}); | |||||
}); | }); | ||||
} | } | ||||
@@ -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. |
@@ -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")} |
@@ -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... | ||||
@@ -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'}} | ||||
]), | ]), | ||||
@@ -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", | ||||
@@ -0,0 +1 @@ | |||||
SMS query parameter for SMS Settings. |
@@ -0,0 +1 @@ | |||||
from __future__ import unicode_literals |
@@ -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 | |||||
} |
@@ -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 |
@@ -0,0 +1 @@ | |||||
Settings for automatically sending SMS from the system. |
@@ -0,0 +1 @@ | |||||
from __future__ import unicode_literals |
@@ -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 | |||||
} |
@@ -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() |
@@ -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() | |||||
]); | |||||
}); |
@@ -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", | ||||
@@ -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: | ||||
@@ -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" | ||||
@@ -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) { | ||||
@@ -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", | ||||
@@ -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.")) |
@@ -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" | ||||
@@ -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), | ||||
() => { | () => { | ||||
@@ -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", | ||||
@@ -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() | |||||
]); | |||||
}); |
@@ -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); | ||||
@@ -561,7 +561,7 @@ var frappe_slides = [ | |||||
} | } | ||||
} | } | ||||
}, | }, | ||||
}, | |||||
} | |||||
]; | ]; | ||||
var utils = { | var utils = { | ||||
@@ -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) | |||||
@@ -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 | |||||
} | } | ||||
@@ -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() | ||||
@@ -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 | ||||
@@ -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'] |
@@ -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", | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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; | ||||
} | } | ||||
@@ -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; | ||||
} | } | ||||
@@ -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; | ||||
@@ -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 { | ||||
@@ -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) { | ||||
@@ -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; | ||||
@@ -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); | ||||
@@ -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"> | ||||
@@ -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 () { | ||||
@@ -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; | |||||
} | |||||
} | } | ||||
}); | }); | ||||
} | } | ||||
@@ -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; | |||||
} | |||||
}; |
@@ -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)); | ||||
@@ -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); | ||||
@@ -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 { | ||||
@@ -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; | |||||
} | } | ||||
}) | }) |
@@ -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"); | ||||
@@ -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; | ||||
@@ -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; | ||||
@@ -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 { | ||||
@@ -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 { | ||||
@@ -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; | ||||
@@ -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; | |||||
} | } | ||||
} | } | ||||
@@ -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) { | ||||
@@ -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 %} |
@@ -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); | |||||
} | |||||
} |
@@ -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 %} |
@@ -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() |
@@ -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(): | ||||
@@ -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 |
@@ -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) | |||||
@@ -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 | ||||
@@ -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 | ||||
@@ -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 | ||||
@@ -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" %} | ||||
@@ -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'))) |
@@ -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> | ||||
@@ -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") | ||||
@@ -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 %} |
@@ -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) |
@@ -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" | |||||
} | } | ||||
} | } |
@@ -41,4 +41,8 @@ oauthlib | |||||
PyJWT | PyJWT | ||||
pypdf | pypdf | ||||
openpyxl | openpyxl | ||||
pyotp | |||||
pyqrcode | |||||
pypng | |||||
premailer | premailer | ||||