@@ -48,7 +47,7 @@
{%- block page_content -%}{%- endblock -%}
diff --git a/frappe/__init__.py b/frappe/__init__.py index 7bd6d72cb4..6e2821df18 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -14,7 +14,7 @@ import os, sys, importlib, inspect, json from .exceptions import * 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" local = Local() diff --git a/frappe/auth.py b/frappe/auth.py index 8845b3e790..bd510b9fcd 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -16,9 +16,14 @@ from frappe.modules.patch_handler import check_session_stopped from frappe.translate import get_lang_code from frappe.utils.password import check_password 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 +import pyotp, base64, os + class HTTPRequest: def __init__(self): # Get Environment variables @@ -62,6 +67,7 @@ class HTTPRequest: def validate_csrf_token(self): if frappe.local.request and frappe.local.request.method=="POST": + if not frappe.local.session: return if not frappe.local.session.data.csrf_token \ or frappe.local.session.data.device=="mobile" \ or frappe.conf.get('ignore_csrf', None): @@ -88,7 +94,7 @@ class HTTPRequest: def connect(self, ac_name = None): """connect to db, from ac_name or 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: def __init__(self): @@ -98,7 +104,7 @@ class LoginManager: self.user_type = None 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 # run login triggers @@ -116,7 +122,12 @@ class LoginManager: def login(self): # clear cache 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() def post_login(self): @@ -183,7 +194,7 @@ class LoginManager: if not (user and pwd): user, pwd = frappe.form_dict.get('usr'), frappe.form_dict.get('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")): user = frappe.db.get_value("User", filters={"mobile_no": user}, fieldname="name") or user @@ -205,7 +216,9 @@ class LoginManager: except frappe.AuthenticationError: 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 add_authentication_log(message, user, status="Failed") frappe.db.commit() @@ -302,6 +315,7 @@ class CookieManager: for key in set(self.to_delete): response.set_cookie(key, "", expires=expires) + @frappe.whitelist() def get_logged_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) return '/' + home_page.strip('/') else: - return '/me' + return '/me' \ No newline at end of file diff --git a/frappe/build.js b/frappe/build.js index 707708236b..45ab9bc9cf 100644 --- a/frappe/build.js +++ b/frappe/build.js @@ -10,6 +10,7 @@ const path_join = path.resolve; const app = require('express')(); const http = require('http').Server(app); const io = require('socket.io')(http); +const touch = require("touch"); // basic setup const sites_path = path_join(__dirname, '..', '..', '..', 'sites'); @@ -42,6 +43,7 @@ function build(minify) { for (const output_path in build_map) { pack(output_path, build_map[output_path], minify); } + touch(path_join(sites_path, '.build'), {force:true}); } 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 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'); var last_index = filename.lastIndexOf('/'); const less_path = filename.slice(0, last_index); @@ -236,17 +238,18 @@ function watch_less(ondirty) { filename = filename.split('/').pop(); 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; } } + touch(path_join(sites_path, '.build'), {force:true}); }); } diff --git a/frappe/change_log/v8/v8_8_0.md b/frappe/change_log/v8/v8_8_0.md new file mode 100644 index 0000000000..e93c0adbeb --- /dev/null +++ b/frappe/change_log/v8/v8_8_0.md @@ -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. \ No newline at end of file diff --git a/frappe/client.py b/frappe/client.py index 7d9eb7bbf5..fafa535e0e 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -296,3 +296,8 @@ def get_js(items): out.append(code) 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")} diff --git a/frappe/contacts/doctype/address/address.js b/frappe/contacts/doctype/address/address.js index a8d4860117..3cbecfe327 100644 --- a/frappe/contacts/doctype/address/address.js +++ b/frappe/contacts/doctype/address/address.js @@ -22,6 +22,7 @@ frappe.ui.form.on("Address", { } } }); + frm.refresh_field("links"); }, validate: function(frm) { // clear linked customer / supplier / sales partner on saving... diff --git a/frappe/core/doctype/doctype/boilerplate/test_controller.js b/frappe/core/doctype/doctype/boilerplate/test_controller.js index 6749c53bb0..ed27ac02f6 100644 --- a/frappe/core/doctype/doctype/boilerplate/test_controller.js +++ b/frappe/core/doctype/doctype/boilerplate/test_controller.js @@ -8,9 +8,9 @@ QUnit.test("test: {doctype}", function (assert) {{ // number of asserts assert.expect(1); - frappe.run_serially('{doctype}', [ + frappe.run_serially([ // insert a new {doctype} - () => frappe.tests.make([ + () => frappe.tests.make('{doctype}', [ // values to be set {{key: 'value'}} ]), diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 104ee7d53c..1eebb71a36 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -105,6 +105,37 @@ "set_only_once": 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_on_submit": 0, @@ -148,7 +179,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-05-04 11:03:41.533058", + "modified": "2017-07-06 12:42:57.097914", "modified_by": "Administrator", "module": "Core", "name": "Role", diff --git a/frappe/core/doctype/sms_parameter/README.md b/frappe/core/doctype/sms_parameter/README.md new file mode 100644 index 0000000000..5935a390d2 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/README.md @@ -0,0 +1 @@ +SMS query parameter for SMS Settings. \ No newline at end of file diff --git a/frappe/core/doctype/sms_parameter/__init__.py b/frappe/core/doctype/sms_parameter/__init__.py new file mode 100755 index 0000000000..baffc48825 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.json b/frappe/core/doctype/sms_parameter/sms_parameter.json new file mode 100755 index 0000000000..b5648ade80 --- /dev/null +++ b/frappe/core/doctype/sms_parameter/sms_parameter.json @@ -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 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_parameter/sms_parameter.py b/frappe/core/doctype/sms_parameter/sms_parameter.py new file mode 100644 index 0000000000..08b220b61a --- /dev/null +++ b/frappe/core/doctype/sms_parameter/sms_parameter.py @@ -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 \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/README.md b/frappe/core/doctype/sms_settings/README.md new file mode 100644 index 0000000000..4fb49803b3 --- /dev/null +++ b/frappe/core/doctype/sms_settings/README.md @@ -0,0 +1 @@ +Settings for automatically sending SMS from the system. \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/__init__.py b/frappe/core/doctype/sms_settings/__init__.py new file mode 100755 index 0000000000..baffc48825 --- /dev/null +++ b/frappe/core/doctype/sms_settings/__init__.py @@ -0,0 +1 @@ +from __future__ import unicode_literals diff --git a/frappe/core/doctype/sms_settings/sms_settings.js b/frappe/core/doctype/sms_settings/sms_settings.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json new file mode 100755 index 0000000000..0898ed389e --- /dev/null +++ b/frappe/core/doctype/sms_settings/sms_settings.json @@ -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 +} \ No newline at end of file diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py new file mode 100644 index 0000000000..a8b59beffa --- /dev/null +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -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() diff --git a/frappe/core/doctype/sms_settings/test_sms_settings.js b/frappe/core/doctype/sms_settings/test_sms_settings.js new file mode 100644 index 0000000000..c090d167f5 --- /dev/null +++ b/frappe/core/doctype/sms_settings/test_sms_settings.js @@ -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() + ]); + +}); diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index bbdf75c085..6405a275bf 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -895,6 +895,165 @@ "set_only_once": 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:240", + "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_on_submit": 0, @@ -1027,7 +1186,7 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2017-07-20 22:57:56.466867", + "modified": "2017-08-07 23:29:18.858797", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index f7ecfc00bb..cd7edc6a53 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -9,6 +9,7 @@ from frappe.model import no_value_fields from frappe.translate import set_default_language from frappe.utils import cint from frappe.utils.momentjs import get_all_timezones +from frappe.twofactor import toggle_two_factor_auth class SystemSettings(Document): def validate(self): @@ -25,6 +26,12 @@ class SystemSettings(Document): 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")) + 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): for df in self.meta.get("fields"): if df.fieldtype not in no_value_fields: diff --git a/frappe/core/doctype/test_runner/test_runner.js b/frappe/core/doctype/test_runner/test_runner.js index 87ea09fab7..da28ab5a2b 100644 --- a/frappe/core/doctype/test_runner/test_runner.js +++ b/frappe/core/doctype/test_runner/test_runner.js @@ -23,6 +23,7 @@ frappe.ui.form.on('Test Runner', { }, run_tests: function(frm, files) { + frappe.flags.in_test = true; let require_list = [ "assets/frappe/js/lib/jquery/qunit.js", "assets/frappe/js/lib/jquery/qunit.css" diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index 49c1f8b437..5409b569c7 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -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.roles_editor && frm.roles_editor.show(); @@ -111,6 +120,7 @@ frappe.ui.form.on('User', { } cur_frm.dirty(); } + }, validate: function(frm) { if(frm.roles_editor) { diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 0796ff76fb..31714b7116 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -1971,7 +1971,7 @@ "istable": 0, "max_attachments": 5, "menu_index": 0, - "modified": "2017-07-12 19:24:00.824902", + "modified": "2017-07-07 17:18:14.047969", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index c5dfbd0e2a..e7d24baf2e 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -14,6 +14,7 @@ import frappe.share import re from frappe.limits import get_limits from frappe.website.utils import is_signup_enabled +from frappe.utils.background_jobs import enqueue STANDARD_USERS = ("Guest", "Administrator") @@ -586,8 +587,8 @@ def get_email_awaiting(user): return waiting else: 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 @frappe.whitelist(allow_guest=False) @@ -675,7 +676,7 @@ def ask_pass_update(): from frappe.utils import set_default 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 ] set_default("email_user_password", u','.join(password_list)) @@ -888,4 +889,84 @@ def handle_password_test_fail(result): def update_gravatar(name): gravatar = has_gravatar(name) if gravatar: - frappe.db.set_value('User', name, 'user_image', gravatar) \ No newline at end of file + 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='
Your verification code is {0}
'.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':'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.
'.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.")) \ No newline at end of file diff --git a/frappe/core/doctype/version/test_version.py b/frappe/core/doctype/version/test_version.py index 82f13242ae..be721b3271 100644 --- a/frappe/core/doctype/version/test_version.py +++ b/frappe/core/doctype/version/test_version.py @@ -14,12 +14,13 @@ class TestVersion(unittest.TestCase): new_doc = copy.deepcopy(old_doc) old_doc.color = None + new_doc.color = '#fafafa' diff = get_diff(old_doc, new_doc)['changed'] self.assertEquals(get_fieldnames(diff)[0], 'color') 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" diff --git a/frappe/custom/doctype/customize_form/test_customize_form.js b/frappe/custom/doctype/customize_form/test_customize_form.js index 5d2be73e0b..d37afa5580 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.js +++ b/frappe/custom/doctype/customize_form/test_customize_form.js @@ -7,7 +7,7 @@ QUnit.test("test customize form", function(assert) { let done = assert.async(); frappe.run_serially([ () => frappe.set_route('Form', 'Customize Form'), - () => frappe.timeout(2), + () => frappe.timeout(1), () => cur_frm.set_value('doc_type', 'ToDo'), () => frappe.timeout(2), () => { diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index 75e949e90f..12fcf5d0af 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -312,9 +312,9 @@ "bold": 0, "collapsible": 0, "columns": 0, - "default": "blue", + "default": "", "fieldname": "color", - "fieldtype": "Select", + "fieldtype": "Color", "hidden": 0, "ignore_user_permissions": 0, "ignore_xss_filter": 0, @@ -325,7 +325,7 @@ "label": "Color", "length": 0, "no_copy": 0, - "options": "red\ngreen\nblue\nyellow\nskyblue\norange", + "options": "", "permlevel": 0, "precision": "", "print_hide": 0, @@ -895,8 +895,8 @@ "issingle": 0, "istable": 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", "name": "Event", "owner": "Administrator", diff --git a/frappe/desk/doctype/event/test_event.js b/frappe/desk/doctype/event/test_event.js new file mode 100644 index 0000000000..50dcd9e9aa --- /dev/null +++ b/frappe/desk/doctype/event/test_event.js @@ -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() + ]); + +}); diff --git a/frappe/desk/doctype/note/note.js b/frappe/desk/doctype/note/note.js index d905b7ad27..c237998ccf 100644 --- a/frappe/desk/doctype/note/note.js +++ b/frappe/desk/doctype/note/note.js @@ -10,7 +10,7 @@ frappe.ui.form.on("Note", { // toggle edit frm.add_custom_button("Edit", function() { frm.events.set_editable(frm, !frm.is_note_editable); - }) + }); 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); // hide all other fields - $.each(frm.fields_dict, function(fieldname, field) { + $.each(frm.fields_dict, function(fieldname) { if(fieldname !== "content") { frm.set_df_property(fieldname, "hidden", editable ? 0: 1); } - }) + }); // no label, description for content either frm.get_field("content").toggle_label(editable); diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index f76787e0e3..5ea6412977 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -561,7 +561,7 @@ var frappe_slides = [ } } }, - }, + } ]; var utils = { diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 8e8fef3359..ad3108b67a 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -267,3 +267,10 @@ def email_setup_wizard_exception(traceback, args): def get_language_code(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) + diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8140a0b11e..073576c437 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -77,7 +77,7 @@ def run(report_name, filters=None, user=None): frappe.msgprint(_("Must have report permission to access this report."), 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 not report.query: 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] if len(res) > 3: chart = res[3] + if len(res) > 4: + data_to_be_printed = res[4] if report.apply_user_permissions and result: result = get_filtered_data(report.ref_doctype, columns, result, user) @@ -110,7 +112,8 @@ def run(report_name, filters=None, user=None): "result": result, "columns": columns, "message": message, - "chart": chart + "chart": chart, + "data_to_be_printed": data_to_be_printed } diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index 4f4713edbe..04790de8b6 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -14,6 +14,7 @@ from frappe.utils.scheduler import log from frappe.email.queue import send from frappe.email.doctype.email_group.email_group import add_subscribers from frappe.utils import parse_addr +from frappe.utils import validate_email_add class Newsletter(Document): @@ -23,6 +24,10 @@ class Newsletter(Document): from `tabEmail Queue` where reference_doctype=%s and reference_name=%s 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"): self.recipients = frappe.utils.split_emails(self.test_email_id) self.queue_all() diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 723c602496..ae9fca7e7a 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -37,6 +37,9 @@ class SessionStopped(Exception): class UnsupportedMediaType(Exception): http_status_code = 415 +class RequestToken(Exception): + http_status_code = 200 + class Redirect(Exception): http_status_code = 301 diff --git a/frappe/hooks.py b/frappe/hooks.py index 2d28b74d91..bf990a9f72 100755 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -128,7 +128,8 @@ scheduler_events = { "frappe.email.doctype.email_account.email_account.pull", "frappe.email.doctype.email_account.email_account.notify_unreplied", "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": [ "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" before_write_file = "frappe.limits.validate_space_limit" + +otp_methods = ['OTP App','Email','SMS'] diff --git a/frappe/public/build.json b/frappe/public/build.json index 054421286e..75dbf5063a 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -87,6 +87,7 @@ "public/js/frappe/ui/messages.js", "public/js/frappe/ui/keyboard.js", "public/js/frappe/ui/emoji.js", + "public/js/frappe/ui/colors.js", "public/js/frappe/request.js", "public/js/frappe/socketio_client.js", diff --git a/frappe/public/css/calendar.css b/frappe/public/css/calendar.css index a46ab7227f..df530f7c30 100644 --- a/frappe/public/css/calendar.css +++ b/frappe/public/css/calendar.css @@ -73,7 +73,6 @@ th.fc-day-header { background: #cfdce5 !important; } .fc-day-grid-event { - background-color: rgba(94, 100, 255, 0.2) !important; border: none !important; margin: 5px 4px 0 !important; padding: 1px 5px !important; diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css index 4e9dfbaa6e..ccedde274d 100644 --- a/frappe/public/css/email.css +++ b/frappe/public/css/email.css @@ -49,7 +49,7 @@ hr { border-top: none; } .email-footer-container { - margin-top: 10px; + margin-top: 30px; } .email-footer-container > div:not(:last-child) { margin-bottom: 5px; diff --git a/frappe/public/css/form.css b/frappe/public/css/form.css index c56811e892..2dd5aaa1e2 100644 --- a/frappe/public/css/form.css +++ b/frappe/public/css/form.css @@ -299,17 +299,32 @@ h6.uppercase, .timeline-item.user-content .action-btns { position: absolute; 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 { background-color: #fafbfc; - padding: 10px 15px 10px 13px; + padding: 10px 15px 8px 13px; margin: 0px; color: #8D99A6; border-bottom: 1px solid #EBEFF2; } .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 { display: none; @@ -334,7 +349,8 @@ h6.uppercase, .timeline-item.user-content .close-btn-container .close { color: inherit; opacity: 1; - padding: 0 0 0 10px; + padding: 0; + font-size: 18px; } .timeline-item.user-content .edit-btn-container { padding: 0; @@ -409,7 +425,8 @@ h6.uppercase, top: 5px; } .timeline-item .reply-link { - padding-left: 7px; + margin-left: 15px; + font-size: 12px; } .timeline-head { background-color: white; diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 05cb1d07b6..49ecce16de 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -183,6 +183,25 @@ .listview-main-section .octicon-heart { 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 { color: #ff5858; } diff --git a/frappe/public/css/mobile.css b/frappe/public/css/mobile.css index ebcc52084f..cc5b926f13 100644 --- a/frappe/public/css/mobile.css +++ b/frappe/public/css/mobile.css @@ -25,6 +25,9 @@ body { body[data-route^="Form"] .page-title h1 { margin-top: 12px; } + body[data-route^="Form"] .page-title h1.editable-title { + padding-right: 80px; + } body[data-route^="Form"] .page-title .indicator { display: inline-block; margin-top: 12px; @@ -197,7 +200,7 @@ body { } body[data-route^="Form"] .page-title .title-text { font-size: 16px; - width: calc(100% - 30px); + width: calc(100% - 90px); } body[data-route^="Form"] .page-title .indicator { float: left; @@ -356,7 +359,10 @@ body { content: none; } .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 { padding: 7px 10px; @@ -364,6 +370,12 @@ body { .timeline .timeline-item.user-content .comment-header .links-active { 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 { margin-right: 10px; } diff --git a/frappe/public/css/page.css b/frappe/public/css/page.css index f5ccdc5a6a..66a7bbd836 100644 --- a/frappe/public/css/page.css +++ b/frappe/public/css/page.css @@ -44,7 +44,6 @@ vertical-align: middle; } .page-title .title-image { - display: inline-block; width: 46px; height: 0; padding: 23px 0; @@ -56,6 +55,7 @@ text-align: center; line-height: 0; float: left; + margin-right: 10px; } .editable-title .title-text { cursor: pointer; diff --git a/frappe/public/css/website.css b/frappe/public/css/website.css index b9b2d733bb..6e33918c6c 100644 --- a/frappe/public/css/website.css +++ b/frappe/public/css/website.css @@ -507,6 +507,7 @@ li { border-top: 1px solid #EBEFF2; } .page_content { + padding-top: 30px; padding-bottom: 30px; } .carousel-control .icon { @@ -554,6 +555,9 @@ li { .panel-body { padding-left: 15px; } +.page-head { + margin-bottom: -30px; +} .page-head h1, .page-head h2 { margin-top: 0px; @@ -588,9 +592,14 @@ fieldset { width: 100%; } .page-container { - padding: 0px; + display: flex; max-width: 970px; - margin: auto; + margin: 0 auto; +} +@media (max-width: 767px) { + .page-container { + flex-direction: column-reverse; + } } .page-max-width { max-width: 800px; @@ -603,30 +612,28 @@ fieldset { .web-sidebar { position: relative; } -.web-sidebar .sidebar-item { +.web-sidebar .sidebar-item:not(:last-child) { margin: 0px; padding-bottom: 12px; border: none; color: #8D99A6; - font-size: 12px; } -.web-sidebar .sidebar-item .badge { +.web-sidebar .sidebar-item:not(:last-child) .badge { font-weight: normal; } .web-sidebar .sidebar-item a { - color: #36414C !important; + color: #8D99A6; } .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 { font-size: 14px; font-weight: bold; } +.web-sidebar .sidebar-items ul { + margin-bottom: 0; +} .page-footer { padding: 15px 0px; border-top: 1px solid #EBEFF2; @@ -712,11 +719,6 @@ textarea { .sidebar-navbar-items a:visited { border-bottom: 0px; } -@media (max-width: 767px) { - .visible-xs { - display: inline-block !important; - } -} .more-block { padding-bottom: 30px; } @@ -790,16 +792,49 @@ a.active { .btn-next-wrapper { 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 { + 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-bottom: 50px; } .your-account-info { 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) { .page-content { diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index e76ef9105e..7c15d260bd 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -45,6 +45,7 @@ frappe.Application = Class.extend({ this.make_nav_bar(); this.set_favicon(); this.setup_analytics(); + this.setup_beforeunload(); frappe.ui.keys.setup(); 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() { var me = this; if(frappe.boot.notes.length) { diff --git a/frappe/public/js/frappe/form/control.js b/frappe/public/js/frappe/form/control.js index 54bb4e0595..f979bb2cb5 100755 --- a/frappe/public/js/frappe/form/control.js +++ b/frappe/public/js/frappe/form/control.js @@ -688,6 +688,8 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ }, set_formatted_input: function(value) { this._super(value); + + if(!value) value = '#ffffff'; this.$input.css({ "background-color": value }); @@ -721,6 +723,9 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ }); }, validate: function (value) { + if(value === '') { + return ''; + } var is_valid = /^#[0-9A-F]{6}$/i.test(value); if(is_valid) { return value; diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 7a48fa2c6f..a950aed6a8 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -159,12 +159,12 @@ frappe.ui.form.Timeline = Class.extend({ this.prepare_timeline_item(c); var $timeline_item = $(frappe.render_template("timeline_item", {data:c, frm:this.frm})) .appendTo(me.list) - .on("click", ".close", function() { + .on("click", ".delete-comment", function() { var name = $timeline_item.data('name'); me.delete_comment(name); return false; }) - .on('click', '.edit', function(e) { + .on('click', '.edit-comment', function(e) { e.preventDefault(); 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(); $edit_btn + .text("Save") .find('i') .removeClass('octicon-pencil') .addClass('octicon-check'); @@ -232,6 +233,7 @@ frappe.ui.form.Timeline = Class.extend({ new frappe.views.CommunicationComposer({ doc: me.frm.doc, txt: "", + title: __('Reply'), frm: me.frm, last_email: last_email }); @@ -251,11 +253,11 @@ frappe.ui.form.Timeline = Class.extend({ c["edit"] = ""; if(c.communication_type=="Comment" && (c.comment_type || "Comment") === "Comment") { if(frappe.model.can_delete("Communication")) { - c["delete"] = ''; + c["delete"] = ''; } if(frappe.user.name == c.sender || (frappe.user.name == 'Administrator')) { - c["edit"] = ''; + c["edit"] = 'Edit'; } } c.comment_on_small = comment_when(c.creation, true); diff --git a/frappe/public/js/frappe/form/footer/timeline_item.html b/frappe/public/js/frappe/form/footer/timeline_item.html index 215dd06fc2..4baa5504c5 100755 --- a/frappe/public/js/frappe/form/footer/timeline_item.html +++ b/frappe/public/js/frappe/form/footer/timeline_item.html @@ -91,7 +91,7 @@ {% if (data.communication_medium === "Email" && data.sender !== frappe.session.user_email) { %} + data-name="{%= data.name %}" title="{%= __("Reply") %}">{%= __("Reply") %} {% } %} {% } %}