From 54dedd349c358b1aadba606797aaa853ef558e89 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 19:33:39 +0200 Subject: [PATCH 001/284] feat: connected app and token cache --- frappe/config/integrations.py | 5 + .../doctype/connected_app/__init__.py | 0 .../doctype/connected_app/connected_app.js | 34 +++ .../doctype/connected_app/connected_app.json | 151 ++++++++++++ .../doctype/connected_app/connected_app.py | 218 ++++++++++++++++++ .../connected_app/test_connected_app.js | 23 ++ .../connected_app/test_connected_app.py | 10 + .../doctype/token_cache/__init__.py | 0 .../doctype/token_cache/test_token_cache.js | 23 ++ .../doctype/token_cache/test_token_cache.py | 10 + .../doctype/token_cache/token_cache.js | 8 + .../doctype/token_cache/token_cache.json | 81 +++++++ .../doctype/token_cache/token_cache.py | 16 ++ 13 files changed, 579 insertions(+) create mode 100644 frappe/integrations/doctype/connected_app/__init__.py create mode 100644 frappe/integrations/doctype/connected_app/connected_app.js create mode 100644 frappe/integrations/doctype/connected_app/connected_app.json create mode 100644 frappe/integrations/doctype/connected_app/connected_app.py create mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.js create mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.py create mode 100644 frappe/integrations/doctype/token_cache/__init__.py create mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.js create mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.py create mode 100644 frappe/integrations/doctype/token_cache/token_cache.js create mode 100644 frappe/integrations/doctype/token_cache/token_cache.json create mode 100644 frappe/integrations/doctype/token_cache/token_cache.py diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index f41adc9ea4..6c1d3d55ae 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,6 +72,11 @@ def get_data(): "name": "OAuth Provider Settings", "description": _("Settings for OAuth Provider"), }, + { + "type": "doctype", + "name": "Connected App", + "description": _("Connected App"), + }, ] }, { diff --git a/frappe/integrations/doctype/connected_app/__init__.py b/frappe/integrations/doctype/connected_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js new file mode 100644 index 0000000000..8e239d277b --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -0,0 +1,34 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Connected App', { + refresh: frm => { + frm.add_custom_button(__("Get OpenID Configuration"), async () => { + if(!frm.doc.openid_configuration) { + frappe.msgprint(__('Please enter OpenID Configuration URL')); + } else { + try { + const response = await fetch(frm.doc.openid_configuration); + const oidc = await response.json(); + frm.set_value('authorization_endpoint', oidc.authorization_endpoint); + frm.set_value('token_endpoint', oidc.token_endpoint); + frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); + frm.set_value('introspection_endpoint', oidc.introspection_endpoint); + frm.set_value('revocation_endpoint', oidc.revocation_endpoint); + } catch(error) { + frappe.msgprint(__('Please check OpenID Configuration URL')); + } + } + }); + + frm.add_custom_button(__("Init"), async () => { + frappe.call({ + method: "initiate_auth_code_flow", + doc: frm.doc, + callback: function(r) { + console.log(r.message); + } + }) + }); + } +}); diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json new file mode 100644 index 0000000000..6ccce573db --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -0,0 +1,151 @@ +{ + "actions": [], + "autoname": "field:callback", + "beta": 1, + "creation": "2019-01-24 15:51:06.362222", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "provider_name", + "cb_00", + "openid_configuration", + "callback", + "sb_client_credentials_section", + "client_id", + "redirect_uri", + "cb_01", + "client_secret", + "sb_scope_section", + "scope", + "sb_endpoints_section", + "authorization_endpoint", + "token_endpoint", + "revocation_endpoint", + "cb_02", + "userinfo_endpoint", + "introspection_endpoint" + ], + "fields": [ + { + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "openid_configuration", + "fieldtype": "Data", + "label": "OpenID Configuration" + }, + { + "fieldname": "callback", + "fieldtype": "Data", + "label": "Callback", + "read_only": 1, + "unique": 1 + }, + { + "collapsible": 1, + "fieldname": "sb_client_credentials_section", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "label": "Client Id" + }, + { + "fieldname": "redirect_uri", + "fieldtype": "Data", + "label": "Redirect URI", + "read_only": 1 + }, + { + "fieldname": "cb_01", + "fieldtype": "Column Break" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "collapsible": 1, + "fieldname": "sb_scope_section", + "fieldtype": "Section Break", + "label": "Scope" + }, + { + "fieldname": "scope", + "fieldtype": "Text", + "label": "Scope" + }, + { + "collapsible": 1, + "fieldname": "sb_endpoints_section", + "fieldtype": "Section Break", + "label": "Endpoints" + }, + { + "fieldname": "authorization_endpoint", + "fieldtype": "Data", + "label": "Authorization Endpoint" + }, + { + "fieldname": "token_endpoint", + "fieldtype": "Data", + "label": "Token Endpoint" + }, + { + "fieldname": "revocation_endpoint", + "fieldtype": "Data", + "label": "Revocation Endpoint" + }, + { + "fieldname": "cb_02", + "fieldtype": "Column Break" + }, + { + "fieldname": "userinfo_endpoint", + "fieldtype": "Data", + "label": "Userinfo Endpoint" + }, + { + "fieldname": "introspection_endpoint", + "fieldtype": "Data", + "label": "Introspection Endpoint" + } + ], + "links": [], + "modified": "2020-07-14 18:52:06.041273", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Connected App", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py new file mode 100644 index 0000000000..26e0a93c14 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -0,0 +1,218 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import json +import requests +import frappe +import base64 +from frappe import _ +from frappe.model.document import Document +from datetime import datetime, timedelta +from urllib.parse import urlencode +from six.moves.urllib.parse import unquote + + +class ConnectedApp(Document): + def autoname(self): + self.callback = frappe.scrub(self.provider_name) + + def validate(self): + self.redirect_uri = frappe.request.host_url + self.redirect_uri += 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.redirect_uri += self.callback + + def get_client_token(self): + try: + token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + token = self.retrieve_client_token() + + token = self.check_validity(token) + return token + + def get_params(self, **kwargs): + return { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'scope': self.scope + }.update(kwargs) + + def retrieve_client_token(self): + client_secret = self.get_password('client_secret') + data = self.get_params(grant_type='client_credentials', client_secret=client_secret) + response = requests.post( + self.token_endpoint, + data=urlencode(data), + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + token = response.json() + return self.update_stored_client_token(token) + + def check_validity(self, token): + if(token.get('__islocal') or (not token.access_token)): + raise frappe.exceptions.DoesNotExistError + + expiry = token.modified + timedelta(seconds=token.expires_in) + if expiry > datetime.now(): + return token + + return self.refresh_token(token) + + def initiate_auth_code_flow(self, user=None, redirect_to=None): + if not redirect_to: + redirect_to = '/desk' + + if not user: + user = frappe.session.user + + uid = frappe.generate_hash() + payload = { + 'uid': uid, + 'redirect_to': redirect_to, + } + state = str_to_b64(json.dumps(payload)) + + try: + token = frappe.get_doc('Token Cache', self.name + '-' + user) + except frappe.exceptions.DoesNotExistError: + token = frappe.new_doc('Token Cache') + token.user = user + token.connected_app = self.name + + token.state = state + token.save() + frappe.db.commit() + + params = self.get_params(response_type='code', state=state.decode('utf-8')) + return self.authorization_endpoint + urlencode(params) + + def get_user_token(self, user=None, redirect_to=None): + if not user: + user = frappe.session.user + + try: + token = self.get_stored_user_token(user) + token = self.check_validity(token) + except frappe.exceptions.DoesNotExistError: + redirect = self.initiate_auth_code_flow(user, redirect_to) + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = redirect + return redirect + + return token + + def refresh_token(self, token): + data = self.get_params(grant_type='refresh_token', refresh_token=token.refresh_token) + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + response = requests.post(self.token_endpoint, data=urlencode(data), headers=headers) + new_token = response.json() + + # Revoke old token + data = urlencode({'token': token.get('access_token')}) + headers['Authorization'] = 'Bearer ' + new_token.get('access_token') + requests.post(self.revocation_endpoint, data=data, headers=headers) + + return self.update_stored_client_token(new_token) + + def get_stored_client_token(self): + return frappe.get_doc('Token Cache', self.name + '-user') + + def get_stored_user_token(self, user): + return frappe.get_doc('Token Cache', self.name + '-' + user) + + def update_stored_client_token(self, token_data): + try: + stored_token = frappe.get_doc('Token Cache', self.name + '-user') + except frappe.exceptions.DoesNotExistError: + stored_token = frappe.new_doc('Token Cache') + + stored_token.connected_app = self.name + stored_token.access_token = token_data.get('access_token') + stored_token.refresh_token = token_data.get('refresh_token') + stored_token.expires_in = token_data.get('expires_in') + stored_token.save(ignore_permissions=True) + frappe.db.commit() + + return frappe.get_doc('Token Cache', stored_token.name) + + +@frappe.whitelist(allow_guest=True) +def callback(code=None, state=None): + """Handle client's code.""" + if frappe.request.method != 'GET': + throw_error(_('Invalid Method')) + return + + if frappe.session.user == 'Guest': + throw_error(_('Please Sign In')) + return + + path = frappe.request.path[1:].split("/") + if len(path) == 4 and path[3]: + connected_app = path[3] + stored_state = frappe.get_all( + 'Token Cache', + filters={ + 'user': frappe.session.user, + 'connected_app': connected_app, + 'name': connected_app + '-' + frappe.session.user, + }, + limit=1 + ) + if not stored_state: + throw_error(_('State Not Found')) + return + + stored_state = frappe.get_doc('Token Cache', stored_state[0].name) + + payload = json.loads(b64_to_str(state)) + stored_payload = json.loads(b64_to_str(stored_state.state)) + + if payload.get('uid') != stored_payload.get('uid'): + throw_error(_('Invalid State')) + return + + try: + app = frappe.get_doc('Connected App', connected_app) + except frappe.exceptions.DoesNotExistError: + throw_error(_('Invalid App')) + return + + data = app.get_params(code=code, grant_type='authorization_code') + response = requests.post( + app.token_endpoint, + data=urlencode(data), + headers={'Content-Type': 'application/x-www-form-urlencoded'} + ) + + token = response.json() + stored_state.access_token = token.get('access_token') + stored_state.refresh_token = token.get('refresh_token') + stored_state.expires_in = token.get('expires_in') + stored_state.state = None + stored_state.save() + frappe.db.commit() + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = payload.get('redirect_to') + else: + throw_error(_('Invalid Parameter(s)')) + return + + +def throw_error(error): + """Set Response Status 400 and show error.""" + frappe.local.response['http_status_code'] = 400 + frappe.local.response['error'] = error + + +def str_to_b64(string): + """Return base64 encoded string.""" + return base64.b64encode(string.encode('utf-8')) + + +def b64_to_str(b64): + """Return base64 decoded string.""" + return base64.b64decode(b64).decode('utf-8') diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.js b/frappe/integrations/doctype/connected_app/test_connected_app.js new file mode 100644 index 0000000000..6db9056efc --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_connected_app.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: Connected App", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Connected App + () => frappe.tests.make('Connected App', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py new file mode 100644 index 0000000000..5c92eddc73 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestConnectedApp(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/token_cache/__init__.py b/frappe/integrations/doctype/token_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.js b/frappe/integrations/doctype/token_cache/test_token_cache.js new file mode 100644 index 0000000000..ee52cd7465 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_token_cache.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: Token Cache", function (assert) { + let done = assert.async(); + + // number of asserts + assert.expect(1); + + frappe.run_serially([ + // insert a new Token Cache + () => frappe.tests.make('Token Cache', [ + // values to be set + {key: 'value'} + ]), + () => { + assert.equal(cur_frm.doc.key, 'value'); + }, + () => done() + ]); + +}); diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py new file mode 100644 index 0000000000..2c42e7f3b8 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestTokenCache(unittest.TestCase): + pass diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js new file mode 100644 index 0000000000..dda742f469 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Token Cache', { + refresh: function(frm) { + + } +}); diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json new file mode 100644 index 0000000000..536985c9dc --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -0,0 +1,81 @@ +{ + "actions": [], + "autoname": "format:{connected_app}-{user}", + "beta": 1, + "creation": "2019-01-24 16:56:55.631096", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "connected_app", + "access_token", + "refresh_token", + "expires_in", + "state" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "connected_app", + "fieldtype": "Link", + "label": "Connected App", + "options": "Connected App", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Data", + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "expires_in", + "fieldtype": "Int", + "label": "Expires In", + "read_only": 1 + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-07-14 18:52:25.452744", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Token Cache", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py new file mode 100644 index 0000000000..e40f207738 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class TokenCache(Document): + + def get_auth_header(self): + if self.access_token: + headers = {'Authorization': 'Bearer ' + self.access_token} + return headers + + raise frappe.exceptions.DoesNotExistError From 61fcceb043352d6aa70ea4baa2ba77bd54369768 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 19:46:51 +0200 Subject: [PATCH 002/284] fix: indentation --- frappe/config/integrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index 6c1d3d55ae..cc467495ce 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -72,7 +72,7 @@ def get_data(): "name": "OAuth Provider Settings", "description": _("Settings for OAuth Provider"), }, - { + { "type": "doctype", "name": "Connected App", "description": _("Connected App"), From ff2412af48ab394049622fafd3690baa8e00c631 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 20:24:55 +0200 Subject: [PATCH 003/284] refactor --- .../doctype/connected_app/connected_app.py | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 26e0a93c14..352fbe6564 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -15,13 +15,13 @@ from six.moves.urllib.parse import unquote class ConnectedApp(Document): + def autoname(self): self.callback = frappe.scrub(self.provider_name) def validate(self): - self.redirect_uri = frappe.request.host_url - self.redirect_uri += 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' - self.redirect_uri += self.callback + callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.redirect_uri = frappe.request.host_url + callback_path + self.callback def get_client_token(self): try: @@ -61,21 +61,17 @@ class ConnectedApp(Document): return self.refresh_token(token) def initiate_auth_code_flow(self, user=None, redirect_to=None): - if not redirect_to: - redirect_to = '/desk' - - if not user: - user = frappe.session.user + redirect_to = redirect_to or '/desk' + user = user or frappe.session.user uid = frappe.generate_hash() - payload = { + state = str_to_b64(json.dumps({ 'uid': uid, 'redirect_to': redirect_to, - } - state = str_to_b64(json.dumps(payload)) + })) try: - token = frappe.get_doc('Token Cache', self.name + '-' + user) + token = self.get_stored_user_token(user) except frappe.exceptions.DoesNotExistError: token = frappe.new_doc('Token Cache') token.user = user @@ -89,8 +85,8 @@ class ConnectedApp(Document): return self.authorization_endpoint + urlencode(params) def get_user_token(self, user=None, redirect_to=None): - if not user: - user = frappe.session.user + redirect_to = redirect_to or '/desk' + user = user or frappe.session.user try: token = self.get_stored_user_token(user) @@ -124,7 +120,7 @@ class ConnectedApp(Document): def update_stored_client_token(self, token_data): try: - stored_token = frappe.get_doc('Token Cache', self.name + '-user') + stored_token = self.get_stored_client_token() except frappe.exceptions.DoesNotExistError: stored_token = frappe.new_doc('Token Cache') From 9be6204451a2810737ed967e8a69c6a22a2a9db4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 14 Jul 2020 20:58:14 +0200 Subject: [PATCH 004/284] start to use requests_oauthlib --- .../doctype/connected_app/connected_app.py | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 352fbe6564..dc0e964bc7 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -12,6 +12,7 @@ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode from six.moves.urllib.parse import unquote +from requests_oauthlib import OAuth2Session class ConnectedApp(Document): @@ -23,6 +24,9 @@ class ConnectedApp(Document): callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' self.redirect_uri = frappe.request.host_url + callback_path + self.callback + def get_oauth2_session(self): + return OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope) + def get_client_token(self): try: token = self.get_stored_client_token() @@ -63,12 +67,8 @@ class ConnectedApp(Document): def initiate_auth_code_flow(self, user=None, redirect_to=None): redirect_to = redirect_to or '/desk' user = user or frappe.session.user - - uid = frappe.generate_hash() - state = str_to_b64(json.dumps({ - 'uid': uid, - 'redirect_to': redirect_to, - })) + oauth = self.get_oauth2_session() + authorization_url, state = oauth.authorization_url(self.authorization_endpoint) try: token = self.get_stored_user_token(user) @@ -81,8 +81,7 @@ class ConnectedApp(Document): token.save() frappe.db.commit() - params = self.get_params(response_type='code', state=state.decode('utf-8')) - return self.authorization_endpoint + urlencode(params) + return authorization_url def get_user_token(self, user=None, redirect_to=None): redirect_to = redirect_to or '/desk' @@ -148,25 +147,12 @@ def callback(code=None, state=None): path = frappe.request.path[1:].split("/") if len(path) == 4 and path[3]: connected_app = path[3] - stored_state = frappe.get_all( - 'Token Cache', - filters={ - 'user': frappe.session.user, - 'connected_app': connected_app, - 'name': connected_app + '-' + frappe.session.user, - }, - limit=1 - ) - if not stored_state: + token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) + if not token_cache: throw_error(_('State Not Found')) return - stored_state = frappe.get_doc('Token Cache', stored_state[0].name) - - payload = json.loads(b64_to_str(state)) - stored_payload = json.loads(b64_to_str(stored_state.state)) - - if payload.get('uid') != stored_payload.get('uid'): + if state != token_cache.state: throw_error(_('Invalid State')) return @@ -176,23 +162,18 @@ def callback(code=None, state=None): throw_error(_('Invalid App')) return - data = app.get_params(code=code, grant_type='authorization_code') - response = requests.post( - app.token_endpoint, - data=urlencode(data), - headers={'Content-Type': 'application/x-www-form-urlencoded'} - ) + oauth = app.get_oauth2_session() + token = oauth.fetch_token(app.token_endpoint, code=code) - token = response.json() - stored_state.access_token = token.get('access_token') - stored_state.refresh_token = token.get('refresh_token') - stored_state.expires_in = token.get('expires_in') - stored_state.state = None - stored_state.save() + token_cache.access_token = token.get('access_token') + token_cache.refresh_token = token.get('refresh_token') + token_cache.expires_in = token.get('expires_in') + token_cache.state = None + token_cache.save() frappe.db.commit() frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = payload.get('redirect_to') + frappe.local.response["location"] = '/desk' else: throw_error(_('Invalid Parameter(s)')) return From 7eacdb307bb9851651b01610b224cd2b91dcc69e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 14:06:20 +0200 Subject: [PATCH 005/284] fix: redirect to provider --- frappe/integrations/doctype/connected_app/connected_app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 8e239d277b..d999300e38 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Connected App', { method: "initiate_auth_code_flow", doc: frm.doc, callback: function(r) { - console.log(r.message); + window.location.replace(r.message); } }) }); From 0cf5cb9a6759fe8b3811de6675328f69d9c373c4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:34:11 +0200 Subject: [PATCH 006/284] add Connected App to desk page --- .../integrations/desk_page/integrations/integrations.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/desk_page/integrations/integrations.json b/frappe/integrations/desk_page/integrations/integrations.json index 9201e223f8..d90ad60e63 100644 --- a/frappe/integrations/desk_page/integrations/integrations.json +++ b/frappe/integrations/desk_page/integrations/integrations.json @@ -18,7 +18,7 @@ { "hidden": 0, "label": "Authentication", - "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"description\": \"Enter keys to enable login via Facebook, Google, GitHub.\",\n \"label\": \"Social Login Key\",\n \"name\": \"Social Login Key\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Ldap settings\",\n \"label\": \"LDAP Settings\",\n \"name\": \"LDAP Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Register OAuth Client App\",\n \"label\": \"OAuth Client\",\n \"name\": \"OAuth Client\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Settings for OAuth Provider\",\n \"label\": \"OAuth Provider Settings\",\n \"name\": \"OAuth Provider Settings\",\n \"type\": \"doctype\"\n }\n ,\n {\n \"description\": \"Connect to any OAuth Provider\",\n \"label\": \"Connected App\",\n \"name\": \"Connected App\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -34,11 +34,11 @@ "docstatus": 0, "doctype": "Desk Page", "extends_another_page": 0, - "icon": "frapicon-dashboard", + "hide_custom": 0, "idx": 0, "is_standard": 1, "label": "Integrations", - "modified": "2020-04-01 11:24:40.751651", + "modified": "2020-07-15 20:10:14.074203", "modified_by": "Administrator", "module": "Integrations", "name": "Integrations", From 604b88be5848c8b93767c41bfa9ba0d8d22788d7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:38:50 +0200 Subject: [PATCH 007/284] OAuth scopes are a table. Also, show Token Cache in Dashboard of Connected App. --- .../doctype/connected_app/connected_app.json | 24 +++++++++------ .../doctype/oauth_scope/__init__.py | 0 .../doctype/oauth_scope/oauth_scope.json | 30 +++++++++++++++++++ .../doctype/oauth_scope/oauth_scope.py | 10 +++++++ .../doctype/token_cache/token_cache.json | 12 ++++++-- 5 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 frappe/integrations/doctype/oauth_scope/__init__.py create mode 100644 frappe/integrations/doctype/oauth_scope/oauth_scope.json create mode 100644 frappe/integrations/doctype/oauth_scope/oauth_scope.py diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 6ccce573db..3d83695621 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -18,7 +18,7 @@ "cb_01", "client_secret", "sb_scope_section", - "scope", + "scopes", "sb_endpoints_section", "authorization_endpoint", "token_endpoint", @@ -81,12 +81,7 @@ "collapsible": 1, "fieldname": "sb_scope_section", "fieldtype": "Section Break", - "label": "Scope" - }, - { - "fieldname": "scope", - "fieldtype": "Text", - "label": "Scope" + "label": "Scopes" }, { "collapsible": 1, @@ -122,10 +117,21 @@ "fieldname": "introspection_endpoint", "fieldtype": "Data", "label": "Introspection Endpoint" + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope" + } + ], + "links": [ + { + "link_doctype": "Token Cache", + "link_fieldname": "connected_app" } ], - "links": [], - "modified": "2020-07-14 18:52:06.041273", + "modified": "2020-07-15 22:10:07.122237", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/oauth_scope/__init__.py b/frappe/integrations/doctype/oauth_scope/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.json b/frappe/integrations/doctype/oauth_scope/oauth_scope.json new file mode 100644 index 0000000000..3a6e528999 --- /dev/null +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-07-15 22:08:14.616585", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "scope" + ], + "fields": [ + { + "fieldname": "scope", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Scope" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-15 22:15:18.930632", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Scope", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py new file mode 100644 index 0000000000..a5dfe7e1ce --- /dev/null +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class OAuthScope(Document): + pass diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 536985c9dc..b674018912 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -13,7 +13,8 @@ "access_token", "refresh_token", "expires_in", - "state" + "state", + "scopes" ], "fields": [ { @@ -53,10 +54,17 @@ "fieldtype": "Data", "label": "State", "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope", + "read_only": 1 } ], "links": [], - "modified": "2020-07-14 18:52:25.452744", + "modified": "2020-07-15 22:32:14.268178", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From fbdd86f38b76b5a6dc002567ef74495facbe38bc Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:39:11 +0200 Subject: [PATCH 008/284] use small text to store long tokens --- frappe/integrations/doctype/token_cache/token_cache.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index b674018912..5a7e8f5d41 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -33,13 +33,13 @@ }, { "fieldname": "access_token", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Access Token", "read_only": 1 }, { "fieldname": "refresh_token", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Refresh Token", "read_only": 1 }, From 714073f0d40af3a4c13007e2250f4880d6eb78ce Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:39:31 +0200 Subject: [PATCH 009/284] open auth flow in a new tab --- frappe/integrations/doctype/connected_app/connected_app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index d999300e38..d4f1b4673e 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -26,7 +26,7 @@ frappe.ui.form.on('Connected App', { method: "initiate_auth_code_flow", doc: frm.doc, callback: function(r) { - window.location.replace(r.message); + window.open(r.message, '_blank'); } }) }); From 169f5f3ef20291681306929998a9bcaf352987c8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:40:49 +0200 Subject: [PATCH 010/284] refactor: connected app --- .../doctype/connected_app/connected_app.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index dc0e964bc7..b10d1615f0 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -14,6 +14,10 @@ from urllib.parse import urlencode from six.moves.urllib.parse import unquote from requests_oauthlib import OAuth2Session +if frappe.conf.developer_mode: + # Disable mandatory TLS in developer mode + import os + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): @@ -25,7 +29,11 @@ class ConnectedApp(Document): self.redirect_uri = frappe.request.host_url + callback_path + self.callback def get_oauth2_session(self): - return OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope) + return OAuth2Session( + self.client_id, + redirect_uri=self.redirect_uri, + scope=[scope.scope for scope in self.scopes] + ) def get_client_token(self): try: @@ -137,37 +145,44 @@ class ConnectedApp(Document): def callback(code=None, state=None): """Handle client's code.""" if frappe.request.method != 'GET': - throw_error(_('Invalid Method')) - return + frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - throw_error(_('Please Sign In')) - return + frappe.throw(_("Log in to access this page."), frappe.PermissionError) path = frappe.request.path[1:].split("/") if len(path) == 4 and path[3]: connected_app = path[3] token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) if not token_cache: - throw_error(_('State Not Found')) - return + frappe.throw(_('State Not Found')) if state != token_cache.state: - throw_error(_('Invalid State')) - return + frappe.throw(_('Invalid State')) try: app = frappe.get_doc('Connected App', connected_app) except frappe.exceptions.DoesNotExistError: - throw_error(_('Invalid App')) - return + frappe.throw(_('Invalid App')) + client_secret = app.get_password('client_secret') oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, code=code) + token = oauth.fetch_token( + app.token_endpoint, + code=code, + client_secret=client_secret + ) token_cache.access_token = token.get('access_token') token_cache.refresh_token = token.get('refresh_token') token_cache.expires_in = token.get('expires_in') + + scopes = token.get('scope') + if isinstance(scopes, str): + scopes = [scopes] + for scope in scopes: + token_cache.append('scopes', {'scope': scope}) + token_cache.state = None token_cache.save() frappe.db.commit() @@ -175,21 +190,4 @@ def callback(code=None, state=None): frappe.local.response["type"] = "redirect" frappe.local.response["location"] = '/desk' else: - throw_error(_('Invalid Parameter(s)')) - return - - -def throw_error(error): - """Set Response Status 400 and show error.""" - frappe.local.response['http_status_code'] = 400 - frappe.local.response['error'] = error - - -def str_to_b64(string): - """Return base64 encoded string.""" - return base64.b64encode(string.encode('utf-8')) - - -def b64_to_str(b64): - """Return base64 decoded string.""" - return base64.b64decode(b64).decode('utf-8') + frappe.throw(_('Invalid Parameter(s)')) From 83dcc8a5836036b9e1d7e4afeb550fc56baddee7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 15 Jul 2020 22:49:24 +0200 Subject: [PATCH 011/284] style: fix sider --- frappe/integrations/doctype/connected_app/connected_app.js | 6 +++--- frappe/integrations/doctype/connected_app/connected_app.py | 3 --- .../doctype/connected_app/test_connected_app.py | 2 +- frappe/integrations/doctype/token_cache/test_token_cache.py | 2 +- frappe/integrations/doctype/token_cache/token_cache.js | 4 ++-- 5 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index d4f1b4673e..004f150b2a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Connected App', { refresh: frm => { frm.add_custom_button(__("Get OpenID Configuration"), async () => { - if(!frm.doc.openid_configuration) { + if (!frm.doc.openid_configuration) { frappe.msgprint(__('Please enter OpenID Configuration URL')); } else { try { @@ -15,7 +15,7 @@ frappe.ui.form.on('Connected App', { frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); frm.set_value('introspection_endpoint', oidc.introspection_endpoint); frm.set_value('revocation_endpoint', oidc.revocation_endpoint); - } catch(error) { + } catch (error) { frappe.msgprint(__('Please check OpenID Configuration URL')); } } @@ -28,7 +28,7 @@ frappe.ui.form.on('Connected App', { callback: function(r) { window.open(r.message, '_blank'); } - }) + }); }); } }); diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b10d1615f0..b669241344 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,15 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals -import json import requests import frappe -import base64 from frappe import _ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode -from six.moves.urllib.parse import unquote from requests_oauthlib import OAuth2Session if frappe.conf.developer_mode: diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 5c92eddc73..bb04ca6677 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -3,7 +3,7 @@ # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest class TestConnectedApp(unittest.TestCase): diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index 2c42e7f3b8..aebac0b52f 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -3,7 +3,7 @@ # See license.txt from __future__ import unicode_literals -import frappe +# import frappe import unittest class TestTokenCache(unittest.TestCase): diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js index dda742f469..b7cac9b804 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.js +++ b/frappe/integrations/doctype/token_cache/token_cache.js @@ -2,7 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Token Cache', { - refresh: function(frm) { + // refresh: function(frm) { - } + // } }); From bdcc98442fd4b90d357ed6ce75812a7b9fdc8c64 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 16:06:43 +0200 Subject: [PATCH 012/284] refactor: token cache updates itself --- .../doctype/connected_app/connected_app.py | 30 +++---------------- .../doctype/token_cache/token_cache.py | 19 ++++++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b669241344..448a6bc1eb 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -128,14 +128,7 @@ class ConnectedApp(Document): except frappe.exceptions.DoesNotExistError: stored_token = frappe.new_doc('Token Cache') - stored_token.connected_app = self.name - stored_token.access_token = token_data.get('access_token') - stored_token.refresh_token = token_data.get('refresh_token') - stored_token.expires_in = token_data.get('expires_in') - stored_token.save(ignore_permissions=True) - frappe.db.commit() - - return frappe.get_doc('Token Cache', stored_token.name) + return stored_token.update_data(token_data) @frappe.whitelist(allow_guest=True) @@ -162,27 +155,12 @@ def callback(code=None, state=None): except frappe.exceptions.DoesNotExistError: frappe.throw(_('Invalid App')) - client_secret = app.get_password('client_secret') oauth = app.get_oauth2_session() - token = oauth.fetch_token( - app.token_endpoint, + token = oauth.fetch_token(app.token_endpoint, code=code, - client_secret=client_secret + client_secret=app.get_password('client_secret') ) - - token_cache.access_token = token.get('access_token') - token_cache.refresh_token = token.get('refresh_token') - token_cache.expires_in = token.get('expires_in') - - scopes = token.get('scope') - if isinstance(scopes, str): - scopes = [scopes] - for scope in scopes: - token_cache.append('scopes', {'scope': scope}) - - token_cache.state = None - token_cache.save() - frappe.db.commit() + token_cache.update_data(token) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = '/desk' diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index e40f207738..aecc33b03e 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -14,3 +14,22 @@ class TokenCache(Document): return headers raise frappe.exceptions.DoesNotExistError + + def update_data(self, data): + self.access_token = data.get('access_token') + self.refresh_token = data.get('refresh_token') + self.expires_in = data.get('expires_in') + + existing_scopes = [scope.scope for scope in self.scopes] + new_scopes = data.get('scope') + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(' ') + scopes = set(existing_scopes) | set(new_scopes) + self.scopes = None + for scope in scopes: + self.append('scopes', {'scope': scope}) + + self.state = None + self.save() + + return self From 72496c0a49d7450a1a544631c505cbe007203f15 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 18:40:28 +0200 Subject: [PATCH 013/284] fix: save only new scopes --- .../doctype/token_cache/token_cache.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index aecc33b03e..93adfb50a7 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -20,14 +20,14 @@ class TokenCache(Document): self.refresh_token = data.get('refresh_token') self.expires_in = data.get('expires_in') - existing_scopes = [scope.scope for scope in self.scopes] new_scopes = data.get('scope') - if isinstance(new_scopes, str): - new_scopes = new_scopes.split(' ') - scopes = set(existing_scopes) | set(new_scopes) - self.scopes = None - for scope in scopes: - self.append('scopes', {'scope': scope}) + if new_scopes: + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(' ') + if isinstance(new_scopes, list): + self.scopes = None + for scope in new_scopes: + self.append('scopes', {'scope': scope}) self.state = None self.save() From 507659cd30b0f0b2c65fd52b876ae266f56e7c0d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 19:48:48 +0200 Subject: [PATCH 014/284] refactor: flatten callback --- .../doctype/connected_app/connected_app.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 448a6bc1eb..23e4a00abd 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -141,28 +141,28 @@ def callback(code=None, state=None): frappe.throw(_("Log in to access this page."), frappe.PermissionError) path = frappe.request.path[1:].split("/") - if len(path) == 4 and path[3]: - connected_app = path[3] - token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) - if not token_cache: - frappe.throw(_('State Not Found')) + if len(path) != 4 or not path[3]: + frappe.throw(_('Invalid Parameter(s)')) - if state != token_cache.state: - frappe.throw(_('Invalid State')) + connected_app = path[3] + token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) + if not token_cache: + frappe.throw(_('State Not Found')) - try: - app = frappe.get_doc('Connected App', connected_app) - except frappe.exceptions.DoesNotExistError: - frappe.throw(_('Invalid App')) + if state != token_cache.state: + frappe.throw(_('Invalid State')) - oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, - code=code, - client_secret=app.get_password('client_secret') - ) - token_cache.update_data(token) + try: + app = frappe.get_doc('Connected App', connected_app) + except frappe.exceptions.DoesNotExistError: + frappe.throw(_('Invalid App')) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = '/desk' - else: - frappe.throw(_('Invalid Parameter(s)')) + oauth = app.get_oauth2_session() + token = oauth.fetch_token(app.token_endpoint, + code=code, + client_secret=app.get_password('client_secret') + ) + token_cache.update_data(token) + + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = token_cache.get('success_uri') or '/desk' From cf560e43a07a04a11bc5a9c292a2a114ebf66591 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 19:59:34 +0200 Subject: [PATCH 015/284] feat: add fields success_uri, token_type --- .../doctype/token_cache/token_cache.json | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 5a7e8f5d41..5f1836e300 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -14,7 +14,9 @@ "refresh_token", "expires_in", "state", - "scopes" + "scopes", + "success_uri", + "token_type" ], "fields": [ { @@ -61,10 +63,20 @@ "label": "Scopes", "options": "OAuth Scope", "read_only": 1 + }, + { + "fieldname": "success_uri", + "fieldtype": "Data", + "label": "Success URI" + }, + { + "fieldname": "token_type", + "fieldtype": "Data", + "label": "Token Type" } ], "links": [], - "modified": "2020-07-15 22:32:14.268178", + "modified": "2020-07-17 19:14:46.132134", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From ddbe0fc0b5368eeab223d2d478d7f591735cd6c4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:03:12 +0200 Subject: [PATCH 016/284] feat: return token as json, calculate expiry --- .../doctype/token_cache/token_cache.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 93adfb50a7..2a8cecd7fb 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from datetime import datetime, timedelta from frappe.model.document import Document class TokenCache(Document): @@ -19,6 +20,7 @@ class TokenCache(Document): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') self.expires_in = data.get('expires_in') + self.token_type = data.get('token_type') new_scopes = data.get('scope') if new_scopes: @@ -33,3 +35,18 @@ class TokenCache(Document): self.save() return self + + def get_expires_in(self): + expiry_time = self.modified + datetime.timedelta(self.expires_in) + return (datetime.now() - expiry_time).total_seconds() + + def is_expired(self): + return self.get_expires_in() < 0 + + def get_json(self): + return { + 'access_token': self.access_token, + 'refresh_token': self.refresh_token, + 'expires_in': self.get_expires_in(), + 'token_type': self.token_type + } From b198c8a932c45e8d810be9407a9b71fbb27a5e6b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:04:41 +0200 Subject: [PATCH 017/284] refactor: connected app --- .../doctype/connected_app/connected_app.py | 75 +++++++++---------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 23e4a00abd..51f8fee513 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -10,6 +10,7 @@ from frappe.model.document import Document from datetime import datetime, timedelta from urllib.parse import urlencode from requests_oauthlib import OAuth2Session +from oauthlib.oauth2 import BackendApplicationClient if frappe.conf.developer_mode: # Disable mandatory TLS in developer mode @@ -29,48 +30,21 @@ class ConnectedApp(Document): return OAuth2Session( self.client_id, redirect_uri=self.redirect_uri, - scope=[scope.scope for scope in self.scopes] + scope=[row.scope for row in self.scopes] ) - def get_client_token(self): - try: - token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - token = self.retrieve_client_token() - - token = self.check_validity(token) - return token - - def get_params(self, **kwargs): - return { - 'client_id': self.client_id, - 'redirect_uri': self.redirect_uri, - 'scope': self.scope - }.update(kwargs) - - def retrieve_client_token(self): - client_secret = self.get_password('client_secret') - data = self.get_params(grant_type='client_credentials', client_secret=client_secret) - response = requests.post( - self.token_endpoint, - data=urlencode(data), - headers={'Content-Type': 'application/x-www-form-urlencoded'} - ) - token = response.json() - return self.update_stored_client_token(token) - def check_validity(self, token): if(token.get('__islocal') or (not token.access_token)): raise frappe.exceptions.DoesNotExistError - expiry = token.modified + timedelta(seconds=token.expires_in) - if expiry > datetime.now(): + if not token.is_expired(): return token return self.refresh_token(token) - def initiate_auth_code_flow(self, user=None, redirect_to=None): - redirect_to = redirect_to or '/desk' + def initiate_web_application_flow(self, user=None, success_uri=None): + """Return an authorization URL for the user. Save state in Token Cache.""" + success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session() authorization_url, state = oauth.authorization_url(self.authorization_endpoint) @@ -82,13 +56,14 @@ class ConnectedApp(Document): token.user = user token.connected_app = self.name + token.success_uri = success_uri token.state = state token.save() - frappe.db.commit() return authorization_url def get_user_token(self, user=None, redirect_to=None): + """Return an existing user token or initiate a Web Application Flow.""" redirect_to = redirect_to or '/desk' user = user or frappe.session.user @@ -96,7 +71,7 @@ class ConnectedApp(Document): token = self.get_stored_user_token(user) token = self.check_validity(token) except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_auth_code_flow(user, redirect_to) + redirect = self.initiate_web_application_flow(user, redirect_to) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = redirect return redirect @@ -104,10 +79,12 @@ class ConnectedApp(Document): return token def refresh_token(self, token): - data = self.get_params(grant_type='refresh_token', refresh_token=token.refresh_token) - headers = {'Content-Type': 'application/x-www-form-urlencoded'} - response = requests.post(self.token_endpoint, data=urlencode(data), headers=headers) - new_token = response.json() + oauth = self.get_oauth2_session() + new_token = oauth.refresh_token( + self.token_endpoint, + client_secret=self.get_password('client_secret'), + token=token.get_json() + ) # Revoke old token data = urlencode({'token': token.get('access_token')}) @@ -116,6 +93,28 @@ class ConnectedApp(Document): return self.update_stored_client_token(new_token) + def get_client_token(self): + """Return an existing client token or initiate a Backend Application Flow.""" + try: + token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + token = self.initiate_backend_application_flow() + + token = self.check_validity(token) + return token + + def initiate_backend_application_flow(self): + """Retrieve token without user interaction. Token is not user specific.""" + client = BackendApplicationClient(client_id=self.client_id) + oauth = OAuth2Session(client=client) + token = oauth.fetch_token( + token_url=self.token_endpoint, + client_id=self.client_id, + client_secret=self.get_password('client_secret') + ) + + return self.update_stored_client_token(token) + def get_stored_client_token(self): return frappe.get_doc('Token Cache', self.name + '-user') From e10851cbee3c348156eced950c35fd72411dad5e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 17 Jul 2020 20:39:03 +0200 Subject: [PATCH 018/284] refactor: move mothods to token cache --- .../doctype/connected_app/connected_app.js | 2 +- .../doctype/connected_app/connected_app.py | 71 ++++++------------- .../doctype/token_cache/token_cache.json | 8 ++- .../doctype/token_cache/token_cache.py | 33 +++++++++ 4 files changed, 60 insertions(+), 54 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 004f150b2a..71ff23fcc3 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -23,7 +23,7 @@ frappe.ui.form.on('Connected App', { frm.add_custom_button(__("Init"), async () => { frappe.call({ - method: "initiate_auth_code_flow", + method: "initiate_web_application_flow", doc: frm.doc, callback: function(r) { window.open(r.message, '_blank'); diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 51f8fee513..4d987baada 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -33,15 +33,6 @@ class ConnectedApp(Document): scope=[row.scope for row in self.scopes] ) - def check_validity(self, token): - if(token.get('__islocal') or (not token.access_token)): - raise frappe.exceptions.DoesNotExistError - - if not token.is_expired(): - return token - - return self.refresh_token(token) - def initiate_web_application_flow(self, user=None, success_uri=None): """Return an authorization URL for the user. Save state in Token Cache.""" success_uri = success_uri or '/desk' @@ -62,37 +53,38 @@ class ConnectedApp(Document): return authorization_url - def get_user_token(self, user=None, redirect_to=None): + def initiate_backend_application_flow(self): + """Retrieve token without user interaction. Token is not user specific.""" + client = BackendApplicationClient(client_id=self.client_id) + oauth = OAuth2Session(client=client) + token = oauth.fetch_token( + token_url=self.token_endpoint, + client_id=self.client_id, + client_secret=self.get_password('client_secret') + ) + + try: + stored_token = self.get_stored_client_token() + except frappe.exceptions.DoesNotExistError: + stored_token = frappe.new_doc('Token Cache') + + return stored_token.update_data(token) + + def get_user_token(self, user=None, success_uri=None): """Return an existing user token or initiate a Web Application Flow.""" - redirect_to = redirect_to or '/desk' user = user or frappe.session.user try: token = self.get_stored_user_token(user) - token = self.check_validity(token) + token = token.check_validity() except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_web_application_flow(user, redirect_to) + redirect = self.initiate_web_application_flow(user, success_uri) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = redirect return redirect return token - def refresh_token(self, token): - oauth = self.get_oauth2_session() - new_token = oauth.refresh_token( - self.token_endpoint, - client_secret=self.get_password('client_secret'), - token=token.get_json() - ) - - # Revoke old token - data = urlencode({'token': token.get('access_token')}) - headers['Authorization'] = 'Bearer ' + new_token.get('access_token') - requests.post(self.revocation_endpoint, data=data, headers=headers) - - return self.update_stored_client_token(new_token) - def get_client_token(self): """Return an existing client token or initiate a Backend Application Flow.""" try: @@ -100,20 +92,7 @@ class ConnectedApp(Document): except frappe.exceptions.DoesNotExistError: token = self.initiate_backend_application_flow() - token = self.check_validity(token) - return token - - def initiate_backend_application_flow(self): - """Retrieve token without user interaction. Token is not user specific.""" - client = BackendApplicationClient(client_id=self.client_id) - oauth = OAuth2Session(client=client) - token = oauth.fetch_token( - token_url=self.token_endpoint, - client_id=self.client_id, - client_secret=self.get_password('client_secret') - ) - - return self.update_stored_client_token(token) + return token.check_validity() def get_stored_client_token(self): return frappe.get_doc('Token Cache', self.name + '-user') @@ -121,14 +100,6 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - def update_stored_client_token(self, token_data): - try: - stored_token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - stored_token = frappe.new_doc('Token Cache') - - return stored_token.update_data(token_data) - @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 5f1836e300..52dd848e14 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -67,16 +67,18 @@ { "fieldname": "success_uri", "fieldtype": "Data", - "label": "Success URI" + "label": "Success URI", + "read_only": 1 }, { "fieldname": "token_type", "fieldtype": "Data", - "label": "Token Type" + "label": "Token Type", + "read_only": 1 } ], "links": [], - "modified": "2020-07-17 19:14:46.132134", + "modified": "2020-07-17 20:10:40.268067", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2a8cecd7fb..a7287360fc 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -4,6 +4,8 @@ from __future__ import unicode_literals import frappe +import requests +from urllib.parse import urlencode from datetime import datetime, timedelta from frappe.model.document import Document @@ -16,6 +18,37 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError + def check_validity(self): + if(self.get('__islocal') or (not self.access_token)): + raise frappe.exceptions.DoesNotExistError + + if not self.is_expired(): + return token + + return self.refresh_token(token) + + def refresh_token(self): + app = frappe.get_doc("Connected App", self.connected_app) + oauth = app.get_oauth2_session() + new_token = oauth.refresh_token( + app.token_endpoint, + client_secret=app.get_password('client_secret'), + token=self.get_json() + ) + + if new_token.get('access_token') and app.revocation_endpoint: + # Revoke old token + requests.post( + app.revocation_endpoint, + data=urlencode({'token': new_token.get('access_token')}), + headers={ + 'Authorization': 'Bearer ' + new_token.get('access_token'), + 'Content-Type': 'application/x-www-form-urlencoded' + } + ) + + return self.update_data(new_token) + def update_data(self, data): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') From 277b082d15976b59cd610efeea997146af8092d8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:30:23 +0200 Subject: [PATCH 019/284] fix(oauth provider): parse cookies correctly --- frappe/oauth.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/frappe/oauth.py b/frappe/oauth.py index 4dc50366be..122c806072 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -4,6 +4,7 @@ import pytz from frappe import _ from frappe.auth import LoginManager +from http import cookies from oauthlib.oauth2.rfc6749.tokens import BearerToken from oauthlib.oauth2.rfc6749.grant_types import AuthorizationCodeGrant, ImplicitGrant, ResourceOwnerPasswordCredentialsGrant, ClientCredentialsGrant, RefreshTokenGrant from oauthlib.oauth2 import RequestValidator @@ -130,15 +131,12 @@ class OAuthWebRequestValidator(RequestValidator): oac.scopes = get_url_delimiter().join(request.scopes) oac.redirect_uri_bound_to_authorization_code = request.redirect_uri oac.client = client_id - oac.user = unquote(cookie_dict['user_id']) + oac.user = unquote(cookie_dict['user_id'].value) oac.authorization_code = code['code'] oac.save(ignore_permissions=True) frappe.db.commit() def authenticate_client(self, request, *args, **kwargs): - - cookie_dict = get_cookie_dict_from_headers(request) - #Get ClientID in URL if request.client_id: oc = frappe.get_doc("OAuth Client", request.client_id) @@ -155,7 +153,9 @@ class OAuthWebRequestValidator(RequestValidator): except Exception as e: print("Failed body authentication: Application %s does not exist".format(cid=request.client_id)) - return frappe.session.user == unquote(cookie_dict.get('user_id', "Guest")) + cookie_dict = get_cookie_dict_from_headers(request) + user_id = unquote(cookie_dict['user_id']) if 'user_id' in cookie_dict else "Guest" + return frappe.session.user == user_id def authenticate_client_id(self, client_id, request, *args, **kwargs): cli_id = frappe.db.get_value('OAuth Client', client_id, 'name') @@ -400,13 +400,10 @@ class OAuthWebRequestValidator(RequestValidator): return True def get_cookie_dict_from_headers(r): + cookie = cookies.BaseCookie() if r.headers.get('Cookie'): - cookie = r.headers.get('Cookie') - cookie = cookie.split("; ") - cookie_dict = {k:v for k,v in (x.split('=') for x in cookie)} - return cookie_dict - else: - return {} + cookie.load(r.headers.get('Cookie')) + return cookie def calculate_at_hash(access_token, hash_alg): """Helper method for calculating an access token From f6b206b7783840c757f41fb98c9a7fed3ec0e242 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:31:50 +0200 Subject: [PATCH 020/284] refactor: extract get_scopes() --- frappe/integrations/doctype/connected_app/connected_app.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 4d987baada..dcf8f9029e 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -30,7 +30,7 @@ class ConnectedApp(Document): return OAuth2Session( self.client_id, redirect_uri=self.redirect_uri, - scope=[row.scope for row in self.scopes] + scope=self.get_scopes() ) def initiate_web_application_flow(self, user=None, success_uri=None): @@ -100,6 +100,8 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) + def get_scopes(self): + return [row.scope for row in self.scopes] @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): From d9506be966bde717243113c9fd66ccfa5b4dda96 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:32:47 +0200 Subject: [PATCH 021/284] fix: BackendApplicationClient also likes scopes --- frappe/integrations/doctype/connected_app/connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index dcf8f9029e..07efc51e47 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -55,7 +55,7 @@ class ConnectedApp(Document): def initiate_backend_application_flow(self): """Retrieve token without user interaction. Token is not user specific.""" - client = BackendApplicationClient(client_id=self.client_id) + client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) oauth = OAuth2Session(client=client) token = oauth.fetch_token( token_url=self.token_endpoint, From 7c22f42d9bce7b074c66d6e0110703b2bea86cb8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:33:46 +0200 Subject: [PATCH 022/284] fix: include client_id when fetching token --- .../integrations/doctype/connected_app/connected_app.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 07efc51e47..08e34f2aaa 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -59,8 +59,8 @@ class ConnectedApp(Document): oauth = OAuth2Session(client=client) token = oauth.fetch_token( token_url=self.token_endpoint, - client_id=self.client_id, - client_secret=self.get_password('client_secret') + client_secret=self.get_password('client_secret'), + include_client_id=True ) try: @@ -99,7 +99,7 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - + def get_scopes(self): return [row.scope for row in self.scopes] @@ -132,7 +132,8 @@ def callback(code=None, state=None): oauth = app.get_oauth2_session() token = oauth.fetch_token(app.token_endpoint, code=code, - client_secret=app.get_password('client_secret') + client_secret=app.get_password('client_secret'), + include_client_id=True ) token_cache.update_data(token) From cdf1b597dbede9223be0c7d079db0b2786dd933d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 20:49:23 +0200 Subject: [PATCH 023/284] fix: sider --- frappe/integrations/doctype/connected_app/connected_app.py | 3 --- frappe/integrations/doctype/token_cache/token_cache.py | 6 +++--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 08e34f2aaa..d6de4c77f9 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,12 +3,9 @@ # For license information, please see license.txt from __future__ import unicode_literals -import requests import frappe from frappe import _ from frappe.model.document import Document -from datetime import datetime, timedelta -from urllib.parse import urlencode from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index a7287360fc..accd72f794 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -23,9 +23,9 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError if not self.is_expired(): - return token + return self - return self.refresh_token(token) + return self.refresh_token() def refresh_token(self): app = frappe.get_doc("Connected App", self.connected_app) @@ -70,7 +70,7 @@ class TokenCache(Document): return self def get_expires_in(self): - expiry_time = self.modified + datetime.timedelta(self.expires_in) + expiry_time = self.modified + timedelta(self.expires_in) return (datetime.now() - expiry_time).total_seconds() def is_expired(self): From 6a482d152759d73ee1aef3525f2b6c496548207b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 21:51:18 +0200 Subject: [PATCH 024/284] fix(database): allow passwords up to 20000 characters / 60000 bytes (#11039) --- frappe/database/mariadb/database.py | 4 ++-- frappe/database/mariadb/framework_mariadb.sql | 2 +- frappe/database/postgres/database.py | 4 ++-- frappe/database/postgres/framework_postgres.sql | 2 +- frappe/utils/password.py | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 4ec89c126d..3cbb2e4f0e 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -46,7 +46,7 @@ class MariaDBDatabase(Database): 'Data': ('varchar', self.VARCHAR_LEN), 'Link': ('varchar', self.VARCHAR_LEN), 'Dynamic Link': ('varchar', self.VARCHAR_LEN), - 'Password': ('varchar', self.VARCHAR_LEN), + 'Password': ('text', ''), 'Select': ('varchar', self.VARCHAR_LEN), 'Rating': ('int', '1'), 'Read Only': ('varchar', self.VARCHAR_LEN), @@ -186,7 +186,7 @@ class MariaDBDatabase(Database): `doctype` VARCHAR(140) NOT NULL, `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, - `password` VARCHAR(255) NOT NULL, + `password` TEXT NOT NULL, `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""") diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index af537e0612..1e3749e030 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -277,7 +277,7 @@ CREATE TABLE `__Auth` ( `doctype` VARCHAR(140) NOT NULL, `name` VARCHAR(255) NOT NULL, `fieldname` VARCHAR(140) NOT NULL, - `password` VARCHAR(255) NOT NULL, + `password` TEXT NOT NULL, `encrypted` INT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`doctype`, `name`, `fieldname`) ) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index e348916705..3d997864e4 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -51,7 +51,7 @@ class PostgresDatabase(Database): 'Data': ('varchar', self.VARCHAR_LEN), 'Link': ('varchar', self.VARCHAR_LEN), 'Dynamic Link': ('varchar', self.VARCHAR_LEN), - 'Password': ('varchar', self.VARCHAR_LEN), + 'Password': ('text', ''), 'Select': ('varchar', self.VARCHAR_LEN), 'Rating': ('smallint', None), 'Read Only': ('varchar', self.VARCHAR_LEN), @@ -179,7 +179,7 @@ class PostgresDatabase(Database): "doctype" VARCHAR(140) NOT NULL, "name" VARCHAR(255) NOT NULL, "fieldname" VARCHAR(140) NOT NULL, - "password" VARCHAR(255) NOT NULL, + "password" TEXT NOT NULL, "encrypted" INT NOT NULL DEFAULT 0, PRIMARY KEY ("doctype", "name", "fieldname") )""") diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index 8f77ed6230..a946a7ee5c 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -281,7 +281,7 @@ CREATE TABLE "__Auth" ( "doctype" VARCHAR(140) NOT NULL, "name" VARCHAR(255) NOT NULL, "fieldname" VARCHAR(140) NOT NULL, - "password" VARCHAR(255) NOT NULL, + "password" TEXT NOT NULL, "encrypted" int NOT NULL DEFAULT 0, PRIMARY KEY ("doctype", "name", "fieldname") ); diff --git a/frappe/utils/password.py b/frappe/utils/password.py index b939607b19..f2d75b87bb 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -131,9 +131,9 @@ def create_auth_table(): frappe.db.create_auth_table() def encrypt(pwd): - if len(pwd) > 127: - # encrypting > 127 chars will lead to truncation - frappe.throw(_('Password cannot be more than 127 characters long')) + if len(pwd) > 20000: + # https://github.com/frappe/frappe/issues/11039 + frappe.throw(_('Password cannot be more than 20000 characters long')) cipher_suite = Fernet(encode(get_encryption_key())) cipher_text = cstr(cipher_suite.encrypt(encode(pwd))) From ca6fce4598e2da9e9e2db3057c23974693ff3c1e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 20 Jul 2020 22:01:39 +0200 Subject: [PATCH 025/284] feat: store tokens as password --- frappe/integrations/doctype/token_cache/token_cache.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 52dd848e14..91758a7332 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -35,13 +35,13 @@ }, { "fieldname": "access_token", - "fieldtype": "Small Text", + "fieldtype": "Password", "label": "Access Token", "read_only": 1 }, { "fieldname": "refresh_token", - "fieldtype": "Small Text", + "fieldtype": "Password", "label": "Refresh Token", "read_only": 1 }, @@ -78,7 +78,7 @@ } ], "links": [], - "modified": "2020-07-17 20:10:40.268067", + "modified": "2020-07-21 00:32:56.225300", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From 1bdcc5d9dbdc0f2b99bddec566ee0e7cde1214a9 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 21 Jul 2020 20:05:53 +0200 Subject: [PATCH 026/284] feat(database): patch for long passswords --- frappe/patches.txt | 1 + frappe/patches/v13_0/increase_password_length.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 frappe/patches/v13_0/increase_password_length.py diff --git a/frappe/patches.txt b/frappe/patches.txt index f8c767f5a3..a8acc8732f 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -295,3 +295,4 @@ frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts +frappe.patches.v13_0.increase_password_length diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py new file mode 100644 index 0000000000..7a053345eb --- /dev/null +++ b/frappe/patches/v13_0/increase_password_length.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + if frappe.db.db_type == "postgres": + frappe.db.sql("""ALTER TABLE "__Auth" ALTER COLUMN "password" TYPE TEXT""") + else: + frappe.db.sql("""ALTER TABLE `__Auth` MODIFY `password` TEXT NOT NULL""") From 218749e4011a236887426465aa58cdbf43b85139 Mon Sep 17 00:00:00 2001 From: Emil Date: Wed, 5 Aug 2020 16:20:17 +0300 Subject: [PATCH 027/284] :sparkles: Add Map View --- frappe/geo/utils.py | 39 ++++++++ frappe/public/build.json | 1 + frappe/public/css/list.css | 4 + frappe/public/js/frappe/list/base_list.js | 2 +- .../public/js/frappe/list/list_sidebar.html | 2 + frappe/public/js/frappe/list/list_sidebar.js | 8 ++ frappe/public/js/frappe/views/map/map_view.js | 97 +++++++++++++++++++ frappe/public/less/list.less | 6 ++ 8 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 frappe/geo/utils.py create mode 100644 frappe/public/js/frappe/views/map/map_view.js diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py new file mode 100644 index 0000000000..4bc07249fe --- /dev/null +++ b/frappe/geo/utils.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe + +from pymysql import InternalError + + +@frappe.whitelist() +def get_coords(doctype, filters): + '''Get list of coordinates in form + returns {names: ['latitude', 'longitude']}''' + filters_sql = get_coords_conditions(doctype, filters)[4:] + if filters_sql: + try: + coords = frappe.db.sql("""SELECT * FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + except InternalError: + frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) + return + else: + coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) + out = frappe._dict() + for i in coords: + out[i.name] = out.get(i.docname, []) + out[i.name].append(i.latitude) + out[i.name].append(i.longitude) + return out + + +def get_coords_conditions(doctype, filters=None): + """Returns SQL conditions with user permissions and filters for event queries""" + from frappe.desk.reportview import get_filters_cond + if not frappe.has_permission(doctype): + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) + + return get_filters_cond(doctype, filters, [], with_match_conditions=True) \ No newline at end of file diff --git a/frappe/public/build.json b/frappe/public/build.json index 997a3092ad..096bb09c6e 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -304,6 +304,7 @@ "public/js/frappe/views/calendar/calendar.js", "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", + "public/js/frappe/views/map/map_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", "public/js/frappe/views/file/file_view.js", diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 5ae77c73ca..49ffbcd9e9 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -401,6 +401,10 @@ input.list-row-checkbox { .pswp__more-item img { max-height: 100%; } +.map-view-container { + display: flex; + flex-wrap: wrap; +} .list-paging-area .gantt-view-mode { margin-left: 15px; margin-right: 15px; diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index bbe2fa2f95..af220a97d3 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -695,5 +695,5 @@ class FilterArea { } // utility function to validate view modes -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Inbox', 'Report', 'Dashboard']; +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Map', 'Inbox', 'Report']; frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_sidebar.html b/frappe/public/js/frappe/list/list_sidebar.html index dcbbe7ac5e..c5b75782b5 100644 --- a/frappe/public/js/frappe/list/list_sidebar.html +++ b/frappe/public/js/frappe/list/list_sidebar.html @@ -30,6 +30,8 @@ {%= __("Dashboard") %} +
  • ${display_name}
  • `).appendTo($dropdown); + $(`
  • ${account.email_id}
  • `).appendTo($dropdown); if (account.email_id === "Sent Mail") divider = false; }); @@ -233,21 +225,40 @@ frappe.views.ListSidebar = class ListSidebar { }); } - setup_keyboard_shortcuts() { - this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => { - frappe.ui.keys - .get_shortcut_group(this.page) - .add($(el)); + setup_assigned_to_me() { + this.page.sidebar.find(".assigned-to-me a").on("click", () => { + this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${frappe.session.user}%`); }); } - setup_list_group_by() { - this.list_group_by = new frappe.views.ListGroupBy({ - doctype: this.doctype, - sidebar: this, - list_view: this.list_view, - page: this.page - }); + setup_upgrade_box() { + let upgrade_list = $(``).appendTo(this.sidebar); + + // Show Renew/Upgrade button, + // if account is holding one user free plan or + // if account's expiry date within range of 30 days from today's date + + let upgrade_date = frappe.datetime.add_days(frappe.datetime.get_today(), 30); + if (frappe.boot.limits.users === 1 || upgrade_date >= frappe.boot.limits.expiry) { + let upgrade_box = $(`
    + +
    Go Premium
    +

    Upgrade to a premium plan with more users, storage and priority support.

    + +
    `).appendTo(upgrade_list); + + upgrade_box.find('.btn-upgrade').on('click', () => { + frappe.set_route('usage-info'); + }); + + upgrade_box.find('.close').on('click', () => { + upgrade_list.remove(); + frappe.flags.upgrade_dismissed = 1; + }); + } } get_cat_tags() { @@ -258,7 +269,6 @@ frappe.views.ListSidebar = class ListSidebar { var me = this; frappe.call({ method: 'frappe.desk.reportview.get_sidebar_stats', - type: 'GET', args: { stats: me.stats, doctype: me.doctype, @@ -266,9 +276,29 @@ frappe.views.ListSidebar = class ListSidebar { filters: (me.list_view.filter_area ? me.list_filter.get_current_filters() : me.default_filters) || [] }, callback: function(r) { - me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); - let stats_dropdown = me.sidebar.find('.list-stats-dropdown'); - frappe.utils.setup_search(stats_dropdown, '.stat-link', '.stat-label'); + me.defined_category = r.message; + if (r.message.defined_cat) { + me.defined_category = r.message.defined_cat; + me.cats = {}; + //structure the tag categories + for (var i in me.defined_category) { + if (me.cats[me.defined_category[i].category] === undefined) { + me.cats[me.defined_category[i].category] = [me.defined_category[i].tag]; + } else { + me.cats[me.defined_category[i].category].push(me.defined_category[i].tag); + } + me.cat_tags[i] = me.defined_category[i].tag; + } + me.tempstats = r.message.stats; + + $.each(me.cats, function(i, v) { + me.render_stat(i, (me.tempstats || {})["_user_tags"], v); + }); + me.render_stat("_user_tags", (me.tempstats || {})["_user_tags"]); + } else { + //render normal stats + me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); + } } }); } @@ -331,7 +361,7 @@ frappe.views.ListSidebar = class ListSidebar { me.list_view.refresh(); }); }) - .appendTo(this.sidebar.find(".list-stats-dropdown")); + .insertBefore(this.sidebar.find(".close-sidebar-button")); } set_fieldtype(df) { @@ -362,8 +392,8 @@ frappe.views.ListSidebar = class ListSidebar { } reload_stats() { - this.sidebar.find(".stat-link").remove(); - this.sidebar.find(".stat-no-records").remove(); + this.sidebar.find(".sidebar-stat").remove(); + this.sidebar.find(".list-tag-preview").remove(); this.get_stats(); } From 526f470bed8aa581c16eed321aecc225971d5bd8 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Mon, 10 Aug 2020 17:16:05 +0300 Subject: [PATCH 032/284] :art: Beauty code --- frappe/public/js/frappe/views/map/map_view.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 66a219162c..d511460798 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -63,13 +63,12 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { lastCoords = [value[0], value[1]]; } } - if (this.type === 'location_field'){ - for (let i = 0; i < this.coords.length; i++){ + if (this.type === 'location_field') { + for (let i = 0; i < this.coords.length; i++) { let features = JSON.parse(this.coords[i].location).features; features.forEach( coords => L.geoJSON(coords).bindPopup(this.coords[i].name).addTo(this.map) ); - console.log(features[0].geometry.coordinates); lastCoords = features[0].geometry.coordinates.reverse(); } } @@ -84,11 +83,11 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { get_coords_method = frappe.listview_settings[this.doctype].get_coords_method; } if (cur_list.meta.fields.find(i => i.fieldname === 'location') && - cur_list.meta.fields.find(i => i.fieldtype === 'Geolocation')){ + cur_list.meta.fields.find(i => i.fieldtype === 'Geolocation')) { this.type = 'location_field'; } if (cur_list.meta.fields.find(i => i.fieldname === "latitude") && - cur_list.meta.fields.find(i => i.fieldname === "longitude")){ + cur_list.meta.fields.find(i => i.fieldname === "longitude")) { this.type = 'coordinates'; } return frappe.call({ From d4dd7e097a0a5da1358598271b3201d1aab331b8 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 14:13:08 +0300 Subject: [PATCH 033/284] :art: Apply suggestions from code review Co-authored-by: Mathieu Brunot --- frappe/geo/utils.py | 6 +++--- frappe/public/build.json | 2 +- frappe/public/js/frappe/list/base_list.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 249f3ffcca..b99ca56ce8 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -14,7 +14,7 @@ from pymysql import InternalError @frappe.whitelist() def get_coords(doctype, filters, type): '''Get list of coordinates in form - returns {names: ['latitude', 'longitude']} or location type''' + returns {name, location} with location being a geojson string''' filters_sql = get_coords_conditions(doctype, filters)[4:] out = None if type == 'coordinates': @@ -28,10 +28,10 @@ def return_location(doctype, filters_sql): try: coords = frappe.db.sql("""SELECT * FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) + frappe.msgprint(frappe._('This Doctype did not contain location field')) return else: - coords = frappe.get_all(doctype, fields = ['location', 'name']) + coords = frappe.get_all(doctype, fields = ['name', 'location']) return coords def return_coordinates(doctype, filters_sql): diff --git a/frappe/public/build.json b/frappe/public/build.json index c5678e485e..874b9d2419 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -304,7 +304,7 @@ "public/js/frappe/views/calendar/calendar.js", "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", - "public/js/frappe/views/map/map_view.js", + "public/js/frappe/views/map/map_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", "public/js/frappe/views/file/file_view.js", diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index af220a97d3..0f8508b4c1 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -695,5 +695,5 @@ class FilterArea { } // utility function to validate view modes -frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Map', 'Inbox', 'Report']; +frappe.views.view_modes = ['List', 'Gantt', 'Kanban', 'Calendar', 'Image', 'Map', 'Inbox', 'Report', 'Dashboard']; frappe.views.is_valid = view_mode => frappe.views.view_modes.includes(view_mode); From 632b41bab7521c97fb3eab060e31cd769133005f Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 14:23:33 +0300 Subject: [PATCH 034/284] :bug: Fix list sidebar --- frappe/public/js/frappe/list/list_sidebar.js | 103 +++++++------------ 1 file changed, 37 insertions(+), 66 deletions(-) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index e0c3c721de..b3f65253c7 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -13,7 +13,6 @@ frappe.views.ListSidebar = class ListSidebar { constructor(opts) { $.extend(this, opts); this.make(); - this.get_stats(); this.cat_tags = []; } @@ -26,17 +25,25 @@ frappe.views.ListSidebar = class ListSidebar { this.setup_reports(); this.setup_list_filter(); - this.setup_assigned_to_me(); this.setup_views(); this.setup_kanban_boards(); this.setup_calendar_view(); this.setup_email_inbox(); + this.setup_keyboard_shortcuts(); + this.setup_list_group_by(); - let limits = frappe.boot.limits; + // do not remove + // used to trigger custom scripts + $(document).trigger('list_sidebar_setup'); - if (limits.upgrade_url && limits.expiry && !frappe.flags.upgrade_dismissed) { - this.setup_upgrade_box(); + if (this.list_view.list_view_settings && this.list_view.list_view_settings.disable_sidebar_stats) { + this.sidebar.find('.sidebar-stat').remove(); + } else { + this.sidebar.find('.list-stats').on('click', (e) => { + this.reload_stats(); + }); } + } setup_views() { @@ -54,7 +61,7 @@ frappe.views.ListSidebar = class ListSidebar { show_list_link = true; } - if (frappe.treeview_settings[this.doctype]) { + if (frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree) { this.sidebar.find(".tree-link").removeClass("hide"); } @@ -83,7 +90,7 @@ frappe.views.ListSidebar = class ListSidebar { this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide'); show_list_link = true; } - // show map link if map_view doctype has get_coords or latitude and longitude + if ((JSON.stringify(frappe.listview_settings) !== '{}' && frappe.listview_settings[this.list_view.doctype].get_coords_method) || (this.list_view.meta.fields.find(i => i.fieldname === "latitude") && @@ -162,7 +169,7 @@ frappe.views.ListSidebar = class ListSidebar { reference_doctype: doctype } }).then(result => { - if (!result) return; + if (!(result && Array.isArray(result) && result.length)) return; const calendar_views = result; const $link_calendar = this.sidebar.find('.list-link[data-view="Calendar"]'); @@ -211,11 +218,13 @@ frappe.views.ListSidebar = class ListSidebar { accounts.forEach((account) => { let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account; let route = ["List", "Communication", "Inbox", email_account].join('/'); + let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id; + if (!divider) { this.get_divider().appendTo($dropdown); divider = true; } - $(`
  • ${account.email_id}
  • `).appendTo($dropdown); + $(`
  • ${display_name}
  • `).appendTo($dropdown); if (account.email_id === "Sent Mail") divider = false; }); @@ -225,40 +234,21 @@ frappe.views.ListSidebar = class ListSidebar { }); } - setup_assigned_to_me() { - this.page.sidebar.find(".assigned-to-me a").on("click", () => { - this.list_view.filter_area.add(this.list_view.doctype, "_assign", "like", `%${frappe.session.user}%`); + setup_keyboard_shortcuts() { + this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => { + frappe.ui.keys + .get_shortcut_group(this.page) + .add($(el)); }); } - setup_upgrade_box() { - let upgrade_list = $(``).appendTo(this.sidebar); - - // Show Renew/Upgrade button, - // if account is holding one user free plan or - // if account's expiry date within range of 30 days from today's date - - let upgrade_date = frappe.datetime.add_days(frappe.datetime.get_today(), 30); - if (frappe.boot.limits.users === 1 || upgrade_date >= frappe.boot.limits.expiry) { - let upgrade_box = $(`
    - -
    Go Premium
    -

    Upgrade to a premium plan with more users, storage and priority support.

    - -
    `).appendTo(upgrade_list); - - upgrade_box.find('.btn-upgrade').on('click', () => { - frappe.set_route('usage-info'); - }); - - upgrade_box.find('.close').on('click', () => { - upgrade_list.remove(); - frappe.flags.upgrade_dismissed = 1; - }); - } + setup_list_group_by() { + this.list_group_by = new frappe.views.ListGroupBy({ + doctype: this.doctype, + sidebar: this, + list_view: this.list_view, + page: this.page + }); } get_cat_tags() { @@ -269,6 +259,7 @@ frappe.views.ListSidebar = class ListSidebar { var me = this; frappe.call({ method: 'frappe.desk.reportview.get_sidebar_stats', + type: 'GET', args: { stats: me.stats, doctype: me.doctype, @@ -276,29 +267,9 @@ frappe.views.ListSidebar = class ListSidebar { filters: (me.list_view.filter_area ? me.list_filter.get_current_filters() : me.default_filters) || [] }, callback: function(r) { - me.defined_category = r.message; - if (r.message.defined_cat) { - me.defined_category = r.message.defined_cat; - me.cats = {}; - //structure the tag categories - for (var i in me.defined_category) { - if (me.cats[me.defined_category[i].category] === undefined) { - me.cats[me.defined_category[i].category] = [me.defined_category[i].tag]; - } else { - me.cats[me.defined_category[i].category].push(me.defined_category[i].tag); - } - me.cat_tags[i] = me.defined_category[i].tag; - } - me.tempstats = r.message.stats; - - $.each(me.cats, function(i, v) { - me.render_stat(i, (me.tempstats || {})["_user_tags"], v); - }); - me.render_stat("_user_tags", (me.tempstats || {})["_user_tags"]); - } else { - //render normal stats - me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); - } + me.render_stat("_user_tags", (r.message.stats || {})["_user_tags"]); + let stats_dropdown = me.sidebar.find('.list-stats-dropdown'); + frappe.utils.setup_search(stats_dropdown, '.stat-link', '.stat-label'); } }); } @@ -361,7 +332,7 @@ frappe.views.ListSidebar = class ListSidebar { me.list_view.refresh(); }); }) - .insertBefore(this.sidebar.find(".close-sidebar-button")); + .appendTo(this.sidebar.find(".list-stats-dropdown")); } set_fieldtype(df) { @@ -392,8 +363,8 @@ frappe.views.ListSidebar = class ListSidebar { } reload_stats() { - this.sidebar.find(".sidebar-stat").remove(); - this.sidebar.find(".list-tag-preview").remove(); + this.sidebar.find(".stat-link").remove(); + this.sidebar.find(".stat-no-records").remove(); this.get_stats(); } From 8373520296751b76a4617b1869acb6e30714f78f Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 14:24:51 +0300 Subject: [PATCH 035/284] :bug: Remove blank line --- frappe/public/js/frappe/list/list_sidebar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index b3f65253c7..4dbd1076db 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -219,7 +219,6 @@ frappe.views.ListSidebar = class ListSidebar { let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account; let route = ["List", "Communication", "Inbox", email_account].join('/'); let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id; - if (!divider) { this.get_divider().appendTo($dropdown); divider = true; From 325d0e32c11f4dc31cd8fe6b8541132d56a118bf Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 17:20:03 +0300 Subject: [PATCH 036/284] :art: Changes from new requirements (python) --- frappe/geo/utils.py | 43 +++++++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index b99ca56ce8..1f9dd2d335 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -4,17 +4,17 @@ from __future__ import unicode_literals +import json + import frappe from pymysql import InternalError - - @frappe.whitelist() def get_coords(doctype, filters, type): '''Get list of coordinates in form - returns {name, location} with location being a geojson string''' + returns {names: ['latitude', 'longitude']} or location type''' filters_sql = get_coords_conditions(doctype, filters)[4:] out = None if type == 'coordinates': @@ -23,18 +23,34 @@ def get_coords(doctype, filters, type): out = return_location(doctype, filters_sql) return out + +def convert_to_geo_json(coords_list): + handled_geo_json_dict = [] + for element in coords_list: + handled_geo_json = json.loads(element['location']) + for coord in handled_geo_json['features']: + coord['properties']['name'] = element['name'] + handled_geo_json_dict.append(coord.copy()) + print(handled_geo_json['features']) + handled_geo_json = {"type": "FeatureCollection", "features": handled_geo_json_dict} + return handled_geo_json + + def return_location(doctype, filters_sql): if filters_sql: try: - coords = frappe.db.sql("""SELECT * FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + coords = frappe.db.sql("""SELECT name,location FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contain location field')) + frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) return else: - coords = frappe.get_all(doctype, fields = ['name', 'location']) - return coords + coords = frappe.get_all(doctype, fields=['location', 'name']) + handled_geo_json = convert_to_geo_json(coords) + return handled_geo_json + def return_coordinates(doctype, filters_sql): + handled_geo_json = {"type": "FeatureCollection", "features": None} if filters_sql: try: coords = frappe.db.sql("""SELECT * FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) @@ -43,12 +59,15 @@ def return_coordinates(doctype, filters_sql): return else: coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) - out = frappe._dict() + out_list = [] for i in coords: - out[i.name] = out.get(i.docname, []) - out[i.name].append(i.latitude) - out[i.name].append(i.longitude) - return out + node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} + node['properties']['name'] = i.name + node['geometry']['coordinates'] = [i.latitude, i.longitude] + out_list.append(node.copy()) + handled_geo_json['features'] = out_list + print(handled_geo_json) + return handled_geo_json def get_coords_conditions(doctype, filters=None): From da46ca2f1234e788a39f353fb677349041356b84 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 17:21:57 +0300 Subject: [PATCH 037/284] :art: Changes from new requirements --- frappe/public/js/frappe/views/map/map_view.js | 26 +++++-------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index d511460798..7e75dd0640 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -53,25 +53,11 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { }).addTo(this.map); L.control.scale().addTo(this.map); - - let lastCoords = []; - if (this.type === 'coordinates') { - for (const [key, value] of Object.entries(this.coords)) { - new L.marker([value[0], value[1]]) - .bindPopup(key) - .addTo(this.map); - lastCoords = [value[0], value[1]]; - } - } - if (this.type === 'location_field') { - for (let i = 0; i < this.coords.length; i++) { - let features = JSON.parse(this.coords[i].location).features; - features.forEach( - coords => L.geoJSON(coords).bindPopup(this.coords[i].name).addTo(this.map) - ); - lastCoords = features[0].geometry.coordinates.reverse(); - } - } + console.log(this.coords); + this.coords.features.forEach( + coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) + ); + let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); this.map.panTo(lastCoords, 8); } @@ -98,7 +84,7 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { type: this.type } }).then(r => { - this.coords = Object.assign(r.message); + this.coords = r.message; }); } From f27eee1ce67ac95b5f4538a6994074f8d93d51e9 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 17:30:36 +0300 Subject: [PATCH 038/284] :bug: Only location or latitude and longitude --- frappe/geo/utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 1f9dd2d335..c1fc5bbb52 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -37,11 +37,11 @@ def convert_to_geo_json(coords_list): def return_location(doctype, filters_sql): - if filters_sql: + if filters_sql: try: - coords = frappe.db.sql("""SELECT name,location FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + coords = frappe.db.sql("""SELECT name, location FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) + frappe.msgprint(frappe._('This Doctype did not contains location fields')) return else: coords = frappe.get_all(doctype, fields=['location', 'name']) @@ -53,7 +53,7 @@ def return_coordinates(doctype, filters_sql): handled_geo_json = {"type": "FeatureCollection", "features": None} if filters_sql: try: - coords = frappe.db.sql("""SELECT * FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + coords = frappe.db.sql("""SELECT name, latitude, longitude FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) except InternalError: frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) return From 32546694b796060c1812ba311f7a4fe54d56e826 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Tue, 11 Aug 2020 17:50:55 +0300 Subject: [PATCH 039/284] :pencil: Update description Co-authored-by: Mathieu Brunot --- frappe/geo/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index c1fc5bbb52..ab7b856c3c 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -14,7 +14,7 @@ from pymysql import InternalError @frappe.whitelist() def get_coords(doctype, filters, type): '''Get list of coordinates in form - returns {names: ['latitude', 'longitude']} or location type''' + returns {name, location} with location being a geojson string''' filters_sql = get_coords_conditions(doctype, filters)[4:] out = None if type == 'coordinates': From 8061b37748b1058bc04b643fa9c7ba1a7c7cff5d Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Tue, 11 Aug 2020 17:10:02 +0200 Subject: [PATCH 040/284] :art: Restore empty line --- frappe/public/js/frappe/list/list_sidebar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 4dbd1076db..b3f65253c7 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -219,6 +219,7 @@ frappe.views.ListSidebar = class ListSidebar { let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account; let route = ["List", "Communication", "Inbox", email_account].join('/'); let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) ? __(account.email_id) : account.email_id; + if (!divider) { this.get_divider().appendTo($dropdown); divider = true; From be76942394b01778767c11880b34630b58d47956 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Wed, 12 Aug 2020 12:57:08 +0300 Subject: [PATCH 041/284] :fire: Remove debug print --- frappe/geo/utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index ab7b856c3c..836b52f4b2 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -31,7 +31,6 @@ def convert_to_geo_json(coords_list): for coord in handled_geo_json['features']: coord['properties']['name'] = element['name'] handled_geo_json_dict.append(coord.copy()) - print(handled_geo_json['features']) handled_geo_json = {"type": "FeatureCollection", "features": handled_geo_json_dict} return handled_geo_json @@ -44,7 +43,7 @@ def return_location(doctype, filters_sql): frappe.msgprint(frappe._('This Doctype did not contains location fields')) return else: - coords = frappe.get_all(doctype, fields=['location', 'name']) + coords = frappe.get_all(doctype, fields=['name', 'location']) handled_geo_json = convert_to_geo_json(coords) return handled_geo_json From 60153cb857bd16139f72a8cf2f364bbffb251864 Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:29:07 +0300 Subject: [PATCH 042/284] :truck: Rename function --- frappe/geo/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 836b52f4b2..949efb6d5e 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -24,7 +24,7 @@ def get_coords(doctype, filters, type): return out -def convert_to_geo_json(coords_list): +def merge_all_feature_collection_in_one(coords_list): handled_geo_json_dict = [] for element in coords_list: handled_geo_json = json.loads(element['location']) @@ -44,7 +44,7 @@ def return_location(doctype, filters_sql): return else: coords = frappe.get_all(doctype, fields=['name', 'location']) - handled_geo_json = convert_to_geo_json(coords) + handled_geo_json = merge_all_feature_collection_in_one(coords) return handled_geo_json From d0728df83b99be00a20094dadb392fd81b06ebae Mon Sep 17 00:00:00 2001 From: AminovE99 <32329685+AminovE99@users.noreply.github.com> Date: Wed, 12 Aug 2020 16:30:01 +0300 Subject: [PATCH 043/284] :fire: Remove console log --- frappe/public/js/frappe/views/map/map_view.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 7e75dd0640..8b46ef1a95 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -53,7 +53,6 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { }).addTo(this.map); L.control.scale().addTo(this.map); - console.log(this.coords); this.coords.features.forEach( coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) ); From e9b3085c6a1ab1376000bac74c8582a22841f4fe Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 12 Aug 2020 17:19:35 +0200 Subject: [PATCH 044/284] :art: Split Geo Utils into specific functions Signed-off-by: mathieu.brunot --- frappe/geo/utils.py | 78 ++++++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 949efb6d5e..919dcfd961 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -13,64 +13,82 @@ from pymysql import InternalError @frappe.whitelist() def get_coords(doctype, filters, type): - '''Get list of coordinates in form - returns {name, location} with location being a geojson string''' + '''Get a geojson dict representing a doctype.''' filters_sql = get_coords_conditions(doctype, filters)[4:] - out = None - if type == 'coordinates': - out = return_coordinates(doctype, filters_sql) + + coords = None if type == 'location_field': - out = return_location(doctype, filters_sql) + coords = return_location(doctype, filters_sql) + elif type == 'coordinates': + coords = return_coordinates(doctype, filters_sql) + + out = convert_to_geojson(type, coords) return out +def convert_to_geojson(type, coords): + '''Converts GPS coordinates to geoJSON string.''' + geojson = {"type": "FeatureCollection", "features": None} + + if type == 'location_field': + geojson['features'] = merge_location_features_in_one(coords) + elif type == 'coordinates': + geojson['features'] = create_gps_markers(coords) + + return geojson -def merge_all_feature_collection_in_one(coords_list): - handled_geo_json_dict = [] - for element in coords_list: - handled_geo_json = json.loads(element['location']) - for coord in handled_geo_json['features']: + +def merge_location_features_in_one(coords): + '''Merging all features from location field.''' + geojson_dict = [] + for element in coords: + geojson_loc = json.loads(element['location']) + for coord in geojson_loc['features']: coord['properties']['name'] = element['name'] - handled_geo_json_dict.append(coord.copy()) - handled_geo_json = {"type": "FeatureCollection", "features": handled_geo_json_dict} - return handled_geo_json + geojson_dict.append(coord.copy()) + + return geojson_dict + + +def create_gps_markers(coords): + '''Build Marker based on latitude and longitude.''' + geojson_dict = [] + for i in coords: + node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} + node['properties']['name'] = i.name + node['geometry']['coordinates'] = [i.latitude, i.longitude] + geojson_dict.append(node.copy()) + + return geojson_dict def return_location(doctype, filters_sql): - if filters_sql: + '''Get name and location fields for Doctype.''' + if filters_sql: try: - coords = frappe.db.sql("""SELECT name, location FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: frappe.msgprint(frappe._('This Doctype did not contains location fields')) return else: coords = frappe.get_all(doctype, fields=['name', 'location']) - handled_geo_json = merge_all_feature_collection_in_one(coords) - return handled_geo_json + return coords def return_coordinates(doctype, filters_sql): - handled_geo_json = {"type": "FeatureCollection", "features": None} + '''Get name, latitude and longitude fields for Doctype.''' if filters_sql: try: - coords = frappe.db.sql("""SELECT name, latitude, longitude FROM `tab{}` WHERE {}""".format(doctype, filters_sql), as_dict=True) + coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) return else: coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) - out_list = [] - for i in coords: - node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} - node['properties']['name'] = i.name - node['geometry']['coordinates'] = [i.latitude, i.longitude] - out_list.append(node.copy()) - handled_geo_json['features'] = out_list - print(handled_geo_json) - return handled_geo_json + return coords def get_coords_conditions(doctype, filters=None): - """Returns SQL conditions with user permissions and filters for event queries""" + '''Returns SQL conditions with user permissions and filters for event queries.''' from frappe.desk.reportview import get_filters_cond if not frappe.has_permission(doctype): frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) From 78debff5b983c052de4c73e3825d0f3816dc9f6f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:21:32 +0200 Subject: [PATCH 045/284] fat: add field base_url --- .../integrations/doctype/connected_app/connected_app.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 3d83695621..8fca1b620f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -9,6 +9,7 @@ "engine": "InnoDB", "field_order": [ "provider_name", + "base_url", "cb_00", "openid_configuration", "callback", @@ -123,6 +124,11 @@ "fieldtype": "Table", "label": "Scopes", "options": "OAuth Scope" + }, + { + "fieldname": "base_url", + "fieldtype": "Data", + "label": "Base URL" } ], "links": [ @@ -131,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-07-15 22:10:07.122237", + "modified": "2020-08-13 11:45:02.983854", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From fe9f8cb295781c9497218f940c39fd4252e7d38b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:35:29 +0200 Subject: [PATCH 046/284] fix: add docstrings --- .../doctype/connected_app/connected_app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index d6de4c77f9..7b210bb9f5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -15,6 +15,9 @@ if frappe.conf.developer_mode: os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): + """Connect to a remote oAuth Server. Retrieve and store user's access token + in a Token Cache. + """ def autoname(self): self.callback = frappe.scrub(self.provider_name) @@ -100,9 +103,15 @@ class ConnectedApp(Document): def get_scopes(self): return [row.scope for row in self.scopes] + @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): - """Handle client's code.""" + """Handle client's code. + + Called during the oauthorization flow by the remote oAuth2 server to + transmit a code that can be used by the local server to obtain an access + token. + """ if frappe.request.method != 'GET': frappe.throw(_('Invalid Method')) From 27b9010c08589be42caa914bc148dcbb11088666 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 12:38:44 +0200 Subject: [PATCH 047/284] fix: redirect to desk#workspace instead of desk#desktop --- frappe/utils/oauth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index 969623c369..6ac5a4fdcd 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -305,7 +305,7 @@ def redirect_post_login(desk_user, redirect_to=None): frappe.local.response["type"] = "redirect" if not redirect_to: - # the #desktop is added to prevent a facebook redirect bug - redirect_to = "/desk#desktop" if desk_user else "/me" + # the #workspace is added to prevent a facebook redirect bug + redirect_to = "/desk#workspace" if desk_user else "/me" frappe.local.response["location"] = redirect_to From 4a53f12cdedad67284cbba1d630c75af013346da Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 16:24:52 +0200 Subject: [PATCH 048/284] fix: remove client_id from test_records Because it will be auto-generated and overwritten anyways. --- frappe/integrations/doctype/oauth_client/test_records.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json index cff06457c5..11e6338a87 100644 --- a/frappe/integrations/doctype/oauth_client/test_records.json +++ b/frappe/integrations/doctype/oauth_client/test_records.json @@ -1,7 +1,6 @@ [ { - "app_name": "_Test OAuth Client", - "client_id": "test_client_id", + "app_name": "_Test OAuth Client", "client_secret": "test_client_secret", "default_redirect_uri": "http://localhost", "docstatus": 0, From f017cfa12d8c9383f5188f0c1d69b2704a814ee8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 19:35:52 +0200 Subject: [PATCH 049/284] refactor(oauth2): better name and docstring --- frappe/integrations/oauth2.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c8dfc52c95..51665325be 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -14,7 +14,8 @@ def get_oauth_server(): return frappe.local.oauth_server -def get_urlparams_from_kwargs(param_kwargs): +def clean_urlparams(param_kwargs): + """Remove 'data' and 'cmd' keys, if present.""" arguments = param_kwargs if arguments.get("data"): arguments.pop("data") @@ -50,7 +51,7 @@ def approve(*args, **kwargs): def authorize(*args, **kwargs): #Fetch provider URL from settings oauth_settings = get_oauth_settings() - params = get_urlparams_from_kwargs(kwargs) + params = clean_urlparams(kwargs) request_url = urlparse(frappe.request.url) success_url = request_url.scheme + "://" + request_url.netloc + "/api/method/frappe.integrations.oauth2.approve?" + params failure_url = frappe.form_dict["redirect_uri"] + "?error=access_denied" From 3f7b8f828228d67b6271983697ee669683e34104 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 19:36:44 +0200 Subject: [PATCH 050/284] feat: method to create social login key for tests --- .../social_login_key/test_social_login_key.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index 58bd48d64a..a1390b39b0 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -22,3 +22,16 @@ def make_social_login_key(**kwargs): kwargs["provider_name"] = "Test OAuth2 Provider" doc = frappe.get_doc(kwargs) return doc + +def create_or_update_social_login_key(): + # used in other tests (connected app, oauth20) + try: + social_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + social_login_key = frappe.new_doc("Social Login Key") + social_login_key.get_social_login_provider("Frappe", initialize=True) + social_login_key.base_url = frappe.get_site_config().host_name or "http://localhost:8000" + social_login_key.enable_social_login = 0 + social_login_key.save() + frappe.db.commit() + return social_login_key From d669b67473775bb1cfc5d5449617aea6a9248d65 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:55:38 +0200 Subject: [PATCH 051/284] fix: add fallback url for tests --- .../doctype/connected_app/connected_app.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 7b210bb9f5..9e3e31d1eb 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import BackendApplicationClient +from urllib.parse import urljoin if frappe.conf.developer_mode: # Disable mandatory TLS in developer mode @@ -23,8 +24,14 @@ class ConnectedApp(Document): self.callback = frappe.scrub(self.provider_name) def validate(self): - callback_path = 'api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' - self.redirect_uri = frappe.request.host_url + callback_path + self.callback + try: + base_url = frappe.request.host_url + except RuntimeError: + # for tests + base_url = frappe.get_site_config().host_name or 'http://localhost:8000' + + callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.callback + self.redirect_uri = urljoin(base_url, callback_path) def get_oauth2_session(self): return OAuth2Session( @@ -50,6 +57,7 @@ class ConnectedApp(Document): token.success_uri = success_uri token.state = state token.save() + frappe.db.commit() return authorization_url From 41eb1ae5f8a872628e5e85201b049457dba36131 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:56:32 +0200 Subject: [PATCH 052/284] fix: commit after saving token --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index accd72f794..3cb213e762 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -66,7 +66,7 @@ class TokenCache(Document): self.state = None self.save() - + frappe.db.commit() return self def get_expires_in(self): From 39440027bb9dda07d43e4cfdcdb243d50e14a6a0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:57:08 +0200 Subject: [PATCH 053/284] fix: add a useless link so that test record gets created --- .../doctype/connected_app/connected_app.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 8fca1b620f..6ab3f004e0 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -26,7 +26,8 @@ "revocation_endpoint", "cb_02", "userinfo_endpoint", - "introspection_endpoint" + "introspection_endpoint", + "useless_link" ], "fields": [ { @@ -129,6 +130,12 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" + }, + { + "fieldname": "useless_link", + "fieldtype": "Link", + "label": "Useless Link", + "options": "User" } ], "links": [ @@ -137,7 +144,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-08-13 11:45:02.983854", + "modified": "2020-08-13 18:55:35.554769", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 58a66b0cdcd8f5bfb2f080918b90ff239622ee12 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 20:57:51 +0200 Subject: [PATCH 054/284] tests: add test record --- .../doctype/connected_app/test_records.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 frappe/integrations/doctype/connected_app/test_records.json diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json new file mode 100644 index 0000000000..f9ba219f54 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_records.json @@ -0,0 +1,15 @@ +[ + { + "doctype": "Connected App", + "provider_name": "frappe", + "base_url": "http://localhost:8000", + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "redirect_uri": "http://localhost:8000/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/frappe", + "scopes": [ + { + "scope": "all" + } + ] + } +] From 5e787eb56f652e71a07149279d7bfd488ae83132 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Aug 2020 21:11:05 +0200 Subject: [PATCH 055/284] tests: web application fow against frappe --- .../connected_app/test_connected_app.py | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index bb04ca6677..9cd5bf5d22 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -3,8 +3,65 @@ # See license.txt from __future__ import unicode_literals -# import frappe import unittest +import requests +import frappe +from requests.auth import HTTPBasicAuth +from urllib.parse import urljoin, urlparse, parse_qs +from frappe.test_runner import make_test_records +from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key +from .connected_app import callback + +test_dependencies = ['OAuth Client', 'User', 'Connected App'] class TestConnectedApp(unittest.TestCase): - pass + + def setUp(self): + """Set up a Connected App that connects to our own oAuth provider. + + Frappe comes with it's own oAuth2 provider that we can test against. The + client credentials can be obtained from an "OAuth Client". All depends + on "Social Login Key" so we create one as well. + + The redirect URIs from "Connected App" and "OAuth Client" have to match. + Frappe's "Authorization URL" and "Access Token URL" (actually they're + just endpoints) are stored in "Social Login Key" so we get them from + there. + """ + connected_app = frappe.get_doc('Connected App', 'frappe') + social_login_key = create_or_update_social_login_key() + self.base_url = social_login_key.get('base_url') + + oauth_client_name = frappe.get_all('OAuth Client', fields=['name'])[0] + oauth_client = frappe.get_doc('OAuth Client', oauth_client_name['name']) + oauth_client.redirect_uris = connected_app.get('redirect_uri') + oauth_client.default_redirect_uri = connected_app.get('redirect_uri') + oauth_client.save() + frappe.db.commit() + + connected_app.client_id = oauth_client.get('client_id') + connected_app.client_secret = oauth_client.get('client_secret') + connected_app.authorization_endpoint = urljoin(self.base_url, social_login_key.get('authorize_url')) + connected_app.token_endpoint = urljoin(self.base_url, social_login_key.get('access_token_url')) + self.app = connected_app.save() + self.user_name = 'test@example.com' + self.user_password = 'Eastern_43A1W' + + def test_web_application_flow(self): + """Simulate a logged in user who opens the authorization URL.""" + session = requests.Session() + session.post(urljoin(self.base_url, '/api/method/login'), data={ + 'usr': self.user_name, + 'pwd': self.user_password + }) + authorization_url = self.app.initiate_web_application_flow(user=self.user_name) + + auth_response = session.get(authorization_url) + self.assertEqual(auth_response.status_code, 200) + + callback_response = session.get(auth_response.url) + self.assertEqual(callback_response.status_code, 200) + + token_cache = frappe.get_doc('Token Cache', self.app.name + '-' + self.user_name) + token = token_cache.get_password('access_token') + self.assertNotEqual(token, None) From 80857f6c90c94094c391f61fe9438b6cd69d9ced Mon Sep 17 00:00:00 2001 From: Emil Date: Fri, 14 Aug 2020 14:52:57 +0300 Subject: [PATCH 056/284] :white_check_mark: Add tests Signed-off-by: Emil --- frappe/geo/utils.py | 4 ++-- frappe/tests/tests_geo_utils.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 frappe/tests/tests_geo_utils.py diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 919dcfd961..d7011a7eb0 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -67,7 +67,7 @@ def return_location(doctype, filters_sql): try: coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains location fields')) + frappe.msgprint(frappe._('This Doctype did not contains location fields'), raise_exception=True) return else: coords = frappe.get_all(doctype, fields=['name', 'location']) @@ -80,7 +80,7 @@ def return_coordinates(doctype, filters_sql): try: coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields')) + frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields'), raise_exception=True) return else: coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) diff --git a/frappe/tests/tests_geo_utils.py b/frappe/tests/tests_geo_utils.py new file mode 100644 index 0000000000..3c5757423e --- /dev/null +++ b/frappe/tests/tests_geo_utils.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import unittest + +import frappe +from frappe.geo.utils import get_coords + + +class TestGeoUtils(unittest.TestCase): + def setUp(self): + self.todo = frappe.get_doc( + dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert() + + self.test_location_dict = {'type': 'FeatureCollection', 'features': [ + {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]} + self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location', + 'location': str(self.test_location_dict)}) + + self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']] + self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']] + self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']] + + def test_get_coords_location_with_filter_exists(self): + coords = get_coords('Location', self.test_filter_exists, 'location_field') + self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry']) + + def test_get_coords_location_with_filter_not_exists(self): + coords = get_coords('Location', self.test_filter_not_exists, 'location_field') + self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []}) + + def test_get_coords_from_not_existable_location(self): + self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field') + + def test_get_coords_from_not_existable_coords(self): + self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates') + + def tearDown(self): + self.todo.delete() From af40088fbe95ee3edbc83ab708cbda253dc73d42 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 16 Sep 2020 11:17:14 +0200 Subject: [PATCH 057/284] fix: delete old js test --- .../doctype/token_cache/test_token_cache.js | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 frappe/integrations/doctype/token_cache/test_token_cache.js diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.js b/frappe/integrations/doctype/token_cache/test_token_cache.js deleted file mode 100644 index ee52cd7465..0000000000 --- a/frappe/integrations/doctype/token_cache/test_token_cache.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Token Cache", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Token Cache - () => frappe.tests.make('Token Cache', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); From b9260cdac157b2754871592f5376ee98913d4b11 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 16 Sep 2020 11:17:33 +0200 Subject: [PATCH 058/284] fix: delete old js test --- .../connected_app/test_connected_app.js | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 frappe/integrations/doctype/connected_app/test_connected_app.js diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.js b/frappe/integrations/doctype/connected_app/test_connected_app.js deleted file mode 100644 index 6db9056efc..0000000000 --- a/frappe/integrations/doctype/connected_app/test_connected_app.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Connected App", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Connected App - () => frappe.tests.make('Connected App', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); From f23fe9a9ec1838baf3bafd2ddd842e1ab1d99649 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 18:52:45 +0200 Subject: [PATCH 059/284] fix: style --- .../doctype/connected_app/connected_app.py | 16 ++++++++-------- .../doctype/connected_app/test_connected_app.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 9e3e31d1eb..57ff238471 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -87,8 +87,8 @@ class ConnectedApp(Document): token = token.check_validity() except frappe.exceptions.DoesNotExistError: redirect = self.initiate_web_application_flow(user, success_uri) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = redirect + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect return redirect return token @@ -107,7 +107,7 @@ class ConnectedApp(Document): def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) - + def get_scopes(self): return [row.scope for row in self.scopes] @@ -116,7 +116,7 @@ class ConnectedApp(Document): def callback(code=None, state=None): """Handle client's code. - Called during the oauthorization flow by the remote oAuth2 server to + Called during the oauthorization flow by the remote oAuth2 server to transmit a code that can be used by the local server to obtain an access token. """ @@ -124,9 +124,9 @@ def callback(code=None, state=None): frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - frappe.throw(_("Log in to access this page."), frappe.PermissionError) + frappe.throw(_('Log in to access this page.'), frappe.PermissionError) - path = frappe.request.path[1:].split("/") + path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: frappe.throw(_('Invalid Parameter(s)')) @@ -151,5 +151,5 @@ def callback(code=None, state=None): ) token_cache.update_data(token) - frappe.local.response["type"] = "redirect" - frappe.local.response["location"] = token_cache.get('success_uri') or '/desk' + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = token_cache.get('success_uri') or '/desk' diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 9cd5bf5d22..566c91768c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -18,7 +18,7 @@ class TestConnectedApp(unittest.TestCase): def setUp(self): """Set up a Connected App that connects to our own oAuth provider. - + Frappe comes with it's own oAuth2 provider that we can test against. The client credentials can be obtained from an "OAuth Client". All depends on "Social Login Key" so we create one as well. From 0bcaa9eea7339da6fc5f36304c65ecca514469e8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 18:53:22 +0200 Subject: [PATCH 060/284] fix: remove useless link --- .../doctype/connected_app/connected_app.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 6ab3f004e0..500f1df5b5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -26,8 +26,7 @@ "revocation_endpoint", "cb_02", "userinfo_endpoint", - "introspection_endpoint", - "useless_link" + "introspection_endpoint" ], "fields": [ { @@ -130,12 +129,6 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" - }, - { - "fieldname": "useless_link", - "fieldtype": "Link", - "label": "Useless Link", - "options": "User" } ], "links": [ @@ -144,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-08-13 18:55:35.554769", + "modified": "2020-09-27 18:23:15.001617", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 565c2177263c768c760fb20b9c7fac72373f417d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:35:59 +0200 Subject: [PATCH 061/284] refactor: rename Endpoint to URI --- .../doctype/connected_app/connected_app.json | 52 +++++++++---------- .../doctype/connected_app/connected_app.py | 4 +- .../connected_app/test_connected_app.py | 4 +- .../doctype/token_cache/token_cache.py | 6 +-- 4 files changed, 33 insertions(+), 33 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 500f1df5b5..59479e34a4 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -21,12 +21,12 @@ "sb_scope_section", "scopes", "sb_endpoints_section", - "authorization_endpoint", - "token_endpoint", - "revocation_endpoint", + "authorization_uri", + "token_uri", + "revocation_uri", "cb_02", - "userinfo_endpoint", - "introspection_endpoint" + "userinfo_uri", + "introspection_uri" ], "fields": [ { @@ -91,44 +91,44 @@ "label": "Endpoints" }, { - "fieldname": "authorization_endpoint", - "fieldtype": "Data", - "label": "Authorization Endpoint" + "fieldname": "cb_02", + "fieldtype": "Column Break" }, { - "fieldname": "token_endpoint", - "fieldtype": "Data", - "label": "Token Endpoint" + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope" }, { - "fieldname": "revocation_endpoint", + "fieldname": "base_url", "fieldtype": "Data", - "label": "Revocation Endpoint" + "label": "Base URL" }, { - "fieldname": "cb_02", - "fieldtype": "Column Break" + "fieldname": "authorization_uri", + "fieldtype": "Data", + "label": "Authorization URI" }, { - "fieldname": "userinfo_endpoint", + "fieldname": "token_uri", "fieldtype": "Data", - "label": "Userinfo Endpoint" + "label": "Token URI" }, { - "fieldname": "introspection_endpoint", + "fieldname": "revocation_uri", "fieldtype": "Data", - "label": "Introspection Endpoint" + "label": "Revocation URI" }, { - "fieldname": "scopes", - "fieldtype": "Table", - "label": "Scopes", - "options": "OAuth Scope" + "fieldname": "userinfo_uri", + "fieldtype": "Data", + "label": "Userinfo URI" }, { - "fieldname": "base_url", + "fieldname": "introspection_uri", "fieldtype": "Data", - "label": "Base URL" + "label": "Introspection URI" } ], "links": [ @@ -137,7 +137,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 18:23:15.001617", + "modified": "2020-09-27 19:16:31.039086", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 57ff238471..293e8d478f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -45,7 +45,7 @@ class ConnectedApp(Document): success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session() - authorization_url, state = oauth.authorization_url(self.authorization_endpoint) + authorization_url, state = oauth.authorization_url(self.authorization_uri) try: token = self.get_stored_user_token(user) @@ -66,7 +66,7 @@ class ConnectedApp(Document): client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) oauth = OAuth2Session(client=client) token = oauth.fetch_token( - token_url=self.token_endpoint, + token_url=self.token_uri, client_secret=self.get_password('client_secret'), include_client_id=True ) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 566c91768c..488d537bba 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -41,8 +41,8 @@ class TestConnectedApp(unittest.TestCase): connected_app.client_id = oauth_client.get('client_id') connected_app.client_secret = oauth_client.get('client_secret') - connected_app.authorization_endpoint = urljoin(self.base_url, social_login_key.get('authorize_url')) - connected_app.token_endpoint = urljoin(self.base_url, social_login_key.get('access_token_url')) + connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) + connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) self.app = connected_app.save() self.user_name = 'test@example.com' self.user_password = 'Eastern_43A1W' diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 3cb213e762..8a10ddbd04 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -31,15 +31,15 @@ class TokenCache(Document): app = frappe.get_doc("Connected App", self.connected_app) oauth = app.get_oauth2_session() new_token = oauth.refresh_token( - app.token_endpoint, + app.token_uri, client_secret=app.get_password('client_secret'), token=self.get_json() ) - if new_token.get('access_token') and app.revocation_endpoint: + if new_token.get('access_token') and app.revocation_uri: # Revoke old token requests.post( - app.revocation_endpoint, + app.revocation_uri, data=urlencode({'token': new_token.get('access_token')}), headers={ 'Authorization': 'Bearer ' + new_token.get('access_token'), From b4bfcc734e5a1e2c32e88c43314f926757939fb2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:42:27 +0200 Subject: [PATCH 062/284] fix: remove 'callback' field --- .../doctype/connected_app/connected_app.json | 11 +---------- .../doctype/connected_app/connected_app.py | 5 +---- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 59479e34a4..0a07c927d4 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -1,6 +1,5 @@ { "actions": [], - "autoname": "field:callback", "beta": 1, "creation": "2019-01-24 15:51:06.362222", "doctype": "DocType", @@ -12,7 +11,6 @@ "base_url", "cb_00", "openid_configuration", - "callback", "sb_client_credentials_section", "client_id", "redirect_uri", @@ -45,13 +43,6 @@ "fieldtype": "Data", "label": "OpenID Configuration" }, - { - "fieldname": "callback", - "fieldtype": "Data", - "label": "Callback", - "read_only": 1, - "unique": 1 - }, { "collapsible": 1, "fieldname": "sb_client_credentials_section", @@ -137,7 +128,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 19:16:31.039086", + "modified": "2020-09-27 19:29:17.835067", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 293e8d478f..b734af2014 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -20,9 +20,6 @@ class ConnectedApp(Document): in a Token Cache. """ - def autoname(self): - self.callback = frappe.scrub(self.provider_name) - def validate(self): try: base_url = frappe.request.host_url @@ -30,7 +27,7 @@ class ConnectedApp(Document): # for tests base_url = frappe.get_site_config().host_name or 'http://localhost:8000' - callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.callback + callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) def get_oauth2_session(self): From 0f40d254e2a27028e361424d15f8aca4ec8df422 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:43:05 +0200 Subject: [PATCH 063/284] test: remove unused imports --- .../doctype/connected_app/test_connected_app.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 488d537bba..adb6a372f8 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -5,12 +5,10 @@ from __future__ import unicode_literals import unittest import requests +from urllib.parse import urljoin + import frappe -from requests.auth import HTTPBasicAuth -from urllib.parse import urljoin, urlparse, parse_qs -from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -from .connected_app import callback test_dependencies = ['OAuth Client', 'User', 'Connected App'] From 89c056998e6a19a104a4a518c47d20cbfeb4e1eb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:49:22 +0200 Subject: [PATCH 064/284] fix(Token Cache): use get_password --- frappe/integrations/doctype/token_cache/token_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 8a10ddbd04..cba8fad017 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -78,8 +78,8 @@ class TokenCache(Document): def get_json(self): return { - 'access_token': self.access_token, - 'refresh_token': self.refresh_token, + 'access_token': self.get_password('access_token'), + 'refresh_token': self.get_password('refresh_token'), 'expires_in': self.get_expires_in(), 'token_type': self.token_type } From 72cd67b9bd4a9a156b0f9adb126bf1bbbfb30615 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:55:40 +0200 Subject: [PATCH 065/284] fix: remove TokenCache.refresh_token() Will be handled by requests_oauthlib via ConnectedApp.get_oauth2_session(user). --- .../doctype/connected_app/connected_app.py | 14 +++++++-- .../doctype/token_cache/token_cache.py | 31 ------------------- 2 files changed, 12 insertions(+), 33 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index b734af2014..1cbca201a1 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -30,9 +30,19 @@ class ConnectedApp(Document): callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) - def get_oauth2_session(self): + def get_oauth2_session(self, user=None): + token = None + token_updater = None + if user: + token_cache = self.get_user_token(user) + token = token_cache.get_json() + token_updater = token_cache.update_data + return OAuth2Session( - self.client_id, + client_id=self.client_id, + token=token, + token_updater=token_updater, + auto_refresh_url=self.token_uri, redirect_uri=self.redirect_uri, scope=self.get_scopes() ) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index cba8fad017..2d6239921d 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -18,37 +18,6 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError - def check_validity(self): - if(self.get('__islocal') or (not self.access_token)): - raise frappe.exceptions.DoesNotExistError - - if not self.is_expired(): - return self - - return self.refresh_token() - - def refresh_token(self): - app = frappe.get_doc("Connected App", self.connected_app) - oauth = app.get_oauth2_session() - new_token = oauth.refresh_token( - app.token_uri, - client_secret=app.get_password('client_secret'), - token=self.get_json() - ) - - if new_token.get('access_token') and app.revocation_uri: - # Revoke old token - requests.post( - app.revocation_uri, - data=urlencode({'token': new_token.get('access_token')}), - headers={ - 'Authorization': 'Bearer ' + new_token.get('access_token'), - 'Content-Type': 'application/x-www-form-urlencoded' - } - ) - - return self.update_data(new_token) - def update_data(self, data): self.access_token = data.get('access_token') self.refresh_token = data.get('refresh_token') From 0d1a76ac4d75b95acf123666a048b012ebb2b46a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 20:03:35 +0200 Subject: [PATCH 066/284] test: authenticated session, backend flow --- .../doctype/connected_app/test_connected_app.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index adb6a372f8..246afb878d 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -60,6 +60,15 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = frappe.get_doc('Token Cache', self.app.name + '-' + self.user_name) + token_cache = self.app.get_stored_user_token(self.user_name) + token = token_cache.get_password('access_token') + self.assertNotEqual(token, None) + + oauth2_session = self.app.get_oauth2_session(self.user_name) + resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) + self.assertEqual(resp.json().get('message'), self.user_name) + + def test_backend_application_flow(self): + token_cache = self.app.initiate_backend_application_flow() token = token_cache.get_password('access_token') self.assertNotEqual(token, None) From d1341bc1100b2b95f2cc60396744bf3dea4f1e45 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Sep 2020 20:05:13 +0200 Subject: [PATCH 067/284] fix: remove base_url use case not supported by requests, so not needed anymore --- .../integrations/doctype/connected_app/connected_app.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 0a07c927d4..9e0f361ec5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -8,7 +8,6 @@ "engine": "InnoDB", "field_order": [ "provider_name", - "base_url", "cb_00", "openid_configuration", "sb_client_credentials_section", @@ -91,11 +90,6 @@ "label": "Scopes", "options": "OAuth Scope" }, - { - "fieldname": "base_url", - "fieldtype": "Data", - "label": "Base URL" - }, { "fieldname": "authorization_uri", "fieldtype": "Data", @@ -128,7 +122,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 19:29:17.835067", + "modified": "2020-09-27 20:04:02.303982", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", From 60cb523439067e84cccb2118ac73ea26815f4c7a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 10:59:49 +0200 Subject: [PATCH 068/284] fix: get_auth_header --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2d6239921d..12f473addc 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -13,7 +13,7 @@ class TokenCache(Document): def get_auth_header(self): if self.access_token: - headers = {'Authorization': 'Bearer ' + self.access_token} + headers = {'Authorization': 'Bearer ' + self.get_password('access_token')} return headers raise frappe.exceptions.DoesNotExistError From ea0ceae42cf19e47e31d12d6fec5a609a6f89835 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:01:16 +0200 Subject: [PATCH 069/284] fix: validate token_type, use cstr --- .../doctype/token_cache/token_cache.py | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 12f473addc..2d3b3f9b4d 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -3,10 +3,11 @@ # For license information, please see license.txt from __future__ import unicode_literals -import frappe -import requests -from urllib.parse import urlencode from datetime import datetime, timedelta + +import frappe +from frappe import _ +from frappe.utils import cstr from frappe.model.document import Document class TokenCache(Document): @@ -19,10 +20,16 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError def update_data(self, data): - self.access_token = data.get('access_token') - self.refresh_token = data.get('refresh_token') - self.expires_in = data.get('expires_in') - self.token_type = data.get('token_type') + token_type = cstr(data.get('token_type', '')).lower() + if token_type not in ['bearer', 'mac']: + frappe.throw(_('Received an invalid token type.')) + # 'Bearer' or 'MAC' + token_type = token_type.title() if token_type == 'bearer' else token_type.upper() + + self.token_type = token_type + self.access_token = cstr(data.get('access_token', '')) + self.refresh_token = cstr(data.get('refresh_token', '')) + self.expires_in = cstr(data.get('expires_in', '')) new_scopes = data.get('scope') if new_scopes: From 20bc3f3a95c92f0b1464771ad433506800c53a8e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Sep 2020 11:01:40 +0200 Subject: [PATCH 070/284] test: Token Cache --- .../doctype/token_cache/test_records.json | 18 +++++++++++ .../doctype/token_cache/test_token_cache.py | 32 +++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 frappe/integrations/doctype/token_cache/test_records.json diff --git a/frappe/integrations/doctype/token_cache/test_records.json b/frappe/integrations/doctype/token_cache/test_records.json new file mode 100644 index 0000000000..05840221a6 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_records.json @@ -0,0 +1,18 @@ +[ + { + "doctype": "Token Cache", + "user": "test@example.com", + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "Bearer", + "expires_in": 1000, + "scopes": [ + { + "scope": "all" + }, + { + "scope": "openid" + } + ] + } +] \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index aebac0b52f..be393b0b60 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -3,8 +3,36 @@ # See license.txt from __future__ import unicode_literals -# import frappe import unittest +import frappe + +test_dependencies = ['User', 'Connected App', 'Token Cache'] class TestTokenCache(unittest.TestCase): - pass + + def setup(self): + token_cache_list = frappe.get_list('Token Cache') + connected_app_list = frappe.get_list('Connected App') + self.token_cache = frappe.get_doc('Token Cache', token_cache_list[0].name) + self.token_cache.update({'connected_app': connected_app_list[0].name}) + + def test_get_auth_header(self): + self.token_cache.get_auth_header() + + def test_update_data(self): + self.token_cache.update_data({ + 'access_token': 'new-access-token', + 'refresh_token': 'new-refresh-token', + 'token_type': 'bearer', + 'expires_in': 2000, + 'scope': 'new scope' + }) + + def test_get_expires_in(self): + self.token_cache.get_expires_in() + + def test_is_expired(self): + self.token_cache.is_expired() + + def get_json(self): + self.token_cache.get_json() From a22347d806f98da3c54b2cab8a4c9f2d59a26022 Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Wed, 7 Oct 2020 18:10:18 +0200 Subject: [PATCH 071/284] chore: Apply suggestions from code review Co-authored-by: Prssanna Desai --- frappe/geo/utils.py | 4 ++-- frappe/public/js/frappe/views/map/map_view.js | 13 ++++--------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index d7011a7eb0..f1102f2289 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -67,7 +67,7 @@ def return_location(doctype, filters_sql): try: coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains location fields'), raise_exception=True) + frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True) return else: coords = frappe.get_all(doctype, fields=['name', 'location']) @@ -80,7 +80,7 @@ def return_coordinates(doctype, filters_sql): try: coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) except InternalError: - frappe.msgprint(frappe._('This Doctype did not contains latitude and longitude fields'), raise_exception=True) + frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True) return else: coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 8b46ef1a95..c70199f041 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -61,17 +61,12 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { } get_coords() { - let get_coords_method; - if (JSON.stringify(frappe.listview_settings) === '{}') { - get_coords_method = 'frappe.geo.utils.get_coords'; - } else { - get_coords_method = frappe.listview_settings[this.doctype].get_coords_method; - } - if (cur_list.meta.fields.find(i => i.fieldname === 'location') && - cur_list.meta.fields.find(i => i.fieldtype === 'Geolocation')) { + let get_coords_method = this.settings && this.settings.get_coords_method || 'frappe.geo.utils.get_coords'; + + if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) { this.type = 'location_field'; } - if (cur_list.meta.fields.find(i => i.fieldname === "latitude") && + else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") && cur_list.meta.fields.find(i => i.fieldname === "longitude")) { this.type = 'coordinates'; } From ad313cf549e3be84fc9c3deaa42b8a59f7fbc61d Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Wed, 7 Oct 2020 18:12:03 +0200 Subject: [PATCH 072/284] chore: Apply suggestions from code review Co-authored-by: Prssanna Desai --- frappe/geo/utils.py | 2 +- frappe/public/js/frappe/list/list_sidebar.js | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index f1102f2289..f4b0284226 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -41,7 +41,7 @@ def merge_location_features_in_one(coords): '''Merging all features from location field.''' geojson_dict = [] for element in coords: - geojson_loc = json.loads(element['location']) + geojson_loc = frappe.parse_json(element['location']) for coord in geojson_loc['features']: coord['properties']['name'] = element['name'] geojson_dict.append(coord.copy()) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index b3f65253c7..4d637602a3 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -91,11 +91,10 @@ frappe.views.ListSidebar = class ListSidebar { show_list_link = true; } - if ((JSON.stringify(frappe.listview_settings) !== '{}' && - frappe.listview_settings[this.list_view.doctype].get_coords_method) || - (this.list_view.meta.fields.find(i => i.fieldname === "latitude") && - this.list_view.meta.fields.find(i => i.fieldname === "longitude")) || - (this.list_view.meta.fields.find(i => i.fieldname === 'location') && this.list_view.meta.fields.find(i => i.fieldtype === 'Geolocation'))) { + if (this.list_view.settings.get_coords_method || + (this.list_view.meta.fields.find(i => i.fieldname === "latitude") && + this.list_view.meta.fields.find(i => i.fieldname === "longitude")) || + (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide'); show_list_link = true; } From 3c181bf2a203a8c82060e70b0e502e90090fca7e Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 7 Oct 2020 18:57:01 +0200 Subject: [PATCH 073/284] Replace spaces by tabs Signed-off-by: mathieu.brunot --- frappe/geo/utils.py | 114 ++++++++++++++++---------------- frappe/tests/tests_geo_utils.py | 44 ++++++------ 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index f4b0284226..ffb27e62dc 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -13,84 +13,84 @@ from pymysql import InternalError @frappe.whitelist() def get_coords(doctype, filters, type): - '''Get a geojson dict representing a doctype.''' - filters_sql = get_coords_conditions(doctype, filters)[4:] + '''Get a geojson dict representing a doctype.''' + filters_sql = get_coords_conditions(doctype, filters)[4:] - coords = None - if type == 'location_field': - coords = return_location(doctype, filters_sql) - elif type == 'coordinates': - coords = return_coordinates(doctype, filters_sql) + coords = None + if type == 'location_field': + coords = return_location(doctype, filters_sql) + elif type == 'coordinates': + coords = return_coordinates(doctype, filters_sql) - out = convert_to_geojson(type, coords) - return out + out = convert_to_geojson(type, coords) + return out def convert_to_geojson(type, coords): - '''Converts GPS coordinates to geoJSON string.''' - geojson = {"type": "FeatureCollection", "features": None} + '''Converts GPS coordinates to geoJSON string.''' + geojson = {"type": "FeatureCollection", "features": None} - if type == 'location_field': - geojson['features'] = merge_location_features_in_one(coords) - elif type == 'coordinates': - geojson['features'] = create_gps_markers(coords) + if type == 'location_field': + geojson['features'] = merge_location_features_in_one(coords) + elif type == 'coordinates': + geojson['features'] = create_gps_markers(coords) - return geojson + return geojson def merge_location_features_in_one(coords): - '''Merging all features from location field.''' - geojson_dict = [] - for element in coords: - geojson_loc = frappe.parse_json(element['location']) - for coord in geojson_loc['features']: - coord['properties']['name'] = element['name'] - geojson_dict.append(coord.copy()) + '''Merging all features from location field.''' + geojson_dict = [] + for element in coords: + geojson_loc = frappe.parse_json(element['location']) + for coord in geojson_loc['features']: + coord['properties']['name'] = element['name'] + geojson_dict.append(coord.copy()) - return geojson_dict + return geojson_dict def create_gps_markers(coords): - '''Build Marker based on latitude and longitude.''' - geojson_dict = [] - for i in coords: - node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} - node['properties']['name'] = i.name - node['geometry']['coordinates'] = [i.latitude, i.longitude] - geojson_dict.append(node.copy()) + '''Build Marker based on latitude and longitude.''' + geojson_dict = [] + for i in coords: + node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} + node['properties']['name'] = i.name + node['geometry']['coordinates'] = [i.latitude, i.longitude] + geojson_dict.append(node.copy()) - return geojson_dict + return geojson_dict def return_location(doctype, filters_sql): - '''Get name and location fields for Doctype.''' - if filters_sql: - try: - coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) - except InternalError: - frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True) - return - else: - coords = frappe.get_all(doctype, fields=['name', 'location']) - return coords + '''Get name and location fields for Doctype.''' + if filters_sql: + try: + coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) + except InternalError: + frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True) + return + else: + coords = frappe.get_all(doctype, fields=['name', 'location']) + return coords def return_coordinates(doctype, filters_sql): - '''Get name, latitude and longitude fields for Doctype.''' - if filters_sql: - try: - coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) - except InternalError: - frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True) - return - else: - coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) - return coords + '''Get name, latitude and longitude fields for Doctype.''' + if filters_sql: + try: + coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) + except InternalError: + frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True) + return + else: + coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) + return coords def get_coords_conditions(doctype, filters=None): - '''Returns SQL conditions with user permissions and filters for event queries.''' - from frappe.desk.reportview import get_filters_cond - if not frappe.has_permission(doctype): - frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) + '''Returns SQL conditions with user permissions and filters for event queries.''' + from frappe.desk.reportview import get_filters_cond + if not frappe.has_permission(doctype): + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) - return get_filters_cond(doctype, filters, [], with_match_conditions=True) + return get_filters_cond(doctype, filters, [], with_match_conditions=True) diff --git a/frappe/tests/tests_geo_utils.py b/frappe/tests/tests_geo_utils.py index 3c5757423e..2067a6aa97 100644 --- a/frappe/tests/tests_geo_utils.py +++ b/frappe/tests/tests_geo_utils.py @@ -11,32 +11,32 @@ from frappe.geo.utils import get_coords class TestGeoUtils(unittest.TestCase): - def setUp(self): - self.todo = frappe.get_doc( - dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert() + def setUp(self): + self.todo = frappe.get_doc( + dict(doctype='ToDo', description='Test description', assigned_by='Administrator')).insert() - self.test_location_dict = {'type': 'FeatureCollection', 'features': [ - {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]} - self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location', - 'location': str(self.test_location_dict)}) + self.test_location_dict = {'type': 'FeatureCollection', 'features': [ + {'type': 'Feature', 'properties': {}, "geometry": {'type': 'Point', 'coordinates': [49.20433, 55.753395]}}]} + self.test_location = frappe.get_doc({'name': 'Test Location', 'doctype': 'Location', + 'location': str(self.test_location_dict)}) - self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']] - self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']] - self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']] + self.test_filter_exists = [['Location', 'name', 'like', '%Test Location%']] + self.test_filter_not_exists = [['Location', 'name', 'like', '%Test Location Not exists%']] + self.test_filter_todo = [['ToDo', 'description', 'like', '%Test description%']] - def test_get_coords_location_with_filter_exists(self): - coords = get_coords('Location', self.test_filter_exists, 'location_field') - self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry']) + def test_get_coords_location_with_filter_exists(self): + coords = get_coords('Location', self.test_filter_exists, 'location_field') + self.assertEqual(self.test_location_dict['features'][0]['geometry'], coords['features'][0]['geometry']) - def test_get_coords_location_with_filter_not_exists(self): - coords = get_coords('Location', self.test_filter_not_exists, 'location_field') - self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []}) + def test_get_coords_location_with_filter_not_exists(self): + coords = get_coords('Location', self.test_filter_not_exists, 'location_field') + self.assertEqual(coords, {'type': 'FeatureCollection', 'features': []}) - def test_get_coords_from_not_existable_location(self): - self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field') + def test_get_coords_from_not_existable_location(self): + self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'location_field') - def test_get_coords_from_not_existable_coords(self): - self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates') + def test_get_coords_from_not_existable_coords(self): + self.assertRaises(frappe.ValidationError, get_coords, 'ToDo', self.test_filter_todo, 'coordinates') - def tearDown(self): - self.todo.delete() + def tearDown(self): + self.todo.delete() From 86280638dd8e551896b10faaea79088a189e8088 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:18:19 +0200 Subject: [PATCH 074/284] fix: remove backend application flow --- .../doctype/connected_app/connected_app.py | 29 ------------------- .../connected_app/test_connected_app.py | 4 --- 2 files changed, 33 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 1cbca201a1..5e6e659e3d 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -68,23 +68,6 @@ class ConnectedApp(Document): return authorization_url - def initiate_backend_application_flow(self): - """Retrieve token without user interaction. Token is not user specific.""" - client = BackendApplicationClient(client_id=self.client_id, scope=self.get_scopes()) - oauth = OAuth2Session(client=client) - token = oauth.fetch_token( - token_url=self.token_uri, - client_secret=self.get_password('client_secret'), - include_client_id=True - ) - - try: - stored_token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - stored_token = frappe.new_doc('Token Cache') - - return stored_token.update_data(token) - def get_user_token(self, user=None, success_uri=None): """Return an existing user token or initiate a Web Application Flow.""" user = user or frappe.session.user @@ -100,18 +83,6 @@ class ConnectedApp(Document): return token - def get_client_token(self): - """Return an existing client token or initiate a Backend Application Flow.""" - try: - token = self.get_stored_client_token() - except frappe.exceptions.DoesNotExistError: - token = self.initiate_backend_application_flow() - - return token.check_validity() - - def get_stored_client_token(self): - return frappe.get_doc('Token Cache', self.name + '-user') - def get_stored_user_token(self, user): return frappe.get_doc('Token Cache', self.name + '-' + user) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 246afb878d..26e66a374c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -68,7 +68,3 @@ class TestConnectedApp(unittest.TestCase): resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) - def test_backend_application_flow(self): - token_cache = self.app.initiate_backend_application_flow() - token = token_cache.get_password('access_token') - self.assertNotEqual(token, None) From df9dfd5a53eb1228937d280c330b6ac292ef5ccb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:43:59 +0200 Subject: [PATCH 075/284] fix: Token Cache permissions --- .../doctype/token_cache/token_cache.json | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 91758a7332..95f19daa08 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -78,23 +78,22 @@ } ], "links": [], - "modified": "2020-07-21 00:32:56.225300", + "modified": "2020-10-18 15:22:48.991735", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", "owner": "Administrator", "permissions": [ { - "create": 1, "delete": 1, - "email": 1, - "export": 1, - "print": 1, "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 + "role": "System Manager" + }, + { + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All" } ], "sort_field": "modified", From 3e75224bd45b64ba6a7c9be714d6fb82a1bea513 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:44:33 +0200 Subject: [PATCH 076/284] feat: add Token Cache to User dashboard --- frappe/core/doctype/user/user.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 2073f41fdd..7d91e8cfe0 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -642,10 +642,15 @@ "group": "Activity", "link_doctype": "ToDo", "link_fieldname": "owner" + }, + { + "group": "Integrations", + "link_doctype": "Token Cache", + "link_fieldname": "user" } ], "max_attachments": 5, - "modified": "2020-08-26 19:48:49.677800", + "modified": "2020-10-18 15:18:53.126800", "modified_by": "Administrator", "module": "Core", "name": "User", From 284ee3c4c3e0ce74b7efe2ead93da7d55f1fd2d0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:46:38 +0200 Subject: [PATCH 077/284] feat: allow role "All" to view Connected Apps --- .../integrations/doctype/connected_app/connected_app.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 9e0f361ec5..014b3b11f5 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -51,6 +51,7 @@ { "fieldname": "client_id", "fieldtype": "Data", + "in_list_view": 1, "label": "Client Id" }, { @@ -122,7 +123,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-09-27 20:04:02.303982", + "modified": "2020-10-18 16:10:13.051678", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", @@ -139,6 +140,10 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "All" } ], "sort_field": "modified", From b79f24aac82506c5a94c3c8402935ec79b95a1d0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:47:29 +0200 Subject: [PATCH 078/284] fix: connected app fix errors from previous refactor --- frappe/integrations/doctype/connected_app/connected_app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 5e6e659e3d..f9764ba652 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -74,7 +74,6 @@ class ConnectedApp(Document): try: token = self.get_stored_user_token(user) - token = token.check_validity() except frappe.exceptions.DoesNotExistError: redirect = self.initiate_web_application_flow(user, success_uri) frappe.local.response['type'] = 'redirect' @@ -122,7 +121,7 @@ def callback(code=None, state=None): frappe.throw(_('Invalid App')) oauth = app.get_oauth2_session() - token = oauth.fetch_token(app.token_endpoint, + token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), include_client_id=True From 03c1d8dc772cdd21ab4bc7d1e7f686670c7dfb62 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:48:07 +0200 Subject: [PATCH 079/284] fix: token cache has to ignore permissions --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 2d3b3f9b4d..f713a9e49c 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -41,7 +41,7 @@ class TokenCache(Document): self.append('scopes', {'scope': scope}) self.state = None - self.save() + self.save(ignore_permissions=True) frappe.db.commit() return self From 83b85cbdbd696f5ce2ba9123a63f720cd44a54cd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:48:31 +0200 Subject: [PATCH 080/284] test: fix connected app --- .../connected_app/test_connected_app.py | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 26e66a374c..f5bc463766 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -10,7 +10,7 @@ from urllib.parse import urljoin import frappe from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -test_dependencies = ['OAuth Client', 'User', 'Connected App'] +test_dependencies = ['Connected App', 'OAuth Client', 'User'] class TestConnectedApp(unittest.TestCase): @@ -26,24 +26,29 @@ class TestConnectedApp(unittest.TestCase): just endpoints) are stored in "Social Login Key" so we get them from there. """ - connected_app = frappe.get_doc('Connected App', 'frappe') + self.user_name = 'test@example.com' + self.user_password = 'Eastern_43A1W' + + connected_app = frappe.get_last_doc('Connected App') + redirect_uri = connected_app.get('redirect_uri') + + web_application_client = frappe.get_last_doc('OAuth Client') + web_application_client.update({ + 'redirect_uris': redirect_uri, + 'default_redirect_uri': redirect_uri + }) + web_application_client.save() + social_login_key = create_or_update_social_login_key() self.base_url = social_login_key.get('base_url') - oauth_client_name = frappe.get_all('OAuth Client', fields=['name'])[0] - oauth_client = frappe.get_doc('OAuth Client', oauth_client_name['name']) - oauth_client.redirect_uris = connected_app.get('redirect_uri') - oauth_client.default_redirect_uri = connected_app.get('redirect_uri') - oauth_client.save() - frappe.db.commit() - - connected_app.client_id = oauth_client.get('client_id') - connected_app.client_secret = oauth_client.get('client_secret') connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) - self.app = connected_app.save() - self.user_name = 'test@example.com' - self.user_password = 'Eastern_43A1W' + connected_app.client_id = web_application_client.get('client_id') + connected_app.client_secret = web_application_client.get('client_secret') + self.connected_app = connected_app.save() + + frappe.db.commit() def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" @@ -52,7 +57,7 @@ class TestConnectedApp(unittest.TestCase): 'usr': self.user_name, 'pwd': self.user_password }) - authorization_url = self.app.initiate_web_application_flow(user=self.user_name) + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) auth_response = session.get(authorization_url) self.assertEqual(auth_response.status_code, 200) @@ -60,11 +65,10 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.app.get_stored_user_token(self.user_name) + token_cache = self.connected_app.get_stored_user_token(self.user_name) token = token_cache.get_password('access_token') self.assertNotEqual(token, None) - oauth2_session = self.app.get_oauth2_session(self.user_name) + oauth2_session = self.connected_app.get_oauth2_session(self.user_name) resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) - From b6674eccffa63bb1b3ad00ff5973b22af738a399 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 18:53:02 +0200 Subject: [PATCH 081/284] fix: remove unused import of BackendApplicationClient --- frappe/integrations/doctype/connected_app/connected_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index f9764ba652..85b1e06169 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -7,7 +7,6 @@ import frappe from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -from oauthlib.oauth2 import BackendApplicationClient from urllib.parse import urljoin if frappe.conf.developer_mode: From 10423b2fc2f2c12037175c3a841635683808adfa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:19:56 +0200 Subject: [PATCH 082/284] fix: TokenCache.get_expires_in() --- frappe/integrations/doctype/token_cache/token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index f713a9e49c..6c513c3734 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -46,7 +46,7 @@ class TokenCache(Document): return self def get_expires_in(self): - expiry_time = self.modified + timedelta(self.expires_in) + expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in) return (datetime.now() - expiry_time).total_seconds() def is_expired(self): From 3297356e175e5d5719d3917300fbcde8bd7dea60 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:20:18 +0200 Subject: [PATCH 083/284] test: fix TestTokenCache.setUp() --- .../integrations/doctype/token_cache/test_token_cache.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index be393b0b60..73c9f38fce 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -10,11 +10,10 @@ test_dependencies = ['User', 'Connected App', 'Token Cache'] class TestTokenCache(unittest.TestCase): - def setup(self): - token_cache_list = frappe.get_list('Token Cache') - connected_app_list = frappe.get_list('Connected App') - self.token_cache = frappe.get_doc('Token Cache', token_cache_list[0].name) - self.token_cache.update({'connected_app': connected_app_list[0].name}) + def setUp(self): + self.token_cache = frappe.get_last_doc('Token Cache') + self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name}) + self.token_cache.save() def test_get_auth_header(self): self.token_cache.get_auth_header() From ab22f75b48f4dc074567f3ccb9f23377d677cfc4 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:31:14 +0200 Subject: [PATCH 084/284] fix: cint for expires_in, docstring --- frappe/integrations/doctype/token_cache/token_cache.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 6c513c3734..fba73f0b0f 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -7,7 +7,7 @@ from datetime import datetime, timedelta import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, cint from frappe.model.document import Document class TokenCache(Document): @@ -20,6 +20,12 @@ class TokenCache(Document): raise frappe.exceptions.DoesNotExistError def update_data(self, data): + """ + Store data returned by authorization flow. + + Params: + data - Dict with access_token, refresh_token, expires_in and scope. + """ token_type = cstr(data.get('token_type', '')).lower() if token_type not in ['bearer', 'mac']: frappe.throw(_('Received an invalid token type.')) @@ -29,7 +35,7 @@ class TokenCache(Document): self.token_type = token_type self.access_token = cstr(data.get('access_token', '')) self.refresh_token = cstr(data.get('refresh_token', '')) - self.expires_in = cstr(data.get('expires_in', '')) + self.expires_in = cint(data.get('expires_in', 0)) new_scopes = data.get('scope') if new_scopes: From bf4fd67dbf434920bc87af3d4dfdad167284d378 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 18 Oct 2020 19:34:43 +0200 Subject: [PATCH 085/284] fix: allow insecure transport in tests --- frappe/integrations/doctype/connected_app/connected_app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 85b1e06169..5da1564692 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -9,8 +9,8 @@ from frappe.model.document import Document from requests_oauthlib import OAuth2Session from urllib.parse import urljoin -if frappe.conf.developer_mode: - # Disable mandatory TLS in developer mode +if frappe.conf.developer_mode or frappe.flags.in_test: + # Disable mandatory TLS in developer mode and tests import os os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' From 007e59184da031beeee99f6f5333dda6676bde1d Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Sat, 24 Oct 2020 03:03:16 +0200 Subject: [PATCH 086/284] chore: Fix sider issues Signed-off-by: mathieu.brunot --- frappe/geo/utils.py | 2 -- frappe/public/js/frappe/views/map/map_view.js | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index ffb27e62dc..77e48acb76 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -4,8 +4,6 @@ from __future__ import unicode_literals -import json - import frappe from pymysql import InternalError diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index c70199f041..48e4ac8b3e 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -65,8 +65,7 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { if (cur_list.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype === 'Geolocation')) { this.type = 'location_field'; - } - else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") && + } else if (cur_list.meta.fields.find(i => i.fieldname === "latitude") && cur_list.meta.fields.find(i => i.fieldname === "longitude")) { this.type = 'coordinates'; } From 6334f2b6d1a4fa2427aced54e6a683c5405a38a7 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 27 Oct 2020 21:23:26 +0530 Subject: [PATCH 087/284] fix: Commit after rename tables to avoid floating tables --- frappe/core/doctype/doctype/doctype.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 8a9c130fbe..d45542a9d0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -387,12 +387,14 @@ class DocType(Document): def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" + if self.issingle: frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) frappe.db.sql("""update tabSingles set value=%s where doctype=%s and field='name' and value = %s""", (new, new, old)) else: frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) + frappe.db.commit() def rename_files_and_folders(self, old, new): # move files From 650ff243b85d7b6751e4c950e50cbefd1cbc5f9e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 27 Oct 2020 21:24:16 +0530 Subject: [PATCH 088/284] fix: Rename files after successful database update --- frappe/core/doctype/doctype/doctype.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index d45542a9d0..d7cc9ba919 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -380,10 +380,6 @@ class DocType(Document): if merge: frappe.throw(_("DocType can not be merged")) - # Do not rename and move files and folders for custom doctype - if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: - self.rename_files_and_folders(old, new) - def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" @@ -396,6 +392,10 @@ class DocType(Document): frappe.db.sql("rename table `tab%s` to `tab%s`" % (old, new)) frappe.db.commit() + # Do not rename and move files and folders for custom doctype + if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: + self.rename_files_and_folders(old, new) + def rename_files_and_folders(self, old, new): # move files new_path = get_doc_path(self.module, 'doctype', new) From c73e779373a5d7a0954df9045e9663cbb6bc232d Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 27 Oct 2020 21:26:47 +0530 Subject: [PATCH 089/284] fix: Validate existing doctype names too --- frappe/model/rename_doc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 7a2129e76e..291a709119 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -49,9 +49,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F old_doc = frappe.get_doc(doctype, old) out = old_doc.run_method("before_rename", old, new, merge) or {} new = (out.get("new") or new) if isinstance(out, dict) else (out or new) - - if doctype != "DocType": - new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) + new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) if not merge: rename_parent_and_child(doctype, old, new, meta) From 33813fda881639e6fcaa05de48d08e777fde5cff Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 28 Oct 2020 14:11:50 +0530 Subject: [PATCH 090/284] fix: Connected App fix Get OpenID Configuration button fix permission error on token save --- .../doctype/connected_app/connected_app.js | 10 +++++----- .../doctype/connected_app/connected_app.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 71ff23fcc3..c78abe28be 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -10,11 +10,11 @@ frappe.ui.form.on('Connected App', { try { const response = await fetch(frm.doc.openid_configuration); const oidc = await response.json(); - frm.set_value('authorization_endpoint', oidc.authorization_endpoint); - frm.set_value('token_endpoint', oidc.token_endpoint); - frm.set_value('userinfo_endpoint', oidc.userinfo_endpoint); - frm.set_value('introspection_endpoint', oidc.introspection_endpoint); - frm.set_value('revocation_endpoint', oidc.revocation_endpoint); + frm.set_value('authorization_uri', oidc.authorization_endpoint); + frm.set_value('token_uri', oidc.token_endpoint); + frm.set_value('userinfo_uri', oidc.userinfo_endpoint); + frm.set_value('introspection_uri', oidc.introspection_endpoint); + frm.set_value('revocation_uri', oidc.revocation_endpoint); } catch (error) { frappe.msgprint(__('Please check OpenID Configuration URL')); } diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 5da1564692..737292b4f8 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -62,7 +62,7 @@ class ConnectedApp(Document): token.success_uri = success_uri token.state = state - token.save() + token.save(ignore_permissions=True) frappe.db.commit() return authorization_url From 50f6f83912e08bf1ea7331bc8f23f81509235dc2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 3 Nov 2020 15:11:10 +0530 Subject: [PATCH 091/284] test: Added tests for dt controller + db sync --- frappe/tests/test_document.py | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index c96076cfba..38f081343a 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -251,6 +251,7 @@ class TestDocument(unittest.TestCase): def test_rename_doc(self): from random import choice, sample + from frappe.model.base_document import get_controller available_documents = [] doctype = "ToDo" @@ -288,4 +289,40 @@ class TestDocument(unittest.TestCase): self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) for docname in available_documents: - frappe.delete_doc(doctype, docname) \ No newline at end of file + frappe.delete_doc(doctype, docname) + + # test 3: rename doctypes with controller code + doctype = frappe._dict({ + "old": "Test Rename Document Old", + "new": "Test Rename Document New", + }) + doc = frappe.get_doc({ + "doctype": "DocType", + "module": "Custom", + "name": doctype.old, + "custom": 0, + "fields": [ + { + "label": "Some Field", + "fieldname": "some_fieldname", + "fieldtype": "Data" + } + ], + "permissions": [ + {"role": "System Manager", "read": 1} + ], + }) + doc.save() + # check if module exists exists; + # if custom, get_controller will return Document class + # if not custom, a different class will be returned + self.assertNotEqual(get_controller(doctype.old), frappe.model.document.Document) + + # rename doc via wrapper API accessible via /desk + frappe.rename_doc("DocType", doctype.old, doctype.new) + + # check if database and controllers are updated + self.assertTrue(frappe.db.exists("DocType", doctype.new)) + self.assertFalse(frappe.db.exists("DocType", doctype.old)) + with self.assertRaises(ImportError): + get_controller(doctype.old) From 5ba92a3ae18a3af2b36e87698e91a4e41ef7bc5e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 3 Nov 2020 15:42:52 +0530 Subject: [PATCH 092/284] test: Use insert instead of save --- frappe/tests/test_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 38f081343a..6409195f92 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -312,7 +312,7 @@ class TestDocument(unittest.TestCase): {"role": "System Manager", "read": 1} ], }) - doc.save() + doc.insert() # check if module exists exists; # if custom, get_controller will return Document class # if not custom, a different class will be returned From 22d3824fee3ac7e4253edc677ba4c98834e88d37 Mon Sep 17 00:00:00 2001 From: Steffen Brennscheidt Date: Thu, 5 Nov 2020 11:32:06 +0000 Subject: [PATCH 093/284] fix: No redis dependency during tests and install Adding a user during after_install hook caused error during install of an app --- frappe/core/doctype/user/user.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 2c5865fb69..0cec7a511c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -98,15 +98,16 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) + now=frappe.flags.in_test or frappe.flags.in_install self.send_password_notification(self.__new_password) frappe.enqueue( 'frappe.core.doctype.user.user.create_contact', user=self, ignore_mandatory=True, - now=frappe.flags.in_test or frappe.flags.in_install + now=now ) if self.name not in ('Administrator', 'Guest') and not self.user_image: - frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) + frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now) def has_website_permission(self, ptype, user, verbose=False): """Returns true if current user is the session user""" From a9bf8023c6862310c294cc810e3c518a49b79ba0 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sat, 7 Nov 2020 19:22:00 +0530 Subject: [PATCH 094/284] Update standard_macros.html --- frappe/templates/print_formats/standard_macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 3681a87f53..24967b9525 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -137,7 +137,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% elif df.fieldtype=="HTML" %} {{ frappe.render_template(df.options, {"doc":doc}) }} {% elif df.fieldtype=="Currency" %} - {{ doc.get_formatted(df.fieldname, doc, translated=df.translatable) }} + {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} {% else %} {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} {% endif %} From 19a14d09e0dc872594fc5ae0f0be9505ee6e76d3 Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Sun, 8 Nov 2020 17:58:10 +0000 Subject: [PATCH 095/284] fix(quick entry): make sure init_callback is always called Prior to this PR, as noted in issue #7638, it is not possible with frappe.new_doc to initialize certain fields of the new document, such as the description of a Task or the posting_date of a Journal Entry (in ERPNext). The reason this occurs is that currently the route_options which can be set in the second argument to frappe.new_doc() are only allowed to set certain field types of a document (namely, Link, Select, Data, and Dynamic Link). Although it turns out that it would not work to allow any field type to be set in the route options (in particular, attempting to allow one to set Table field types in this way is non-functional), it would be reasonable simply to try setting other fields that cannot be set in the route_options via the callback allowed as the third argument of frappe.new_doc. And indeed, this approach works for those DocTypes that have a Quick Entry Form. For those DocTypes that do not, however, the callback is never called. This PR modifies frappe.ui.form.make_quick_entry() -- which frappe.new_doc calls to do most of its work -- so that the callback is called regardless of whether the DocType has a Quick Entry Form or not. The only slight awkwardness in this is that if there is a Quick Entry, the callback is passed the dialog object of that Quick Entry, whereas if there is no Quick Entry, the callback is only passed the doc object that is about to be edited in the standard Form interface for a new document. Nevertheless, in any case, it is now possible to write a callback which will initialize any field in the new document being created. Resolves #7638. --- frappe/public/js/frappe/form/quick_entry.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 2da7b8f236..0a489e26d6 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -35,7 +35,11 @@ frappe.ui.form.QuickEntryForm = Class.extend({ if (this.is_quick_entry() || this.force) { this.render_dialog(); resolve(this); - } else { + } else { // No quick entry, use full Form + // but still give callback a shot at the doc + if (this.init_callback) { + this.init_callback(this.doc); + } frappe.quick_entry = null; frappe.set_route('Form', this.doctype, this.doc.name) .then(() => resolve(this)); From 1e6674b4726ed22c6fedf64303532189d4f4fe42 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 9 Nov 2020 15:58:21 +0530 Subject: [PATCH 096/284] fix: Allow doctype export in test mode --- frappe/core/doctype/doctype/doctype.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index d7cc9ba919..da76da8db9 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -288,9 +288,12 @@ class DocType(Document): self.update_fields_to_fetch() - from frappe import conf - allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode')) - if not self.custom and not frappe.flags.in_import and allow_doctype_export: + allow_doctype_export = ( + not self.custom + and not frappe.flags.in_import + and (frappe.flags.allow_doctype_export or frappe.conf.developer_mode) + ) + if allow_doctype_export: self.export_doc() self.make_controller_template() From d29b0504fc8106511e3b77b8ae53629fb47eb9eb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 11 Nov 2020 15:09:02 +0530 Subject: [PATCH 097/284] fix: Allow rename controllers in test via rename_doc --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index da76da8db9..7063e64352 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -396,7 +396,7 @@ class DocType(Document): frappe.db.commit() # Do not rename and move files and folders for custom doctype - if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: + if not self.custom and not frappe.flags.in_patch: self.rename_files_and_folders(old, new) def rename_files_and_folders(self, old, new): From 3581e591f3d83de5cab98042a602a37a86cee795 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 11 Nov 2020 19:40:41 +0530 Subject: [PATCH 098/284] fix: Strip html tags for frappe.msgprint output to stdout --- frappe/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index fac0927428..63ac57909b 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -326,7 +326,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal """ - from frappe.utils import encode + from frappe.utils import encode, strip_html_tags msg = safe_decode(msg) out = _dict(message=msg) @@ -353,7 +353,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, out.as_list = 1 if flags.print_messages and out.message: - print(f"Message: {repr(out.message).encode('utf-8')}") + print(f"Message: {strip_html_tags(out.message)}") if title: out.title = title From 2a615226d50b43b1dc2aa0c7acd28928da9aed36 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 11 Nov 2020 19:41:31 +0530 Subject: [PATCH 099/284] fix: Clear class_doctypes cache for doctype rename, deletes for all sites --- frappe/core/doctype/doctype/doctype.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 7063e64352..0144103f47 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -396,8 +396,17 @@ class DocType(Document): frappe.db.commit() # Do not rename and move files and folders for custom doctype - if not self.custom and not frappe.flags.in_patch: - self.rename_files_and_folders(old, new) + if not self.custom: + if not frappe.flags.in_patch: + self.rename_files_and_folders(old, new) + + for site in frappe.utils.get_sites(): + frappe.cache().delete(f"{site}:doctype_classes", old) + + def after_delete(self): + if not self.custom: + for site in frappe.utils.get_sites(): + frappe.cache().delete(f"{site}:doctype_classes", self.name) def rename_files_and_folders(self, old, new): # move files From 18c0270168e825a284388fbf073977e2f8fd032f Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 12 Nov 2020 09:11:19 +0530 Subject: [PATCH 100/284] fix: Delete controllers if delete_doc triggered via tests too --- frappe/model/delete_doc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index a38470e3f5..0599368f33 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -76,7 +76,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa delete_from_table(doctype, name, ignore_doctypes, None) - if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_uninstall or frappe.flags.in_test): + if not doc.custom and not ( + for_reload + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_uninstall + ): try: delete_controllers(name, doc.module) except (FileNotFoundError, OSError, KeyError): From 71225783e6edab50e2236fee59d30a18bc3b26e9 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 12 Nov 2020 11:11:19 +0530 Subject: [PATCH 101/284] test: Rename doc tests --- frappe/tests/test_document.py | 78 ------------------------- frappe/tests/test_rename_doc.py | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 78 deletions(-) create mode 100644 frappe/tests/test_rename_doc.py diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 3c07f3e02a..2be92be1f5 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -249,84 +249,6 @@ class TestDocument(unittest.TestCase): self.assertEqual(cint(old_current) - 1, new_current) - def test_rename_doc(self): - from random import choice, sample - from frappe.model.base_document import get_controller - - available_documents = [] - doctype = "ToDo" - - # data generation: 4 todo documents - for num in range(1, 5): - doc = frappe.get_doc({ - "doctype": doctype, - "date": add_to_date(now(), days=num), - "description": "this is todo #{}".format(num) - }).insert() - available_documents.append(doc.name) - - # test 1: document renaming - old_name = choice(available_documents) - new_name = old_name + '.new' - self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True)) - available_documents.remove(old_name) - available_documents.append(new_name) - - # test 2: merge documents - first_todo, second_todo = sample(available_documents, 2) - - second_todo_doc = frappe.get_doc(doctype, second_todo) - second_todo_doc.priority = "High" - second_todo_doc.save() - - merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True) - merged_todo_doc = frappe.get_doc(doctype, merged_todo) - available_documents.remove(first_todo) - - with self.assertRaises(DoesNotExistError): - frappe.get_doc(doctype, first_todo) - - self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) - - for docname in available_documents: - frappe.delete_doc(doctype, docname) - - # test 3: rename doctypes with controller code - doctype = frappe._dict({ - "old": "Test Rename Document Old", - "new": "Test Rename Document New", - }) - doc = frappe.get_doc({ - "doctype": "DocType", - "module": "Custom", - "name": doctype.old, - "custom": 0, - "fields": [ - { - "label": "Some Field", - "fieldname": "some_fieldname", - "fieldtype": "Data" - } - ], - "permissions": [ - {"role": "System Manager", "read": 1} - ], - }) - doc.insert() - # check if module exists exists; - # if custom, get_controller will return Document class - # if not custom, a different class will be returned - self.assertNotEqual(get_controller(doctype.old), frappe.model.document.Document) - - # rename doc via wrapper API accessible via /desk - frappe.rename_doc("DocType", doctype.old, doctype.new) - - # check if database and controllers are updated - self.assertTrue(frappe.db.exists("DocType", doctype.new)) - self.assertFalse(frappe.db.exists("DocType", doctype.old)) - with self.assertRaises(ImportError): - get_controller(doctype.old) - def test_non_negative_check(self): frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py new file mode 100644 index 0000000000..f69597b6de --- /dev/null +++ b/frappe/tests/test_rename_doc.py @@ -0,0 +1,100 @@ +import os +import unittest + +import frappe +from frappe.utils import add_to_date, now +from frappe.exceptions import DoesNotExistError + +from random import choice, sample +from frappe.model.base_document import get_controller +from frappe.modules.utils import get_doc_path + + +class TestRenameDoc(unittest.TestCase): + @classmethod + def setUpClass(self): + """Setting Up data for the tests defined under TestRenameDoc""" + # data generation: for base and merge tests + self.available_documents = [] + self.test_doctype = "ToDo" + + for num in range(1, 5): + doc = frappe.get_doc({ + "doctype": self.test_doctype, + "date": add_to_date(now(), days=num), + "description": "this is todo #{}".format(num), + }).insert() + self.available_documents.append(doc.name) + + # data generation: for controllers tests + self.doctype = frappe._dict({ + "old": "Test Rename Document Old", + "new": "Test Rename Document New", + }) + + frappe.get_doc({ + "doctype": "DocType", + "module": "Custom", + "name": self.doctype.old, + "custom": 0, + "fields": [ + {"label": "Some Field", "fieldname": "some_fieldname", "fieldtype": "Data"} + ], + "permissions": [{"role": "System Manager", "read": 1}], + }).insert() + + @classmethod + def tearDownClass(self): + """Deleting data generated for the tests defined under TestRenameDoc""" + # delete the documents created + for docname in self.available_documents: + frappe.delete_doc(self.test_doctype, docname) + + for dt in self.doctype.values(): + if frappe.db.exists("DocType", dt): + frappe.delete_doc("DocType", dt) + frappe.db.sql_ddl(f"DROP TABLE `tab{dt}`") + + def test_rename_doc(self): + """Rename an existing document via frappe.rename_doc""" + old_name = choice(self.available_documents) + new_name = old_name + ".new" + self.assertEqual(new_name, frappe.rename_doc(self.test_doctype, old_name, new_name, force=True)) + self.available_documents.remove(old_name) + self.available_documents.append(new_name) + + def test_merging_docs(self): + """Merge two documents via frappe.rename_doc""" + first_todo, second_todo = sample(self.available_documents, 2) + + second_todo_doc = frappe.get_doc(self.test_doctype, second_todo) + second_todo_doc.priority = "High" + second_todo_doc.save() + + merged_todo = frappe.rename_doc( + self.test_doctype, first_todo, second_todo, merge=True, force=True + ) + merged_todo_doc = frappe.get_doc(self.test_doctype, merged_todo) + self.available_documents.remove(first_todo) + + with self.assertRaises(DoesNotExistError): + frappe.get_doc(self.test_doctype, first_todo) + + self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) + + def test_rename_controllers(self): + """Rename doctypes with controller code paths""" + # check if module exists exists; + # if custom, get_controller will return Document class + # if not custom, a different class will be returned + self.assertNotEqual(get_controller(self.doctype.old), frappe.model.document.Document) + + old_doctype_path = get_doc_path("Custom", "DocType", self.doctype.old) + + # rename doc via wrapper API accessible via /desk + frappe.rename_doc("DocType", self.doctype.old, self.doctype.new) + + # check if database and controllers are updated + self.assertTrue(frappe.db.exists("DocType", self.doctype.new)) + self.assertFalse(frappe.db.exists("DocType", self.doctype.old)) + self.assertFalse(os.path.exists(old_doctype_path)) From f024b48f52c2ac6c8449e1ff587c1881db14d6f8 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 11 Nov 2020 20:11:30 +0530 Subject: [PATCH 102/284] fix: Remove unnecessary import Removed via 3581e591f3d83de5cab98042a602a37a86cee795 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 63ac57909b..e6eea7ed4a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -326,7 +326,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal """ - from frappe.utils import encode, strip_html_tags + from frappe.utils import strip_html_tags msg = safe_decode(msg) out = _dict(message=msg) From c5a420ffc79f0e1058ae1459523d5883d075d1d2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 12 Nov 2020 10:43:36 +0530 Subject: [PATCH 103/284] fix: Allow doctype export with in_test flag set --- frappe/core/doctype/doctype/doctype.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 0144103f47..d421317a8a 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -291,7 +291,11 @@ class DocType(Document): allow_doctype_export = ( not self.custom and not frappe.flags.in_import - and (frappe.flags.allow_doctype_export or frappe.conf.developer_mode) + and ( + frappe.conf.developer_mode + or frappe.flags.allow_doctype_export + or frappe.flags.in_test + ) ) if allow_doctype_export: self.export_doc() From 6357bb63929b9b528fe6925fa184433f2db5a940 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 12 Nov 2020 11:51:42 +0530 Subject: [PATCH 104/284] test: Drop table if exists --- frappe/tests/test_rename_doc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index f69597b6de..4db877e586 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -53,7 +53,7 @@ class TestRenameDoc(unittest.TestCase): for dt in self.doctype.values(): if frappe.db.exists("DocType", dt): frappe.delete_doc("DocType", dt) - frappe.db.sql_ddl(f"DROP TABLE `tab{dt}`") + frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`") def test_rename_doc(self): """Rename an existing document via frappe.rename_doc""" From a6c25fa9ad46884f01b8b982ceaa24ae6db0ff93 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 12 Nov 2020 16:21:51 +0530 Subject: [PATCH 105/284] ci: Run tests with set verbosity --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 63895675ea..2331217363 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,12 +31,12 @@ matrix: - name: "Python 3.7 MariaDB" python: 3.7 env: DB=mariadb TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Python 3.7 PostgreSQL" python: 3.7 env: DB=postgres TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Cypress" python: 3.7 From a582b1c36463e5cc3a8efefc329cfbbb1b3208e5 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 12 Nov 2020 19:43:21 +0100 Subject: [PATCH 106/284] fix: allow get_oauth2_session without user arg --- .../doctype/connected_app/connected_app.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 737292b4f8..14b4d4a1cf 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -29,10 +29,12 @@ class ConnectedApp(Document): callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) - def get_oauth2_session(self, user=None): + def get_oauth2_session(self, user=None, init=False): token = None token_updater = None - if user: + + if not init: + user = user or frappe.session.user token_cache = self.get_user_token(user) token = token_cache.get_json() token_updater = token_cache.update_data @@ -50,7 +52,7 @@ class ConnectedApp(Document): """Return an authorization URL for the user. Save state in Token Cache.""" success_uri = success_uri or '/desk' user = user or frappe.session.user - oauth = self.get_oauth2_session() + oauth = self.get_oauth2_session(init=True) authorization_url, state = oauth.authorization_url(self.authorization_uri) try: @@ -119,7 +121,7 @@ def callback(code=None, state=None): except frappe.exceptions.DoesNotExistError: frappe.throw(_('Invalid App')) - oauth = app.get_oauth2_session() + oauth = app.get_oauth2_session(init=True) token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), From 631f4fab7c6934467921936a172cd643ae6b53fa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:16:47 +0100 Subject: [PATCH 107/284] fix: show "Connect to {provider}" if doc is not new --- .../doctype/connected_app/connected_app.js | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index c78abe28be..700e630a6a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Connected App', { refresh: frm => { - frm.add_custom_button(__("Get OpenID Configuration"), async () => { + frm.add_custom_button(__('Get OpenID Configuration'), async () => { if (!frm.doc.openid_configuration) { frappe.msgprint(__('Please enter OpenID Configuration URL')); } else { @@ -21,14 +21,16 @@ frappe.ui.form.on('Connected App', { } }); - frm.add_custom_button(__("Init"), async () => { - frappe.call({ - method: "initiate_web_application_flow", - doc: frm.doc, - callback: function(r) { - window.open(r.message, '_blank'); - } + if (!frm.is_new()) { + frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => { + frappe.call({ + method: 'initiate_web_application_flow', + doc: frm.doc, + callback: function(r) { + window.open(r.message, '_blank'); + } + }); }); - }); + } } }); From 931a39c998c5ac00e2ce89227e194d4116e4c95c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 16:17:34 +0100 Subject: [PATCH 108/284] faet: display provider name in token cache --- .../integrations/doctype/token_cache/token_cache.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json index 95f19daa08..c016405031 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.json +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -10,6 +10,7 @@ "field_order": [ "user", "connected_app", + "provider_name", "access_token", "refresh_token", "expires_in", @@ -75,10 +76,17 @@ "fieldtype": "Data", "label": "Token Type", "read_only": 1 + }, + { + "fetch_from": "connected_app.provider_name", + "fieldname": "provider_name", + "fieldtype": "Data", + "label": "Provider Name", + "read_only": 1 } ], "links": [], - "modified": "2020-10-18 15:22:48.991735", + "modified": "2020-11-13 13:35:53.714352", "modified_by": "Administrator", "module": "Integrations", "name": "Token Cache", From 956e407c5f17248aeeba26be308645768a995832 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 18:07:46 +0100 Subject: [PATCH 109/284] feat: toggle client credential section --- frappe/integrations/doctype/connected_app/connected_app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js index 700e630a6a..4d20f65559 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.js +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -32,5 +32,7 @@ frappe.ui.form.on('Connected App', { }); }); } + + frm.toggle_display('sb_client_credentials_section', !frm.is_new()); } }); From 74a51aa1776b917f0acc2d9cf63f9f1d570d58b1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 13 Nov 2020 18:08:36 +0100 Subject: [PATCH 110/284] fix: default if password is empty --- frappe/integrations/doctype/token_cache/token_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index fba73f0b0f..7cac58fae0 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -60,8 +60,8 @@ class TokenCache(Document): def get_json(self): return { - 'access_token': self.get_password('access_token'), - 'refresh_token': self.get_password('refresh_token'), + 'access_token': self.get_password('access_token', ''), + 'refresh_token': self.get_password('refresh_token', ''), 'expires_in': self.get_expires_in(), 'token_type': self.token_type } From 63de77477779dd49a59a60ab3d2f9216365956b8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:33:29 +0100 Subject: [PATCH 111/284] feat: allow extra parameters --- .../doctype/connected_app/connected_app.json | 17 ++++++++- .../doctype/connected_app/connected_app.py | 10 ++++- .../doctype/query_parameters/__init__.py | 0 .../query_parameters/query_parameters.json | 37 +++++++++++++++++++ .../query_parameters/query_parameters.py | 10 +++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 frappe/integrations/doctype/query_parameters/__init__.py create mode 100644 frappe/integrations/doctype/query_parameters/query_parameters.json create mode 100644 frappe/integrations/doctype/query_parameters/query_parameters.py diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json index 014b3b11f5..e5dbb0472a 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.json +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -23,7 +23,9 @@ "revocation_uri", "cb_02", "userinfo_uri", - "introspection_uri" + "introspection_uri", + "section_break_18", + "query_parameters" ], "fields": [ { @@ -115,6 +117,17 @@ "fieldname": "introspection_uri", "fieldtype": "Data", "label": "Introspection URI" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Extra Parameters" + }, + { + "fieldname": "query_parameters", + "fieldtype": "Table", + "label": "Query Parameters", + "options": "Query Parameters" } ], "links": [ @@ -123,7 +136,7 @@ "link_fieldname": "connected_app" } ], - "modified": "2020-10-18 16:10:13.051678", + "modified": "2020-11-16 16:29:50.277405", "modified_by": "Administrator", "module": "Integrations", "name": "Connected App", diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 14b4d4a1cf..a281d9d850 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -53,7 +53,8 @@ class ConnectedApp(Document): success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session(init=True) - authorization_url, state = oauth.authorization_url(self.authorization_uri) + query_params = self.get_query_params() + authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) try: token = self.get_stored_user_token(user) @@ -89,6 +90,9 @@ class ConnectedApp(Document): def get_scopes(self): return [row.scope for row in self.scopes] + def get_query_params(self): + return {param.key: param.value for param in self.query_parameters} + @frappe.whitelist(allow_guest=True) def callback(code=None, state=None): @@ -122,10 +126,12 @@ def callback(code=None, state=None): frappe.throw(_('Invalid App')) oauth = app.get_oauth2_session(init=True) + query_params = app.get_query_params() token = oauth.fetch_token(app.token_uri, code=code, client_secret=app.get_password('client_secret'), - include_client_id=True + include_client_id=True, + **query_params ) token_cache.update_data(token) diff --git a/frappe/integrations/doctype/query_parameters/__init__.py b/frappe/integrations/doctype/query_parameters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.json b/frappe/integrations/doctype/query_parameters/query_parameters.json new file mode 100644 index 0000000000..de31c28df7 --- /dev/null +++ b/frappe/integrations/doctype/query_parameters/query_parameters.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-11-16 14:54:37.226914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "key", + "value" + ], + "fields": [ + { + "fieldname": "key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Key", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-16 15:18:35.887149", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Query Parameters", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.py b/frappe/integrations/doctype/query_parameters/query_parameters.py new file mode 100644 index 0000000000..bfb8eae0b6 --- /dev/null +++ b/frappe/integrations/doctype/query_parameters/query_parameters.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class QueryParameters(Document): + pass From 645cacce30418cc89cf5ae5ceada6b3b10463603 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:36:03 +0100 Subject: [PATCH 112/284] fix: redirect back to connected app by default (instead of to desk) --- frappe/integrations/doctype/connected_app/connected_app.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index a281d9d850..146caf8657 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -50,7 +50,6 @@ class ConnectedApp(Document): def initiate_web_application_flow(self, user=None, success_uri=None): """Return an authorization URL for the user. Save state in Token Cache.""" - success_uri = success_uri or '/desk' user = user or frappe.session.user oauth = self.get_oauth2_session(init=True) query_params = self.get_query_params() @@ -136,4 +135,4 @@ def callback(code=None, state=None): token_cache.update_data(token) frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = token_cache.get('success_uri') or '/desk' + frappe.local.response['location'] = token_cache.get('success_uri') or app.get_url() From da0ea7c225197b1d45c5283b63de8d223a30d700 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:37:06 +0100 Subject: [PATCH 113/284] refactor: frappe.db.exists instead of try/except --- .../doctype/connected_app/connected_app.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 146caf8657..9254aa7631 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -3,11 +3,12 @@ # For license information, please see license.txt from __future__ import unicode_literals +from urllib.parse import urljoin + import frappe from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -from urllib.parse import urljoin if frappe.conf.developer_mode or frappe.flags.in_test: # Disable mandatory TLS in developer mode and tests @@ -54,17 +55,16 @@ class ConnectedApp(Document): oauth = self.get_oauth2_session(init=True) query_params = self.get_query_params() authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) + token_cache = self.get_token_cache(user) - try: - token = self.get_stored_user_token(user) - except frappe.exceptions.DoesNotExistError: - token = frappe.new_doc('Token Cache') - token.user = user - token.connected_app = self.name - - token.success_uri = success_uri - token.state = state - token.save(ignore_permissions=True) + if not token_cache: + token_cache = frappe.new_doc('Token Cache') + token_cache.user = user + token_cache.connected_app = self.name + + token_cache.success_uri = success_uri + token_cache.state = state + token_cache.save(ignore_permissions=True) frappe.db.commit() return authorization_url @@ -72,19 +72,24 @@ class ConnectedApp(Document): def get_user_token(self, user=None, success_uri=None): """Return an existing user token or initiate a Web Application Flow.""" user = user or frappe.session.user + token_cache = self.get_token_cache(user) - try: - token = self.get_stored_user_token(user) - except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_web_application_flow(user, success_uri) - frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = redirect - return redirect + if token_cache: + return token_cache + + redirect = self.initiate_web_application_flow(user, success_uri) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect + return redirect + + def get_token_cache(self, user): + token_cache = None + token_cache_name = self.name + '-' + user - return token + if frappe.db.exists('Token Cache', token_cache_name): + token_cache = frappe.get_doc('Token Cache', token_cache_name) - def get_stored_user_token(self, user): - return frappe.get_doc('Token Cache', self.name + '-' + user) + return token_cache def get_scopes(self): return [row.scope for row in self.scopes] From e3e8e1dca222c23778c28ea6a3fc8081b7d5612d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 16 Nov 2020 16:41:16 +0100 Subject: [PATCH 114/284] test: fix method name --- frappe/integrations/doctype/connected_app/test_connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index f5bc463766..4d8acb9b59 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -65,7 +65,7 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.connected_app.get_stored_user_token(self.user_name) + token_cache = self.connected_app.get_token_cache(self.user_name) token = token_cache.get_password('access_token') self.assertNotEqual(token, None) From 4d8ef4690eb9e364e244f296f935dab45733d5f6 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 12 Nov 2020 17:42:25 +0530 Subject: [PATCH 115/284] feat: Ability to set weekdays for Auto Repeat with weekly frequency --- .../doctype/auto_repeat/auto_repeat.json | 18 ++++++- .../doctype/auto_repeat/auto_repeat.py | 54 +++++++++++++------ .../doctype/auto_repeat_day/__init__.py | 0 .../auto_repeat_day/auto_repeat_day.json | 33 ++++++++++++ .../auto_repeat_day/auto_repeat_day.py | 10 ++++ 5 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 frappe/automation/doctype/auto_repeat_day/__init__.py create mode 100644 frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json create mode 100644 frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 8ee6ca1d45..87cf423e2c 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "format:AUT-AR-{#####}", @@ -21,6 +22,8 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", + "section_break_12", + "repeat_on_days", "notification", "notify_by_email", "recipients", @@ -186,9 +189,22 @@ "fieldname": "repeat_on_last_day", "fieldtype": "Check", "label": "Repeat on Last Day of the Month" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "repeat_on_days", + "fieldtype": "Table", + "label": "Repeat on Days", + "options": "Auto Repeat Day" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_12", + "fieldtype": "Section Break" } ], - "modified": "2019-07-17 11:30:51.412317", + "links": [], + "modified": "2020-11-10 22:44:51.815740", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fcf24bf1a9..dc6ec554d5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from datetime import timedelta from frappe.desk.form import assign_to from frappe.utils.jinja import validate_template from dateutil.relativedelta import relativedelta @@ -15,6 +16,7 @@ from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} +week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} class AutoRepeat(Document): @@ -48,7 +50,7 @@ class AutoRepeat(Document): if self.disabled: self.next_schedule_date = None else: - self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date) + self.next_schedule_date = get_next_schedule_date(schedule_date=self.start_date, auto_repeat_doc=self) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -107,7 +109,7 @@ class AutoRepeat(Document): end_date = getdate(self.end_date) if not self.end_date: - next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day) + next_date = get_next_schedule_date(schedule_date=start_date, auto_repeat_doc=self) row = { "reference_document": self.reference_document, "frequency": self.frequency, @@ -117,7 +119,7 @@ class AutoRepeat(Document): if self.end_date: next_date = get_next_schedule_date( - start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) + schedule_date=start_date, auto_repeat_doc=self, for_full_schedule=True) while (getdate(next_date) < getdate(end_date)): row = { @@ -127,7 +129,7 @@ class AutoRepeat(Document): } schedule_details.append(row) next_date = get_next_schedule_date( - next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) + schedule_date=next_date, auto_repeat_doc=self, for_full_schedule=True) return schedule_details @@ -282,31 +284,34 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False): - if month_map.get(frequency): - month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1 +def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedule=False): + if month_map.get(auto_repeat_doc.frequency): + month_count = month_map.get(auto_repeat_doc.frequency) + month_diff(schedule_date, auto_repeat_doc.start_date) - 1 else: month_count = 0 day_count = 0 - if month_count and repeat_on_last_day: + if month_count and auto_repeat_doc.repeat_on_last_day: day_count = 31 - next_date = get_next_date(start_date, month_count, day_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count and repeat_on_day: day_count = repeat_on_day - next_date = get_next_date(start_date, month_count, day_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count: - next_date = get_next_date(start_date, month_count) + next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: - days = 7 if frequency == 'Weekly' else 1 + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(auto_repeat_doc) + else: + days = 1 next_date = add_days(schedule_date, days) # next schedule date should be after or on current date if not for_full_schedule: while getdate(next_date) < getdate(today()): if month_count: - month_count += month_map.get(frequency) - next_date = get_next_date(start_date, month_count, day_count) + month_count += month_map.get(auto_repeat_doc.frequency) + next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: next_date = add_days(next_date, days) @@ -318,6 +323,25 @@ def get_next_date(dt, mcount, day=None): dt += relativedelta(months=mcount, day=day) return dt + +def get_offset_for_weekly_frequency(auto_repeat_doc): + if not auto_repeat_doc.repeat_on_days: + return 7 + + repeat_on_days = [entry.day for entry in auto_repeat_doc.repeat_on_days] + current_day = getdate().weekday() + weekday = get_next_weekday(current_day, repeat_on_days) + return timedelta((7 + week_map.get(weekday) - current_day) % 7).days + + +def get_next_weekday(current_day, weekdays): + days = list(week_map.keys()) + days = days[current_day:] + days[:current_day] + for entry in days: + if entry in weekdays: + return entry + + #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' @@ -337,7 +361,7 @@ def create_repeated_entries(data): if schedule_date == current_date and not doc.disabled: doc.create_documents() - schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) + schedule_date = get_next_schedule_date(schedule_date=schedule_date, auto_repeat_doc=doc) if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) diff --git a/frappe/automation/doctype/auto_repeat_day/__init__.py b/frappe/automation/doctype/auto_repeat_day/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json new file mode 100644 index 0000000000..6f5c3060cd --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-11-10 22:30:53.690228", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day" + ], + "fields": [ + { + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-10 22:30:53.690228", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat Day", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py new file mode 100644 index 0000000000..3a7ced1370 --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class AutoRepeatDay(Document): + pass From 5e7e7cc922fe0cad987f32ede3d39ea9ef3f02c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 14:13:27 +0530 Subject: [PATCH 116/284] fix: full schedule calculation for weekdays --- .../doctype/auto_repeat/auto_repeat.js | 4 +- .../doctype/auto_repeat/auto_repeat.py | 37 +++++++++++++++---- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index a11de1d881..2b1102f681 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -86,10 +86,10 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frappe.call({ + frm.call({ method: "get_auto_repeat_schedule", doc: frm.doc - }).done((r) => { + }).then((r) => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index dc6ec554d5..d0f01f4ad5 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -14,6 +14,7 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_ from frappe.model.document import Document from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs +from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} @@ -25,6 +26,7 @@ class AutoRepeat(Document): self.validate_reference_doctype() self.validate_dates() self.validate_email_id() + self.validate_auto_repeat_days() self.set_dates() self.update_auto_repeat_id() self.unlink_if_applicable() @@ -84,6 +86,12 @@ class AutoRepeat(Document): else: frappe.throw(_("'Recipients' not specified")) + def validate_auto_repeat_days(self): + auto_repeat_days = get_auto_repeat_days(self) + if not len(set(auto_repeat_days)) == len(auto_repeat_days): + repeated_days = get_repeated(auto_repeat_days) + frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) + def update_auto_repeat_id(self): #check if document is already on auto repeat auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") @@ -301,7 +309,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(auto_repeat_doc) + days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) else: days = 1 next_date = add_days(schedule_date, days) @@ -324,24 +332,37 @@ def get_next_date(dt, mcount, day=None): return dt -def get_offset_for_weekly_frequency(auto_repeat_doc): +def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): if not auto_repeat_doc.repeat_on_days: return 7 - repeat_on_days = [entry.day for entry in auto_repeat_doc.repeat_on_days] - current_day = getdate().weekday() - weekday = get_next_weekday(current_day, repeat_on_days) - return timedelta((7 + week_map.get(weekday) - current_day) % 7).days + repeat_on_days = get_auto_repeat_days(auto_repeat_doc) + current_schedule_day = getdate(schedule_date).weekday() + + if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday) + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + else: + return 7 -def get_next_weekday(current_day, weekdays): +def get_next_weekday(current_schedule_day, weekdays): days = list(week_map.keys()) - days = days[current_day:] + days[:current_day] + if current_schedule_day > 0: + days = days[(current_schedule_day + 1):] + days[:current_schedule_day] + else: + days = days[(current_schedule_day + 1):] + for entry in days: if entry in weekdays: return entry +def get_auto_repeat_days(doc): + return [d.day for d in doc.get('repeat_on_days', [])] + + #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' From 6f6a20d6b5e880a7dde0f23976088ae8626978e9 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 14:35:30 +0530 Subject: [PATCH 117/284] fix: handle schedule for a past start date --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index d0f01f4ad5..fe2fccf99d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -311,6 +311,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul if auto_repeat_doc.frequency == "Weekly": days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) else: + # daily frequency days = 1 next_date = add_days(schedule_date, days) @@ -321,6 +322,11 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul month_count += month_map.get(auto_repeat_doc.frequency) next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(next_date, auto_repeat_doc) + else: + # daily frequency + days = 1 next_date = add_days(next_date, days) return next_date @@ -333,15 +339,20 @@ def get_next_date(dt, mcount, day=None): def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): + # if weekdays are not set, offset is 7 from current schedule date if not auto_repeat_doc.repeat_on_days: return 7 repeat_on_days = get_auto_repeat_days(auto_repeat_doc) current_schedule_day = getdate(schedule_date).weekday() + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) next_weekday_number = week_map.get(weekday) + # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days else: return 7 From 4b7120c5c4dd2d6286666d9a04f676d6c00c21da Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 15:30:07 +0530 Subject: [PATCH 118/284] test: Auto Repeat with weekly frequency --- .../doctype/auto_repeat/auto_repeat.py | 7 +-- .../doctype/auto_repeat/test_auto_repeat.py | 50 ++++++++++++++++++- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index fe2fccf99d..a41fef1f6b 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -302,8 +302,8 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul if month_count and auto_repeat_doc.repeat_on_last_day: day_count = 31 next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count and repeat_on_day: - day_count = repeat_on_day + elif month_count and auto_repeat_doc.repeat_on_day: + day_count = auto_repeat_doc.repeat_on_day next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif month_count: next_date = get_next_date(auto_repeat_doc.start_date, month_count) @@ -345,11 +345,12 @@ def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): repeat_on_days = get_auto_repeat_days(auto_repeat_doc) current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) # if repeats on more than 1 day or # start date's weekday is not in repeat days, then get next weekday # else offset is 7 - if len(repeat_on_days) > 1 or list(week_map.keys())[current_schedule_day] not in repeat_on_days: + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) next_weekday_number = week_map.get(weekday) # offset for upcoming weekday diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 60fa9cb59e..69e9d98c0a 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -10,6 +10,7 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries from frappe.utils import today, add_days, getdate, add_months +week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} def add_custom_fields(): df = dict( @@ -42,6 +43,52 @@ class TestAutoRepeat(unittest.TestCase): self.assertEqual(todo.get('description'), new_todo.get('description')) + def test_weekly_auto_repeat(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() + + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + new_todo = frappe.db.get_value('ToDo', + {'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') + + new_todo = frappe.get_doc('ToDo', new_todo) + + self.assertEqual(todo.get('description'), new_todo.get('description')) + + def test_weekly_auto_repeat_with_weekdays(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() + + weekdays = list(week_map.keys()) + current_weekday = getdate().weekday() + days = [ + {'day': weekdays[current_weekday]}, + {'day': weekdays[current_weekday + 2]} + ] + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + doc.reload() + self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2)) + def test_monthly_auto_repeat(self): start_date = today() end_date = add_months(start_date, 12) @@ -124,7 +171,8 @@ def make_auto_repeat(**args): 'notify_by_email': args.notify or 0, 'recipients': args.recipients or "", 'subject': args.subject or "", - 'message': args.message or "" + 'message': args.message or "", + 'repeat_on_days': args.days or [] }).insert(ignore_permissions=True) return doc From 4ebc7be1a3ad256aa2d8be9f633bf8063b526df3 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 16:25:12 +0530 Subject: [PATCH 119/284] chore: code clean-up --- .../doctype/auto_repeat/auto_repeat.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index a41fef1f6b..c47b672595 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -308,11 +308,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul elif month_count: next_date = get_next_date(auto_repeat_doc.start_date, month_count) else: - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) - else: - # daily frequency - days = 1 + days = get_days(schedule_date, auto_repeat_doc) next_date = add_days(schedule_date, days) # next schedule date should be after or on current date @@ -322,11 +318,7 @@ def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedul month_count += month_map.get(auto_repeat_doc.frequency) next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) elif days: - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(next_date, auto_repeat_doc) - else: - # daily frequency - days = 1 + days = get_days(next_date, auto_repeat_doc) next_date = add_days(next_date, days) return next_date @@ -338,6 +330,16 @@ def get_next_date(dt, mcount, day=None): return dt +def get_days(schedule_date, auto_repeat_doc): + if auto_repeat_doc.frequency == "Weekly": + days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): # if weekdays are not set, offset is 7 from current schedule date if not auto_repeat_doc.repeat_on_days: @@ -355,8 +357,7 @@ def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): next_weekday_number = week_map.get(weekday) # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days - else: - return 7 + return 7 def get_next_weekday(current_schedule_day, weekdays): @@ -385,6 +386,7 @@ def make_auto_repeat_entry(): data = get_auto_repeat_entries(date) frappe.enqueue(enqueued_method, data=data) + def create_repeated_entries(data): for d in data: doc = frappe.get_doc('Auto Repeat', d.name) @@ -398,6 +400,7 @@ def create_repeated_entries(data): if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) + def get_auto_repeat_entries(date=None): if not date: date = getdate(today()) @@ -406,6 +409,7 @@ def get_auto_repeat_entries(date=None): ['status', '=', 'Active'] ]) + #called through hooks def set_auto_repeat_as_completed(): auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) @@ -415,6 +419,7 @@ def set_auto_repeat_as_completed(): doc.status = 'Completed' doc.save() + @frappe.whitelist() def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): if not start_date: From 17ec45b92a11ac7f11c547f06240a4987b284342 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 19 Nov 2020 16:41:17 +0530 Subject: [PATCH 120/284] fix: test case for edge case scenario --- frappe/automation/doctype/auto_repeat/test_auto_repeat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 69e9d98c0a..3cd10e8a61 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -73,7 +73,7 @@ class TestAutoRepeat(unittest.TestCase): current_weekday = getdate().weekday() days = [ {'day': weekdays[current_weekday]}, - {'day': weekdays[current_weekday + 2]} + {'day': weekdays[(current_weekday + 2) % 7]} ] doc = make_auto_repeat(reference_doctype='ToDo', frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) From 2c4b5c67b03c199c572cd1c0ae48b48fff9417b5 Mon Sep 17 00:00:00 2001 From: conncampbell Date: Sun, 8 Nov 2020 10:02:35 -0700 Subject: [PATCH 121/284] fix: Read-only table has read-only form fields. --- cypress/integration/depends_on.js | 57 +++- cypress/support/commands.js | 41 ++- .../js/frappe/form/controls/base_control.js | 27 +- .../public/js/frappe/form/controls/table.js | 3 +- frappe/public/js/frappe/form/grid.js | 2 + frappe/public/js/frappe/form/grid_row_form.js | 3 + frappe/public/js/frappe/form/layout.js | 259 +++++++++--------- .../js/frappe/web_form/webform_script.js | 4 +- frappe/tests/ui_test_helpers.py | 18 ++ 9 files changed, 273 insertions(+), 141 deletions(-) diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js index 93417014c5..aa80afb59a 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -3,7 +3,31 @@ context('Depends On', () => { cy.login(); cy.visit('/desk#workspace/Website'); return cy.window().its('frappe').then(frappe => { - return frappe.call('frappe.tests.ui_test_helpers.create_doctype', { + return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', { + name: 'Child Test Depends On', + fields: [ + { + "label": "Child Test Field", + "fieldname": "child_test_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + { + "label": "Child Dependant Field", + "fieldname": "child_dependant_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + { + "label": "Child Display Dependant Field", + "fieldname": "child_display_dependant_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + ] + }); + }).then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { name: 'Test Depends On', fields: [ { @@ -24,6 +48,13 @@ context('Depends On', () => { "fieldtype": "Data", 'depends_on': "eval:doc.test_field=='Value'" }, + { + "label": "Child Test Depends On Field", + "fieldname": "child_test_depends_on_field", + "fieldtype": "Table", + 'read_only_depends_on': "eval:doc.test_field=='Some Other Value'", + 'options': "Child Test Depends On" + }, ] }); }); @@ -48,6 +79,30 @@ context('Depends On', () => { cy.get('body').click(); cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled'); }); + it('should set the table and its fields as read only depending on other fields value', () => { + cy.new_form('Test Depends On'); + cy.fill_field('dependant_field', 'Some Value'); + //cy.fill_field('test_field', 'Some Other Value'); + cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('@table').find('[data-idx="1"]').as('row1'); + cy.get('@row1').find('.btn-open-row').click(); + cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid'); + //cy.get('@row1-form_in_grid').find('') + cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value'); + cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value'); + + cy.get('@row1-form_in_grid').find('.octicon-triangle-up').click(); + + // set the table to read-only + cy.fill_field('test_field', 'Some Other Value'); + + // grid row form fields should be read-only + cy.get('@row1').find('.btn-open-row').click(); + + cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled'); + cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled'); + }); it('should display the field depending on other fields value', () => { cy.new_form('Test Depends On'); cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible'); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 7816d5526f..3e54a9cd4c 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => { Cypress.Commands.add('create_records', doc => { return cy - .call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc }) + .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) .then(r => r.message); }); @@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { if (fieldtype === 'Select') { cy.get('@input').select(value); } else { - cy.get('@input').type(value, { waitForAnimations: false, force: true }); + cy.get('@input').type(value, {waitForAnimations: false, force: true}); } return cy.get('@input'); }); @@ -204,8 +204,43 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { return cy.get(selector); }); +Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { + let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + selector += ` .form-in-grid`; + + if (fieldtype === 'Text Editor') { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === 'Code') { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` .form-control[data-fieldname="${fieldname}"]`; + } + + return cy.get(selector); +}); + Cypress.Commands.add('awesomebar', text => { - cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 }); + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); }); Cypress.Commands.add('new_form', doctype => { diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 319aa067cc..d7f873bee0 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({ return this.df.get_status(this); } - if((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form') { + if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { // like in case of a dialog box if (cint(this.df.hidden)) { // eslint-disable-next-line - if(explain) console.log("By Hidden: None"); + if (explain) console.log("By Hidden: None"); // eslint-disable-line no-console return "None"; } else if (cint(this.df.hidden_due_to_dependency)) { // eslint-disable-next-line - if(explain) console.log("By Hidden Dependency: None"); + if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console return "None"; } else if (cint(this.df.read_only)) { // eslint-disable-next-line - if(explain) console.log("By Read Only: Read"); + if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console return "Read"; + } else if ((this.grid && + this.grid.display_status == 'Read') || + (this.layout && + this.layout.grid && + this.layout.grid.display_status == 'Read')) { + // parent grid is read + if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console + return "Read"; } return "Write"; @@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({ var status = frappe.perm.get_field_display_status(this.df, frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain); + // Match parent grid controls read only status + if (status === 'Write' && (this.grid || (this.layout && this.layout.grid))) { + var grid = this.grid || this.layout.grid; + if (grid.display_status == 'Read') { + status = 'Read'; + if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console + } + } + // hide if no value if (this.doctype && status==="Read" && !this.only_input && is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname)) && !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) { // eslint-disable-next-line - if(explain) console.log("By Hide Read-only, null fields: None"); + if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console status = "None"; } diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 14fad1c010..a87a4ad2a6 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ frm: this.frm, df: this.df, perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, - parent: this.wrapper + parent: this.wrapper, + control: this }); if(this.frm) { this.frm.grids[this.frm.grids.length] = this; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 9c916ccc4a..8ef5860d0d 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -264,6 +264,8 @@ export default class Grid { if (this.frm) { this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc, this.perm); + } else if (this.df.is_web_form && this.control) { + this.display_status = this.control.get_status(); } else { // not in form this.display_status = 'Write'; diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js index f93640936f..71c0c6e679 100644 --- a/frappe/public/js/frappe/form/grid_row_form.js +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -16,6 +16,9 @@ export default class GridRowForm { body: this.form_area, no_submit_on_enter: true, frm: this.row.frm, + grid: this.row.grid, + grid_row: this.row, + grid_row_form: this, }); this.layout.make(); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 2195568317..85daecc57a 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -1,7 +1,7 @@ import '../class'; frappe.ui.form.Layout = Class.extend({ - init: function(opts) { + init: function (opts) { this.views = {}; this.pages = []; this.sections = []; @@ -10,24 +10,24 @@ frappe.ui.form.Layout = Class.extend({ $.extend(this, opts); }, - make: function() { - if(!this.parent && this.body) { + make: function () { + if (!this.parent && this.body) { this.parent = this.body; } this.wrapper = $('
    ').appendTo(this.parent); this.message = $('').appendTo(this.wrapper); - if(!this.fields) { + if (!this.fields) { this.fields = this.get_doctype_fields(); } this.setup_tabbing(); this.render(); }, - show_empty_form_message: function() { - if(!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { + show_empty_form_message: function () { + if (!(this.wrapper.find(".frappe-control:visible").length || this.wrapper.find(".section-head.collapsed").length)) { this.show_message(__("This form does not have any input")); } }, - get_doctype_fields: function() { + get_doctype_fields: function () { let fields = [ { parent: this.frm.doctype, @@ -36,7 +36,7 @@ frappe.ui.form.Layout = Class.extend({ reqd: 1, hidden: 1, label: __('Name'), - get_status: function(field) { + get_status: function (field) { if (field.frm && field.frm.is_new() && field.frm.meta.autoname && ['prompt', 'name'].includes(field.frm.meta.autoname.toLowerCase())) { @@ -49,14 +49,14 @@ frappe.ui.form.Layout = Class.extend({ fields = fields.concat(frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype])); return fields; }, - show_message: function(html, color) { + show_message: function (html, color) { if (this.message_color) { // remove previous color this.message.removeClass(this.message_color); } this.message_color = (color && ['yellow', 'blue'].includes(color)) ? color : 'blue'; - if(html) { - if(html.substr(0, 1)!=='<') { + if (html) { + if (html.substr(0, 1) !== '<') { // wrap in a block html = '
    ' + html + '
    '; } @@ -66,7 +66,7 @@ frappe.ui.form.Layout = Class.extend({ this.message.empty().addClass('hidden'); } }, - render: function(new_fields) { + render: function (new_fields) { var me = this; var fields = new_fields || this.fields; @@ -80,8 +80,8 @@ frappe.ui.form.Layout = Class.extend({ if (this.no_opening_section()) { this.make_section(); } - $.each(fields, function(i, df) { - switch(df.fieldtype) { + $.each(fields, function (i, df) { + switch (df.fieldtype) { case "Fold": me.make_page(df); break; @@ -98,11 +98,11 @@ frappe.ui.form.Layout = Class.extend({ }, - no_opening_section: function() { - return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length; + no_opening_section: function () { + return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length; }, - setup_dashboard_section: function() { + setup_dashboard_section: function () { if (this.no_opening_section()) { this.fields.unshift({fieldtype: 'Section Break'}); } @@ -117,7 +117,7 @@ frappe.ui.form.Layout = Class.extend({ }); }, - replace_field: function(fieldname, df, render) { + replace_field: function (fieldname, df, render) { df.fieldname = fieldname; // change of fieldname is avoided if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { const fieldobj = this.init_field(df, render); @@ -133,14 +133,14 @@ frappe.ui.form.Layout = Class.extend({ } }, - make_field: function(df, colspan, render) { + make_field: function (df, colspan, render) { !this.section && this.make_section(); !this.column && this.make_column(); const fieldobj = this.init_field(df, render); this.fields_list.push(fieldobj); this.fields_dict[df.fieldname] = fieldobj; - if(this.frm) { + if (this.frm) { fieldobj.perm = this.frm.perm; } @@ -149,31 +149,32 @@ frappe.ui.form.Layout = Class.extend({ fieldobj.section = this.section; }, - init_field: function(df, render = false) { + init_field: function (df, render = false) { const fieldobj = frappe.ui.form.make_control({ df: df, doctype: this.doctype, parent: this.column.wrapper.get(0), frm: this.frm, render_input: render, - doc: this.doc + doc: this.doc, + layout: this }); fieldobj.layout = this; return fieldobj; }, - make_page: function(df) { + make_page: function (df) { // eslint-disable-line no-unused-vars var me = this, head = $('').appendTo(this.wrapper); this.page = $('
    ').appendTo(this.wrapper); - this.fold_btn = head.find(".btn-fold").on("click", function() { + this.fold_btn = head.find(".btn-fold").on("click", function () { var page = $(this).parent().next(); - if(page.hasClass("hide")) { + if (page.hasClass("hide")) { $(this).removeClass("btn-fold").html(__("Hide details")); page.removeClass("hide"); frappe.utils.scroll_to($(this), true, 30); @@ -189,15 +190,15 @@ frappe.ui.form.Layout = Class.extend({ this.folded = true; }, - unfold: function() { + unfold: function () { this.fold_btn.trigger('click'); }, - make_section: function(df) { + make_section: function (df) { this.section = new frappe.ui.form.Section(this, df); // append to layout fields - if(df) { + if (df) { this.fields_dict[df.fieldname] = this.section; this.fields_list.push(this.section); } @@ -205,16 +206,16 @@ frappe.ui.form.Layout = Class.extend({ this.column = null; }, - make_column: function(df) { + make_column: function (df) { this.column = new frappe.ui.form.Column(this.section, df); - if(df && df.fieldname) { + if (df && df.fieldname) { this.fields_list.push(this.column); } }, - refresh: function(doc) { + refresh: function (doc) { var me = this; - if(doc) this.doc = doc; + if (doc) this.doc = doc; if (this.frm) { this.wrapper.find(".empty-form-alert").remove(); @@ -223,7 +224,7 @@ frappe.ui.form.Layout = Class.extend({ // NOTE this might seem redundant at first, but it needs to be executed when frm.refresh_fields is called me.attach_doc_and_docfields(true); - if(this.frm && this.frm.wrapper) { + if (this.frm && this.frm.wrapper) { $(this.frm.wrapper).trigger("refresh-fields"); } @@ -234,26 +235,26 @@ frappe.ui.form.Layout = Class.extend({ this.refresh_sections(); // collapse sections - if(this.frm) { + if (this.frm) { this.refresh_section_collapse(); } }, - refresh_sections: function() { + refresh_sections: function () { var cnt = 0; // hide invisible sections and set alternate background color - this.wrapper.find(".form-section:not(.hide-control)").each(function() { + this.wrapper.find(".form-section:not(.hide-control)").each(function () { var $this = $(this).removeClass("empty-section") .removeClass("visible-section") .removeClass("shaded-section"); - if(!$this.find(".frappe-control:not(.hide-control)").length + if (!$this.find(".frappe-control:not(.hide-control)").length && !$this.hasClass('form-dashboard')) { // nothing visible, hide the section $this.addClass("empty-section"); } else { $this.addClass("visible-section"); - if(cnt % 2) { + if (cnt % 2) { $this.addClass("shaded-section"); } cnt++; @@ -261,36 +262,36 @@ frappe.ui.form.Layout = Class.extend({ }); }, - refresh_fields: function(fields) { + refresh_fields: function (fields) { let fieldnames = fields.map((field) => { - if(field.fieldname) return field.fieldname; + if (field.fieldname) return field.fieldname; }); this.fields_list.map(fieldobj => { - if(fieldnames.includes(fieldobj.df.fieldname)) { + if (fieldnames.includes(fieldobj.df.fieldname)) { fieldobj.refresh(); - if(fieldobj.df["default"]) { + if (fieldobj.df["default"]) { fieldobj.set_input(fieldobj.df["default"]); } } }); }, - add_fields: function(fields) { + add_fields: function (fields) { this.render(fields); this.refresh_fields(fields); }, - refresh_section_collapse: function() { - if(!this.doc) return; + refresh_section_collapse: function () { + if (!this.doc) return; - for(var i=0; i=0;i--) { + for (var i = me.fields_list.length - 1; i >= 0; i--) { var f = me.fields_list[i]; f.guardian_has_value = true; if (f.df.depends_on) { @@ -473,12 +474,12 @@ frappe.ui.form.Layout = Class.extend({ // show / hide if (f.guardian_has_value) { - if(f.df.hidden_due_to_dependency) { + if (f.df.hidden_due_to_dependency) { f.df.hidden_due_to_dependency = false; f.refresh(); } } else { - if(!f.df.hidden_due_to_dependency) { + if (!f.df.hidden_due_to_dependency) { f.df.hidden_due_to_dependency = true; f.refresh(); } @@ -496,14 +497,14 @@ frappe.ui.form.Layout = Class.extend({ this.refresh_section_count(); }, - set_dependant_property: function(condition, fieldname, property) { + set_dependant_property: function (condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); let value = set_property ? 1 : 0; let form_obj; if (this.frm) { form_obj = this.frm; - } else if (this.is_dialog) { + } else if (this.is_dialog || this.doctype === 'Web Form') { form_obj = this; } if (form_obj) { @@ -514,7 +515,7 @@ frappe.ui.form.Layout = Class.extend({ } } }, - evaluate_depends_on_value: function(expression) { + evaluate_depends_on_value: function (expression) { var out = null; var doc = this.doc; @@ -528,27 +529,27 @@ frappe.ui.form.Layout = Class.extend({ var parent = this.frm ? this.frm.doc : this.doc || null; - if(typeof(expression) === 'boolean') { + if (typeof (expression) === 'boolean') { out = expression; - } else if(typeof(expression) === 'function') { + } else if (typeof (expression) === 'function') { out = expression(doc); - } else if(expression.substr(0,5)=='eval:') { + } else if (expression.substr(0, 5) == 'eval:') { try { out = eval(expression.substr(5)); - if(parent && parent.istable && expression.includes('is_submittable')) { + if (parent && parent.istable && expression.includes('is_submittable')) { out = true; } - } catch(e) { + } catch (e) { frappe.throw(__('Invalid "depends_on" expression')); } - } else if(expression.substr(0,3)=='fn:' && this.frm) { + } else if (expression.substr(0, 3) == 'fn:' && this.frm) { out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname); } else { var value = doc[expression]; - if($.isArray(value)) { + if ($.isArray(value)) { out = !!value.length; } else { out = !!value; @@ -560,7 +561,7 @@ frappe.ui.form.Layout = Class.extend({ }); frappe.ui.form.Section = Class.extend({ - init: function(layout, df) { + init: function (layout, df) { var me = this; this.layout = layout; this.df = df || {}; @@ -580,8 +581,8 @@ frappe.ui.form.Section = Class.extend({ this.refresh(); }, - make: function() { - if(!this.layout.page) { + make: function () { + if (!this.layout.page) { this.layout.page = $('
    ').appendTo(this.layout.wrapper); } @@ -589,15 +590,15 @@ frappe.ui.form.Section = Class.extend({ .appendTo(this.layout.page); this.layout.sections.push(this); - if(this.df) { - if(this.df.label) { + if (this.df) { + if (this.df.label) { this.make_head(); } - if(this.df.description) { + if (this.df.description) { $('
    ' + __(this.df.description) + '
    ') .appendTo(this.wrapper); } - if(this.df.cssClass) { + if (this.df.cssClass) { this.wrapper.addClass(this.df.cssClass); } if (this.df.hide_border) { @@ -609,49 +610,49 @@ frappe.ui.form.Section = Class.extend({ this.body = $('
    ').appendTo(this.wrapper); }, - make_head: function() { + make_head: function () { var me = this; - if(!this.df.collapsible) { + if (!this.df.collapsible) { $('
    ' + __(this.df.label) + '
    ') .appendTo(this.wrapper); } else { this.head = $('').appendTo(this.wrapper); + + __(this.df.label) + '
    ').appendTo(this.wrapper); // show / hide based on status - this.collapse_link = this.head.on("click", function() { + this.collapse_link = this.head.on("click", function () { me.collapse(); }); this.indicator = this.head.find(".collapse-indicator"); } }, - refresh: function() { - if(!this.df) + refresh: function () { + if (!this.df) return; // hide if explictly hidden var hide = this.df.hidden || this.df.hidden_due_to_dependency; // hide if no perm - if(!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { + if (!hide && this.layout && this.layout.frm && !this.layout.frm.get_perm(this.df.permlevel || 0, "read")) { hide = true; } this.wrapper.toggleClass("hide-control", !!hide); }, - collapse: function(hide) { + collapse: function (hide) { // unknown edge case if (!(this.head && this.body)) { return; } - if(hide===undefined) { + if (hide === undefined) { hide = !this.body.hasClass("hide"); } - if (this.df.fieldname==='_form_dashboard') { + if (this.df.fieldname === '_form_dashboard') { localStorage.setItem('collapseFormDashboard', hide ? 'yes' : 'no'); } @@ -662,7 +663,7 @@ frappe.ui.form.Section = Class.extend({ // refresh signature fields this.fields_list.forEach((f) => { - if (f.df.fieldtype=='Signature') { + if (f.df.fieldtype == 'Signature') { f.refresh(); } }); @@ -672,11 +673,11 @@ frappe.ui.form.Section = Class.extend({ return this.body.hasClass('hide'); }, - has_missing_mandatory: function() { + has_missing_mandatory: function () { var missing_mandatory = false; - for (var j=0, l=this.fields_list.length; j < l; j++) { + for (var j = 0, l = this.fields_list.length; j < l; j++) { var section_df = this.fields_list[j].df; - if (section_df.reqd && this.layout.doc[section_df.fieldname]==null) { + if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { missing_mandatory = true; break; } @@ -686,21 +687,21 @@ frappe.ui.form.Section = Class.extend({ }); frappe.ui.form.Column = Class.extend({ - init: function(section, df) { - if(!df) df = {}; + init: function (section, df) { + if (!df) df = {}; this.df = df; this.section = section; this.make(); this.resize_all_columns(); }, - make: function() { + make: function () { this.wrapper = $('
    \
    \
    \
    ').appendTo(this.section.body) .find("form") - .on("submit", function() { + .on("submit", function () { return false; }); @@ -709,7 +710,7 @@ frappe.ui.form.Column = Class.extend({ + '').appendTo(this.wrapper); } }, - resize_all_columns: function() { + resize_all_columns: function () { // distribute all columns equally var colspan = cint(12 / this.section.wrapper.find(".form-column").length); @@ -718,7 +719,7 @@ frappe.ui.form.Column = Class.extend({ .addClass("col-sm-" + colspan); }, - refresh: function() { + refresh: function () { this.section.refresh(); } }); diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index c3211de99f..6df526e7ac 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -85,6 +85,7 @@ frappe.ready(function() { function setup_fields(form_data) { form_data.web_form.web_form_fields.map(df => { + df.is_web_form = true; if (df.fieldtype === "Table") { df.get_data = () => { let data = []; @@ -99,14 +100,13 @@ frappe.ready(function() { if (field.fieldtype === "Link") { field.only_select = true; } + field.is_web_form = true; }); if (df.fieldtype === "Attach") { df.is_private = true; } - df.is_web_form = true; - delete df.parent; delete df.parentfield; delete df.parenttype; diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index ef572c6971..54a5a24acf 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -95,6 +95,24 @@ def create_doctype(name, fields): "name": name }).insert() +@frappe.whitelist() +def create_child_doctype(name, fields): + fields = frappe.parse_json(fields) + if frappe.db.exists('DocType', name): + return + frappe.get_doc({ + "doctype": "DocType", + "module": "Core", + "istable": 1, + "custom": 1, + "fields": fields, + "permissions": [{ + "role": "System Manager", + "read": 1 + }], + "name": name + }).insert() + @frappe.whitelist() def create_contact_records(): if frappe.db.get_all('Contact', {'first_name': 'Test Form Contact 1'}): From df72f80d25deefbb27dbaee58e123bca0c29bf43 Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Fri, 20 Nov 2020 00:25:43 +0100 Subject: [PATCH 122/284] Update frappe/public/js/frappe/list/list_sidebar.js Co-authored-by: Prssanna Desai --- frappe/public/js/frappe/list/list_sidebar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 4d637602a3..0622e26dbd 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -94,7 +94,7 @@ frappe.views.ListSidebar = class ListSidebar { if (this.list_view.settings.get_coords_method || (this.list_view.meta.fields.find(i => i.fieldname === "latitude") && this.list_view.meta.fields.find(i => i.fieldname === "longitude")) || - (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) + (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) { this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide'); show_list_link = true; } From a08692346994a0928550479b5dbf447cffe76d41 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Fri, 20 Nov 2020 00:37:19 +0100 Subject: [PATCH 123/284] chore: Remove useless prepare_data Signed-off-by: mathieu.brunot --- frappe/public/js/frappe/views/map/map_view.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 48e4ac8b3e..84e5b70ab6 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -20,13 +20,6 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { this.get_coords(); } - prepare_data(data) { - super.prepare_data(data); - this.items = this.data.map(d => { - return d; - }); - } - render() { this.get_coords() .then(() => { From 5270643bcb285c8ecdc8834e3c4c7240afb7ad75 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 24 Nov 2020 11:53:57 +0100 Subject: [PATCH 124/284] fix: description in config --- frappe/config/integrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/config/integrations.py b/frappe/config/integrations.py index deba1945f3..672c0c4acc 100644 --- a/frappe/config/integrations.py +++ b/frappe/config/integrations.py @@ -80,7 +80,7 @@ def get_data(): { "type": "doctype", "name": "Connected App", - "description": _("Connected App"), + "description": _("Connect to any OAuth Provider"), }, ] }, From 53974ff1a640130340bb46213865f9527d3a1c1c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Wed, 25 Nov 2020 14:13:21 +0530 Subject: [PATCH 125/284] fix: grid infinte loop while evaluating depends on --- frappe/public/js/frappe/form/form.js | 4 +++- frappe/public/js/frappe/form/layout.js | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index bb9e8c22d1..9272d1f6f5 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1275,7 +1275,9 @@ frappe.ui.form.Form = class FrappeForm { } if (df && df[property] != value) { df[property] = value; - this.refresh_field(fieldname); + if (!docname || !table_field) { + this.refresh_field(fieldname); + } } } diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 3505cf4857..6c94663802 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -508,9 +508,16 @@ frappe.ui.form.Layout = Class.extend({ } if (form_obj) { if (this.doc && this.doc.parent) { - form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); + const df = form_obj.get_docfield(this.doc.parentfield, fieldname); + if (df && df[property] != value) { + form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); + this.fields_dict[fname] && this.fields_dict[fieldname].refresh(); + } } else { - form_obj.set_df_property(fieldname, property, value); + const df = form_obj.get_docfield(fieldname); + if (df && df[property] != value) { + form_obj.set_df_property(fieldname, property, value); + } } } }, From 90875ef21c3c3e4b32667970ff6bf1816ca938de Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Thu, 26 Nov 2020 02:26:57 +0100 Subject: [PATCH 126/284] Update frappe/geo/utils.py Co-authored-by: Prssanna Desai --- frappe/geo/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index 77e48acb76..d94a13ea41 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -40,6 +40,8 @@ def merge_location_features_in_one(coords): geojson_dict = [] for element in coords: geojson_loc = frappe.parse_json(element['location']) + if not geojson_loc: + continue for coord in geojson_loc['features']: coord['properties']['name'] = element['name'] geojson_dict.append(coord.copy()) From 56b775a3deafd8d15b341c14abcf9d7c79c2bbfa Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Thu, 26 Nov 2020 02:54:41 +0100 Subject: [PATCH 127/284] Update frappe/public/js/frappe/views/map/map_view.js Co-authored-by: Prssanna Desai --- frappe/public/js/frappe/views/map/map_view.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 84e5b70ab6..2c068277ad 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -46,11 +46,13 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { }).addTo(this.map); L.control.scale().addTo(this.map); +if (this.coords.features && this.coords.features.length) { this.coords.features.forEach( coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) ); let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); this.map.panTo(lastCoords, 8); +} } get_coords() { From 8a6bdf546652d46d13377e016c7fdf6c2173d2e3 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 16:49:49 +0530 Subject: [PATCH 128/284] chore: Move rename doctype method to dedicated TestCase Co-authored-by: Marica --- frappe/tests/test_document.py | 76 --------------------------------- frappe/tests/test_rename_doc.py | 36 ++++++++++++++++ 2 files changed, 36 insertions(+), 76 deletions(-) diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 4f595c9419..2be92be1f5 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -249,82 +249,6 @@ class TestDocument(unittest.TestCase): self.assertEqual(cint(old_current) - 1, new_current) - def test_rename_doc(self): - from random import choice, sample - - available_documents = [] - doctype = "ToDo" - - # data generation: 4 todo documents - for num in range(1, 5): - doc = frappe.get_doc({ - "doctype": doctype, - "date": add_to_date(now(), days=num), - "description": "this is todo #{}".format(num) - }).insert() - available_documents.append(doc.name) - - # test 1: document renaming - old_name = choice(available_documents) - new_name = old_name + '.new' - self.assertEqual(new_name, frappe.rename_doc(doctype, old_name, new_name, force=True)) - available_documents.remove(old_name) - available_documents.append(new_name) - - # test 2: merge documents - first_todo, second_todo = sample(available_documents, 2) - - second_todo_doc = frappe.get_doc(doctype, second_todo) - second_todo_doc.priority = "High" - second_todo_doc.save() - - merged_todo = frappe.rename_doc(doctype, first_todo, second_todo, merge=True, force=True) - merged_todo_doc = frappe.get_doc(doctype, merged_todo) - available_documents.remove(first_todo) - - with self.assertRaises(DoesNotExistError): - frappe.get_doc(doctype, first_todo) - - self.assertEqual(merged_todo_doc.priority, second_todo_doc.priority) - - for docname in available_documents: - frappe.delete_doc(doctype, docname) - - def test_rename_doctype(self): - from frappe.core.doctype.doctype.test_doctype import new_doctype - - fields =[{ - "label": "Linked To", - "fieldname": "linked_to_doctype", - "fieldtype": "Link", - "options": "DocType", - "unique": 0 - }] - if not frappe.db.exists("DocType", "Rename This"): - new_doctype("Rename This", unique=0, fields=fields).insert() - - to_rename_record = frappe.get_doc({ - "doctype": "Rename This", - "linked_to_doctype": "Rename This" - }) - to_rename_record.insert() - - # Rename doctype - self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)) - - # Test if Doctype value has changed in Link field - renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name) - self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc") - - # Test if there are conflicts between a record and a DocType - # having the same name - old_name = to_rename_record.name - new_name = "ToDo" - self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)) - - frappe.delete_doc_if_exists("Renamed Doc", "ToDo") - frappe.delete_doc_if_exists("DocType", "Renamed Doc") - def test_non_negative_check(self): frappe.delete_doc_if_exists("Currency", "Frappe Coin", 1) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 4db877e586..fcb82e2755 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -98,3 +98,39 @@ class TestRenameDoc(unittest.TestCase): self.assertTrue(frappe.db.exists("DocType", self.doctype.new)) self.assertFalse(frappe.db.exists("DocType", self.doctype.old)) self.assertFalse(os.path.exists(old_doctype_path)) + + def test_rename_doctype(self): + """Rename DocType via frappe.rename_doc""" + from frappe.core.doctype.doctype.test_doctype import new_doctype + + fields =[{ + "label": "Linked To", + "fieldname": "linked_to_doctype", + "fieldtype": "Link", + "options": "DocType", + "unique": 0 + }] + if not frappe.db.exists("DocType", "Rename This"): + new_doctype("Rename This", unique=0, fields=fields).insert() + + to_rename_record = frappe.get_doc({ + "doctype": "Rename This", + "linked_to_doctype": "Rename This" + }) + to_rename_record.insert() + + # Rename doctype + self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)) + + # Test if Doctype value has changed in Link field + renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name) + self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc") + + # Test if there are conflicts between a record and a DocType + # having the same name + old_name = to_rename_record.name + new_name = "ToDo" + self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)) + + frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + frappe.delete_doc_if_exists("DocType", "Renamed Doc") From 147a3def0dcab7173d000b91d9d998a72a8b87de Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 27 Nov 2020 18:36:23 +0530 Subject: [PATCH 129/284] fix: Use get_value to find value of linked_to_doctype instead Tests ran successfully when run locally for mariaDB and postgres --- frappe/tests/test_rename_doc.py | 44 +++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index fcb82e2755..fb0776a485 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -103,34 +103,42 @@ class TestRenameDoc(unittest.TestCase): """Rename DocType via frappe.rename_doc""" from frappe.core.doctype.doctype.test_doctype import new_doctype - fields =[{ - "label": "Linked To", - "fieldname": "linked_to_doctype", - "fieldtype": "Link", - "options": "DocType", - "unique": 0 - }] if not frappe.db.exists("DocType", "Rename This"): - new_doctype("Rename This", unique=0, fields=fields).insert() - - to_rename_record = frappe.get_doc({ - "doctype": "Rename This", - "linked_to_doctype": "Rename This" - }) - to_rename_record.insert() + new_doctype( + "Rename This", + fields=[ + { + "label": "Linked To", + "fieldname": "linked_to_doctype", + "fieldtype": "Link", + "options": "DocType", + "unique": 0, + } + ], + ).insert() + + to_rename_record = frappe.get_doc( + {"doctype": "Rename This", "linked_to_doctype": "Rename This"} + ).insert() # Rename doctype - self.assertEqual("Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True)) + self.assertEqual( + "Renamed Doc", frappe.rename_doc("DocType", "Rename This", "Renamed Doc", force=True) + ) # Test if Doctype value has changed in Link field - renamed_doctype_record = frappe.get_doc("Renamed Doc", to_rename_record.name) - self.assertEqual(renamed_doctype_record.linked_to_doctype, "Renamed Doc") + linked_to_doctype = frappe.db.get_value( + "Renamed Doc", to_rename_record.name, "linked_to_doctype" + ) + self.assertEqual(linked_to_doctype, "Renamed Doc") # Test if there are conflicts between a record and a DocType # having the same name old_name = to_rename_record.name new_name = "ToDo" - self.assertEqual(new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True)) + self.assertEqual( + new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True) + ) frappe.delete_doc_if_exists("Renamed Doc", "ToDo") frappe.delete_doc_if_exists("DocType", "Renamed Doc") From 729ee6ea9ea1138043a501632891cfd03fc68396 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 27 Nov 2020 19:03:23 +0530 Subject: [PATCH 130/284] fix: changing percent field precision to global default --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 3f422d0a9b..c456739add 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, 2) + "%", options) + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options) }, Rating: function(value) { return ` From 8aeb1716642a1e275ddbc83c77501a24529dd1c5 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 27 Nov 2020 19:58:41 +0530 Subject: [PATCH 131/284] fix: sider issue fixed --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index c456739add..7f5c3bf472 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options) + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options); }, Rating: function(value) { return ` From be6724c7c8288589d616b73a641a2440ef580f26 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 30 Nov 2020 11:15:18 +0530 Subject: [PATCH 132/284] fix: check if print settings has landscape --- frappe/public/js/frappe/microtemplate.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index d233a47893..7b45db952e 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -89,11 +89,19 @@ frappe.render_template = function(name, data) { } frappe.render_grid = function(opts) { // build context - if(opts.grid) { + if (opts.grid) { opts.columns = opts.grid.getColumns(); opts.data = opts.grid.getData().getItems(); } + if ( + opts.print_settings && + opts.print_settings.orientation && + opts.print_settings.orientation.toLowerCase() === "landscape" + ) { + opts.landscape = true; + } + // show landscape view if columns more than 10 if (opts.landscape == null) { if(opts.columns && opts.columns.length > 10) { From 29941ef46a396dcbc0d79762988107e05b98fd2d Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 30 Nov 2020 12:03:53 +0530 Subject: [PATCH 133/284] fix: Use ORM instead of raw SQL Co-authored-by: Marica --- frappe/model/rename_doc.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index a7714a0ae7..697978ff98 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -247,6 +247,7 @@ def update_link_field_values(link_fields, old, new, doctype): pass else: parent = field['parent'] + docfield = field["fieldname"] # Handles the case where one of the link fields belongs to # the DocType being renamed. @@ -258,11 +259,8 @@ def update_link_field_values(link_fields, old, new, doctype): if parent == new and doctype == "DocType": parent = old - frappe.db.sql(""" - update `tab{table_name}` set `{fieldname}`=%s - where `{fieldname}`=%s""".format( - table_name=parent, - fieldname=field['fieldname']), (new, old)) + frappe.db.set_value(parent, {docfield: old}, docfield, new) + # update cached link_fields as per new if doctype=='DocType' and field['parent'] == old: field['parent'] = new From c5767b818facb2e3b9e61a8cede7c7653dca5a67 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 1 Dec 2020 18:04:12 +0530 Subject: [PATCH 134/284] feat: setup record for background color while setting up website theme --- frappe/patches/v13_0/website_theme_custom_scss.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py index 0035283428..a5f08324e8 100644 --- a/frappe/patches/v13_0/website_theme_custom_scss.py +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -2,9 +2,23 @@ import frappe def execute(): frappe.reload_doctype('Website Theme') + frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app') + frappe.reload_doc('website', 'doctype', 'color') + for theme in frappe.get_all('Website Theme'): doc = frappe.get_doc('Website Theme', theme.name) if not doc.get('custom_scss') and doc.theme_scss: # move old theme to new theme doc.custom_scss = doc.theme_scss + + if doc.background_color: + setup_color_record(doc.background_color) + doc.save() + +def setup_color_record(color): + frappe.get_doc({ + "doctype": "Color", + "__newname": color, + "color": color, + }).save() From 46edda896e2f9756ab2d85e599d0bdff33a8e7c6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 4 Dec 2020 17:44:36 +0100 Subject: [PATCH 135/284] fix: improve translation pattern --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 340f4f8772..184d5bde73 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +pattern = re.compile(r"_{1,2}\(([\"'`]{1,3})(?P((?!\1)[\s\S])*)\1(\s*,\s*context\s*=\s*([\"']{1,3})(?P((?!\5)[\s\S])*)\5){0,1}(\s*,\s*([\s\S])*\s*(,\s*([\"'`])(?P((?!\11).)*)\11)*){0,1}\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From b1cab9a9c1788ee654fe6d08d49a71c67eb6e163 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 5 Dec 2020 19:26:52 +0530 Subject: [PATCH 136/284] fix: Query report filter --- frappe/core/doctype/report_filter/report_filter.json | 4 ++-- frappe/public/js/frappe/views/reports/query_report.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json index 9d277db11d..964294b96e 100644 --- a/frappe/core/doctype/report_filter/report_filter.json +++ b/frappe/core/doctype/report_filter/report_filter.json @@ -44,7 +44,7 @@ }, { "fieldname": "options", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Options" }, { @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 16:15:46.937267", + "modified": "2020-12-05 19:20:00.503097", "modified_by": "Administrator", "module": "Core", "name": "Report Filter", diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 60abb187ae..eccfa9c089 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1112,7 +1112,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } get_filter_values(raise) { - const mandatory = this.filters.filter(f => f.df.reqd); + + // check for mandatory property for filters added via UI + const mandatory = this.filters.filter(f => (f.df.reqd || f.df.mandatory)); const missing_mandatory = mandatory.filter(f => !f.get_value()); if (raise && missing_mandatory.length > 0) { let message = __('Please set filters'); From 9dec6e84bd3f6043a10003b6e5bff17f07a383c0 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Sun, 6 Dec 2020 23:07:09 +0000 Subject: [PATCH 137/284] fix: package.json & yarn.lock to reduce vulnerabilities The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-HIGHLIGHTJS-1048676 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d1a94d0e35..2ef6ceab2d 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", - "highlight.js": "^9.18.2", + "highlight.js": "^10.4.1", "js-sha256": "^0.9.0", "jsbarcode": "^3.9.0", "moment": "^2.20.1", diff --git a/yarn.lock b/yarn.lock index 26797675c6..5cbea3f938 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2702,10 +2702,10 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -highlight.js@^9.18.2: - version "9.18.5" - resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.18.5.tgz#d18a359867f378c138d6819edfc2a8acd5f29825" - integrity sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA== +highlight.js@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.4.1.tgz#d48fbcf4a9971c4361b3f95f302747afe19dbad0" + integrity sha512-yR5lWvNz7c85OhVAEAeFhVCc/GV4C30Fjzc/rCP0aCWzc1UUOPUk55dK/qdwTZHBvMZo+eZ2jpk62ndX/xMFlg== homedir-polyfill@^1.0.1: version "1.0.3" From fc97e080c67ddc241af9548fb08d9672fb3c0470 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 7 Dec 2020 16:32:19 +0100 Subject: [PATCH 138/284] feat: remove useless functionality --- .../s3_backup_settings.json | 32 ++-------- .../s3_backup_settings/s3_backup_settings.py | 58 +++++-------------- 2 files changed, 19 insertions(+), 71 deletions(-) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json index 123bb21e88..2ca1723cb2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json @@ -18,12 +18,9 @@ "bucket", "endpoint_url", "column_break_13", - "region", "backup_details_section", "frequency", - "backup_files", - "column_break_18", - "backup_limit" + "backup_files" ], "fields": [ { @@ -42,7 +39,7 @@ }, { "default": "1", - "description": "Note: By default emails for failed backups are sent.", + "description": "By default, emails are only sent for failed backups.", "fieldname": "send_email_for_successful_backup", "fieldtype": "Check", "label": "Send Email for Successful Backup" @@ -73,14 +70,7 @@ "reqd": 1 }, { - "default": "us-east-1", - "description": "See https://docs.aws.amazon.com/general/latest/gr/s3.html for details.", - "fieldname": "region", - "fieldtype": "Select", - "label": "Region", - "options": "us-east-1\nus-east-2\nus-west-1\nus-west-2\naf-south-1\nap-east-1\nap-south-1\nap-southeast-1\nap-southeast-2\nap-northeast-1\nap-northeast-2\nap-northeast-3\nca-central-1\ncn-north-1\ncn-northwest-1\neu-central-1\neu-west-1\neu-west-2\neu-west-3\neu-south-1\neu-north-1\nme-south-1\nsa-east-1" - }, - { + "default": "https://s3.amazonaws.com", "fieldname": "endpoint_url", "fieldtype": "Data", "label": "Endpoint URL" @@ -92,14 +82,6 @@ "mandatory_depends_on": "enabled", "reqd": 1 }, - { - "description": "Set to 0 for no limit on the number of backups taken", - "fieldname": "backup_limit", - "fieldtype": "Int", - "label": "Backup Limit", - "mandatory_depends_on": "enabled", - "reqd": 1 - }, { "depends_on": "enabled", "fieldname": "api_access_section", @@ -142,16 +124,12 @@ "fieldname": "backup_files", "fieldtype": "Check", "label": "Backup Files" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" } ], "hide_toolbar": 1, "issingle": 1, "links": [], - "modified": "2020-07-27 17:27:21.400000", + "modified": "2020-12-07 15:30:55.047689", "modified_by": "Administrator", "module": "Integrations", "name": "S3 Backup Settings", @@ -172,4 +150,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 7c90d37f82..308d34c5c2 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -24,6 +24,7 @@ class S3BackupSettings(Document): if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' + conn = boto3.client( 's3', aws_access_key_id=self.access_key_id, @@ -31,25 +32,21 @@ class S3BackupSettings(Document): endpoint_url=self.endpoint_url ) - bucket_lower = str(self.bucket) - - try: - conn.list_buckets() - - except ClientError: - frappe.throw(_("Invalid Access Key ID or Secret Access Key.")) - try: # Head_bucket returns a 200 OK if the bucket exists and have access to it. - conn.head_bucket(Bucket=bucket_lower) + # Requires ListBucket permission + conn.head_bucket(Bucket=self.bucket) except ClientError as e: error_code = e.response['Error']['Code'] + bucket_name = frappe.bold(self.bucket) if error_code == '403': - frappe.throw(_("Do not have permission to access {0} bucket.").format(bucket_lower)) - else: # '400'-Bad request or '404'-Not Found return - # try to create bucket - conn.create_bucket(Bucket=bucket_lower, CreateBucketConfiguration={ - 'LocationConstraint': self.region}) + msg = _("Do not have permission to access bucket {0}.").format(bucket_name) + elif error_code == '404': + msg = _("Bucket {0} not found.").format(bucket_name) + else: + msg = e.args[0] + + frappe.throw(msg) @frappe.whitelist() @@ -70,11 +67,13 @@ def take_backups_weekly(): def take_backups_monthly(): take_backups_if("Monthly") + def take_backups_if(freq): if cint(frappe.db.get_value("S3 Backup Settings", None, "enabled")): if frappe.db.get_value("S3 Backup Settings", None, "frequency") == freq: take_backups_s3() + @frappe.whitelist() def take_backups_s3(retry_count=0): try: @@ -146,42 +145,13 @@ def backup_to_s3(): if files_filename: upload_file_to_s3(files_filename, folder, conn, bucket) - delete_old_backups(doc.backup_limit, bucket) - def upload_file_to_s3(filename, folder, conn, bucket): destpath = os.path.join(folder, os.path.basename(filename)) try: print("Uploading file:", filename) - conn.upload_file(filename, bucket, destpath) + conn.upload_file(filename, bucket, destpath) # Requires PutObject permission except Exception as e: frappe.log_error() print("Error uploading: %s" % (e)) - - -def delete_old_backups(limit, bucket): - all_backups = [] - doc = frappe.get_single("S3 Backup Settings") - backup_limit = int(limit) - - s3 = boto3.resource( - 's3', - aws_access_key_id=doc.access_key_id, - aws_secret_access_key=doc.get_password('secret_access_key'), - endpoint_url=doc.endpoint_url or 'https://s3.amazonaws.com' - ) - - bucket = s3.Bucket(bucket) - objects = bucket.meta.client.list_objects_v2(Bucket=bucket.name, Delimiter='/') - if objects: - for obj in objects.get('CommonPrefixes'): - all_backups.append(obj.get('Prefix')) - - oldest_backup = sorted(all_backups)[0] if all_backups else '' - - if len(all_backups) > backup_limit: - print("Deleting Backup: {0}".format(oldest_backup)) - for obj in bucket.objects.filter(Prefix=oldest_backup): - # delete all keys that are inside the oldest_backup - s3.Object(bucket.name, obj.key).delete() From 585215ccee981cdc0b2a986b443d8cbdd7ad1f15 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 8 Dec 2020 13:38:12 +0530 Subject: [PATCH 139/284] fix: fields get reordered after adding new columns --- .../js/frappe/views/reports/report_view.js | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 026e120c50..13c07a21e7 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -708,6 +708,32 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { super.build_fields(); } + reorder_fields() { + // generate table fields in the required format ["name", "DocType"] + // these are fields in the column before adding new fields + let table_fields = this.columns.map(df => [df.field, df.docfield.parent]); + + // filter fields that are already in table + // iterate over table_fields to preserve the existing order of fields + // The filter will ensure the unchecked fields are removed + let fields_already_in_table = table_fields.filter(df => { + return this.fields.find((field) => { + return df[0] == field[0] && df[1] == field[1] + }) + }) + + // find new fields that didn't already exists + // This will be appended to the end of the table + let fields_to_add = this.fields.filter(df => { + return !table_fields.find((field) => { + return df[0] == field[0] && df[1] == field[1] + }) + }) + + // rebuild fields + this.fields = [...fields_already_in_table, ...fields_to_add]; + } + get_fields() { let fields = this.fields.map(f => { let column_name = frappe.model.get_full_column_name(f[0], f[1]); @@ -1329,6 +1355,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { this.fields.map(f => this.add_currency_column(f[0], f[1])); + this.reorder_fields(); this.build_fields(); this.setup_columns(); From a4e11583ddff106fff66451d062271374966e0b6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 10 Dec 2020 11:56:31 +0100 Subject: [PATCH 140/284] fix: no multiline source or context, allow null --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 184d5bde73..863175a028 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(([\"'`]{1,3})(?P((?!\1)[\s\S])*)\1(\s*,\s*context\s*=\s*([\"']{1,3})(?P((?!\5)[\s\S])*)\5){0,1}(\s*,\s*([\s\S])*\s*(,\s*([\"'`])(?P((?!\11).)*)\11)*){0,1}\)") +pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From f0ae807df83009fc2fe9f7e82111b3404a395f60 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 10:31:36 +0530 Subject: [PATCH 141/284] fix: fallback for global default precision --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 7f5c3bf472..a03a4d8405 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,7 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision")) + "%", options); + return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision") || 3) + "%", options); }, Rating: function(value) { return ` From e7417ebe486bd5d60a73056735743c079fc44597 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 11 Dec 2020 11:31:56 +0530 Subject: [PATCH 142/284] fix: fallback for percent field precision to 2 --- frappe/public/js/frappe/form/formatters.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index a03a4d8405..f163f59171 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,8 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, frappe.defaults.get_default("float_precision") || 3) + "%", options); + var precision = docfield.precision || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || 2; + return frappe.form.formatters._right(flt(value, precision) + "%", options); }, Rating: function(value) { return ` From 6398ca7db7ea9565f5fef78cab584ac771c59f51 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Fri, 11 Dec 2020 12:11:07 +0530 Subject: [PATCH 143/284] refactor: move common functions as methods to the Auto Repeat class --- .../doctype/auto_repeat/auto_repeat.js | 5 +- .../doctype/auto_repeat/auto_repeat.py | 143 +++++++++--------- .../doctype/auto_repeat/test_auto_repeat.py | 4 +- 3 files changed, 70 insertions(+), 82 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index afee9b98bb..e914ff27b0 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -100,10 +100,7 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frm.call({ - method: "get_auto_repeat_schedule", - doc: frm.doc - }).then((r) => { + frm.call("get_auto_repeat_schedule").then((r) => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 205d49df56..1fd2cdf1b3 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -19,7 +19,6 @@ from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeat month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} - class AutoRepeat(Document): def validate(self): self.update_status() @@ -53,7 +52,7 @@ class AutoRepeat(Document): if self.disabled: self.next_schedule_date = None else: - self.next_schedule_date = get_next_schedule_date(schedule_date=self.start_date, auto_repeat_doc=self) + self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -93,7 +92,7 @@ class AutoRepeat(Document): frappe.throw(_("'Recipients' not specified")) def validate_auto_repeat_days(self): - auto_repeat_days = get_auto_repeat_days(self) + auto_repeat_days = self.get_auto_repeat_days() if not len(set(auto_repeat_days)) == len(auto_repeat_days): repeated_days = get_repeated(auto_repeat_days) frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) @@ -123,7 +122,7 @@ class AutoRepeat(Document): end_date = getdate(self.end_date) if not self.end_date: - next_date = get_next_schedule_date(schedule_date=start_date, auto_repeat_doc=self) + next_date = self.get_next_schedule_date(schedule_date=start_date) row = { "reference_document": self.reference_document, "frequency": self.frequency, @@ -132,8 +131,7 @@ class AutoRepeat(Document): schedule_details.append(row) if self.end_date: - next_date = get_next_schedule_date( - schedule_date=start_date, auto_repeat_doc=self, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) while (getdate(next_date) < getdate(end_date)): row = { @@ -142,8 +140,7 @@ class AutoRepeat(Document): "next_scheduled_date" : next_date } schedule_details.append(row) - next_date = get_next_schedule_date( - schedule_date=next_date, auto_repeat_doc=self, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) return schedule_details @@ -221,6 +218,68 @@ class AutoRepeat(Document): new_doc.set('from_date', from_date) new_doc.set('to_date', to_date) + def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + if month_map.get(self.frequency): + month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 + else: + month_count = 0 + + day_count = 0 + if month_count and self.repeat_on_last_day: + day_count = 31 + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count and self.repeat_on_day: + day_count = self.repeat_on_day + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count: + next_date = get_next_date(self.start_date, month_count) + else: + days = self.get_days(schedule_date) + next_date = add_days(schedule_date, days) + + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + if month_count: + month_count += month_map.get(self.frequency) + next_date = get_next_date(self.start_date, month_count, day_count) + elif days: + days = self.get_days(next_date) + next_date = add_days(next_date, days) + + return next_date + + def get_days(self, schedule_date): + if self.frequency == "Weekly": + days = self.get_offset_for_weekly_frequency(schedule_date) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(self, schedule_date): + # if weekdays are not set, offset is 7 from current schedule date + if not self.repeat_on_days: + return 7 + + repeat_on_days = self.get_auto_repeat_days() + current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) + + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday) + # offset for upcoming weekday + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + return 7 + + def get_auto_repeat_days(self): + return [d.day for d in self.get('repeat_on_days', [])] + def send_notification(self, new_doc): """Notify concerned people about recurring document generation""" subject = self.subject or '' @@ -301,74 +360,12 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(schedule_date, auto_repeat_doc=None, for_full_schedule=False): - if month_map.get(auto_repeat_doc.frequency): - month_count = month_map.get(auto_repeat_doc.frequency) + month_diff(schedule_date, auto_repeat_doc.start_date) - 1 - else: - month_count = 0 - - day_count = 0 - if month_count and auto_repeat_doc.repeat_on_last_day: - day_count = 31 - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count and auto_repeat_doc.repeat_on_day: - day_count = auto_repeat_doc.repeat_on_day - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif month_count: - next_date = get_next_date(auto_repeat_doc.start_date, month_count) - else: - days = get_days(schedule_date, auto_repeat_doc) - next_date = add_days(schedule_date, days) - - # next schedule date should be after or on current date - if not for_full_schedule: - while getdate(next_date) < getdate(today()): - if month_count: - month_count += month_map.get(auto_repeat_doc.frequency) - next_date = get_next_date(auto_repeat_doc.start_date, month_count, day_count) - elif days: - days = get_days(next_date, auto_repeat_doc) - next_date = add_days(next_date, days) - - return next_date - - def get_next_date(dt, mcount, day=None): dt = getdate(dt) dt += relativedelta(months=mcount, day=day) return dt -def get_days(schedule_date, auto_repeat_doc): - if auto_repeat_doc.frequency == "Weekly": - days = get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc) - else: - # daily frequency - days = 1 - - return days - - -def get_offset_for_weekly_frequency(schedule_date, auto_repeat_doc): - # if weekdays are not set, offset is 7 from current schedule date - if not auto_repeat_doc.repeat_on_days: - return 7 - - repeat_on_days = get_auto_repeat_days(auto_repeat_doc) - current_schedule_day = getdate(schedule_date).weekday() - weekdays = list(week_map.keys()) - - # if repeats on more than 1 day or - # start date's weekday is not in repeat days, then get next weekday - # else offset is 7 - if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: - weekday = get_next_weekday(current_schedule_day, repeat_on_days) - next_weekday_number = week_map.get(weekday) - # offset for upcoming weekday - return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days - return 7 - - def get_next_weekday(current_schedule_day, weekdays): days = list(week_map.keys()) if current_schedule_day > 0: @@ -381,10 +378,6 @@ def get_next_weekday(current_schedule_day, weekdays): return entry -def get_auto_repeat_days(doc): - return [d.day for d in doc.get('repeat_on_days', [])] - - #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' @@ -405,7 +398,7 @@ def create_repeated_entries(data): if schedule_date == current_date and not doc.disabled: doc.create_documents() - schedule_date = get_next_schedule_date(schedule_date=schedule_date, auto_repeat_doc=doc) + schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index a9bfbb1cf8..0d6229cd9e 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -7,11 +7,9 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries +from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map from frappe.utils import today, add_days, getdate, add_months -week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} - def add_custom_fields(): df = dict( fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', From 56302986036a367c86d38445a59116928f826624 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 11 Dec 2020 12:34:09 +0530 Subject: [PATCH 144/284] fix: Reload server script via patch (#12076) --- frappe/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/patches.txt b/frappe/patches.txt index 0daf29e001..b459019dd7 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -21,6 +21,7 @@ execute:frappe.reload_doc('email', 'doctype', 'document_follow') execute:frappe.reload_doc('core', 'doctype', 'communication_link') #2019-10-02 execute:frappe.reload_doc('core', 'doctype', 'has_role') execute:frappe.reload_doc('core', 'doctype', 'communication') #2019-10-02 +execute:frappe.reload_doc('core', 'doctype', 'server_script') frappe.patches.v11_0.replicate_old_user_permissions frappe.patches.v11_0.reload_and_rename_view_log #2019-01-03 frappe.patches.v7_1.rename_scheduler_log_to_error_log From 5f708fa160ef9df5553aca0eb7292539ccb07cac Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 11 Dec 2020 12:47:16 +0530 Subject: [PATCH 145/284] feat: cache bench apps --- frappe/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index c5f13f2295..2bab1cd294 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -348,7 +348,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, if as_table and type(msg) in (list, tuple): out.as_table = 1 - + if as_list and type(msg) in (list, tuple) and len(msg) > 1: out.as_list = 1 @@ -939,7 +939,11 @@ def get_installed_apps(sort=False, frappe_last=False): connect() if not local.all_apps: - local.all_apps = get_all_apps(True) + local.all_apps = cache().get_value('all_apps', get_all_apps) + + #cache bench apps + if not cache().get_value('all_apps'): + cache().set_value('all_apps', local.all_apps) installed = json.loads(db.get_global("installed_apps") or "[]") From 568426668fb60bc6c9ac044dd50930825274db11 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 11 Dec 2020 13:55:31 +0530 Subject: [PATCH 146/284] feat: add alert flag for permission validation In case default permissions are not set, the alert flag will indicate if an alert has to be shown in the UI or not --- frappe/core/doctype/doctype/doctype.py | 9 +++++---- .../page/permission_manager/permission_manager.py | 14 +++++++++++++- frappe/permissions.py | 4 ++-- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index cce5968f9c..fced5d1fa1 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1000,10 +1000,10 @@ def validate_fields(meta): check_sort_field(meta) check_image_field(meta) -def validate_permissions_for_doctype(doctype, for_remove=False): +def validate_permissions_for_doctype(doctype, for_remove=False, alert=True): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) - validate_permissions(doctype, for_remove) + validate_permissions(doctype, for_remove, alert=alert) # save permissions for perm in doctype.get("permissions"): @@ -1026,9 +1026,10 @@ def clear_permissions_cache(doctype): """, doctype): frappe.clear_cache(user=user) -def validate_permissions(doctype, for_remove=False): +def validate_permissions(doctype, for_remove=False, alert=True): permissions = doctype.get("permissions") - if not permissions: + # Some DocTypes may not have permissions by default, don't show alert for them + if not permissions and alert: frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange') issingle = issubmittable = isimportable = False if doctype: diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 637b526d5c..5b4ccb6ce0 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -77,8 +77,20 @@ def add(parent, role, permlevel): @frappe.whitelist() def update(doctype, role, permlevel, ptype, value=None): + """Update role permission params + + Args: + doctype (str): Name of the DocType to update params for + role (str): Role to be updated for, eg "Website Manager". + permlevel (int): perm level the provided rule applies to + ptype (str): permission type, example "read", "delete", etc. + value (None, optional): value for ptype, None indicates False + + Returns: + str: Refresh flag is permission is updated successfully + """ frappe.only_for("System Manager") - out = update_permission_property(doctype, role, permlevel, ptype, value) + out = update_permission_property(doctype, role, permlevel, ptype, value, alert=False) return 'refresh' if out else None @frappe.whitelist() diff --git a/frappe/permissions.py b/frappe/permissions.py index 0d766aec8d..e9724b7418 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -446,7 +446,7 @@ def can_export(doctype, raise_exception=False): raise frappe.PermissionError(_("You are not allowed to export {} doctype").format(doctype)) return has_access -def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): +def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True, alert=True): '''Update a property in Custom Perm''' from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype out = setup_custom_perms(doctype) @@ -458,7 +458,7 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali update `tabCustom DocPerm` set `{0}`=%s where name=%s""".format(ptype), (value, name)) if validate: - validate_permissions_for_doctype(doctype) + validate_permissions_for_doctype(doctype, alert=alert) return out From 24e021474f094fc3e7bd9fd46af5b900860cee70 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Mon, 14 Dec 2020 02:01:24 +0100 Subject: [PATCH 147/284] chore: Move inline styles to CSS class Signed-off-by: mathieu.brunot --- frappe/public/css/list.css | 3 +++ frappe/public/js/frappe/views/map/map_view.js | 19 ++++++++++--------- frappe/public/less/list.less | 3 +++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 49ffbcd9e9..88ad147d33 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -404,6 +404,9 @@ input.list-row-checkbox { .map-view-container { display: flex; flex-wrap: wrap; + width: 100%; + height: calc(100vh - 284px); + z-index: 0; } .list-paging-area .gantt-view-mode { margin-left: 15px; diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 2c068277ad..8a75ecc457 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -32,13 +32,14 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { this.map_id = frappe.dom.get_unique_id(); this.$result.html(` -
    +
    `); - this.map = L.map(this.map_id).setView([12.3112899, -85.7384542], 8); //coords of India if markers does not exists + //coords of India if markers does not exists + this.map = L.map(this.map_id).setView([12.3112899, -85.7384542], 8); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', @@ -46,13 +47,13 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { }).addTo(this.map); L.control.scale().addTo(this.map); -if (this.coords.features && this.coords.features.length) { - this.coords.features.forEach( - coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) - ); - let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); - this.map.panTo(lastCoords, 8); -} + if (this.coords.features && this.coords.features.length) { + this.coords.features.forEach( + coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) + ); + let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); + this.map.panTo(lastCoords, 8); + } } get_coords() { diff --git a/frappe/public/less/list.less b/frappe/public/less/list.less index 662e753b38..fba99ee50d 100644 --- a/frappe/public/less/list.less +++ b/frappe/public/less/list.less @@ -487,6 +487,9 @@ input.list-check-all, input.list-row-checkbox { .map-view-container { display: flex; flex-wrap: wrap; + width: 100%; + height: calc(100vh - 284px); + z-index: 0; } // list view From f2747a0de4258649782644e5a350bb5ebae25029 Mon Sep 17 00:00:00 2001 From: Mathieu Brunot Date: Mon, 14 Dec 2020 02:02:01 +0100 Subject: [PATCH 148/284] Update frappe/public/js/frappe/views/map/map_view.js Co-authored-by: Prssanna Desai --- frappe/public/js/frappe/views/map/map_view.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 2c068277ad..0ec8353ccb 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -31,11 +31,7 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { render_map_view() { this.map_id = frappe.dom.get_unique_id(); - this.$result.html(` -
    - -
    - `); + this.$result.html(`
    `); this.map = L.map(this.map_id).setView([12.3112899, -85.7384542], 8); //coords of India if markers does not exists From 881c42629650837dc05940fb45167b399d8fb6a1 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 11:39:21 +0530 Subject: [PATCH 149/284] fix: typo --- frappe/printing/doctype/print_format/print_format.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 6e64e802c9..3a47fb554f 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -201,17 +201,17 @@ { "default": "0", "depends_on": "doc_type", - "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive", "fieldname": "absolute_value", "fieldtype": "Check", - "label": "Show absolute values" + "label": "Show Absolute Values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-10 18:58:55.598269", + "modified": "2020-12-14 11:38:49.132061", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", From 9e4e36041ae2ec5e0c85a3ae929a716626f09312 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 14 Dec 2020 11:48:10 +0530 Subject: [PATCH 150/284] fix: toggling of show absolute values checkbox --- frappe/printing/doctype/print_format/print_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 9ef5652dda..786f8f97ab 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", { hide_absolute_value_field: function (frm) { // TODO: make it work with frm.doc.doc_type // Problem: frm isn't updated in some random cases - const doctype = locals[frm.doc.doctype][frm.doc.name]; + const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; if (doctype) { frappe.model.with_doctype(doctype, () => { const meta = frappe.get_meta(doctype); From 8c539743cf5ad77cc8cb2dd42034b7d5ef4cc5ae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:18:58 +0000 Subject: [PATCH 151/284] chore(deps): bump ini from 1.3.5 to 1.3.8 Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8. - [Release notes](https://github.com/isaacs/ini/releases) - [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8) Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 26797675c6..e0383afccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2918,9 +2918,9 @@ inherits@2.0.3: integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: - version "1.3.5" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" - integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== inquirer@^7.3.3: version "7.3.3" From 7a773d24611fa259749da0b0190a88e9d4590f62 Mon Sep 17 00:00:00 2001 From: conncampbell Date: Sun, 8 Nov 2020 10:02:35 -0700 Subject: [PATCH 152/284] feat: Workflow transition condition allows datetime functions --- frappe/model/workflow.py | 10 ++++++++-- .../workflow_transition/workflow_transition.json | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 72ce8c9ce4..d0206a9f60 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -53,14 +53,20 @@ def get_transitions(doc, workflow = None, raise_exception=False): return transitions def get_workflow_safe_globals(): - # access to frappe.db.get_value and frappe.db.get_list + # access to frappe.db.get_value, frappe.db.get_list, and date time utils. return dict( frappe=frappe._dict( db=frappe._dict( get_value=frappe.db.get_value, get_list=frappe.db.get_list ), - session=frappe.session + session=frappe.session, + utils=frappe._dict( + now_datetime=frappe.utils.now_datetime, + add_to_date=frappe.utils.add_to_date, + get_datetime=frappe.utils.get_datetime, + now=frappe.utils.now + ) ) ) diff --git a/frappe/workflow/doctype/workflow_transition/workflow_transition.json b/frappe/workflow/doctype/workflow_transition/workflow_transition.json index 8bc06bf18a..5e5cec5880 100644 --- a/frappe/workflow/doctype/workflow_transition/workflow_transition.json +++ b/frappe/workflow/doctype/workflow_transition/workflow_transition.json @@ -295,7 +295,7 @@ "label": "Example", "length": 0, "no_copy": 0, - "options": "
    doc.grand_total > 0
    \n\n

    Conditions should be written in simple Python. Please use properties available in the form only.

    ", + "options": "
    doc.grand_total > 0
    \n\n

    Conditions should be written in simple Python. Please use properties available in the form only.

    \n

    Allowed functions: \n

      \n
    • frappe.db.get_value
    • \n
    • frappe.db.get_list
    • \n
    • frappe.session
    • \n
    • frappe.utils.now_datetime
    • \n
    • frappe.utils.get_datetime
    • \n
    • frappe.utils.add_to_date
    • \n
    • frappe.utils.now
    • \n
    \n

    Example:

    doc.creation > frappe.utils.add_to_date(frappe.utils.now_datetime(), days=-5, as_string=True, as_datetime=True) 

    ", "permlevel": 0, "precision": "", "print_hide": 0, @@ -320,7 +320,7 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-10-09 10:28:53.294908", + "modified": "2020-11-08 12:11:00.294908", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow Transition", From 06fcb822054d364089646eb947b431180852a46f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 15 Dec 2020 12:35:30 +0530 Subject: [PATCH 153/284] fix: call init_callback after set_route --- frappe/public/js/frappe/form/quick_entry.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index 0a489e26d6..eed49e070b 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -35,14 +35,15 @@ frappe.ui.form.QuickEntryForm = Class.extend({ if (this.is_quick_entry() || this.force) { this.render_dialog(); resolve(this); - } else { // No quick entry, use full Form - // but still give callback a shot at the doc - if (this.init_callback) { - this.init_callback(this.doc); - } + } else { + // no quick entry, open full form frappe.quick_entry = null; frappe.set_route('Form', this.doctype, this.doc.name) .then(() => resolve(this)); + // call init_callback for consistency + if (this.init_callback) { + this.init_callback(this.doc); + } } }); }); From 8ef2f7ed1896f7a055e42b456bba8152a69ac1a7 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 15 Dec 2020 13:54:27 +0530 Subject: [PATCH 154/284] style: Re-format and use const instead of var --- frappe/public/js/frappe/form/formatters.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index f163f59171..f9a1d0b643 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,14 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - var precision = docfield.precision || cint(frappe.boot.sysdefaults && frappe.boot.sysdefaults.float_precision) || 2; + const precision = ( + docfield.precision + || cint( + frappe.boot.sysdefaults + && frappe.boot.sysdefaults.float_precision + ) + || 2 + ); return frappe.form.formatters._right(flt(value, precision) + "%", options); }, Rating: function(value) { From 2d8550619d2701b2d5230e33457db1a21b6249d3 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 15 Dec 2020 16:04:16 +0530 Subject: [PATCH 155/284] fix: Reset frappe.flag.link_fields in test - Reset frappe.flag.link_fields in rename doctype tests - Teardown doctypes appropriately after test --- frappe/tests/test_rename_doc.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index fb0776a485..0f094749d0 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -55,6 +55,8 @@ class TestRenameDoc(unittest.TestCase): frappe.delete_doc("DocType", dt) frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{dt}`") + frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + def test_rename_doc(self): """Rename an existing document via frappe.rename_doc""" old_name = choice(self.available_documents) @@ -87,6 +89,7 @@ class TestRenameDoc(unittest.TestCase): # check if module exists exists; # if custom, get_controller will return Document class # if not custom, a different class will be returned + frappe.flags.link_fields = {} self.assertNotEqual(get_controller(self.doctype.old), frappe.model.document.Document) old_doctype_path = get_doc_path("Custom", "DocType", self.doctype.old) @@ -103,6 +106,7 @@ class TestRenameDoc(unittest.TestCase): """Rename DocType via frappe.rename_doc""" from frappe.core.doctype.doctype.test_doctype import new_doctype + frappe.flags.link_fields = {} if not frappe.db.exists("DocType", "Rename This"): new_doctype( "Rename This", @@ -140,5 +144,7 @@ class TestRenameDoc(unittest.TestCase): new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True) ) - frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + # delete_doc doesnt drop tables + # this is done to bypass inconsistencies in the db frappe.delete_doc_if_exists("DocType", "Renamed Doc") + frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`") \ No newline at end of file From 9d75094ba9f5d5c32e98eca56123d8f1b8674d3e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 15 Dec 2020 19:06:42 +0530 Subject: [PATCH 156/284] chore: Move reset flags to setup method Reason for unsetting flags in setUp: 1. frappe.flags.whatever is reset in each request...since it's a werzeug local (ref: https://werkzeug.palletsprojects.com/en/1.0.x/local/) 2. so until it is in the context of current request, it has the same value 3. when you call rename_doc via a request, it's flags are forgotten post that 4. but in a test suite, the whole module's tests run in a single process and the flags aren't (un|re)set 5. which is why frappe.flags.whatever has the same value throughout this module's tests --- frappe/tests/test_rename_doc.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 0f094749d0..9aa175d2d4 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -57,6 +57,10 @@ class TestRenameDoc(unittest.TestCase): frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + def setUp(self): + frappe.flags.link_fields = {} + super().setUp() + def test_rename_doc(self): """Rename an existing document via frappe.rename_doc""" old_name = choice(self.available_documents) @@ -89,7 +93,6 @@ class TestRenameDoc(unittest.TestCase): # check if module exists exists; # if custom, get_controller will return Document class # if not custom, a different class will be returned - frappe.flags.link_fields = {} self.assertNotEqual(get_controller(self.doctype.old), frappe.model.document.Document) old_doctype_path = get_doc_path("Custom", "DocType", self.doctype.old) @@ -106,7 +109,6 @@ class TestRenameDoc(unittest.TestCase): """Rename DocType via frappe.rename_doc""" from frappe.core.doctype.doctype.test_doctype import new_doctype - frappe.flags.link_fields = {} if not frappe.db.exists("DocType", "Rename This"): new_doctype( "Rename This", @@ -144,7 +146,7 @@ class TestRenameDoc(unittest.TestCase): new_name, frappe.rename_doc("Renamed Doc", old_name, new_name, force=True) ) - # delete_doc doesnt drop tables + # delete_doc doesnt drop tables # this is done to bypass inconsistencies in the db frappe.delete_doc_if_exists("DocType", "Renamed Doc") - frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`") \ No newline at end of file + frappe.db.sql_ddl("drop table if exists `tabRenamed Doc`") From f4f7e8e798940684cfbdb76d05ac7b88d6f4dce0 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Wed, 16 Dec 2020 07:04:07 +0200 Subject: [PATCH 157/284] chore(Snyk): Security upgrade frappe-charts from 1.5.4 to 1.5.5 (#12086) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index d1a94d0e35..114de53384 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "driver.js": "^0.9.8", "express": "^4.17.1", "fast-deep-equal": "^2.0.1", - "frappe-charts": "^1.5.1", + "frappe-charts": "^1.5.5", "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", diff --git a/yarn.lock b/yarn.lock index e0383afccb..3f8eaa3d9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2299,10 +2299,10 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -frappe-charts@^1.5.1: - version "1.5.4" - resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.4.tgz#5870f77ac6ffc8ea4dab32adda1d4e5e4fbda64b" - integrity sha512-hBr7cRLmsCC5VBj/HwKOCgdwyXnkeAO5CAvOd5H4IYFbk84VD9jOjx9fSaqAE0MygVVbY1nCN+5nb08WThW4Xw== +frappe-charts@^1.5.5: + version "1.5.5" + resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-1.5.5.tgz#5f44a3639aecc6f8fc7d15752abc80bb68e26734" + integrity sha512-L9pJTsrSuRobS/EaBKT8i1x+DVOjkXyUwT85cteZAPqynU/7K+uqjQOy4tMSTv5zsTWJNWFJ37ax68T73YdR3g== frappe-datatable@^1.15.3: version "1.15.3" From 4c1756b3d633568500179c39066aeaa6da50962c Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 11:29:05 +0530 Subject: [PATCH 158/284] fix: code clean-up and fallbacks for get method --- frappe/automation/doctype/auto_repeat/auto_repeat.js | 2 +- frappe/automation/doctype/auto_repeat/auto_repeat.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index e914ff27b0..c2c84692d8 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -100,7 +100,7 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frm.call("get_auto_repeat_schedule").then((r) => { + frm.call("get_auto_repeat_schedule").then(r => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 1fd2cdf1b3..7dbbcdd05d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -241,9 +241,9 @@ class AutoRepeat(Document): if not for_full_schedule: while getdate(next_date) < getdate(today()): if month_count: - month_count += month_map.get(self.frequency) + month_count += month_map.get(self.frequency, 0) next_date = get_next_date(self.start_date, month_count, day_count) - elif days: + else: days = self.get_days(next_date) next_date = add_days(next_date, days) @@ -272,7 +272,7 @@ class AutoRepeat(Document): # else offset is 7 if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: weekday = get_next_weekday(current_schedule_day, repeat_on_days) - next_weekday_number = week_map.get(weekday) + next_weekday_number = week_map.get(weekday, 0) # offset for upcoming weekday return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days return 7 From 7d70e552aaa7bde72bc88a324b7c09d097f9718f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 16 Dec 2020 12:09:10 +0530 Subject: [PATCH 159/284] chore: added a docstring for the get_next_schedule_date method --- frappe/automation/doctype/auto_repeat/auto_repeat.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 7dbbcdd05d..830af68de7 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -219,6 +219,13 @@ class AutoRepeat(Document): new_doc.set('to_date', to_date) def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + """ + Returns the next schedule date for auto repeat after a recurring document has been created. + Adds required offset to the schedule_date param and returns the next schedule date. + + :param schedule_date: The date when the last recurring document was created. + :param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. + """ if month_map.get(self.frequency): month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 else: From ed01c67f543412fda99e747bf7d99b86320cb34c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Dec 2020 15:17:20 +0530 Subject: [PATCH 160/284] style: Black over updated diffs --- frappe/model/workflow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index d0206a9f60..43e26cc5d0 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -56,17 +56,14 @@ def get_workflow_safe_globals(): # access to frappe.db.get_value, frappe.db.get_list, and date time utils. return dict( frappe=frappe._dict( - db=frappe._dict( - get_value=frappe.db.get_value, - get_list=frappe.db.get_list - ), + db=frappe._dict(get_value=frappe.db.get_value, get_list=frappe.db.get_list), session=frappe.session, utils=frappe._dict( now_datetime=frappe.utils.now_datetime, add_to_date=frappe.utils.add_to_date, get_datetime=frappe.utils.get_datetime, - now=frappe.utils.now - ) + now=frappe.utils.now, + ), ) ) From aea6123348e78c17008d2a4104aacb0a3f55f42a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Dec 2020 17:39:29 +0530 Subject: [PATCH 161/284] chore: Drop unused? highlight.pack.js --- frappe/public/js/lib/highlight.pack.js | 1 - 1 file changed, 1 deletion(-) delete mode 100755 frappe/public/js/lib/highlight.pack.js diff --git a/frappe/public/js/lib/highlight.pack.js b/frappe/public/js/lib/highlight.pack.js deleted file mode 100755 index ecee8ad109..0000000000 --- a/frappe/public/js/lib/highlight.pack.js +++ /dev/null @@ -1 +0,0 @@ -var hljs=new function(){function e(e){return e.replace(/&/gm,"&").replace(//gm,">")}function t(e){return e.nodeName.toLowerCase()}function n(e,t){var n=e&&e.exec(t);return n&&0==n.index}function r(e){var t=(e.className+" "+(e.parentNode?e.parentNode.className:"")).split(/\s+/);return t=t.map(function(e){return e.replace(/^lang(uage)?-/,"")}),t.filter(function(e){return m(e)||/no(-?)highlight/.test(e)})[0]}function i(e,t){var n={};for(var r in e)n[r]=e[r];if(t)for(var r in t)n[r]=t[r];return n}function a(e){var n=[];return function r(e,i){for(var a=e.firstChild;a;a=a.nextSibling)3==a.nodeType?i+=a.nodeValue.length:1==a.nodeType&&(n.push({event:"start",offset:i,node:a}),i=r(a,i),t(a).match(/br|hr|img|input/)||n.push({event:"stop",offset:i,node:a}));return i}(e,0),n}function s(n,r,i){function a(){return n.length&&r.length?n[0].offset!=r[0].offset?n[0].offset"}function o(e){l+=""}function c(e){("start"==e.event?s:o)(e.node)}for(var u=0,l="",f=[];n.length||r.length;){var h=a();if(l+=e(i.substr(u,h[0].offset-u)),u=h[0].offset,h==n){f.reverse().forEach(o);do c(h.splice(0,1)[0]),h=a();while(h==n&&h.length&&h[0].offset==u);f.reverse().forEach(s)}else"start"==h[0].event?f.push(h[0].node):f.pop(),c(h.splice(0,1)[0])}return l+e(i.substr(u))}function o(e){function t(e){return e&&e.source||e}function n(n,r){return RegExp(t(n),"m"+(e.cI?"i":"")+(r?"g":""))}function r(a,s){if(!a.compiled){if(a.compiled=!0,a.k=a.k||a.bK,a.k){var o={},c=function(t,n){e.cI&&(n=n.toLowerCase()),n.split(" ").forEach(function(e){var n=e.split("|");o[n[0]]=[t,n[1]?Number(n[1]):1]})};"string"==typeof a.k?c("keyword",a.k):Object.keys(a.k).forEach(function(e){c(e,a.k[e])}),a.k=o}a.lR=n(a.l||/\b[A-Za-z0-9_]+\b/,!0),s&&(a.bK&&(a.b="\\b("+a.bK.split(" ").join("|")+")\\b"),a.b||(a.b=/\B|\b/),a.bR=n(a.b),a.e||a.eW||(a.e=/\B|\b/),a.e&&(a.eR=n(a.e)),a.tE=t(a.e)||"",a.eW&&s.tE&&(a.tE+=(a.e?"|":"")+s.tE)),a.i&&(a.iR=n(a.i)),void 0===a.r&&(a.r=1),a.c||(a.c=[]);var u=[];a.c.forEach(function(e){e.v?e.v.forEach(function(t){u.push(i(e,t))}):u.push("self"==e?a:e)}),a.c=u,a.c.forEach(function(e){r(e,a)}),a.starts&&r(a.starts,s);var l=a.c.map(function(e){return e.bK?"\\.?("+e.b+")\\.?":e.b}).concat([a.tE,a.i]).map(t).filter(Boolean);a.t=l.length?n(l.join("|"),!0):{exec:function(){return null}}}}r(e)}function c(t,r,i,a){function s(e,t){for(var r=0;r";return a+=e+'">',a+t+s}function p(){if(!w.k)return e(B);var t="",n=0;w.lR.lastIndex=0;for(var r=w.lR.exec(B);r;){t+=e(B.substr(n,r.index-n));var i=h(w,r);i?(y+=i[1],t+=g(i[0],e(r[0]))):t+=e(r[0]),n=w.lR.lastIndex,r=w.lR.exec(B)}return t+e(B.substr(n))}function v(){if(w.sL&&!E[w.sL])return e(B);var t=w.sL?c(w.sL,B,!0,L[w.sL]):u(B);return w.r>0&&(y+=t.r),"continuous"==w.subLanguageMode&&(L[w.sL]=t.top),g(t.language,t.value,!1,!0)}function b(){return void 0!==w.sL?v():p()}function d(t,n){var r=t.cN?g(t.cN,"",!0):"";t.rB?(M+=r,B=""):t.eB?(M+=e(n)+r,B=""):(M+=r,B=n),w=Object.create(t,{parent:{value:w}})}function R(t,n){if(B+=t,void 0===n)return M+=b(),0;var r=s(n,w);if(r)return M+=b(),d(r,n),r.rB?0:n.length;var i=l(w,n);if(i){var a=w;a.rE||a.eE||(B+=n),M+=b();do w.cN&&(M+=""),y+=w.r,w=w.parent;while(w!=i.parent);return a.eE&&(M+=e(n)),B="",i.starts&&d(i.starts,""),a.rE?0:n.length}if(f(n,w))throw new Error('Illegal lexeme "'+n+'" for mode "'+(w.cN||"")+'"');return B+=n,n.length||1}var x=m(t);if(!x)throw new Error('Unknown language: "'+t+'"');o(x);for(var w=a||x,L={},M="",k=w;k!=x;k=k.parent)k.cN&&(M=g(k.cN,"",!0)+M);var B="",y=0;try{for(var C,I,j=0;;){if(w.t.lastIndex=j,C=w.t.exec(r),!C)break;I=R(r.substr(j,C.index-j),C[0]),j=C.index+I}R(r.substr(j));for(var k=w;k.parent;k=k.parent)k.cN&&(M+="");return{r:y,value:M,language:t,top:w}}catch(A){if(-1!=A.message.indexOf("Illegal"))return{r:0,value:e(r)};throw A}}function u(t,n){n=n||N.languages||Object.keys(E);var r={r:0,value:e(t)},i=r;return n.forEach(function(e){if(m(e)){var n=c(e,t,!1);n.language=e,n.r>i.r&&(i=n),n.r>r.r&&(i=r,r=n)}}),i.language&&(r.second_best=i),r}function l(e){return N.tabReplace&&(e=e.replace(/^((<[^>]+>|\t)+)/gm,function(e,t){return t.replace(/\t/g,N.tabReplace)})),N.useBR&&(e=e.replace(/\n/g,"
    ")),e}function f(e,t,n){var r=t?R[t]:n,i=[e.trim()];return e.match(/(\s|^)hljs(\s|$)/)||i.push("hljs"),r&&i.push(r),i.join(" ").trim()}function h(e){var t=r(e);if(!/no(-?)highlight/.test(t)){var n;N.useBR?(n=document.createElementNS("http://www.w3.org/1999/xhtml","div"),n.innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n")):n=e;var i=n.textContent,o=t?c(t,i,!0):u(i),h=a(n);if(h.length){var g=document.createElementNS("http://www.w3.org/1999/xhtml","div");g.innerHTML=o.value,o.value=s(h,a(g),i)}o.value=l(o.value),e.innerHTML=o.value,e.className=f(e.className,t,o.language),e.result={language:o.language,re:o.r},o.second_best&&(e.second_best={language:o.second_best.language,re:o.second_best.r})}}function g(e){N=i(N,e)}function p(){if(!p.called){p.called=!0;var e=document.querySelectorAll("pre code");Array.prototype.forEach.call(e,h)}}function v(){addEventListener("DOMContentLoaded",p,!1),addEventListener("load",p,!1)}function b(e,t){var n=E[e]=t(this);n.aliases&&n.aliases.forEach(function(t){R[t]=e})}function d(){return Object.keys(E)}function m(e){return E[e]||E[R[e]]}var N={classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0},E={},R={};this.highlight=c,this.highlightAuto=u,this.fixMarkup=l,this.highlightBlock=h,this.configure=g,this.initHighlighting=p,this.initHighlightingOnLoad=v,this.registerLanguage=b,this.listLanguages=d,this.getLanguage=m,this.inherit=i,this.IR="[a-zA-Z][a-zA-Z0-9_]*",this.UIR="[a-zA-Z_][a-zA-Z0-9_]*",this.NR="\\b\\d+(\\.\\d+)?",this.CNR="(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",this.BNR="\\b(0b[01]+)",this.RSR="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",this.BE={b:"\\\\[\\s\\S]",r:0},this.ASM={cN:"string",b:"'",e:"'",i:"\\n",c:[this.BE]},this.QSM={cN:"string",b:'"',e:'"',i:"\\n",c:[this.BE]},this.PWM={b:/\b(a|an|the|are|I|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such)\b/},this.CLCM={cN:"comment",b:"//",e:"$",c:[this.PWM]},this.CBCM={cN:"comment",b:"/\\*",e:"\\*/",c:[this.PWM]},this.HCM={cN:"comment",b:"#",e:"$",c:[this.PWM]},this.NM={cN:"number",b:this.NR,r:0},this.CNM={cN:"number",b:this.CNR,r:0},this.BNM={cN:"number",b:this.BNR,r:0},this.CSSNM={cN:"number",b:this.NR+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",r:0},this.RM={cN:"regexp",b:/\//,e:/\/[gimuy]*/,i:/\n/,c:[this.BE,{b:/\[/,e:/\]/,r:0,c:[this.BE]}]},this.TM={cN:"title",b:this.IR,r:0},this.UTM={cN:"title",b:this.UIR,r:0}};hljs.registerLanguage("markdown",function(){return{aliases:["md","mkdown","mkd"],c:[{cN:"header",v:[{b:"^#{1,6}",e:"$"},{b:"^.+?\\n[=-]{2,}$"}]},{b:"<",e:">",sL:"xml",r:0},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",v:[{b:"\\*.+?\\*"},{b:"_.+?_",r:0}]},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",v:[{b:"`.+?`"},{b:"^( {4}| )",e:"$",r:0}]},{cN:"horizontal_rule",b:"^[-\\*]{3,}",e:"$"},{b:"\\[.+?\\][\\(\\[].*?[\\)\\]]",rB:!0,c:[{cN:"link_label",b:"\\[",e:"\\]",eB:!0,rE:!0,r:0},{cN:"link_url",b:"\\]\\(",e:"\\)",eB:!0,eE:!0},{cN:"link_reference",b:"\\]\\[",e:"\\]",eB:!0,eE:!0}],r:10},{b:"^\\[.+\\]:",rB:!0,c:[{cN:"link_reference",b:"\\[",e:"\\]:",eB:!0,eE:!0,starts:{cN:"link_url",e:"$"}}]}]}});hljs.registerLanguage("javascript",function(r){return{aliases:["js"],k:{keyword:"in if for while finally var new function do return void else break catch instanceof with throw case default try this switch continue typeof delete let yield const class",literal:"true false null undefined NaN Infinity",built_in:"eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent encodeURI encodeURIComponent escape unescape Object Function Boolean Error EvalError InternalError RangeError ReferenceError StopIteration SyntaxError TypeError URIError Number Math Date String RegExp Array Float32Array Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require module console window document"},c:[{cN:"pi",b:/^\s*('|")use strict('|")/,r:10},r.ASM,r.QSM,r.CLCM,r.CBCM,r.CNM,{b:"("+r.RSR+"|\\b(case|return|throw)\\b)\\s*",k:"return throw case",c:[r.CLCM,r.CBCM,r.RM,{b:/;/,r:0,sL:"xml"}],r:0},{cN:"function",bK:"function",e:/\{/,eE:!0,c:[r.inherit(r.TM,{b:/[A-Za-z$_][0-9A-Za-z$_]*/}),{cN:"params",b:/\(/,e:/\)/,c:[r.CLCM,r.CBCM],i:/["'\(]/}],i:/\[|%/},{b:/\$[(.]/},{b:"\\."+r.IR,r:0}]}});hljs.registerLanguage("json",function(e){var t={literal:"true false null"},i=[e.QSM,e.CNM],l={cN:"value",e:",",eW:!0,eE:!0,c:i,k:t},c={b:"{",e:"}",c:[{cN:"attribute",b:'\\s*"',e:'"\\s*:\\s*',eB:!0,eE:!0,c:[e.BE],i:"\\n",starts:l}],i:"\\S"},n={b:"\\[",e:"\\]",c:[e.inherit(l,{cN:null})],i:"\\S"};return i.splice(i.length,0,c,n),{c:i,k:t,i:"\\S"}});hljs.registerLanguage("python",function(e){var r={cN:"prompt",b:/^(>>>|\.\.\.) /},b={cN:"string",c:[e.BE],v:[{b:/(u|b)?r?'''/,e:/'''/,c:[r],r:10},{b:/(u|b)?r?"""/,e:/"""/,c:[r],r:10},{b:/(u|r|ur)'/,e:/'/,r:10},{b:/(u|r|ur)"/,e:/"/,r:10},{b:/(b|br)'/,e:/'/},{b:/(b|br)"/,e:/"/},e.ASM,e.QSM]},i={cN:"number",r:0,v:[{b:e.BNR+"[lLjJ]?"},{b:"\\b(0o[0-7]+)[lLjJ]?"},{b:e.CNR+"[lLjJ]?"}]},l={cN:"params",b:/\(/,e:/\)/,c:["self",r,i,b]},n={e:/:/,i:/[${=;\n]/,c:[e.UTM,l]};return{aliases:["py","gyp"],k:{keyword:"and elif is global as in if from raise for except finally print import pass return exec else break not with class assert yield try while continue del or def lambda nonlocal|10 None True False",built_in:"Ellipsis NotImplemented"},i:/(<\/|->|\?)/,c:[r,i,b,e.HCM,e.inherit(n,{cN:"function",bK:"def",r:10}),e.inherit(n,{cN:"class",bK:"class"}),{cN:"decorator",b:/@/,e:/$/},{b:/\b(print|exec)\(/}]}});hljs.registerLanguage("xml",function(){var t="[A-Za-z0-9\\._:-]+",e={b:/<\?(php)?(?!\w)/,e:/\?>/,sL:"php",subLanguageMode:"continuous"},c={eW:!0,i:/]+/}]}]}]};return{aliases:["html","xhtml","rss","atom","xsl","plist"],cI:!0,c:[{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:"style"},c:[c],starts:{e:"",rE:!0,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:"script"},c:[c],starts:{e:"",rE:!0,sL:"javascript"}},e,{cN:"pi",b:/<\?\w+/,e:/\?>/,r:10},{cN:"tag",b:"",c:[{cN:"title",b:/[^ \/><\n\t]+/,r:0},c]}]}});hljs.registerLanguage("css",function(e){var c="[a-zA-Z-][a-zA-Z0-9_-]*",a={cN:"function",b:c+"\\(",rB:!0,eE:!0,e:"\\("};return{cI:!0,i:"[=/|']",c:[e.CBCM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:"font-face page"},{cN:"at_rule",b:"@",e:"[{;]",c:[{cN:"keyword",b:/\S+/},{b:/\s/,eW:!0,eE:!0,r:0,c:[a,e.ASM,e.QSM,e.CSSNM]}]},{cN:"tag",b:c,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[e.CBCM,{cN:"rule",b:"[^\\s]",rB:!0,e:";",eW:!0,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:!0,i:"[^\\s]",starts:{cN:"value",eW:!0,eE:!0,c:[a,e.CSSNM,e.QSM,e.ASM,e.CBCM,{cN:"hexcolor",b:"#[0-9A-Fa-f]+"},{cN:"important",b:"!important"}]}}]}]}]}}); \ No newline at end of file From 00255a4c9e8e2829a15b27c1fe1a40bfbd9f95d0 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Wed, 16 Dec 2020 17:40:18 +0530 Subject: [PATCH 162/284] fix: strip html before checking for null values Co-authored-by: Sahil Khan --- frappe/public/js/frappe/ui/field_group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index d432e553f1..393f2af154 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -86,7 +86,7 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ var f = this.fields_dict[key]; if (f.get_value) { var v = f.get_value(); - if (f.df.reqd && is_null(v)) + if(f.df.reqd && is_null(strip_html(v))) errors.push(__(f.df.label)); if (f.df.reqd From c876cdc63e256b2e281c931c5fb17974fb56f6d1 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Dec 2020 19:04:59 +0530 Subject: [PATCH 163/284] fix: Update controllers via delete_doc and rename_doc only in developer_mode --- frappe/core/doctype/doctype/doctype.py | 1 - frappe/model/delete_doc.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index a4d53351d6..3e283e1699 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -296,7 +296,6 @@ class DocType(Document): and ( frappe.conf.developer_mode or frappe.flags.allow_doctype_export - or frappe.flags.in_test ) ) if allow_doctype_export: diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index f8344f014c..15de673e4b 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -76,7 +76,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa delete_from_table(doctype, name, ignore_doctypes, None) - if not doc.custom and not ( + if frappe.conf.developer_mode and not doc.custom and not ( for_reload or frappe.flags.in_migrate or frappe.flags.in_install From bd7ff5277cbad85f7b26a4b20159439f49b46a1a Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Dec 2020 19:27:02 +0530 Subject: [PATCH 164/284] fix(hljs): Import core instead of highlight ref: https://unpkg.com/browse/highlight.js@10.4.1/lib/highlight.js --- frappe/website/js/syntax_highlight.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/js/syntax_highlight.js b/frappe/website/js/syntax_highlight.js index 199174b1e5..80914d9d99 100644 --- a/frappe/website/js/syntax_highlight.js +++ b/frappe/website/js/syntax_highlight.js @@ -1,4 +1,4 @@ -const hljs = require('highlight.js/lib/highlight'); +const hljs = require('highlight.js/lib/core'); hljs.registerLanguage('javascript', require('highlight.js/lib/languages/javascript')); hljs.registerLanguage('python', require('highlight.js/lib/languages/python')); From 2b4299a10524723c84b353d4621de87b0f7180d3 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Dec 2020 19:10:37 +0530 Subject: [PATCH 165/284] test: Set frappe.conf.developer_mode for test_rename_doc --- frappe/tests/test_rename_doc.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 9aa175d2d4..58cc5bb125 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -14,6 +14,10 @@ class TestRenameDoc(unittest.TestCase): @classmethod def setUpClass(self): """Setting Up data for the tests defined under TestRenameDoc""" + # set developer_mode to rename doc controllers + self._original_developer_flag = frappe.conf.developer_mode + frappe.conf.developer_mode = 1 + # data generation: for base and merge tests self.available_documents = [] self.test_doctype = "ToDo" @@ -57,6 +61,9 @@ class TestRenameDoc(unittest.TestCase): frappe.delete_doc_if_exists("Renamed Doc", "ToDo") + # reset original value of developer_mode conf + frappe.conf.developer_mode = self._original_developer_flag + def setUp(self): frappe.flags.link_fields = {} super().setUp() From e04b40aab4279300b3d26bd8f0199150167390ca Mon Sep 17 00:00:00 2001 From: Ishan Loya Date: Thu, 17 Dec 2020 12:29:35 +0530 Subject: [PATCH 166/284] Display field descriptions on mobile view Field descriptions can contain help text and should not be hidden on the mobile view --- frappe/public/js/frappe/form/controls/base_input.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 2f051a4701..acce79da40 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
    \
    \ \ - \ +

    \
    \
    \
    ').appendTo(this.parent); From 72c25d28b33075ee024f45c203dd83b79f02aeb4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 17 Dec 2020 17:37:22 +0530 Subject: [PATCH 167/284] fix: Improve breadrumbs markup schema for website --- frappe/templates/includes/breadcrumbs.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html index e281c4b111..61c03201bc 100644 --- a/frappe/templates/includes/breadcrumbs.html +++ b/frappe/templates/includes/breadcrumbs.html @@ -6,12 +6,12 @@ {% for parent in parents %} {% endfor %} - From c873c36971313997ecfb5ddbb025b1d43fc89a4a Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 17 Dec 2020 13:12:49 +0530 Subject: [PATCH 168/284] feat(minor): add before_commit methods --- frappe/__init__.py | 1 + frappe/database/database.py | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/frappe/__init__.py b/frappe/__init__.py index 276e296cd3..9958ae9700 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -148,6 +148,7 @@ def init(site, sites_path=None, new_site=False): "new_site": new_site }) local.rollback_observers = [] + local.before_commit = [] local.test_objects = {} local.site = site diff --git a/frappe/database/database.py b/frappe/database/database.py index 616dd3c3ec..179206a4af 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -746,6 +746,9 @@ class Database(object): def commit(self): """Commit current transaction. Calls SQL `COMMIT`.""" + for method in frappe.local.before_commit: + frappe.call(method[0], *(method[1] or []), **(method[2] or {})) + self.sql("commit") frappe.local.rollback_observers = [] @@ -753,6 +756,9 @@ class Database(object): enqueue_jobs_after_commit() flush_local_link_count() + def add_before_commit(self, method, args=None, kwargs=None): + frappe.local.before_commit.append([method, args, kwargs]) + @staticmethod def flush_realtime_log(): for args in frappe.local.realtime_log: From 41a3b8c2842ff066194970cffb2fa8cb446e9246 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 17 Dec 2020 20:44:23 +0530 Subject: [PATCH 169/284] fix(workflow): Update modified timestamp (#12093) To sync changes in https://github.com/frappe/frappe/pull/11787 after migrate --- frappe/workflow/doctype/workflow/workflow.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/workflow/doctype/workflow/workflow.json b/frappe/workflow/doctype/workflow/workflow.json index 3cb72d0eed..e8db8dcb10 100644 --- a/frappe/workflow/doctype/workflow/workflow.json +++ b/frappe/workflow/doctype/workflow/workflow.json @@ -99,7 +99,7 @@ "icon": "fa fa-random", "idx": 1, "links": [], - "modified": "2020-07-16 04:29:20.898040", + "modified": "2020-12-17 20:35:16.898040", "modified_by": "Administrator", "module": "Workflow", "name": "Workflow", From 018e45fdcc9c73187ad27c6972376e75821aa4c9 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Fri, 18 Dec 2020 00:13:49 +0530 Subject: [PATCH 170/284] chore: add space after if keyword --- frappe/public/js/frappe/ui/field_group.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 393f2af154..67aeb4474e 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -86,7 +86,7 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ var f = this.fields_dict[key]; if (f.get_value) { var v = f.get_value(); - if(f.df.reqd && is_null(strip_html(v))) + if (f.df.reqd && is_null(strip_html(v))) errors.push(__(f.df.label)); if (f.df.reqd From ebd6de1ae14c5a5c8cac11db6e38d7ace4c821cb Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 18 Dec 2020 12:04:50 +0530 Subject: [PATCH 171/284] fix: document naming rule validation for fields --- .../document_naming_rule/document_naming_rule.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 3ff47facc3..62d007609f 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -6,8 +6,19 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils.data import evaluate_filters +from frappe import _ class DocumentNamingRule(Document): + def validate(self): + self.validate_fields_in_conditions() + + def validate_fields_in_conditions(self): + for condition in self.conditions: + docfields = frappe.get_meta(self.document_type).fields + matching_field = list(filter(lambda x: x.fieldname == condition.field, docfields)) + if not len(matching_field): + frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) + def apply(self, doc): ''' Apply naming rules for the given document. Will set `name` if the rule is matched. From afa8a744970985b60583e158f781c9262123422e Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 17 Dec 2020 10:59:47 +0530 Subject: [PATCH 172/284] fix: rendering of percentage stat --- .../js/frappe/widgets/number_card_widget.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index c41f9bc6e7..ccc9ea7bfd 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -172,7 +172,7 @@ export default class NumberCardWidget extends Widget { get_number_for_custom_card(res) { if (typeof res === 'object') { this.number = res.value; - this.get_formatted_number(res); + this.set_formatted_number(res); } else { this.formatted_number = res; } @@ -184,7 +184,7 @@ export default class NumberCardWidget extends Widget { return frappe.model.with_doctype(this.card_doc.document_type, () => { const based_on_df = frappe.meta.get_docfield(this.card_doc.document_type, this.card_doc.aggregate_function_based_on); - this.get_formatted_number(based_on_df); + this.set_formatted_number(based_on_df); }); } else { this.formatted_number = res; @@ -199,10 +199,10 @@ export default class NumberCardWidget extends Widget { }, []); const col = res.columns.find(col => col.fieldname == field); this.number = frappe.report_utils.get_result_of_fn(this.card_doc.report_function, vals); - this.get_formatted_number(col); + this.set_formatted_number(col); } - get_formatted_number(df) { + set_formatted_number(df) { const default_country = frappe.sys_defaults.country; const shortened_number = frappe.utils.shorten_number(this.number, default_country, 5); let number_parts = shortened_number.split(' '); @@ -250,10 +250,16 @@ export default class NumberCardWidget extends Widget { }; const stats_qualifier = stats_qualifier_map[this.card_doc.stats_time_interval]; + let get_stat = () => { + const parts = this.percentage_stat.split(' '); + const symbol = parts[1] || ''; + return Math.abs(parts[0]) + ' ' + symbol; + }; + $(this.body).find('.widget-content').append(`
    ${caret_html} - ${Math.abs(this.percentage_stat)} % + ${get_stat()} % ${stats_qualifier} From 6d989531914dc6b6a0d0c9b9e653aaab6bb520be Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 18 Dec 2020 14:35:00 +0530 Subject: [PATCH 173/284] fix: change request --- .../doctype/document_naming_rule/document_naming_rule.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 62d007609f..5ae9528cea 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -13,10 +13,9 @@ class DocumentNamingRule(Document): self.validate_fields_in_conditions() def validate_fields_in_conditions(self): + docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] for condition in self.conditions: - docfields = frappe.get_meta(self.document_type).fields - matching_field = list(filter(lambda x: x.fieldname == condition.field, docfields)) - if not len(matching_field): + if condition.field not in docfields: frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) def apply(self, doc): From c7824f6211a0c7ca429c63f77e9f8b8737236ade Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 18 Dec 2020 15:59:18 +0530 Subject: [PATCH 174/284] fix: allow images from links in print formats --- frappe/templates/print_formats/standard_macros.html | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 0d904bb59c..9a14b860ff 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -6,7 +6,10 @@ {%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} {{ render_text_field(df, doc) }} {%- elif df.fieldtype in ("Image", "Attach Image", "Attach") - and (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") -%} + and ( + (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") + or doc[df.fieldname].startswith("http") + ) -%} {{ render_image(df, doc) }} {%- elif df.fieldtype=="Geolocation" -%} {{ render_geolocation(df, doc) }} @@ -123,15 +126,14 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}" {% include doc.print_templates[df.fieldname] %} {% elif df.fieldtype=="Check" %} - {% elif df.fieldtype=="Image" %} + {% elif df.fieldtype in ("Image", "Attach Image") %} {% elif df.fieldtype=="Signature" %} - {% elif df.fieldtype in ("Attach", "Attach Image") and doc[df.fieldname] - and frappe.utils.is_image(doc[df.fieldname]) %} + {% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %} {% elif df.fieldtype=="HTML" %} From d3a046a72ca20d1da1364b47a814963c83b691a5 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 18 Dec 2020 21:18:47 +0530 Subject: [PATCH 175/284] fix: check for doctype change before validation --- .../doctype/document_naming_rule/document_naming_rule.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 5ae9528cea..4b34293af6 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -13,10 +13,11 @@ class DocumentNamingRule(Document): self.validate_fields_in_conditions() def validate_fields_in_conditions(self): - docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] - for condition in self.conditions: - if condition.field not in docfields: - frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) + if self.has_value_changed("document_type"): + docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] + for condition in self.conditions: + if condition.field not in docfields: + frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) def apply(self, doc): ''' From a6705135dcb8679d3f82deacf1cd93476182d867 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 19 Dec 2020 08:49:14 +0530 Subject: [PATCH 176/284] fix: Link attachment in webform for new file (bp #12097) (#12102) (cherry picked from commit 85d6a640358fa0337aaac405e71b7d14395fb298) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/web_form/web_form.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/public/js/frappe/web_form/web_form.js b/frappe/public/js/frappe/web_form/web_form.js index 4dc1a50bc4..0421147d49 100644 --- a/frappe/public/js/frappe/web_form/web_form.js +++ b/frappe/public/js/frappe/web_form/web_form.js @@ -103,6 +103,7 @@ export default class WebForm extends frappe.ui.FieldGroup { } save() { + let is_new = this.is_new; if (this.validate && !this.validate()) { frappe.throw(__("Couldn't save, please check the data you have entered"), __("Validation Error")); } @@ -139,6 +140,18 @@ export default class WebForm extends frappe.ui.FieldGroup { this.handle_success(response.message); frappe.web_form.events.trigger('after_save'); this.after_save && this.after_save(); + // args doctype and docname added to link doctype in file manager + if (is_new) { + frappe.call({ + type: 'POST', + method: "frappe.handler.upload_file", + args: { + file_url: response.message.attachment, + doctype: response.message.doctype, + docname: response.message.name + } + }); + } } }, always: function() { From 5a77383aa8ec771e1bc55a23ac2ec95a53920753 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 19 Dec 2020 13:19:07 +0530 Subject: [PATCH 177/284] fix: donot remove the modules from txt file if developer mode is off --- frappe/core/doctype/module_def/module_def.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 930c46e60b..7e63572162 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -43,7 +43,7 @@ class ModuleDef(Document): def on_trash(self): """Delete module name from modules.txt""" - if frappe.flags.in_uninstall or self.custom: + if not frappe.conf.get('developer_mode') or frappe.flags.in_uninstall or self.custom: return modules = None From 05770275b0292ebf37e175cd99503de69cfc43ec Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Dec 2020 12:33:15 +0530 Subject: [PATCH 178/284] chore: remove unwanted conditions --- frappe/public/js/frappe/form/layout.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 6c94663802..25b83bc0b0 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -508,16 +508,10 @@ frappe.ui.form.Layout = Class.extend({ } if (form_obj) { if (this.doc && this.doc.parent) { - const df = form_obj.get_docfield(this.doc.parentfield, fieldname); - if (df && df[property] != value) { - form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); - this.fields_dict[fname] && this.fields_dict[fieldname].refresh(); - } + form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); + this.fields_dict[fname] && this.fields_dict[fieldname].refresh(); } else { - const df = form_obj.get_docfield(fieldname); - if (df && df[property] != value) { - form_obj.set_df_property(fieldname, property, value); - } + form_obj.set_df_property(fieldname, property, value); } } }, From 8e380fe151e940e1389fff60959b2dc396eb57e6 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 21 Dec 2020 12:34:14 +0530 Subject: [PATCH 179/284] fix: don't set Message as mandatory in communication dialog --- frappe/public/js/frappe/views/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 29b21242af..3ee4b54621 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -67,7 +67,7 @@ frappe.views.CommunicationComposer = Class.extend({ {fieldtype: "Section Break"}, { label:__("Message"), - fieldtype:"Text Editor", reqd: 1, + fieldtype:"Text Editor", fieldname:"content", onchange: frappe.utils.debounce(this.save_as_draft.bind(this), 300) }, From 4f9edffed82c5fec3c5c6351687560c6dc35f696 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Dec 2020 13:16:29 +0530 Subject: [PATCH 180/284] fix: fname is not defined --- frappe/public/js/frappe/form/layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 25b83bc0b0..c96c487f3d 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -509,7 +509,7 @@ frappe.ui.form.Layout = Class.extend({ if (form_obj) { if (this.doc && this.doc.parent) { form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); - this.fields_dict[fname] && this.fields_dict[fieldname].refresh(); + this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh(); } else { form_obj.set_df_property(fieldname, property, value); } From 5d56b36e18b3e756a3ec59b61bdb4cf55a808c69 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Dec 2020 13:16:41 +0530 Subject: [PATCH 181/284] chore: add comments --- frappe/public/js/frappe/form/form.js | 1 + frappe/public/js/frappe/form/layout.js | 1 + 2 files changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 9272d1f6f5..fc348704fa 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1276,6 +1276,7 @@ frappe.ui.form.Form = class FrappeForm { if (df && df[property] != value) { df[property] = value; if (!docname || !table_field) { + // do not refresh childtable fields since `this.fields_dict` doesn't have child table fields this.refresh_field(fieldname); } } diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index c96c487f3d..22c885e0cb 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -509,6 +509,7 @@ frappe.ui.form.Layout = Class.extend({ if (form_obj) { if (this.doc && this.doc.parent) { form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); + // refresh child fields this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh(); } else { form_obj.set_df_property(fieldname, property, value); From 8cf120e722b198e11dd151be118b03e3a00eb3da Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 21 Dec 2020 13:11:57 +0530 Subject: [PATCH 182/284] style: fix formatting --- .../public/js/frappe/views/communication.js | 105 +++++++++++++----- 1 file changed, 76 insertions(+), 29 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 3ee4b54621..c69be04347 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -55,38 +55,85 @@ frappe.views.CommunicationComposer = Class.extend({ get_fields: function() { let contactList = []; var fields= [ - {label:__("To"), fieldtype:"MultiSelect", reqd: 0, fieldname:"recipients",options:contactList}, - {fieldtype: "Section Break", collapsible: 1, label: __("CC, BCC & Email Template")}, - {label:__("CC"), fieldtype:"MultiSelect", fieldname:"cc",options:contactList}, - {label:__("BCC"), fieldtype:"MultiSelect", fieldname:"bcc",options:contactList}, - {label:__("Email Template"), fieldtype:"Link", options:"Email Template", - fieldname:"email_template"}, - {fieldtype: "Section Break"}, - {label:__("Subject"), fieldtype:"Data", reqd: 1, - fieldname:"subject", length:524288}, - {fieldtype: "Section Break"}, { - label:__("Message"), - fieldtype:"Text Editor", - fieldname:"content", + label: __("To"), + fieldtype: "MultiSelect", + reqd: 0, + fieldname: "recipients", + options: contactList + }, + { + fieldtype: "Section Break", + collapsible: 1, + label: __("CC, BCC & Email Template") + }, + { + label: __("CC"), + fieldtype: "MultiSelect", + fieldname: "cc", + options: contactList + }, + { + label: __("BCC"), + fieldtype: "MultiSelect", + fieldname: "bcc", + options: contactList + }, + { + label: __("Email Template"), + fieldtype: "Link", + options: "Email Template", + fieldname: "email_template" + }, + { fieldtype: "Section Break" }, + { + label: __("Subject"), + fieldtype: "Data", + reqd: 1, + fieldname: "subject", + length: 524288 + }, + { fieldtype: "Section Break" }, + { + label: __("Message"), + fieldtype: "Text Editor", + fieldname: "content", onchange: frappe.utils.debounce(this.save_as_draft.bind(this), 300) }, - - {fieldtype: "Section Break"}, - {fieldtype: "Column Break"}, - {label:__("Send me a copy"), fieldtype:"Check", - fieldname:"send_me_a_copy", 'default': frappe.boot.user.send_me_a_copy}, - {label:__("Send Read Receipt"), fieldtype:"Check", - fieldname:"send_read_receipt"}, - {label:__("Attach Document Print"), fieldtype:"Check", - fieldname:"attach_document_print"}, - {label:__("Select Print Format"), fieldtype:"Select", - fieldname:"select_print_format"}, - {label:__("Select Languages"), fieldtype:"Select", - fieldname:"language_sel"}, - {fieldtype: "Column Break"}, - {label:__("Select Attachments"), fieldtype:"HTML", - fieldname:"select_attachments"} + { fieldtype: "Section Break" }, + { fieldtype: "Column Break" }, + { + label: __("Send me a copy"), + fieldtype: "Check", + fieldname: "send_me_a_copy", + 'default': frappe.boot.user.send_me_a_copy + }, + { + label: __("Send Read Receipt"), + fieldtype: "Check", + fieldname: "send_read_receipt" + }, + { + label: __("Attach Document Print"), + fieldtype: "Check", + fieldname: "attach_document_print" + }, + { + label: __("Select Print Format"), + fieldtype: "Select", + fieldname: "select_print_format" + }, + { + label: __("Select Languages"), + fieldtype: "Select", + fieldname: "language_sel" + }, + { fieldtype: "Column Break" }, + { + label: __("Select Attachments"), + fieldtype: "HTML", + fieldname: "select_attachments" + } ]; // add from if user has access to multiple email accounts From 9f6fc4618feb85adf8c7ec7bb56c2b1494598fa0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Dec 2020 17:25:16 +0100 Subject: [PATCH 183/284] fix: base_url --- .../doctype/connected_app/connected_app.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 9254aa7631..64ec6d11c8 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -21,11 +21,13 @@ class ConnectedApp(Document): """ def validate(self): - try: - base_url = frappe.request.host_url - except RuntimeError: - # for tests - base_url = frappe.get_site_config().host_name or 'http://localhost:8000' + if not frappe.flags.in_test: + try: + base_url = frappe.request.host_url + except RuntimeError: + base_url = frappe.utils.get_url() + else: + base_url = 'http://localhost:8000' callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) From bf8b40aceceb88c2aac619fa9efe65922355a01e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Dec 2020 17:25:26 +0100 Subject: [PATCH 184/284] fix: test records --- frappe/integrations/doctype/connected_app/test_records.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json index f9ba219f54..4d19369248 100644 --- a/frappe/integrations/doctype/connected_app/test_records.json +++ b/frappe/integrations/doctype/connected_app/test_records.json @@ -2,10 +2,8 @@ { "doctype": "Connected App", "provider_name": "frappe", - "base_url": "http://localhost:8000", "client_id": "test_client_id", "client_secret": "test_client_secret", - "redirect_uri": "http://localhost:8000/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/frappe", "scopes": [ { "scope": "all" From f4ba3e7c0a9dc91cf80fa5e1c8b0ab354fe2128f Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 22 Dec 2020 12:42:58 +0530 Subject: [PATCH 185/284] fix: Update breadcrumb markup schema --- frappe/templates/includes/breadcrumbs.html | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/templates/includes/breadcrumbs.html b/frappe/templates/includes/breadcrumbs.html index 61c03201bc..ccc77de253 100644 --- a/frappe/templates/includes/breadcrumbs.html +++ b/frappe/templates/includes/breadcrumbs.html @@ -3,16 +3,20 @@ From 877a25225a5f4b0d35808f7f2042b36c9b018fdd Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 22 Dec 2020 13:16:19 +0530 Subject: [PATCH 186/284] fix: null as default value for rating field --- frappe/public/js/frappe/form/controls/rating.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js index 34e890d45c..191db35538 100644 --- a/frappe/public/js/frappe/form/controls/rating.js +++ b/frappe/public/js/frappe/form/controls/rating.js @@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ }); }, get_value() { - return cint(this.value); + return cint(this.value, null); }, set_formatted_input(value) { let el = $(this.input_area).find('i'); From 301ed4c7e322fb1de93a197ee3423992d8719435 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 22 Dec 2020 18:35:26 +0100 Subject: [PATCH 187/284] fix: rely on frappe.utils.get_url --- .../integrations/doctype/connected_app/connected_app.py | 9 +-------- .../doctype/social_login_key/test_social_login_key.py | 3 ++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 64ec6d11c8..a26f93f676 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -21,14 +21,7 @@ class ConnectedApp(Document): """ def validate(self): - if not frappe.flags.in_test: - try: - base_url = frappe.request.host_url - except RuntimeError: - base_url = frappe.utils.get_url() - else: - base_url = 'http://localhost:8000' - + base_url = frappe.utils.get_url() callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name self.redirect_uri = urljoin(base_url, callback_path) diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index a1390b39b0..e0b99ad391 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -30,8 +30,9 @@ def create_or_update_social_login_key(): except frappe.DoesNotExistError: social_login_key = frappe.new_doc("Social Login Key") social_login_key.get_social_login_provider("Frappe", initialize=True) - social_login_key.base_url = frappe.get_site_config().host_name or "http://localhost:8000" + social_login_key.base_url = frappe.utils.get_url() social_login_key.enable_social_login = 0 social_login_key.save() frappe.db.commit() + return social_login_key From 4961774facbe7bb70d87d396d61f61c736b3ef4f Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 23 Dec 2020 01:20:39 +0100 Subject: [PATCH 188/284] chore: Define constants for map and geolocation Signed-off-by: mathieu.brunot --- frappe/public/js/frappe/form/controls/geolocation.js | 8 ++++---- frappe/public/js/frappe/views/map/map_view.js | 12 +++++------- frappe/public/js/frappe/widgets/utils.js | 11 ++++++++++- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 9dfad09299..b6a04e5218 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -1,3 +1,5 @@ +import { map_defaults } from "../../widgets/utils"; + frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ horizontal: false, @@ -90,11 +92,9 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ }); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13); + this.map = L.map(this.map_id).setView(map_defaults.center, map_defaults.zoom); - L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(this.map); + L.tileLayer(map_defaults.tiles, map_defaults.options).addTo(this.map); }, bind_leaflet_locate_control() { diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 12b4cef921..b6119eef1a 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -1,6 +1,8 @@ /** * frappe.views.MapView */ +import { map_defaults } from "../../widgets/utils"; + frappe.provide("frappe.views"); frappe.views.MapView = class MapView extends frappe.views.ListView { @@ -33,14 +35,10 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { this.$result.html(`
    `); + L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; + this.map = L.map(this.map_id).setView(map_defaults.center, map_defaults.zoom); - //coords of India if markers does not exists - this.map = L.map(this.map_id).setView([12.3112899, -85.7384542], 8); - - L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors', - maxZoom: 18 - }).addTo(this.map); + L.tileLayer(map_defaults.tiles, map_defaults.options).addTo(this.map); L.control.scale().addTo(this.map); if (this.coords.features && this.coords.features.length) { diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 4599b4adc8..03fb6995e0 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -163,4 +163,13 @@ function get_number_system(country) { return number_system_map[country]; } -export { generate_route, generate_grid, build_summary_item, shorten_number }; +const map_defaults = { + center: [19.0800, 72.8961], + zoom: 13, + tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { + attribution: '© OpenStreetMap contributors' + } +}; + +export { generate_route, generate_grid, build_summary_item, shorten_number, map_defaults }; From 5131697f3cebde4a258fc44f5aa61529a9b94c41 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Wed, 23 Dec 2020 01:34:59 +0100 Subject: [PATCH 189/284] chore: Improve format of map defaults Signed-off-by: mathieu.brunot --- .../js/frappe/form/controls/geolocation.js | 8 +++++--- frappe/public/js/frappe/views/map/map_view.js | 9 +++++---- frappe/public/js/frappe/widgets/utils.js | 19 ++++++++----------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index b6a04e5218..31a5854f9a 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -1,4 +1,4 @@ -import { map_defaults } from "../../widgets/utils"; +frappe.provide('frappe.widget.utils'); frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ horizontal: false, @@ -92,9 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ }); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView(map_defaults.center, map_defaults.zoom); + this.map = L.map(this.map_id).setView(frappe.widget.utils.map_defaults.center, + frappe.widget.utils.map_defaults.zoom); - L.tileLayer(map_defaults.tiles, map_defaults.options).addTo(this.map); + L.tileLayer(frappe.widget.utils.map_defaults.tiles, + frappe.widget.utils.map_defaults.options).addTo(this.map); }, bind_leaflet_locate_control() { diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index b6119eef1a..539ac86e99 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -1,8 +1,7 @@ /** * frappe.views.MapView */ -import { map_defaults } from "../../widgets/utils"; - +frappe.provide('frappe.widget.utils'); frappe.provide("frappe.views"); frappe.views.MapView = class MapView extends frappe.views.ListView { @@ -36,9 +35,11 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { this.$result.html(`
    `); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView(map_defaults.center, map_defaults.zoom); + this.map = L.map(this.map_id).setView(frappe.widget.utils.map_defaults.center, + frappe.widget.utils.map_defaults.zoom); - L.tileLayer(map_defaults.tiles, map_defaults.options).addTo(this.map); + L.tileLayer(frappe.widget.utils.map_defaults.tiles, + frappe.widget.utils.map_defaults.options).addTo(this.map); L.control.scale().addTo(this.map); if (this.coords.features && this.coords.features.length) { diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index 5121ee398e..e3632856bb 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -21,15 +21,12 @@ frappe.widget.utils = { }

    ${value}

    ` ); }, + map_defaults: { + center: [19.0800, 72.8961], + zoom: 13, + tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { + attribution: '© OpenStreetMap contributors' + } + }, }; - -const map_defaults = { - center: [19.0800, 72.8961], - zoom: 13, - tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { - attribution: '© OpenStreetMap contributors' - } -}; - -export { map_defaults }; From bf97382002e07e4e568e4a4c509c0f97f56feedc Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 23 Dec 2020 13:09:53 +0530 Subject: [PATCH 190/284] fix: row removed in child table not syncing when it has a dependency field --- .../doctype/event_producer/event_producer.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index d8a6a55510..e43b4d131c 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -295,7 +295,7 @@ def set_update(update, producer_site): if data.changed: local_doc.update(data.changed) if data.removed: - update_row_removed(local_doc, data.removed) + local_doc = update_row_removed(local_doc, data.removed) if data.row_changed: update_row_changed(local_doc, data.row_changed) if data.added: @@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed): for tablename, rownames in iteritems(removed): table = local_doc.get_table_field_doctype(tablename) for row in rownames: - frappe.db.delete(table, row) + table_rows = local_doc.get(tablename) + child_table_row = get_child_table_row(table_rows, row) + table_rows.remove(child_table_row) + local_doc.set(tablename, table_rows) + return local_doc + + +def get_child_table_row(table_rows, row): + for entry in table_rows: + if entry.get('name') == row: + return entry def update_row_changed(local_doc, changed): From 424c0c50f8055bc18feed762c8c3e7279e4f74ac Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 23 Dec 2020 13:50:18 +0530 Subject: [PATCH 191/284] fix: set alert flag to false by default --- frappe/core/doctype/doctype/doctype.py | 4 ++-- frappe/core/page/permission_manager/permission_manager.py | 4 ++-- frappe/permissions.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index fced5d1fa1..71fca9b597 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1000,7 +1000,7 @@ def validate_fields(meta): check_sort_field(meta) check_image_field(meta) -def validate_permissions_for_doctype(doctype, for_remove=False, alert=True): +def validate_permissions_for_doctype(doctype, for_remove=False, alert=False): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) validate_permissions(doctype, for_remove, alert=alert) @@ -1026,7 +1026,7 @@ def clear_permissions_cache(doctype): """, doctype): frappe.clear_cache(user=user) -def validate_permissions(doctype, for_remove=False, alert=True): +def validate_permissions(doctype, for_remove=False, alert=False): permissions = doctype.get("permissions") # Some DocTypes may not have permissions by default, don't show alert for them if not permissions and alert: diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 5b4ccb6ce0..be8921e2ff 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -90,7 +90,7 @@ def update(doctype, role, permlevel, ptype, value=None): str: Refresh flag is permission is updated successfully """ frappe.only_for("System Manager") - out = update_permission_property(doctype, role, permlevel, ptype, value, alert=False) + out = update_permission_property(doctype, role, permlevel, ptype, value) return 'refresh' if out else None @frappe.whitelist() @@ -104,7 +104,7 @@ def remove(doctype, role, permlevel): if not frappe.get_all('Custom DocPerm', dict(parent=doctype)): frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove')) - validate_permissions_for_doctype(doctype, for_remove=True) + validate_permissions_for_doctype(doctype, for_remove=True, alert=True) @frappe.whitelist() def reset(doctype): diff --git a/frappe/permissions.py b/frappe/permissions.py index e9724b7418..15bb2c8887 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -446,7 +446,7 @@ def can_export(doctype, raise_exception=False): raise frappe.PermissionError(_("You are not allowed to export {} doctype").format(doctype)) return has_access -def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True, alert=True): +def update_permission_property(doctype, role, permlevel, ptype, value=None, validate=True): '''Update a property in Custom Perm''' from frappe.core.doctype.doctype.doctype import validate_permissions_for_doctype out = setup_custom_perms(doctype) From 3b6ae2de6c0f1fab9ef858b0736ed43b47e87adb Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 23 Dec 2020 13:58:11 +0530 Subject: [PATCH 192/284] fix: reference error --- frappe/permissions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/permissions.py b/frappe/permissions.py index 15bb2c8887..0d766aec8d 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -458,7 +458,7 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali update `tabCustom DocPerm` set `{0}`=%s where name=%s""".format(ptype), (value, name)) if validate: - validate_permissions_for_doctype(doctype, alert=alert) + validate_permissions_for_doctype(doctype) return out From 3db2fd2c9f24155edc8651f3b35a2bc3e731dae2 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Wed, 23 Dec 2020 21:37:24 +0530 Subject: [PATCH 193/284] fix: Email Section label typo --- frappe/core/doctype/system_settings/system_settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 79fb84923a..7443c1b34a 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,4 +1,4 @@ -{ +"label": "EMail"{ "actions": [], "creation": "2014-04-17 16:53:52.640856", "doctype": "DocType", @@ -357,7 +357,7 @@ "collapsible": 1, "fieldname": "email", "fieldtype": "Section Break", - "label": "EMail" + "label": "Email" }, { "description": "Your organization name and address for the email footer.", @@ -490,4 +490,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} From f38615585b924b9781f1da6e7c02de97b7809c09 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Wed, 23 Dec 2020 21:38:43 +0530 Subject: [PATCH 194/284] fix:email section typo --- frappe/core/doctype/system_settings/system_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 7443c1b34a..565ee373f1 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -1,4 +1,4 @@ -"label": "EMail"{ +{ "actions": [], "creation": "2014-04-17 16:53:52.640856", "doctype": "DocType", From 5b8294f92b3c05f39442f6a85af0e6156f66dad6 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 21 Dec 2020 15:16:34 +0530 Subject: [PATCH 195/284] fix: throw error if name already exists --- frappe/model/rename_doc.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 35fbf94dc6..2de1c51ea1 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -21,8 +21,15 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge) if old_title and new_title and not old_title == new_title: - frappe.db.set_value(doctype, docname, title_field, new_title) - frappe.msgprint(_('Saved'), alert=True, indicator='green') + try: + frappe.db.set_value(doctype, docname, title_field, new_title) + frappe.msgprint(_('Saved'), alert=True, indicator='green') + except Exception as e: + if frappe.db.is_duplicate_entry(e): + frappe.msgprint(_("{0} {1} already exists").format( + doctype, frappe.bold(docname)), title=_("Duplicate Name"), indicator="red" + ) + raise frappe.DuplicateEntryError(doctype, docname, e) return docname From d6488a043ee0be982db5e54199fb242dab8dda0e Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 24 Dec 2020 13:59:02 +0100 Subject: [PATCH 196/284] refactor: Move map default to utils Signed-off-by: mathieu.brunot --- frappe/public/js/frappe/form/controls/geolocation.js | 2 +- frappe/public/js/frappe/utils/utils.js | 8 ++++++++ frappe/public/js/frappe/views/map/map_view.js | 2 +- frappe/public/js/frappe/widgets/utils.js | 8 -------- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 31a5854f9a..96a80fb1d1 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -1,4 +1,4 @@ -frappe.provide('frappe.widget.utils'); +frappe.provide('frappe.utils.utils'); frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ horizontal: false, diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index f8f25293b3..32be29df92 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1051,6 +1051,14 @@ Object.assign(frappe.utils, { return number_system_map[country]; }, + map_defaults: { + center: [19.0800, 72.8961], + zoom: 13, + tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + options: { + attribution: '© OpenStreetMap contributors' + } + }, }); // Array de duplicate diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 539ac86e99..205df5f4d3 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -1,7 +1,7 @@ /** * frappe.views.MapView */ -frappe.provide('frappe.widget.utils'); +frappe.provide('frappe.utils.utils'); frappe.provide("frappe.views"); frappe.views.MapView = class MapView extends frappe.views.ListView { diff --git a/frappe/public/js/frappe/widgets/utils.js b/frappe/public/js/frappe/widgets/utils.js index e3632856bb..ade35dae35 100644 --- a/frappe/public/js/frappe/widgets/utils.js +++ b/frappe/public/js/frappe/widgets/utils.js @@ -21,12 +21,4 @@ frappe.widget.utils = { }

    ${value}

    ` ); }, - map_defaults: { - center: [19.0800, 72.8961], - zoom: 13, - tiles: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', - options: { - attribution: '© OpenStreetMap contributors' - } - }, }; From bc9d6cff2e2a3be17101f0957f771ab9e4f5b377 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 24 Dec 2020 18:36:59 +0530 Subject: [PATCH 197/284] fix(patch): Remove Package Publish Tool doctypes (#12113) --- frappe/patches.txt | 1 + frappe/patches/v13_0/delete_package_publish_tool.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 frappe/patches/v13_0/delete_package_publish_tool.py diff --git a/frappe/patches.txt b/frappe/patches.txt index b459019dd7..1a086303ba 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -317,3 +317,4 @@ frappe.patches.v13_0.web_template_set_module #2020-10-05 frappe.patches.v13_0.remove_custom_link execute:frappe.delete_doc("DocType", "Footer Item") frappe.patches.v13_0.replace_field_target_with_open_in_new_tab +frappe.patches.v13_0.delete_package_publish_tool diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py new file mode 100644 index 0000000000..25024f58dd --- /dev/null +++ b/frappe/patches/v13_0/delete_package_publish_tool.py @@ -0,0 +1,11 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + frappe.delete_doc("DocType", "Package Publish Tool", ignore_missing=True) + frappe.delete_doc("DocType", "Package Document Type", ignore_missing=True) + frappe.delete_doc("DocType", "Package Publish Target", ignore_missing=True) From 9dde944f102a165ca2668b7c1e93207950a456fc Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Thu, 24 Dec 2020 15:32:23 +0100 Subject: [PATCH 198/284] fix: Fix call to utils map defaults Signed-off-by: mathieu.brunot --- frappe/public/js/frappe/form/controls/geolocation.js | 8 ++++---- frappe/public/js/frappe/views/map/map_view.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 96a80fb1d1..9e4d1d82ec 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -92,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ }); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView(frappe.widget.utils.map_defaults.center, - frappe.widget.utils.map_defaults.zoom); + this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center, + frappe.utils.map_defaults.zoom); - L.tileLayer(frappe.widget.utils.map_defaults.tiles, - frappe.widget.utils.map_defaults.options).addTo(this.map); + L.tileLayer(frappe.utils.map_defaults.tiles, + frappe.utils.map_defaults.options).addTo(this.map); }, bind_leaflet_locate_control() { diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index 205df5f4d3..a6936d58e1 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -35,11 +35,11 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { this.$result.html(`
    `); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView(frappe.widget.utils.map_defaults.center, - frappe.widget.utils.map_defaults.zoom); + this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center, + frappe.utils.map_defaults.zoom); - L.tileLayer(frappe.widget.utils.map_defaults.tiles, - frappe.widget.utils.map_defaults.options).addTo(this.map); + L.tileLayer(frappe.utils.map_defaults.tiles, + frappe.utils.map_defaults.options).addTo(this.map); L.control.scale().addTo(this.map); if (this.coords.features && this.coords.features.length) { From 9a1d8ae6b20d587d2884e2b29a9abe5dbee5b05d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 25 Dec 2020 15:23:43 +0100 Subject: [PATCH 199/284] feat: redirect to Guest to login --- frappe/integrations/doctype/connected_app/connected_app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index a26f93f676..92b3977585 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals from urllib.parse import urljoin +from urllib.parse import urlencode import frappe from frappe import _ @@ -105,7 +106,9 @@ def callback(code=None, state=None): frappe.throw(_('Invalid Method')) if frappe.session.user == 'Guest': - frappe.throw(_('Log in to access this page.'), frappe.PermissionError) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url}) + return path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: From f06961e001dd322f01f09caa6b551bf7b67c47db Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 25 Dec 2020 15:28:34 +0100 Subject: [PATCH 200/284] refactor: useless try except, better error messages --- .../doctype/connected_app/connected_app.py | 27 +++++++------------ 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 92b3977585..0711be697f 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -103,7 +103,7 @@ def callback(code=None, state=None): token. """ if frappe.request.method != 'GET': - frappe.throw(_('Invalid Method')) + frappe.throw(_('Invalid request method: {}').format(frappe.request.method)) if frappe.session.user == 'Guest': frappe.local.response['type'] = 'redirect' @@ -112,30 +112,23 @@ def callback(code=None, state=None): path = frappe.request.path[1:].split('/') if len(path) != 4 or not path[3]: - frappe.throw(_('Invalid Parameter(s)')) + frappe.throw(_('Invalid Parameters.')) - connected_app = path[3] - token_cache = frappe.get_doc('Token Cache', connected_app + '-' + frappe.session.user) - if not token_cache: - frappe.throw(_('State Not Found')) + connected_app = frappe.get_doc('Connected App', path[3]) + token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user) if state != token_cache.state: - frappe.throw(_('Invalid State')) + frappe.throw(_('Invalid state.')) - try: - app = frappe.get_doc('Connected App', connected_app) - except frappe.exceptions.DoesNotExistError: - frappe.throw(_('Invalid App')) - - oauth = app.get_oauth2_session(init=True) - query_params = app.get_query_params() - token = oauth.fetch_token(app.token_uri, + oauth_session = connected_app.get_oauth2_session(init=True) + query_params = connected_app.get_query_params() + token = oauth_session.fetch_token(connected_app.token_uri, code=code, - client_secret=app.get_password('client_secret'), + client_secret=connected_app.get_password('client_secret'), include_client_id=True, **query_params ) token_cache.update_data(token) frappe.local.response['type'] = 'redirect' - frappe.local.response['location'] = token_cache.get('success_uri') or app.get_url() + frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url() From c982875b6a5490dc706d0c5412d5183c43ddb24f Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 28 Dec 2020 13:40:12 +0530 Subject: [PATCH 201/284] fix: Show cancel button only if document is cancellable --- frappe/model/workflow.py | 5 ++--- frappe/public/js/frappe/form/toolbar.js | 20 +++++++++++++++++--- frappe/public/js/frappe/form/workflow.js | 13 ++----------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 43e26cc5d0..3e8125f9b1 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -120,9 +120,8 @@ def apply_workflow(doc, action): return doc @frappe.whitelist() -def can_cancel_document(doc): - doc = frappe.get_doc(frappe.parse_json(doc)) - workflow = get_workflow(doc.doctype) +def can_cancel_document(doctype): + workflow = get_workflow(doctype) for state_doc in workflow.states: if state_doc.doc_status == '2': for transition in workflow.transitions: diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index c7fb69a2b5..d8a2b91277 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -441,9 +441,23 @@ frappe.ui.form.Toolbar = Class.extend({ me.frm.page.set_view('main'); }, 'octicon octicon-pencil'); } else if(status === "Cancel") { - this.page.set_secondary_action(__(status), function() { - me.frm.savecancel(this); - }, "octicon octicon-circle-slash"); + let add_cancel_button = () => { + this.page.set_secondary_action(__(status), function() { + me.frm.savecancel(this); + }, "octicon octicon-circle-slash"); + }; + if (this.has_workflow()) { + frappe.xcall( + 'frappe.model.workflow.can_cancel_document', { + 'doctype': this.frm.doc.doctype, + }).then((can_cancel) => { + if (can_cancel) { + add_cancel_button(); + } + }); + } else { + add_cancel_button(); + } } else { var click = { "Save": function() { diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index 4c59e8219b..16d9f8676b 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({ frappe.workflow.get_transitions(this.frm.doc).then(transitions => { this.frm.page.clear_actions_menu(); transitions.forEach(d => { - if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { + if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { added = true; me.frm.page.add_action_item(__(d.action), function() { // set the workflow_action for use in form scripts @@ -103,17 +103,8 @@ frappe.ui.form.States = Class.extend({ }); } }); - if (!added) { - //call function and clear cancel button if Cancel doc state is defined in the workfloe - frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => { - if (!can_cancel) { - this.frm.page.clear_secondary_action(); - } - }); - } else { - this.setup_btn(added); - } + this.setup_btn(added); }); }, From 1d975f07e7b52ab3e8022023675f32759840d0bf Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Dec 2020 17:26:57 +0100 Subject: [PATCH 202/284] fix: allow insecure transport in travis --- frappe/integrations/doctype/connected_app/connected_app.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py index 0711be697f..ec08f8e4be 100644 --- a/frappe/integrations/doctype/connected_app/connected_app.py +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -2,7 +2,7 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals +import os from urllib.parse import urljoin from urllib.parse import urlencode @@ -11,9 +11,8 @@ from frappe import _ from frappe.model.document import Document from requests_oauthlib import OAuth2Session -if frappe.conf.developer_mode or frappe.flags.in_test: +if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)): # Disable mandatory TLS in developer mode and tests - import os os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' class ConnectedApp(Document): From 6a02f5ad52cda9ae9fe534a70a04f8b5bc310756 Mon Sep 17 00:00:00 2001 From: "mathieu.brunot" Date: Tue, 29 Dec 2020 01:27:13 +0100 Subject: [PATCH 203/284] style: Fix Sider issues Signed-off-by: mathieu.brunot --- frappe/public/js/frappe/views/map/map_view.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/views/map/map_view.js b/frappe/public/js/frappe/views/map/map_view.js index a6936d58e1..878311b9bd 100644 --- a/frappe/public/js/frappe/views/map/map_view.js +++ b/frappe/public/js/frappe/views/map/map_view.js @@ -43,11 +43,11 @@ frappe.views.MapView = class MapView extends frappe.views.ListView { L.control.scale().addTo(this.map); if (this.coords.features && this.coords.features.length) { - this.coords.features.forEach( - coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) - ); - let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); - this.map.panTo(lastCoords, 8); + this.coords.features.forEach( + coords => L.geoJSON(coords).bindPopup(coords.properties.name).addTo(this.map) + ); + let lastCoords = this.coords.features[0].geometry.coordinates.reverse(); + this.map.panTo(lastCoords, 8); } } From 4df9a203f0019dfd1faacaf1be5c47cb5b2eb073 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 29 Dec 2020 14:14:29 +0530 Subject: [PATCH 204/284] feat: fetch email account signature in email dialog --- frappe/public/js/frappe/views/communication.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 29b21242af..fe14ad4793 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -464,6 +464,7 @@ frappe.views.CommunicationComposer = Class.extend({ }, send_action: function() { + debugger; var me = this; var btn = me.dialog.get_primary_btn(); @@ -625,10 +626,19 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - setup_earlier_reply: function() { + get_default_outgoing_email_account_signature: function() { + return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature'); + }, + + setup_earlier_reply: async function() { let fields = this.dialog.fields_dict; let signature = frappe.boot.user.email_signature || ""; + if (!signature) { + const res = await this.get_default_outgoing_email_account_signature(); + signature = res.message.signature; + } + if(!frappe.utils.is_html(signature)) { signature = signature.replace(/\n/g, "
    "); } From ce0b243d9394fe25a23adab36776b294f76eaa20 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 29 Dec 2020 12:07:32 +0100 Subject: [PATCH 205/284] chore: flake8 allow long line --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 863175a028..9220c8b605 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") +pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") # noqa: E501 words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From 97b693c6b0c31f7ae652d2faa88648440fd5f81a Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 29 Dec 2020 16:58:28 +0530 Subject: [PATCH 206/284] feat: Added permission to grant only `Select` access to document (#12063) * feat: add permtype 'select' to DocPerm and CustomDocPerm * feat: add 'select' perm in rights tupple * feat: provisions to handle select permission * feat: toggle href based on permissions * feat: pass permission type explicitly while validating link in permission check * fix: sider * feat: added test cases to validate select perm * feat: add method frappe.only_has_select_perm to explicitly check the select perm * fix: if user only has select perm then do not show anchor tag for link fields * fix: sider --- frappe/__init__.py | 15 + .../custom_docperm/custom_docperm.json | 11 +- frappe/core/doctype/docperm/docperm.json | 664 ++---------------- .../permission_manager/permission_manager.js | 2 +- frappe/desk/search.py | 3 +- frappe/model/db_query.py | 12 +- frappe/permissions.py | 7 +- frappe/public/js/frappe/form/controls/link.js | 10 + frappe/public/js/frappe/form/formatters.js | 15 +- frappe/public/js/frappe/model/model.js | 8 +- frappe/tests/test_permissions.py | 20 +- frappe/utils/user.py | 9 +- 12 files changed, 151 insertions(+), 625 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 9958ae9700..4040a38e62 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -628,6 +628,21 @@ def clear_cache(user=None, doctype=None): local.role_permissions = {} +def only_has_select_perm(doctype, user=None, ignore_permissions=False): + if ignore_permissions: + return False + + if not user: + user = local.session.user + + import frappe.permissions + permissions = frappe.permissions.get_role_permissions(doctype, user=user) + + if permissions.get('select') and not permissions.get('read'): + return True + else: + return False + def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False): """Raises `frappe.PermissionError` if not permitted. diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.json b/frappe/core/doctype/custom_docperm/custom_docperm.json index f8f7f58be1..93f5431903 100644 --- a/frappe/core/doctype/custom_docperm/custom_docperm.json +++ b/frappe/core/doctype/custom_docperm/custom_docperm.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "hash", "creation": "2017-01-11 04:21:35.217943", @@ -13,6 +14,7 @@ "column_break_2", "permlevel", "section_break_4", + "select", "read", "write", "create", @@ -211,9 +213,16 @@ "fieldtype": "Data", "label": "Reference Document Type", "read_only": 1 + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "label": "Select" } ], - "modified": "2019-10-31 16:58:16.157079", + "links": [], + "modified": "2020-12-03 15:20:48.296730", "modified_by": "Administrator", "module": "Core", "name": "Custom DocPerm", diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json index 1a23118a29..4411a67435 100644 --- a/frappe/core/doctype/docperm/docperm.json +++ b/frappe/core/doctype/docperm/docperm.json @@ -1,775 +1,229 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "hash", - "beta": 0, "creation": "2013-02-22 01:27:33", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role_and_level", + "role", + "if_owner", + "column_break_2", + "permlevel", + "section_break_4", + "select", + "read", + "write", + "create", + "delete", + "column_break_8", + "submit", + "cancel", + "amend", + "additional_permissions", + "report", + "export", + "import", + "set_user_permissions", + "column_break_19", + "share", + "print", + "email" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "role_and_level", "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": "Role and Level", - "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, - "translatable": 0, - "unique": 0 + "label": "Role and Level" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "role", "fieldtype": "Link", - "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": "Role", - "length": 0, - "no_copy": 0, "oldfieldname": "role", "oldfieldtype": "Link", "options": "Role", - "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, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "Apply this rule if the User is the Owner", "fieldname": "if_owner", "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": "If user is the owner", - "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, - "translatable": 0, - "unique": 0 + "label": "If user is the owner" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", - "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": "Level", - "length": 0, - "no_copy": 0, "oldfieldname": "permlevel", "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "40px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "40px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_4", "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": "Permissions", - "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, - "translatable": 0, - "unique": 0 + "label": "Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "read", "fieldtype": "Check", - "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": "Read", - "length": 0, - "no_copy": 0, "oldfieldname": "read", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "write", "fieldtype": "Check", - "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": "Write", - "length": 0, - "no_copy": 0, "oldfieldname": "write", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "create", "fieldtype": "Check", - "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": "Create", - "length": 0, - "no_copy": 0, "oldfieldname": "create", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "delete", "fieldtype": "Check", - "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": "Delete", - "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, - "translatable": 0, - "unique": 0 + "label": "Delete" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_8", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "submit", "fieldtype": "Check", - "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": "Submit", - "length": 0, - "no_copy": 0, "oldfieldname": "submit", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "cancel", "fieldtype": "Check", - "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": "Cancel", - "length": 0, - "no_copy": 0, "oldfieldname": "cancel", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "amend", "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": "Amend", - "length": 0, - "no_copy": 0, "oldfieldname": "amend", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "additional_permissions", "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": "Additional Permissions", - "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, - "translatable": 0, - "unique": 0 + "label": "Additional Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "report", "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": "Report", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "export", "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": "Export", - "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, - "translatable": 0, - "unique": 0 + "label": "Export" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "import", "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": "Import", - "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, - "translatable": 0, - "unique": 0 + "label": "Import" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "This role update User Permissions for a user", "fieldname": "set_user_permissions", "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": "Set User Permissions", - "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, - "translatable": 0, - "unique": 0 + "label": "Set User Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_19", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "share", "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": "Share", - "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, - "translatable": 0, - "unique": 0 + "label": "Share" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "print", "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": "Print", - "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, - "translatable": 0, - "unique": 0 + "label": "Print" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "email", "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": "Email", - "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, - "translatable": 0, - "unique": 0 + "label": "Email" + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Select" } ], - "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": "2018-05-29 11:54:38.613936", + "links": [], + "modified": "2020-12-03 15:15:30.488212", "modified_by": "Administrator", "module": "Core", "name": "DocPerm", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 0d3267c7d5..02fbf943d5 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -269,7 +269,7 @@ frappe.PermissionEngine = Class.extend({ .css({"margin-top": "15px"}); }, - rights: ["read", "write", "create", "delete", "submit", "cancel", "amend", + rights: ["select", "read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share"], set_show_users: function(cell, role) { diff --git a/frappe/desk/search.py b/frappe/desk/search.py index f249c36746..f4e6543844 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, # 2 is the index of _relevance column order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype) - ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype)) + ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' + ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype)) if doctype in UNTRANSLATED_DOCTYPES: page_length = None diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index b936251b50..c799586d61 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -40,7 +40,10 @@ class DatabaseQuery(object): ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, update=None, add_total_row=None, user_settings=None, reference_doctype=None, return_query=False, strict=True, pluck=None, ignore_ddl=False): - if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): + if not ignore_permissions and \ + not frappe.has_permission(self.doctype, "select", user=user) and \ + not frappe.has_permission(self.doctype, "read", user=user): + frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) raise frappe.PermissionError(self.doctype) @@ -315,7 +318,10 @@ class DatabaseQuery(object): def append_table(self, table_name): self.tables.append(table_name) doctype = table_name[4:-1] - if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)): + ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' + + if (not self.flags.ignore_permissions) and\ + (not frappe.has_permission(doctype, ptype=ptype)): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype)) raise frappe.PermissionError(doctype) @@ -576,7 +582,7 @@ class DatabaseQuery(object): self.shared = frappe.share.get_shared(self.doctype, self.user) if (not meta.istable and - not role_permissions.get("read") and + not (role_permissions.get("select") or role_permissions.get("read")) and not self.flags.ignore_permissions and not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)): only_if_shared = True diff --git a/frappe/permissions.py b/frappe/permissions.py index 0d766aec8d..a45fbdcd06 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,7 +7,7 @@ import frappe, copy, json from frappe import _, msgprint from frappe.utils import cint import frappe.share -rights = ("read", "write", "create", "delete", "submit", "cancel", "amend", +rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") # TODO: @@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra role_permissions = get_role_permissions(meta, user=user) perm = role_permissions.get(ptype) + if not perm: push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype))) @@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None): and ptype != 'create'): perms['if_owner'][ptype] = 1 # has no access if not owner - # only provide read access so that user is able to at-least access list + # only provide select or read access so that user is able to at-least access list # (and the documents will be filtered based on owner sin further checks) - perms[ptype] = 1 if ptype == 'read' else 0 + perms[ptype] = 1 if ptype in ['select', 'read'] else 0 frappe.local.role_permissions[cache_key] = perms diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 56f9430238..111ee7d8f6 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -215,6 +215,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } me.$input.cache[doctype][term] = r.results; me.awesomplete.list = me.$input.cache[doctype][term]; + me.toggle_href(doctype); } }); }, 500)); @@ -296,6 +297,15 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ // returns [{value: 'Manufacturer 1', 'description': 'mobile part 1, mobile part 2'}] }, + toggle_href(doctype) { + if (frappe.model.can_select(doctype) && !frappe.model.can_read(doctype)) { + // remove href from link field as user has only select perm + this.$input_area.find(".link-btn").addClass('hide'); + } else { + this.$input_area.find(".link-btn").removeClass('hide'); + } + }, + get_filter_description(filters) { let doctype = this.get_options(); let filter_array = []; diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index f9a1d0b643..2b8956653b 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -128,11 +128,16 @@ frappe.form.formatters = { return repl('%(value)s', {onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); } else if(docfield && doctype) { - return ` - ${__(options && options.label || value)}` + if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) { + return ` + ${__(options && options.label || value)}`; + } else { + return value; + } + } else { return value; } diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 1d302215dd..e82f64c6fc 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -135,8 +135,8 @@ $.extend(frappe.model, { let cached_timestamp = null; let cached_doc = null; - let cached_docs = frappe.model.get_from_localstorage(doctype) - + let cached_docs = frappe.model.get_from_localstorage(doctype); + if (cached_docs) { cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; if(cached_doc) { @@ -252,6 +252,10 @@ $.extend(frappe.model, { return frappe.boot.user.can_create.indexOf(doctype)!==-1; }, + can_select: function(doctype) { + return frappe.boot.user.can_select.indexOf(doctype)!==-1; + }, + can_read: function(doctype) { return frappe.boot.user.can_read.indexOf(doctype)!==-1; }, diff --git a/frappe/tests/test_permissions.py b/frappe/tests/test_permissions.py index dddc790c94..6897d500c9 100644 --- a/frappe/tests/test_permissions.py +++ b/frappe/tests/test_permissions.py @@ -9,7 +9,7 @@ import frappe.defaults import unittest import frappe.model.meta from frappe.permissions import (add_user_permission, remove_user_permission, - clear_user_permissions_for_doctype, get_doc_permissions, add_permission) + clear_user_permissions_for_doctype, get_doc_permissions, add_permission, update_permission_property) from frappe.core.page.permission_manager.permission_manager import update, reset from frappe.test_runner import make_test_records_for_doctype from frappe.core.doctype.user_permission.user_permission import clear_user_permissions @@ -58,6 +58,24 @@ class TestPermissions(unittest.TestCase): post = frappe.get_doc("Blog Post", "-test-blog-post") self.assertTrue(post.has_permission("read")) + def test_select_permission(self): + # grant only select perm to blog post + add_permission('Blog Post', 'Sales User', 0) + update_permission_property('Blog Post', 'Sales User', 0, 'select', 1) + update_permission_property('Blog Post', 'Sales User', 0, 'read', 0) + update_permission_property('Blog Post', 'Sales User', 0, 'write', 0) + + frappe.clear_cache(doctype="Blog Post") + frappe.set_user("test3@example.com") + + # validate select perm + post = frappe.get_doc("Blog Post", "-test-blog-post") + self.assertTrue(post.has_permission("select")) + + # validate does not have read and write perm + self.assertFalse(post.has_permission("read")) + self.assertRaises(frappe.PermissionError, post.save) + def test_user_permissions_in_doc(self): add_user_permission("Blog Category", "-test-blog-category-1", "test2@example.com") diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 7ee47cb197..ee9ee5dae9 100755 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -22,6 +22,7 @@ class UserPermissions: self.all_read = [] self.can_create = [] + self.can_select = [] self.can_read = [] self.can_write = [] self.can_cancel = [] @@ -104,6 +105,9 @@ class UserPermissions: if not p.get("read") and (dt in user_shared): p["read"] = 1 + if p.get('select'): + self.can_select.append(dt) + if not dtp.get('istable'): if p.get('create') and not dtp.get('issingle'): if dtp.get('in_create'): @@ -193,9 +197,8 @@ class UserPermissions: d.name = self.name d.roles = self.get_roles() d.defaults = self.get_defaults() - - for key in ("can_create", "can_write", "can_read", "can_cancel", "can_delete", - "can_get_report", "allow_modules", "all_read", "can_search", + for key in ("can_select", "can_create", "can_write", "can_read", "can_cancel", + "can_delete", "can_get_report", "allow_modules", "all_read", "can_search", "in_create", "can_export", "can_import", "can_print", "can_email", "can_set_user_permissions"): d[key] = list(set(getattr(self, key))) From f055604167cf762655d6d0d49cee07016e1b3317 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 31 Dec 2020 13:11:46 +0530 Subject: [PATCH 207/284] fix: default str for json dumps --- frappe/integrations/doctype/webhook/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index f1556aa661..ad64d9f714 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook): for i in range(3): try: - r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) + r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) break From a498751d8866e5dd9aaf165227b439926ab8b449 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Thu, 31 Dec 2020 16:42:19 +0530 Subject: [PATCH 208/284] fix: secondary button in dialog for website --- frappe/website/js/bootstrap-4.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/website/js/bootstrap-4.js b/frappe/website/js/bootstrap-4.js index dbe837b101..da720eedaf 100644 --- a/frappe/website/js/bootstrap-4.js +++ b/frappe/website/js/bootstrap-4.js @@ -18,7 +18,7 @@ $('.dropdown-menu a.dropdown-toggle').on('click', function (e) { return false; }); -frappe.get_modal = function(title, content) { +frappe.get_modal = function (title, content) { return $( ` From 282030be5fb7e9485d3f3702228b5f6966ba8870 Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 31 Dec 2020 15:19:35 +0530 Subject: [PATCH 209/284] fix: auto-repeat issue --- frappe/automation/doctype/auto_repeat/auto_repeat.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index c2c84692d8..d54ae8d62c 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', { toggle_submit_on_creation: function(frm) { // submit on creation checkbox - frappe.model.with_doctype(frm.doc.reference_doctype, () => { - let meta = frappe.get_meta(frm.doc.reference_doctype); - frm.toggle_display('submit_on_creation', meta.is_submittable); - }); + if (frm.doc.reference_doctype) { + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); + } }, template: function(frm) { From dd46199dd75560d378e090a7f0c18e0c553f3d8a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sun, 3 Jan 2021 20:04:45 +0530 Subject: [PATCH 210/284] feat: Allow ignoring validations via server script --- frappe/core/doctype/server_script/server_script.json | 4 ++-- frappe/core/doctype/server_script/server_script_utils.py | 1 + frappe/model/document.py | 6 ++++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 94a48f196c..9aa7b5afe5 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -47,7 +47,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -88,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-03 22:42:02.708148", + "modified": "2021-01-03 18:50:14.767595", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 4dc4f12b34..12a8fa47fa 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -6,6 +6,7 @@ import frappe EVENT_MAP = { 'before_insert': 'Before Insert', 'after_insert': 'After Insert', + 'before_validate': 'Before Validate', 'validate': 'Before Save', 'on_update': 'After Save', 'before_submit': 'Before Submit', diff --git a/frappe/model/document.py b/frappe/model/document.py index 3789e20b19..9efd8b6c94 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -939,15 +939,17 @@ class Document(BaseDocument): self.load_doc_before_save() self.reset_seen() + # before_validate method should be executed before ignoring validations + if self._action in ("save", "submit"): + self.run_method("before_validate") + if self.flags.ignore_validate: return if self._action=="save": - self.run_method("before_validate") self.run_method("validate") self.run_method("before_save") elif self._action=="submit": - self.run_method("before_validate") self.run_method("validate") self.run_method("before_submit") elif self._action=="cancel": From aad5ace31ab02665e48fe1f5083477bedace6adc Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 4 Jan 2021 11:38:39 +0530 Subject: [PATCH 211/284] fix: clear cache after removing server scripts --- frappe/core/doctype/server_script/test_server_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 957cbbf72d..8dd6d03fee 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase): def tearDownClass(cls): frappe.db.commit() frappe.db.sql('truncate `tabServer Script`') + frappe.cache().delete_key('server_script_map') def setUp(self): frappe.cache().delete_value('server_script_map') From b73a1a8710b4a19a166266351efbd73b2b04ef4a Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 4 Jan 2021 13:42:12 +0530 Subject: [PATCH 212/284] fix: reset customizations don't get committed --- .../doctype/customize_form/customize_form.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 82513783c7..50acab46b5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -455,11 +455,15 @@ class CustomizeForm(Document): self.fetch_to_customize() def reset_customization(doctype): - frappe.db.sql(""" - DELETE FROM `tabProperty Setter` WHERE doc_type=%s - and `field_name`!='naming_series' - and `property`!='options' - """, doctype) + setters = frappe.get_all("Property Setter", filters={ + 'doc_type': doctype, + 'field_name': ['!=', 'naming_series'], + 'property': ['!=', 'options'] + }, pluck='name') + + for setter in setters: + frappe.delete_doc("Property Setter", setter) + frappe.clear_cache(doctype=doctype) doctype_properties = { From 51d8046da15b6539f9badecdff506ccb32eee26b Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Mon, 4 Jan 2021 15:30:28 +0530 Subject: [PATCH 213/284] fix: use frappe.throw Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/model/rename_doc.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 2de1c51ea1..15e044cc38 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -26,10 +26,11 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne frappe.msgprint(_('Saved'), alert=True, indicator='green') except Exception as e: if frappe.db.is_duplicate_entry(e): - frappe.msgprint(_("{0} {1} already exists").format( - doctype, frappe.bold(docname)), title=_("Duplicate Name"), indicator="red" + frappe.throw( + _("{0} {1} already exists").format(doctype, frappe.bold(docname)), + title=_("Duplicate Name"), + exc=frappe.DuplicateEntryError ) - raise frappe.DuplicateEntryError(doctype, docname, e) return docname From edc959a7035b1cdb07371badbe056ea65e1a3b93 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 Jan 2021 14:46:00 +0530 Subject: [PATCH 214/284] fix: Clear server script maap after test --- frappe/core/doctype/server_script/test_server_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 957cbbf72d..aac8b3deed 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase): def tearDownClass(cls): frappe.db.commit() frappe.db.sql('truncate `tabServer Script`') + frappe.cache().delete_value('server_script_map') def setUp(self): frappe.cache().delete_value('server_script_map') From 35c612e07629362cb0b9ce275b1e5133c39fbb70 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 5 Jan 2021 16:15:25 +0530 Subject: [PATCH 215/284] fix: translator url (#12144) --- frappe/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 3d7ae0abb4..ea0a91a639 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -18,7 +18,7 @@ app_email = "info@frappe.io" docs_app = "frappe_io" -translator_url = "https://translatev2.erpnext.com" +translator_url = "https://translate.erpnext.com" before_install = "frappe.utils.install.before_install" after_install = "frappe.utils.install.after_install" From 0b5868af0072fe5bb3dbe7e9dc4a97a5872fe800 Mon Sep 17 00:00:00 2001 From: gavin Date: Tue, 5 Jan 2021 16:52:38 +0530 Subject: [PATCH 216/284] fix: Strip HTML only if string is passed, else evaluate like before (#12157) --- frappe/public/js/frappe/ui/field_group.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js index 67aeb4474e..c37ea57dae 100644 --- a/frappe/public/js/frappe/ui/field_group.js +++ b/frappe/public/js/frappe/ui/field_group.js @@ -86,7 +86,10 @@ frappe.ui.FieldGroup = frappe.ui.form.Layout.extend({ var f = this.fields_dict[key]; if (f.get_value) { var v = f.get_value(); - if (f.df.reqd && is_null(strip_html(v))) + if ( + f.df.reqd && + is_null(typeof v === 'string' ? strip_html(v) : v) + ) errors.push(__(f.df.label)); if (f.df.reqd From dfc5fb3b5d0d9d00b01ccfa72d2f3a3ec7716625 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 5 Jan 2021 17:05:35 +0530 Subject: [PATCH 217/284] fix: list view comment count (#12156) --- frappe/core/doctype/comment/comment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index a2105c1511..04ecc83b38 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -150,7 +150,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): try: # use sql, so that we do not mess with the timestamp frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec - (json.dumps(_comments[-50:]), reference_name)) + (json.dumps(_comments[-100:]), reference_name)) except Exception as e: if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): From f54ec2ba11bb386cf88afe2cfe94106ef732af2d Mon Sep 17 00:00:00 2001 From: Tim Gates Date: Tue, 5 Jan 2021 22:48:09 +1100 Subject: [PATCH 218/284] docs: fix simple typo, transaltion -> translation (#12136) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 3685daf986..2cee8c34b5 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -190,7 +190,7 @@ def get_full_dict(lang): frappe.local.lang_full_dict = load_lang(lang) try: - # get user specific transaltion data + # get user specific translation data user_translations = get_user_translations(lang) frappe.local.lang_full_dict.update(user_translations) except Exception: From 3e6dd594efc321406100b2dca816ebe6d0a9bd16 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Tue, 5 Jan 2021 12:48:54 +0100 Subject: [PATCH 219/284] fix: translation (#12117) --- frappe/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index f1d72c1443..5b45d8c217 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -1577,7 +1577,7 @@ Monospace,Monospace, More articles on {0},Weitere Artikel zum {0}, More content for the bottom of the page.,Zusätzlicher Inhalt für den unteren Teil der Seite., Most Used,Am Meisten verwendet, -Move To,Ziehen nach, +Move To,Bewegen nach, Move To Trash,In den Papierkorb verschieben, Move to Row Number,Gehe zu Zeilennummer, Mr,Hr., From c8ef51a8ec60a594b4d816a67ba2445b9e48e2ee Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 6 Jan 2021 13:50:37 +0530 Subject: [PATCH 220/284] perf: revert to using _classes global instead of frappe.cache --- frappe/model/base_document.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 5d86b3bac8..44394841d1 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -26,11 +26,14 @@ max_positive_value = { DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link') +_classes = {} + def get_controller(doctype): """Returns the **class** object of the given DocType. For `custom` type, returns `frappe.model.document.Document`. :param doctype: DocType name as string.""" + global _classes def _get_controller(): from frappe.model.document import Document @@ -48,7 +51,7 @@ def get_controller(doctype): else: class_overrides = frappe.get_hooks('override_doctype_class') if class_overrides and class_overrides.get(doctype): - import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1] + import_path = class_overrides[doctype][-1] module_path, classname = import_path.rsplit('.', 1) module = frappe.get_module(module_path) if not hasattr(module, classname): @@ -69,10 +72,13 @@ def get_controller(doctype): if frappe.local.dev_server: return _get_controller() - - key = '{}:doctype_classes'.format(frappe.local.site) - return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True) - + + site_classes = _classes.setdefault(frappe.local.site, {}) + if doctype not in site_classes: + site_classes[doctype] = _get_controller() + + return site_classes[doctype] + class BaseDocument(object): ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") From bc305dab100ab42611182ee467ac7f6b6bcf2383 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 6 Jan 2021 16:52:52 +0530 Subject: [PATCH 221/284] test: fix test_override_doctype_class by resetting cached values --- frappe/tests/test_hooks.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py index f19904c8fc..fada861b79 100644 --- a/frappe/tests/test_hooks.py +++ b/frappe/tests/test_hooks.py @@ -17,21 +17,21 @@ class TestHooks(unittest.TestCase): hooks.get("doc_events").get("*").get("on_update")) def test_override_doctype_class(self): - # mock get_hooks - original = frappe.get_hooks - def get_hooks(hook=None, default=None, app_name=None): - if hook == 'override_doctype_class': - return { - 'ToDo': ['frappe.tests.test_hooks.CustomToDo'] - } - return original(hook, default, app_name) - frappe.get_hooks = get_hooks + from frappe import hooks + from frappe.model import base_document + + # Set hook + hooks.override_doctype_class = { + 'ToDo': ['frappe.tests.test_hooks.CustomToDo'] + } + + # Clear cache + frappe.cache().delete_value('app_hooks') + base_document._classes = {} todo = frappe.get_doc(doctype='ToDo', description='asdf') self.assertTrue(isinstance(todo, CustomToDo)) - # restore - frappe.get_hooks = original class CustomToDo(ToDo): pass From 1591cee1457b51d1d58765abd92afbd435b5cfac Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 6 Jan 2021 17:08:57 +0530 Subject: [PATCH 222/284] fix: remove debugger --- frappe/public/js/frappe/views/communication.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index fe14ad4793..e974cd52ed 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -464,7 +464,6 @@ frappe.views.CommunicationComposer = Class.extend({ }, send_action: function() { - debugger; var me = this; var btn = me.dialog.get_primary_btn(); @@ -719,4 +718,3 @@ frappe.views.CommunicationComposer = Class.extend({ return text.replace(/\n{3,}/g, '\n\n'); } }); - From 213744aa4949279c13f8f92c95700bab9c711be6 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 6 Jan 2021 17:34:48 +0530 Subject: [PATCH 223/284] test: fix test_money_in_words (#12166) --- frappe/tests/test_translate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index 4dcaf3e979..4f1b69cc76 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -18,6 +18,7 @@ class TestTranslate(unittest.TestCase): frappe.local.lang = 'fr' self.assertEqual(_('Change'), 'Changement') self.assertEqual(_('Change', context='Coins'), 'la monnaie') + frappe.local.lang = 'en' expected_output = [ ('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2), From 4f407ac2f4f40102c95098d6cad47dd0d799f4e0 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Thu, 7 Jan 2021 10:46:28 +0530 Subject: [PATCH 224/284] fix: strip_html breaks when it gets undefined --- frappe/public/js/frappe/utils/common.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js index 0a145b098b..20eb4393a3 100644 --- a/frappe/public/js/frappe/utils/common.js +++ b/frappe/public/js/frappe/utils/common.js @@ -108,7 +108,7 @@ window.replace_all = function(s, t1, t2) { } window.strip_html = function(txt) { - return txt.replace(/<[^>]*>/g, ""); + return cstr(txt).replace(/<[^>]*>/g, ""); } window.strip = function(s, chars) { From 34ad0cf331f65a02b53314f432b3f82a0e7e2d35 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 7 Jan 2021 10:57:08 +0530 Subject: [PATCH 225/284] fix: introduce frappe.controllers; clear global if cache is cleared; replace old references --- frappe/__init__.py | 1 + frappe/cache_manager.py | 10 ++++++++++ frappe/core/doctype/doctype/doctype.py | 8 +++----- frappe/model/base_document.py | 11 ++++------- frappe/tests/test_hooks.py | 4 ++-- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 4040a38e62..f8ae6b4ec1 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -27,6 +27,7 @@ __version__ = '13.0.0-dev' __title__ = "Frappe Framework" local = Local() +controllers = {} class _dict(dict): """dict like object that exposes keys as attributes""" diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 3b3d188999..ed5c7b64ad 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -72,6 +72,7 @@ def clear_document_cache(): frappe.cache().delete_key("document_cache") def clear_doctype_cache(doctype=None): + clear_controller_cache(doctype) cache = frappe.cache() if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): @@ -104,6 +105,15 @@ def clear_doctype_cache(doctype=None): # Clear all document's cache. To clear documents of a specific DocType document_cache should be restructured clear_document_cache() +def clear_controller_cache(doctype=None): + if not doctype: + del frappe.controllers + frappe.controllers = {} + return + + for site_controllers in frappe.controllers.values(): + site_controllers.pop(doctype, None) + def get_doctype_map(doctype, name, filters=None, order_by=None): cache = frappe.cache() cache_key = frappe.scrub(doctype) + '_map' diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 3e283e1699..1daa7e8af7 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import re, copy, os, shutil import json -from frappe.cache_manager import clear_user_cache +from frappe.cache_manager import clear_user_cache, clear_controller_cache # imports - third party imports import six @@ -408,13 +408,11 @@ class DocType(Document): if not frappe.flags.in_patch: self.rename_files_and_folders(old, new) - for site in frappe.utils.get_sites(): - frappe.cache().delete(f"{site}:doctype_classes", old) + clear_controller_cache(old) def after_delete(self): if not self.custom: - for site in frappe.utils.get_sites(): - frappe.cache().delete(f"{site}:doctype_classes", self.name) + clear_controller_cache(self.name) def rename_files_and_folders(self, old, new): # move files diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 44394841d1..7a90ecaca5 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -26,14 +26,11 @@ max_positive_value = { DOCTYPES_FOR_DOCTYPE = ('DocType', 'DocField', 'DocPerm', 'DocType Action', 'DocType Link') -_classes = {} - def get_controller(doctype): """Returns the **class** object of the given DocType. For `custom` type, returns `frappe.model.document.Document`. :param doctype: DocType name as string.""" - global _classes def _get_controller(): from frappe.model.document import Document @@ -73,11 +70,11 @@ def get_controller(doctype): if frappe.local.dev_server: return _get_controller() - site_classes = _classes.setdefault(frappe.local.site, {}) - if doctype not in site_classes: - site_classes[doctype] = _get_controller() + site_controllers = frappe.controllers.setdefault(frappe.local.site, {}) + if doctype not in site_controllers: + site_controllers[doctype] = _get_controller() - return site_classes[doctype] + return site_controllers[doctype] class BaseDocument(object): ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py index fada861b79..ff71e2414c 100644 --- a/frappe/tests/test_hooks.py +++ b/frappe/tests/test_hooks.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import unittest import frappe from frappe.desk.doctype.todo.todo import ToDo +from frappe.cache_manager import clear_controller_cache class TestHooks(unittest.TestCase): def test_hooks(self): @@ -18,7 +19,6 @@ class TestHooks(unittest.TestCase): def test_override_doctype_class(self): from frappe import hooks - from frappe.model import base_document # Set hook hooks.override_doctype_class = { @@ -27,7 +27,7 @@ class TestHooks(unittest.TestCase): # Clear cache frappe.cache().delete_value('app_hooks') - base_document._classes = {} + clear_controller_cache('ToDo') todo = frappe.get_doc(doctype='ToDo', description='asdf') self.assertTrue(isinstance(todo, CustomToDo)) From aa2360e589368d58b909fc9f49ad9dd028990da4 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 7 Jan 2021 14:46:32 +0530 Subject: [PATCH 226/284] fix: cannot refresh grid_row --- frappe/public/js/frappe/form/script_helpers.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js index 83ba191d4d..a0caae33e5 100644 --- a/frappe/public/js/frappe/form/script_helpers.js +++ b/frappe/public/js/frappe/form/script_helpers.js @@ -18,15 +18,17 @@ window.refresh_field = function(n, docname, table_field) { if (n && typeof n==='string' && table_field){ var grid = cur_frm.fields_dict[table_field].grid, - field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}); + field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}), + grid_row = grid.grid_rows_by_docname[docname]; + if (field && field.length){ field = field[0]; var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname); $.extend(field, meta); - if (docname){ - cur_frm.fields_dict[table_field].grid.grid_rows_by_docname[docname].refresh_field(n); + if (grid_row){ + grid_row.refresh_field(n); } else { - cur_frm.fields_dict[table_field].grid.refresh(); + grid.refresh(); } } } else if(cur_frm) { From 54f9b894edd2c6c6973468f1b8db8ba054b36dd2 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 7 Jan 2021 21:18:42 +0530 Subject: [PATCH 227/284] fix: add yesterday and tomorrow to timespan; optimise get_timespan_date_range --- frappe/public/js/frappe/ui/filters/filter.js | 3 +- frappe/utils/data.py | 36 +++++++++++--------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index da19ce7eb0..4a047c76ae 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -518,7 +518,7 @@ frappe.ui.filter_utils = { ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) ) { df.fieldtype = 'Select'; - df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']); + df.options = this.get_timespan_options(['Last', 'Yesterday', 'Today', 'Tomorrow', 'This', 'Next']); } if (condition === 'is') { df.fieldtype = 'Select'; @@ -533,7 +533,6 @@ frappe.ui.filter_utils = { get_timespan_options(periods) { const period_map = { Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], - Today: null, This: ['Week', 'Month', 'Quarter', 'Year'], Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], }; diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 4a88b5fda1..c24b9f186e 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -444,25 +444,29 @@ def get_weekday(datetime=None): return weekdays[datetime.weekday()] def get_timespan_date_range(timespan): + today = nowdate() date_range_map = { - "last week": [add_to_date(nowdate(), days=-7), nowdate()], - "last month": [add_to_date(nowdate(), months=-1), nowdate()], - "last quarter": [add_to_date(nowdate(), months=-3), nowdate()], - "last 6 months": [add_to_date(nowdate(), months=-6), nowdate()], - "last year": [add_to_date(nowdate(), years=-1), nowdate()], - "today": [nowdate(), nowdate()], - "this week": [get_first_day_of_week(nowdate(), as_str=True), nowdate()], - "this month": [get_first_day(nowdate(), as_str=True), nowdate()], - "this quarter": [get_quarter_start(nowdate(), as_str=True), nowdate()], - "this year": [get_year_start(nowdate(), as_str=True), nowdate()], - "next week": [nowdate(), add_to_date(nowdate(), days=7)], - "next month": [nowdate(), add_to_date(nowdate(), months=1)], - "next quarter": [nowdate(), add_to_date(nowdate(), months=3)], - "next 6 months": [nowdate(), add_to_date(nowdate(), months=6)], - "next year": [nowdate(), add_to_date(nowdate(), years=1)], + "last week": lambda: (add_to_date(today, days=-7), today), + "last month": lambda: (add_to_date(today, months=-1), today), + "last quarter": lambda: (add_to_date(today, months=-3), today), + "last 6 months": lambda: (add_to_date(today, months=-6), today), + "last year": lambda: (add_to_date(today, years=-1), today), + "yesterday": lambda: (add_to_date(today, days=-1),) * 2, + "today": lambda: (today, today), + "tomorrow": lambda: (add_to_date(today, days=1),) * 2, + "this week": lambda: (get_first_day_of_week(today, as_str=True), today), + "this month": lambda: (get_first_day(today, as_str=True), today), + "this quarter": lambda: (get_quarter_start(today, as_str=True), today), + "this year": lambda: (get_year_start(today, as_str=True), today), + "next week": lambda: (today, add_to_date(today, days=7)), + "next month": lambda: (today, add_to_date(today, months=1)), + "next quarter": lambda: (today, add_to_date(today, months=3)), + "next 6 months": lambda: (today, add_to_date(today, months=6)), + "next year": lambda: (today, add_to_date(today, years=1)), } - return date_range_map.get(timespan) + if timespan in date_range_map: + return date_range_map[timespan]() def global_date_format(date, format="long"): """returns localized date in the form of January 1, 2012""" From 11c01cd52d557722b5f6e53ede65370f2e97f5ff Mon Sep 17 00:00:00 2001 From: "Manduul. B" Date: Fri, 8 Jan 2021 13:41:50 +0800 Subject: [PATCH 228/284] fix: Wrong closing of h5 tag (#12178) --- frappe/website/doctype/blog_post/templates/blog_post_row.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/blog_post/templates/blog_post_row.html b/frappe/website/doctype/blog_post/templates/blog_post_row.html index 7daf27adc8..53539c33e0 100644 --- a/frappe/website/doctype/blog_post/templates/blog_post_row.html +++ b/frappe/website/doctype/blog_post/templates/blog_post_row.html @@ -21,7 +21,7 @@ {%- if post.featured -%}
    {{ post.title }}
    {%- else -%} -
    {{ post.title }}
    +
    {{ post.title }}
    {%- endif -%}

    {{ post.intro }}

    @@ -38,4 +38,4 @@ - \ No newline at end of file + From 988367828086655fea2d7ed8f5bfe60689019bf2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 8 Jan 2021 11:28:16 +0530 Subject: [PATCH 229/284] fix(DocType): typos in version_html (endif instead of endfor) (bp #12106) (#12112) * fix(DocType): typos in version_html (endif instead of endfor) (cherry picked from commit a4f48766a1d44497eaab7306c1b900ddc3d297c2) Co-authored-by: ci2014 --- frappe/core/doctype/version/version_view.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html index 5383be82a1..67f005ed4c 100644 --- a/frappe/core/doctype/version/version_view.html +++ b/frappe/core/doctype/version/version_view.html @@ -21,7 +21,7 @@ {{ item[1] }} {{ item[2] }} - {% endif %} + {% endfor %} {% endif %} @@ -58,7 +58,7 @@ - {% endif %} + {% endfor %} @@ -93,4 +93,4 @@ {% endfor %} {% endif %} - \ No newline at end of file + From f939ec87cc41b41f008850a85505376874dc3083 Mon Sep 17 00:00:00 2001 From: Snyk bot Date: Fri, 8 Jan 2021 08:01:33 +0200 Subject: [PATCH 230/284] fix(Snyk): Security upgrade socket.io from 2.3.0 to 2.4.0 (#12181) The following vulnerabilities are fixed with an upgrade: - https://snyk.io/vuln/SNYK-JS-SOCKETIO-1024859 --- package.json | 2 +- yarn.lock | 135 ++++++++++++++++++++------------------------------- 2 files changed, 53 insertions(+), 84 deletions(-) diff --git a/package.json b/package.json index 8603d8e071..fcbc349307 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "redis": "^2.8.0", "showdown": "^1.9.1", "snyk": "^1.425.4", - "socket.io": "^2.3.0", + "socket.io": "^2.4.0", "superagent": "^3.8.2", "touch": "^3.1.0", "vue": "^2.6.11", diff --git a/yarn.lock b/yarn.lock index 072810faa3..3810b88e47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -569,11 +569,6 @@ async-foreach@^0.1.3: resolved "https://registry.yarnpkg.com/async-foreach/-/async-foreach-0.1.3.tgz#36121f845c0578172de419a97dbeb1d16ec34542" integrity sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI= -async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" - integrity sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg== - async@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720" @@ -677,13 +672,6 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -better-assert@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" - integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= - dependencies: - callsite "1.0.0" - big.js@^3.1.3: version "3.2.0" resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e" @@ -914,11 +902,6 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" -callsite@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" - integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= - callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" @@ -1172,6 +1155,11 @@ component-emitter@1.2.1, component-emitter@^1.2.0, component-emitter@^1.2.1: resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= +component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + component-inherit@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" @@ -1230,16 +1218,16 @@ cookie-signature@1.0.6: resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= -cookie@0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" - integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= - cookie@0.4.0, cookie@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@~0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + cookiejar@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" @@ -1829,20 +1817,20 @@ endian-reader@^0.3.0: resolved "https://registry.yarnpkg.com/endian-reader/-/endian-reader-0.3.0.tgz#84eca436b80aed0d0639c47291338b932efe50a0" integrity sha1-hOykNrgK7Q0GOcRykTOLky7+UKA= -engine.io-client@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.0.tgz#82a642b42862a9b3f7a188f41776b2deab643700" - integrity sha512-a4J5QO2k99CM2a0b12IznnyQndoEvtA4UAldhGzKqnHf42I3Qs2W5SPnDvatZRcMaNZs4IevVicBPayxYt6FwA== +engine.io-client@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.5.0.tgz#fc1b4d9616288ce4f2daf06dcf612413dec941c7" + integrity sha512-12wPRfMrugVw/DNyJk34GQ5vIVArEcVMXWugQGGuw2XxUSztFNmJggZmv8IZlLyEdnpO1QB9LkcjeWewO2vxtA== dependencies: - component-emitter "1.2.1" + component-emitter "~1.3.0" component-inherit "0.0.3" - debug "~4.1.0" + debug "~3.1.0" engine.io-parser "~2.2.0" has-cors "1.1.0" indexof "0.0.1" - parseqs "0.0.5" - parseuri "0.0.5" - ws "~6.1.0" + parseqs "0.0.6" + parseuri "0.0.6" + ws "~7.4.2" xmlhttprequest-ssl "~1.5.4" yeast "0.1.2" @@ -1857,17 +1845,17 @@ engine.io-parser@~2.2.0: blob "0.0.5" has-binary2 "~1.0.2" -engine.io@~3.4.0: - version "3.4.0" - resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.0.tgz#3a962cc4535928c252759a00f98519cb46c53ff3" - integrity sha512-XCyYVWzcHnK5cMz7G4VTu2W7zJS7SM1QkcelghyIk/FmobWBtXE7fwhBusEKvCSqc3bMh8fNFMlUkCKTFRxH2w== +engine.io@~3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.5.0.tgz#9d6b985c8a39b1fe87cd91eb014de0552259821b" + integrity sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA== dependencies: accepts "~1.3.4" base64id "2.0.0" - cookie "0.3.1" + cookie "~0.4.1" debug "~4.1.0" engine.io-parser "~2.2.0" - ws "^7.1.2" + ws "~7.4.2" entities@^1.1.1: version "1.1.2" @@ -4184,11 +4172,6 @@ object-assign@^4.0.1, object-assign@^4.1.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-component@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" - integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= - object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" @@ -4473,19 +4456,15 @@ parse-passwd@^1.0.0: resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" integrity sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY= -parseqs@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" - integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= - dependencies: - better-assert "~1.0.0" +parseqs@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.6.tgz#8e4bb5a19d1cdc844a08ac974d34e273afa670d5" + integrity sha512-jeAGzMDbfSHHA091hr0r31eYfTig+29g3GKKE/PPbEQ65X0lmMwlEoqmhzu0iztID5uJpZsFlUPDP8ThPL7M8w== -parseuri@0.0.5: - version "0.0.5" - resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" - integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= - dependencies: - better-assert "~1.0.0" +parseuri@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.6.tgz#e1496e829e3ac2ff47f39a4dd044b32823c4a25a" + integrity sha512-AUjen8sAkGgao7UyCX6Ahv0gIK2fABKmYjvP4xmy5JaKvcbTRueIqIPHLAfq30xJddqSE033IOMUSOMCcK3Sow== parseurl@~1.3.3: version "1.3.3" @@ -6231,23 +6210,20 @@ socket.io-adapter@~1.1.0: resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.1.tgz#2a805e8a14d6372124dd9159ad4502f8cb07f06b" integrity sha1-KoBeihTWNyEk3ZFZrUUC+MsH8Gs= -socket.io-client@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" - integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== +socket.io-client@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.4.0.tgz#aafb5d594a3c55a34355562fc8aea22ed9119a35" + integrity sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ== dependencies: backo2 "1.0.2" - base64-arraybuffer "0.1.5" component-bind "1.0.0" - component-emitter "1.2.1" - debug "~4.1.0" - engine.io-client "~3.4.0" + component-emitter "~1.3.0" + debug "~3.1.0" + engine.io-client "~3.5.0" has-binary2 "~1.0.2" - has-cors "1.1.0" indexof "0.0.1" - object-component "0.0.3" - parseqs "0.0.5" - parseuri "0.0.5" + parseqs "0.0.6" + parseuri "0.0.6" socket.io-parser "~3.3.0" to-array "0.1.4" @@ -6269,16 +6245,16 @@ socket.io-parser@~3.4.0: debug "~4.1.0" isarray "2.0.1" -socket.io@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" - integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== +socket.io@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.4.1.tgz#95ad861c9a52369d7f1a68acf0d4a1b16da451d2" + integrity sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w== dependencies: debug "~4.1.0" - engine.io "~3.4.0" + engine.io "~3.5.0" has-binary2 "~1.0.2" socket.io-adapter "~1.1.0" - socket.io-client "2.3.0" + socket.io-client "2.4.0" socket.io-parser "~3.4.0" socks-proxy-agent@^4.0.1: @@ -7267,17 +7243,10 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -ws@^7.1.2: - version "7.2.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.1.tgz#03ed52423cd744084b2cf42ed197c8b65a936b8e" - integrity sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A== - -ws@~6.1.0: - version "6.1.4" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" - integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== - dependencies: - async-limiter "~1.0.0" +ws@~7.4.2: + version "7.4.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" + integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== xdg-basedir@^4.0.0: version "4.0.0" From c88ef9d603f22dcbe758c9e0f49adb2427769895 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 29 Dec 2020 13:39:00 +0530 Subject: [PATCH 231/284] feat: removed Roles from special documents --- frappe/model/meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index c740d495c1..53fcadce42 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -68,7 +68,7 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link') + special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link') def __init__(self, doctype): self._fields = {} From b59f818f74fd8bf6d4ae1bf4381f6a1065f785f8 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 31 Dec 2020 14:30:02 +0530 Subject: [PATCH 232/284] feat: allow to save with select permission --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 572ee4bd28..80a576230c 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1056,7 +1056,7 @@ def validate_permissions(doctype, for_remove=False, alert=False): return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx) def check_atleast_one_set(d): - if not d.read and not d.write and not d.submit and not d.cancel and not d.create: + if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create: frappe.throw(_("{0}: No basic permissions set").format(get_txt(d))) def check_double(d): From f35e8045d94f1de7ba58772c2dfddb068327597c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 9 Jan 2021 14:06:08 +0530 Subject: [PATCH 233/284] feat: set CORS headers based on allow_cors site config --- frappe/app.py | 66 ++++++++++++++++++++++++++++++--------- frappe/tests/test_cors.py | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 frappe/tests/test_cors.py diff --git a/frappe/app.py b/frappe/app.py index 82471c4e32..adf2bfa8c9 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -7,8 +7,8 @@ import os from six import iteritems import logging -from werkzeug.wrappers import Request from werkzeug.local import LocalManager +from werkzeug.wrappers import Request, Response from werkzeug.exceptions import HTTPException, NotFound from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.middleware.shared_data import SharedDataMiddleware @@ -57,19 +57,22 @@ def application(request): frappe.monitor.start() frappe.rate_limiter.apply() - if frappe.local.form_dict.cmd: + if request.method == "OPTIONS": + response = Response() + + elif frappe.form_dict.cmd: response = frappe.handler.handle() - elif frappe.request.path.startswith("/api/"): + elif request.path.startswith("/api/"): response = frappe.api.handle() - elif frappe.request.path.startswith('/backups'): + elif request.path.startswith('/backups'): response = frappe.utils.response.download_backup(request.path) - elif frappe.request.path.startswith('/private/files/'): + elif request.path.startswith('/private/files/'): response = frappe.utils.response.download_private_file(request.path) - elif frappe.local.request.method in ('GET', 'HEAD', 'POST'): + elif request.method in ('GET', 'HEAD', 'POST'): response = frappe.website.render.render() else: @@ -88,13 +91,9 @@ def application(request): rollback = after_request(rollback) finally: - if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback: + if request.method in ("POST", "PUT") and frappe.db and rollback: frappe.db.rollback() - # set cookies - if response and hasattr(frappe.local, 'cookie_manager'): - frappe.local.cookie_manager.flush_cookies(response=response) - frappe.rate_limiter.update() frappe.monitor.stop(response) frappe.recorder.dump() @@ -110,9 +109,7 @@ def application(request): "http_status_code": getattr(response, "status_code", "NOTFOUND") }) - if response and hasattr(frappe.local, 'rate_limiter'): - response.headers.extend(frappe.local.rate_limiter.headers()) - + process_response(response) frappe.destroy() return response @@ -134,7 +131,46 @@ def init_request(request): make_form_dict(request) - frappe.local.http_request = frappe.auth.HTTPRequest() + if request.method != "OPTIONS": + frappe.local.http_request = frappe.auth.HTTPRequest() + +def process_response(response): + if not response: + return + + # set cookies + if hasattr(frappe.local, 'cookie_manager'): + frappe.local.cookie_manager.flush_cookies(response=response) + + # rate limiter headers + if hasattr(frappe.local, 'rate_limiter'): + response.headers.extend(frappe.local.rate_limiter.headers()) + + # CORS headers + if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors: + set_cors_headers(response) + +def set_cors_headers(response): + origin = frappe.request.headers.get('Origin') + if not origin: + return + + allow_cors = frappe.conf.allow_cors + if allow_cors != "*": + if not isinstance(allow_cors, list): + allow_cors = [allow_cors] + + if origin not in allow_cors: + return + + response.headers.extend({ + 'Access-Control-Allow-Origin': origin, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,' + 'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,' + 'Cache-Control,Content-Type') + }) def make_form_dict(request): import json diff --git a/frappe/tests/test_cors.py b/frappe/tests/test_cors.py new file mode 100644 index 0000000000..d4ed260f61 --- /dev/null +++ b/frappe/tests/test_cors.py @@ -0,0 +1,57 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt +from __future__ import unicode_literals + +import frappe, unittest +from werkzeug.wrappers import Response +from frappe.app import process_response + +HEADERS = ('Access-Control-Allow-Origin', 'Access-Control-Allow-Credentials', + 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers') + +class TestCORS(unittest.TestCase): + def make_request_and_test(self, origin='http://example.com', absent=False): + self.origin = origin + + headers = {} + if origin: + headers = {'Origin': origin} + + frappe.utils.set_request(headers=headers) + + self.response = Response() + process_response(self.response) + + for header in HEADERS: + if absent: + self.assertNotIn(header, self.response.headers) + else: + if header == 'Access-Control-Allow-Origin': + self.assertEqual(self.response.headers.get(header), self.origin) + else: + self.assertIn(header, self.response.headers) + + def test_cors_disabled(self): + frappe.conf.allow_cors = None + self.make_request_and_test('http://example.com', True) + + def test_request_without_origin(self): + frappe.conf.allow_cors = 'http://example.com' + self.make_request_and_test(None, True) + + def test_valid_origin(self): + frappe.conf.allow_cors = 'http://example.com' + self.make_request_and_test() + + frappe.conf.allow_cors = "*" + self.make_request_and_test() + + frappe.conf.allow_cors = ['http://example.com', 'https://example.com'] + self.make_request_and_test() + + def test_invalid_origin(self): + frappe.conf.allow_cors = 'http://example1.com' + self.make_request_and_test(absent=True) + + frappe.conf.allow_cors = ['http://example1.com', 'https://example.com'] + self.make_request_and_test(absent=True) From 4806dcff32fa4f3187c05877659741bb7b67179c Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 11 Jan 2021 14:26:05 +0530 Subject: [PATCH 234/284] fix: sider issues --- frappe/public/js/frappe/form/script_helpers.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js index a0caae33e5..0465624975 100644 --- a/frappe/public/js/frappe/form/script_helpers.js +++ b/frappe/public/js/frappe/form/script_helpers.js @@ -16,16 +16,16 @@ window.refresh_field = function(n, docname, table_field) { if(typeof n==typeof []) refresh_many(n, docname, table_field); - if (n && typeof n==='string' && table_field){ + if (n && typeof n==='string' && table_field) { var grid = cur_frm.fields_dict[table_field].grid, field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}), grid_row = grid.grid_rows_by_docname[docname]; - if (field && field.length){ + if (field && field.length) { field = field[0]; var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname); $.extend(field, meta); - if (grid_row){ + if (grid_row) { grid_row.refresh_field(n); } else { grid.refresh(); From d2d905be140647d404f089e9abb97bcc55a1c97e Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 11 Jan 2021 14:48:33 +0530 Subject: [PATCH 235/284] fix: grid row index no longer dependant on doc index --- frappe/public/js/frappe/form/grid_row.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index ec9cee9c39..466032dbef 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -568,13 +568,15 @@ export default class GridRow { this.wrapper.removeClass("grid-row-open"); } open_prev() { - if(this.grid.grid_rows[this.doc.idx-2]) { - this.grid.grid_rows[this.doc.idx-2].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index - 1]) { + this.grid.grid_rows[row_index - 1].toggle_view(true); } } open_next() { - if(this.grid.grid_rows[this.doc.idx]) { - this.grid.grid_rows[this.doc.idx].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index + 1]) { + this.grid.grid_rows[row_index + 1].toggle_view(true); } else { this.grid.add_new_row(null, null, true); } From 0964f07ee47aad676236977fc7a2d5473c8a8ef4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 12 Jan 2021 09:25:52 +0530 Subject: [PATCH 236/284] fix: Auto Repeat JSON file not updated --- .../doctype/auto_repeat/auto_repeat.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 5ff4cbeead..74965346fd 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -23,7 +23,7 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", - "section_break_12", + "section_break_16", "repeat_on_days", "notification", "notify_by_email", @@ -198,20 +198,20 @@ "label": "Repeat on Days", "options": "Auto Repeat Day" }, - { - "depends_on": "eval:doc.frequency==='Weekly';", - "fieldname": "section_break_12", - "fieldtype": "Section Break" - }, { "default": "0", "fieldname": "submit_on_creation", "fieldtype": "Check", "label": "Submit on Creation" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_16", + "fieldtype": "Section Break" } ], "links": [], - "modified": "2020-12-10 10:43:13.449172", + "modified": "2021-01-12 09:24:49.719611", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", From 4f5002251de9b2f33feffe985a41586915ba1e99 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 Jan 2021 13:29:33 +0530 Subject: [PATCH 237/284] chore: hide 'did not cancel' message if exception is raised --- frappe/desk/form/save.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 5219a98cbd..50f9c984e4 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -42,7 +42,8 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat except Exception: frappe.errprint(frappe.utils.get_traceback()) - frappe.msgprint(frappe._("Did not cancel")) + if len(frappe.get_message_log()) == 0: + frappe.msgprint(frappe._("Did not cancel")) raise def send_updated_docs(doc): From 9a1b876934c6b0afa2627d22d7d50265bb37966d Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 12 Jan 2021 14:59:00 +0530 Subject: [PATCH 238/284] fix: Bind input change event for link control (#12193) --- frappe/public/js/frappe/form/controls/link.js | 1 + frappe/public/js/frappe/views/treeview.js | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 111ee7d8f6..4c0fe39f60 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -49,6 +49,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.translate_values = true; this.setup_buttons(); this.setup_awesomeplete(); + this.bind_change_event(); }, get_options: function() { return this.df.options; diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 777ce14da6..1a53c14974 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -93,17 +93,17 @@ frappe.views.TreeView = Class.extend({ var me = this; this.opts.onload && this.opts.onload(me); }, - make_filters: function(){ + make_filters: function() { var me = this; frappe.treeview_settings.filters = [] $.each(this.opts.filters || [], function(i, filter) { - if(frappe.route_options && frappe.route_options[filter.fieldname]) { - filter.default = frappe.route_options[filter.fieldname] + if (frappe.route_options && frappe.route_options[filter.fieldname]) { + filter.default = frappe.route_options[filter.fieldname]; } - if(!filter.disable_onchange) { + if (!filter.disable_onchange) { filter.change = function() { - filter.on_change && filter.on_change(); + filter.onchange && filter.onchange(); var val = this.get_value(); me.args[filter.fieldname] = val; if (val) { @@ -113,7 +113,7 @@ frappe.views.TreeView = Class.extend({ } me.set_title(); me.make_tree(); - } + }; } me.page.add_field(filter); @@ -121,7 +121,7 @@ frappe.views.TreeView = Class.extend({ if (filter.default) { $("[data-fieldname='"+filter.fieldname+"']").trigger("change"); } - }) + }); }, get_root: function() { var me = this; From 58eeefe993ba1e161639fb2b563cef921ffa44e9 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Tue, 12 Jan 2021 15:29:37 +0530 Subject: [PATCH 239/284] fix(email): error object is not json parseable --- frappe/email/doctype/email_account/email_account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 343141c66d..ca4dbb83e2 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -210,7 +210,7 @@ class EmailAccount(Document): elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): self.throw_invalid_credentials_exception() else: - frappe.throw(e) + frappe.throw(cstr(e)) except socket.error: if in_receive: From a85842b6d115f1d7705c8c67a4b313f77a6e4d36 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 12 Jan 2021 16:02:15 +0530 Subject: [PATCH 240/284] fix: remove unwanted message --- frappe/desk/form/save.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 50f9c984e4..da43b14fce 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -42,8 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat except Exception: frappe.errprint(frappe.utils.get_traceback()) - if len(frappe.get_message_log()) == 0: - frappe.msgprint(frappe._("Did not cancel")) raise def send_updated_docs(doc): From 38d349d31fa13918af390cded85e749f8d34ea86 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 12 Jan 2021 23:09:42 +0530 Subject: [PATCH 241/284] fix: Center align hero title --- frappe/public/scss/page-builder.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index 24dbca3e21..1803e52cf7 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -29,11 +29,11 @@ } .hero.align-center { - h1, .hero-subtitle, .hero-buttons { + h1, .hero-title, .hero-subtitle, .hero-buttons { text-align: center; } - .hero-subtitle { + .hero-title, .hero-subtitle { margin-left: auto; margin-right: auto; } From 2efe5e9cdefc7c75c6eb304ed64fa9e5b7df38a2 Mon Sep 17 00:00:00 2001 From: Aditya Hase Date: Wed, 13 Jan 2021 14:55:04 +0530 Subject: [PATCH 242/284] fix(desk): Correctly format and render Link fields This seems to be broken after https://github.com/frappe/frappe/pull/12063 https://github.com/saurabh6790/frappe/commit/00cc7df05e53f682a64a53b1479dd3140164a202 --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 2b8956653b..be3f10fd0c 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -130,7 +130,7 @@ frappe.form.formatters = { } else if(docfield && doctype) { if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) { return ` ${__(options && options.label || value)}`; From 64f3887dce25da4d93d9bc2b0eccc4f04a744ce2 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Wed, 13 Jan 2021 16:00:23 +0530 Subject: [PATCH 243/284] fix: html download of auto download report broken --- .../doctype/auto_email_report/auto_email_report.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 539f6c9db8..de27fafee3 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -81,7 +81,7 @@ class AutoEmailReport(Document): if self.format == 'HTML': columns, data = make_links(columns, data) - + columns = update_field_types(columns) return self.get_html_table(columns, data) elif self.format == 'XLSX': @@ -236,5 +236,14 @@ def make_links(columns, data): elif col.fieldtype == "Dynamic Link": if col.options and row.get(col.fieldname) and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) + elif col.fieldtype == "Currency": + row[col.fieldname] = frappe.format_value(row[col.fieldname], col) return columns, data + +def update_field_types(columns): + for col in columns: + if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency": + col.fieldtype = "Data" + col.options = "" + return columns \ No newline at end of file From 8ac66bbb37b784c95fc5b25223a3ba613e9accf7 Mon Sep 17 00:00:00 2001 From: UrvashiKishnani <41088003+UrvashiKishnani@users.noreply.github.com> Date: Wed, 13 Jan 2021 17:52:08 +0400 Subject: [PATCH 244/284] fix: removed keyboard smash (#12186) --- frappe/core/doctype/data_import/importer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 7880648b6f..dde3dfaee9 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -751,7 +751,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "message": _("{0} is a mandatory field asdadsf").format(id_field.label), + "message": _("{0} is a mandatory field").format(id_field.label), } ) return From 26e40a9d68910a59b6313b25b83660cf6d10b921 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 13 Jan 2021 19:24:24 +0530 Subject: [PATCH 245/284] fix: data.non_standard_fieldnames is None (#12174) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/model/meta.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 53fcadce42..88ed1a7e78 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -484,6 +484,8 @@ class Meta(Document): if not data.transactions: # init groups data.transactions = [] + + if not data.non_standard_fieldnames: data.non_standard_fieldnames = {} for link in dashboard_links: From f619ef4a5b856666582e8ab7254e8f7d3fcf9652 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 13 Jan 2021 19:32:31 +0530 Subject: [PATCH 246/284] fix: Skip "Attach" from auto image render Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- frappe/templates/print_formats/standard_macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 9a14b860ff..7a0dce7f5e 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -5,7 +5,7 @@
    {{ frappe.render_template(df.options, {"doc": doc}) or "" }}
    {%- elif df.fieldtype in ("Text", "Text Editor", "Code", "Long Text") -%} {{ render_text_field(df, doc) }} - {%- elif df.fieldtype in ("Image", "Attach Image", "Attach") + {%- elif df.fieldtype in ("Image", "Attach Image") and ( (guess_mimetype(doc[df.fieldname])[0] or "").startswith("image/") or doc[df.fieldname].startswith("http") From 00896f36fefd2d464c069252b7869ab8c9c0bff3 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 13 Jan 2021 20:46:27 +0530 Subject: [PATCH 247/284] revert: "fix: improve translation pattern" (#12205) --- .github/helper/translation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 9220c8b605..340f4f8772 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_{1,2}\(\s*([\"'`])(?P((?!\1).)+?)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)+?)\5)?(\s*,\s*(\[[\s\S]*\])?(null)?(\s*,\s*([\"'`])(?P((?!\12).)+?)\12)?)?\s*\)") # noqa: E501 +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") From ac2ab580db52a9dcd54ea31b1ccd0a068b30e8de Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Jan 2021 23:55:18 +0530 Subject: [PATCH 248/284] feat: Hide Child Records for a Nested DocType via User Permissions --- .../user_permission/test_user_permission.py | 52 +++- .../user_permission/user_permission.js | 13 +- .../user_permission/user_permission.json | 288 +++--------------- .../user_permission/user_permission.py | 32 +- frappe/permissions.py | 4 +- 5 files changed, 117 insertions(+), 272 deletions(-) diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 82dd2ab27e..38ffbd1490 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -3,6 +3,7 @@ # See license.txt from __future__ import unicode_literals from frappe.core.doctype.user_permission.user_permission import add_user_permissions +from frappe.permissions import has_user_permission import frappe import unittest @@ -10,7 +11,12 @@ import unittest class TestUserPermission(unittest.TestCase): def setUp(self): frappe.db.sql("""DELETE FROM `tabUser Permission` - WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""") + WHERE `user` in ( + 'test_bulk_creation_update@example.com', + 'test_user_perm1@example.com', + 'nested_doc_user@example.com')""") + frappe.delete_doc_if_exists("DocType", "Person") + frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tabPerson`") def test_default_user_permission_validation(self): user = create_user('test_default_permission@example.com') @@ -108,6 +114,45 @@ class TestUserPermission(unittest.TestCase): self.assertIsNone(removed_applicable_second) self.assertEquals(is_created, 1) + def test_user_perm_for_nested_doctype(self): + """Test if descendants' visibility is controlled for a nested DocType.""" + from frappe.core.doctype.doctype.test_doctype import new_doctype + + user = create_user("nested_doc_user@example.com", "Blogger") + if not frappe.db.exists("DocType", "Person"): + doc = new_doctype("Person", + fields=[ + { + "label": "Person Name", + "fieldname": "person_name", + "fieldtype": "Data" + } + ], unique=0) + doc.is_tree = 1 + doc.insert() + + parent_record = frappe.get_doc( + {"doctype": "Person", "person_name": "Parent", "is_group": 1} + ).insert() + + child_record = frappe.get_doc( + {"doctype": "Person", "person_name": "Child", "is_group": 0, "parent_person": parent_record.name} + ).insert() + + add_user_permissions(get_params(user, "Person", parent_record.name)) + + # check if adding perm on a group record, makes child record visible + self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) + self.assertTrue(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) + + frappe.db.set_value("User Permission", {"allow": "Person", "for_value": parent_record.name}, "exclude_descendants", 1) + frappe.cache().delete_value("user_permissions") + + # check if adding perm on a group record with exclude_descendants enabled, + # hides child records + self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) + self.assertFalse(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) + def create_user(email, role="System Manager"): ''' create user with role system manager ''' if frappe.db.exists('User', email): @@ -119,7 +164,7 @@ def create_user(email, role="System Manager"): user.add_roles(role) return user -def get_params(user, doctype, docname, is_default=0, applicable=None): +def get_params(user, doctype, docname, is_default=0, exclude_descendants=0, applicable=None): ''' Return param to insert ''' param = { "user": user.name, @@ -127,7 +172,8 @@ def get_params(user, doctype, docname, is_default=0, applicable=None): "docname":docname, "is_default": is_default, "apply_to_all_doctypes": 1, - "applicable_doctypes": [] + "applicable_doctypes": [], + "exclude_descendants": exclude_descendants } if applicable: param.update({"apply_to_all_doctypes": 0}) diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index 9f824b1350..b06837aa64 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -26,11 +26,15 @@ frappe.ui.form.on('User Permission', { () => frappe.set_route('query-report', 'Permitted Documents For User', { user: frm.doc.user })); frm.trigger('set_applicable_for_constraint'); + frm.trigger('show_exclude_descendants'); }, allow: frm => { - if(frm.doc.for_value) { - frm.set_value('for_value', null); + if (frm.doc.allow) { + if(frm.doc.for_value) { + frm.set_value('for_value', null); + } + frm.trigger('show_exclude_descendants'); } }, @@ -43,6 +47,11 @@ frappe.ui.form.on('User Permission', { if (frm.doc.apply_to_all_doctypes) { frm.set_value('applicable_for', null); } + }, + + show_exclude_descendants: frm => { + let show = frappe.boot.nested_set_doctypes.includes(frm.doc.allow); + frm.toggle_display('exclude_descendants', show); } diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 33a8d58bbb..6dc626ba04 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -1,330 +1,116 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, - "beta": 0, "creation": "2017-07-17 14:25:27.881871", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "user", + "allow", + "column_break_3", + "for_value", + "is_default", + "advanced_control_section", + "apply_to_all_doctypes", + "applicable_for", + "column_break_9", + "exclude_descendants" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "user", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "User", - "length": 0, - "no_copy": 0, "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "allow", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Allow", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "precision": "", - "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, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_3", - "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, - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "for_value", "fieldtype": "Dynamic Link", - "hidden": 0, "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "For Value", - "length": 0, - "no_copy": 0, "options": "allow", - "permlevel": 0, - "precision": "", - "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, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "is_default", "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": "Is Default", - "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, - "translatable": 0, - "unique": 0 + "label": "Is Default" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "advanced_control_section", "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": "Advanced Control", - "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, - "translatable": 0, - "unique": 0 + "label": "Advanced Control" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "fetch_if_empty": 0, "fieldname": "apply_to_all_doctypes", "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": "Apply To All Document Types", - "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, - "translatable": 0, - "unique": 0 + "label": "Apply To All Document Types" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:!doc.apply_to_all_doctypes", - "fetch_if_empty": 0, "fieldname": "applicable_for", "fieldtype": "Link", - "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": "Applicable For", - "length": 0, - "no_copy": 0, - "options": "DocType", - "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, - "translatable": 0, - "unique": 0 + "options": "DocType" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Hide descendant records of For Value.", + "fieldname": "exclude_descendants", + "fieldtype": "Check", + "hidden": 1, + "label": "Exclude Descendants" } ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-16 19:17:23.644724", + "links": [], + "modified": "2021-01-13 19:31:05.618273", "modified_by": "Administrator", "module": "Core", "name": "User Permission", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "title_field": "user", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index ba14583c2f..48d56c8b28 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -49,7 +49,8 @@ class UserPermission(Document): 'name': ['!=', self.name] }, or_filters={ 'applicable_for': cstr(self.applicable_for), - 'apply_to_all_doctypes': 1 + 'apply_to_all_doctypes': 1, + 'exclude_descendants': cstr(self.exclude_descendants) }, limit=1) if overlap_exists: ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) @@ -91,13 +92,13 @@ def get_user_permissions(user=None): try: for perm in frappe.get_all('User Permission', - fields=['allow', 'for_value', 'applicable_for', 'is_default'], + fields=['allow', 'for_value', 'applicable_for', 'is_default', 'exclude_descendants'], filters=dict(user=user)): meta = frappe.get_meta(perm.allow) add_doc_to_perm(perm, perm.for_value, perm.is_default) - if meta.is_nested_set(): + if meta.is_nested_set() and not perm.exclude_descendants: decendants = frappe.db.get_descendants(perm.allow, perm.for_value) for doc in decendants: add_doc_to_perm(perm, doc, False) @@ -172,8 +173,8 @@ def check_applicable_doc_perm(user, doctype, docname): "allow": doctype, "for_value":docname, }) - for d in data: - applicable.append(d.applicable_for) + for permission in data: + applicable.append(permission.applicable_for) return applicable @@ -194,7 +195,7 @@ def add_user_permissions(data): data = json.loads(data) data = frappe._dict(data) - d = check_applicable_doc_perm(data.user, data.doctype, data.docname) + perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) exists = frappe.db.exists("User Permission", { "user": data.user, "allow": data.doctype, @@ -202,26 +203,27 @@ def add_user_permissions(data): "apply_to_all_doctypes": 1 }) if data.apply_to_all_doctypes == 1 and not exists: - remove_applicable(d, data.user, data.doctype, data.docname) - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1) + remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, apply_to_all=1) return 1 elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: remove_apply_to_all(data.user, data.doctype, data.docname) - update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) + update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname) for applicable in data.applicable_doctypes : - if applicable not in d: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) + if applicable not in perm_applied_docs: + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, applicable=applicable) elif exists: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, applicable=applicable) return 1 return 0 -def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, applicable=None): +def insert_user_perm(user, doctype, docname, is_default=0, exclude_descendants=0, apply_to_all=None, applicable=None): user_perm = frappe.new_doc("User Permission") user_perm.user = user user_perm.allow = doctype user_perm.for_value = docname user_perm.is_default = is_default + user_perm.exclude_descendants = exclude_descendants if applicable: user_perm.applicable_for = applicable user_perm.apply_to_all_doctypes = 0 @@ -229,8 +231,8 @@ def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, ap user_perm.apply_to_all_doctypes = 1 user_perm.insert() -def remove_applicable(d, user, doctype, docname): - for applicable_for in d: +def remove_applicable(perm_applied_docs, user, doctype, docname): + for applicable_for in perm_applied_docs: frappe.db.sql("""DELETE FROM `tabUser Permission` WHERE `user`=%s AND `applicable_for`=%s diff --git a/frappe/permissions.py b/frappe/permissions.py index a45fbdcd06..ce551d0617 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -398,7 +398,8 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: add_user_permission(doctype, name, user) -def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, is_default=0): +def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, + is_default=0, exclude_descendants=0): '''Add user permission''' from frappe.core.doctype.user_permission.user_permission import user_permission_exists @@ -413,6 +414,7 @@ def add_user_permission(doctype, name, user, ignore_permissions=False, applicabl for_value=name, is_default=is_default, applicable_for=applicable_for, + exclude_descendants=exclude_descendants, )).insert(ignore_permissions=ignore_permissions) def remove_user_permission(doctype, name, user): From f23d88be5e0209cb7e4cf60aed1ca217f1f3065a Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 15 Jan 2021 13:48:53 +0530 Subject: [PATCH 249/284] fix: Remove whitespace from testimonial text --- frappe/website/web_template/testimonial/testimonial.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/website/web_template/testimonial/testimonial.html b/frappe/website/web_template/testimonial/testimonial.html index b656d3b03d..f860abbae6 100644 --- a/frappe/website/web_template/testimonial/testimonial.html +++ b/frappe/website/web_template/testimonial/testimonial.html @@ -5,9 +5,7 @@ {% endif %}
    - - {{ content }} - + “{{ content }}”
    {{ name }} From 3959deb0523f7c8caa4c15ebdfa0d38d91aaa73f Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 15 Jan 2021 14:59:23 +0530 Subject: [PATCH 250/284] chore: Add 'Exclude Descendants' to add / update perms in bulk dialog --- .../user_permission/user_permission.py | 1 + .../user_permission/user_permission_list.js | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 48d56c8b28..6c926ba10f 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -195,6 +195,7 @@ def add_user_permissions(data): data = json.loads(data) data = frappe._dict(data) + # get all doctypes on whom this permission os applied perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) exists = frappe.db.exists("User Permission", { "user": data.user, diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 3e822f0007..49da74347f 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -19,6 +19,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("is_default", "hidden", 1); dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); dialog.set_df_property("applicable_doctypes", "hidden", 1); + dialog.set_df_property("exclude_descendants", "hidden", 1); } }, { @@ -54,6 +55,10 @@ frappe.listview_settings['User Permission'] = { } } }, + { + fieldtype: "Section Break", + hide_border: 1 + }, { fieldname: 'is_default', label: __('Is Default'), @@ -74,6 +79,19 @@ frappe.listview_settings['User Permission'] = { } } }, + { + fieldtype: "Column Break" + }, + { + fieldname: 'exclude_descendants', + label: __('Exclude Descendants'), + fieldtype: 'Check', + hidden: 1 + }, + { + fieldtype: "Section Break", + hide_border: 1 + }, { label: __("Applicable Document Types"), fieldname: "applicable_doctypes", @@ -214,6 +232,9 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("is_default", "hidden", 0); dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); dialog.set_value("apply_to_all_doctypes", "checked", 1); + let show = frappe.boot.nested_set_doctypes.includes(dialog.get_value("doctype")); + dialog.set_df_property("exclude_descendants", "hidden", !show); + dialog.refresh(); }, on_docname_change: function(dialog, options, applicable) { @@ -233,6 +254,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("applicable_doctypes", "options", options); dialog.set_df_property("applicable_doctypes", "hidden", 1); } + dialog.refresh(); }, on_apply_to_all_doctypes_change: function(dialog, options) { @@ -243,5 +265,6 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("applicable_doctypes", "options", options); dialog.set_df_property("applicable_doctypes", "hidden", 1); } + dialog.refresh_sections(); } }; \ No newline at end of file From b923d4e8ecbe51acc8502a9723069964d543be3f Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 15 Jan 2021 16:38:03 +0530 Subject: [PATCH 251/284] fix: Sider Issues - Removed unwanted f string in test - Space after if (client side) --- frappe/core/doctype/user_permission/test_user_permission.py | 2 +- frappe/core/doctype/user_permission/user_permission.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 38ffbd1490..6773af004f 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -16,7 +16,7 @@ class TestUserPermission(unittest.TestCase): 'test_user_perm1@example.com', 'nested_doc_user@example.com')""") frappe.delete_doc_if_exists("DocType", "Person") - frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tabPerson`") + frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`") def test_default_user_permission_validation(self): user = create_user('test_default_permission@example.com') diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index b06837aa64..665d3c963c 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -31,7 +31,7 @@ frappe.ui.form.on('User Permission', { allow: frm => { if (frm.doc.allow) { - if(frm.doc.for_value) { + if (frm.doc.for_value) { frm.set_value('for_value', null); } frm.trigger('show_exclude_descendants'); From b00b325688effc75c82d6f478a77ef9c63d8411c Mon Sep 17 00:00:00 2001 From: ollyboy Date: Sun, 17 Jan 2021 17:45:08 +0000 Subject: [PATCH 252/284] fix field list passed as string so append works --- frappe/desk/calendar.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index ce9fb7f177..127194671a 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -44,6 +44,8 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None): fields = [field_map.start, field_map.end, field_map.title, 'name'] if field_map.color: + if isinstance(fields, str): + fields = json.loads ( fields ) fields.append(field_map.color) start_date = "ifnull(%s, '0001-01-01 00:00:00')" % field_map.start From cd693d5a17e5e90668f53ca8e4caccab417c9a45 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Mon, 18 Jan 2021 17:45:14 +0530 Subject: [PATCH 253/284] fix: hide theme url --- frappe/website/doctype/website_theme/website_theme.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index 78c3c696e9..ee4b33d854 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -65,8 +65,10 @@ }, { "fieldname": "theme_url", - "fieldtype": "Read Only", - "label": "Theme URL" + "fieldtype": "Data", + "hidden": 1, + "label": "Theme URL", + "read_only": 1 }, { "collapsible": 1, @@ -179,7 +181,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-24 11:42:33.867840", + "modified": "2021-01-18 17:43:39.804765", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", From 962fff15c4a350362edeedabb5cf2a452972f045 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 18 Jan 2021 20:49:09 +0530 Subject: [PATCH 254/284] Link Field Validation doesn't use Filter criteria defined for link field In link field if we enter invalid i.e not available record it will clear field but if we enter valid record that is not available in filter but available in db than it will accept that record --- frappe/public/js/frappe/form/controls/link.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 4c0fe39f60..019ea5ca58 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -455,6 +455,11 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } + // check if value exist in the filtered dropdown values + if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ + value = "" + } + return frappe.call({ method:'frappe.desk.form.utils.validate_link', type: "GET", From ebcb8438de691eb085528912253a0b17ee03dee9 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 18 Jan 2021 21:45:07 +0530 Subject: [PATCH 255/284] fix: semantic commit and sider issues --- frappe/public/js/frappe/form/controls/link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 019ea5ca58..6ac10c8534 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,8 +456,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ - value = "" + if (this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)) { + value = ""; } return frappe.call({ From b9f2f5bc22cdafd735011ab109b3fe376cbef29e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:41:11 +0100 Subject: [PATCH 256/284] test: fix connected app --- .../connected_app/test_connected_app.py | 116 +++++++++++++++--- 1 file changed, 98 insertions(+), 18 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 4d8acb9b59..af5a5d8e3c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -8,9 +8,50 @@ import requests from urllib.parse import urljoin import frappe +from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key -test_dependencies = ['Connected App', 'OAuth Client', 'User'] + +def get_user(usr, pwd): + user = frappe.new_doc('User') + user.email = usr + user.enabled = 1 + user.first_name = "_Test" + user.new_password = pwd + user.roles = [] + user.append('roles', { + 'doctype': 'Has Role', + 'parentfield': 'roles', + 'role': 'System Manager' + }) + user.insert() + + return user + + +def get_connected_app(): + doctype = 'Connected App' + connected_app = frappe.new_doc(doctype) + connected_app.provider_name = 'frappe' + connected_app.scopes = [] + connected_app.append('scopes', {'scope': 'all'}) + connected_app.insert() + + return connected_app + + +def get_oauth_client(): + oauth_client = frappe.new_doc('OAuth Client') + oauth_client.app_name = '_Test Connected App' + oauth_client.redirect_uris = 'to be replaced' + oauth_client.default_redirect_uri = 'to be replaced' + oauth_client.grant_type = 'Authorization Code' + oauth_client.response_type = 'Code' + oauth_client.skip_authorization = 1 + oauth_client.insert() + + return oauth_client + class TestConnectedApp(unittest.TestCase): @@ -26,37 +67,47 @@ class TestConnectedApp(unittest.TestCase): just endpoints) are stored in "Social Login Key" so we get them from there. """ - self.user_name = 'test@example.com' + self.user_name = 'test-connected-app@example.com' self.user_password = 'Eastern_43A1W' - connected_app = frappe.get_last_doc('Connected App') - redirect_uri = connected_app.get('redirect_uri') + self.user = get_user(self.user_name, self.user_password) + self.connected_app = get_connected_app() + self.oauth_client = get_oauth_client() + social_login_key = create_or_update_social_login_key() + self.base_url = social_login_key.get('base_url') - web_application_client = frappe.get_last_doc('OAuth Client') - web_application_client.update({ + frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + redirect_uri = self.connected_app.get('redirect_uri') + self.oauth_client.update({ 'redirect_uris': redirect_uri, 'default_redirect_uri': redirect_uri }) - web_application_client.save() - - social_login_key = create_or_update_social_login_key() - self.base_url = social_login_key.get('base_url') + self.oauth_client.save() - connected_app.authorization_uri = urljoin(self.base_url, social_login_key.get('authorize_url')) - connected_app.token_uri = urljoin(self.base_url, social_login_key.get('access_token_url')) - connected_app.client_id = web_application_client.get('client_id') - connected_app.client_secret = web_application_client.get('client_secret') - self.connected_app = connected_app.save() + self.connected_app.update({ + 'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')), + 'client_id': self.oauth_client.get('client_id'), + 'client_secret': self.oauth_client.get('client_secret'), + 'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url')) + }) + self.connected_app.save() frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" session = requests.Session() - session.post(urljoin(self.base_url, '/api/method/login'), data={ + login_response = session.post(urljoin(self.base_url, '/api/method/login'), data={ 'usr': self.user_name, 'pwd': self.user_password }) + self.assertEqual(login_response.status_code, 200) + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) auth_response = session.get(authorization_url) @@ -65,10 +116,39 @@ class TestConnectedApp(unittest.TestCase): callback_response = session.get(auth_response.url) self.assertEqual(callback_response.status_code, 200) - token_cache = self.connected_app.get_token_cache(self.user_name) - token = token_cache.get_password('access_token') + self.token_cache = self.connected_app.get_token_cache(self.user_name) + token = self.token_cache.get_password('access_token') self.assertNotEqual(token, None) oauth2_session = self.connected_app.get_oauth2_session(self.user_name) resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) self.assertEqual(resp.json().get('message'), self.user_name) + + def tearDown(self): + def delete_if_exists(attribute): + doc = getattr(self, attribute, None) + if doc: + doc.delete() + + delete_if_exists('token_cache') + delete_if_exists('connected_app') + + if getattr(self, 'oauth_client', None): + tokens = frappe.get_all('OAuth Bearer Token', filters={ + 'client': self.oauth_client.name + }) + for token in tokens: + doc = frappe.get_doc('OAuth Bearer Token', token.name) + doc.delete() + + codes = frappe.get_all('OAuth Authorization Code', filters={ + 'client': self.oauth_client.name + }) + for code in codes: + doc = frappe.get_doc('OAuth Authorization Code', code.name) + doc.delete() + + delete_if_exists('user') + delete_if_exists('oauth_client') + + frappe.db.commit() From aa9a8b2f1b3f319ef943260f06d4befc606c1546 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:46:55 +0100 Subject: [PATCH 257/284] fix: remove unused import --- frappe/integrations/doctype/connected_app/test_connected_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index af5a5d8e3c..e2c229f26c 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -8,7 +8,6 @@ import requests from urllib.parse import urljoin import frappe -from frappe.test_runner import make_test_records from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key From 7fd4a114a17707b0349696f0d1de43842deb6764 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 18:53:35 +0100 Subject: [PATCH 258/284] test: use get for login --- frappe/integrations/doctype/connected_app/test_connected_app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index e2c229f26c..324600d76e 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -101,7 +101,7 @@ class TestConnectedApp(unittest.TestCase): def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" session = requests.Session() - login_response = session.post(urljoin(self.base_url, '/api/method/login'), data={ + login_response = session.get(urljoin(self.base_url, '/api/method/login'), params={ 'usr': self.user_name, 'pwd': self.user_password }) From a0678a4d5f96093f452c68e72833d3a51d5080ae Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 18 Jan 2021 19:27:05 +0100 Subject: [PATCH 259/284] test: login twice --- .../connected_app/test_connected_app.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py index 324600d76e..6faa542a60 100644 --- a/frappe/integrations/doctype/connected_app/test_connected_app.py +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -100,12 +100,21 @@ class TestConnectedApp(unittest.TestCase): def test_web_application_flow(self): """Simulate a logged in user who opens the authorization URL.""" + def login(): + return session.get(urljoin(self.base_url, '/api/method/login'), params={ + 'usr': self.user_name, + 'pwd': self.user_password + }) + session = requests.Session() - login_response = session.get(urljoin(self.base_url, '/api/method/login'), params={ - 'usr': self.user_name, - 'pwd': self.user_password - }) - self.assertEqual(login_response.status_code, 200) + + # first login of a new user on a new site fails with "401 UNAUTHORIZED" + # when anybody fixes that, the two lines below can be removed + first_login = login() + self.assertEqual(first_login.status_code, 401) + + second_login = login() + self.assertEqual(second_login.status_code, 200) authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) From 83822b14c6e6aded305c00fefee679833c4ee2ab Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 14:07:19 +0530 Subject: [PATCH 260/284] fix: Updated if condition --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 6ac10c8534..77716ee60a 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,7 +456,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if (this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)) { + if (this.$input.cache[doctype] && !this.$input.cache[doctype][""].some(d => d.value === value)) { value = ""; } From bcae1cc294fac0aeeb90f68d57f7db1f673865aa Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:29:23 +0530 Subject: [PATCH 261/284] revert --- frappe/public/js/frappe/form/controls/link.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 77716ee60a..019ea5ca58 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -456,8 +456,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } // check if value exist in the filtered dropdown values - if (this.$input.cache[doctype] && !this.$input.cache[doctype][""].some(d => d.value === value)) { - value = ""; + if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ + value = "" } return frappe.call({ From 099474a441f922fab40e8ac1aa1fc26c851b7344 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:29:30 +0530 Subject: [PATCH 262/284] Revert "Link Field Validation doesn't use Filter criteria defined for link field" This reverts commit 962fff15c4a350362edeedabb5cf2a452972f045. --- frappe/public/js/frappe/form/controls/link.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 019ea5ca58..4c0fe39f60 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -455,11 +455,6 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } - // check if value exist in the filtered dropdown values - if(this.$input.cache[doctype][""] && !this.$input.cache[doctype][""].some(d => d.value === value)){ - value = "" - } - return frappe.call({ method:'frappe.desk.form.utils.validate_link', type: "GET", From bddb7034cfe67f3669c1a97b915621b03cab422d Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 19 Jan 2021 22:36:33 +0530 Subject: [PATCH 263/284] fix: Geolocation field with Column Break & Section Break (Collapsible) does not seems to work. --- frappe/public/js/frappe/form/controls/geolocation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 9e4d1d82ec..dfd0f4d174 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -17,7 +17,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ this.map_area.prependTo($input_wrapper); this.$wrapper.find('.control-input').addClass("hidden"); - if ($input_wrapper.is(':visible')) { + if (this.frm) { this.make_map(); } else { $(document).on('frappe.ui.Dialog:shown', () => { From 9591d01c2c2458b459e132d0cd28d3f777cf865c Mon Sep 17 00:00:00 2001 From: robert kimutai Date: Wed, 20 Jan 2021 12:52:12 +0300 Subject: [PATCH 264/284] chore: Update CONTRIBUTING.md (#12241) --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e1f16970fe..5be3a87884 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos ### General Issue Guidelines 1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. -2. **Report each issue separately:** Don't club multiple, unreleated issues in one note. +2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. 3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. ### Bug Report Guidelines From f47d2c32b144dc19ddac2a273644d973c2895561 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 20 Jan 2021 18:44:18 +0100 Subject: [PATCH 265/284] feat: Add translation context (#12043) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/form/controls/code.js | 9 ++++++--- frappe/public/js/frappe/form/sidebar/form_sidebar.js | 4 ++-- frappe/public/js/frappe/utils/user.js | 2 +- frappe/public/js/frappe/widgets/onboarding_widget.js | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index f3c51e0232..6df7094c26 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -10,7 +10,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ .appendTo(this.input_area); this.expanded = false; - this.$expand_button = $(``).click(() => { + this.$expand_button = $(``).click(() => { this.expanded = !this.expanded; this.refresh_height(); this.toggle_label(); @@ -38,8 +38,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, toggle_label() { - const button_label = this.expanded ? __('Collapse') : __('Expand'); - this.$expand_button && this.$expand_button.text(button_label); + this.$expand_button && this.$expand_button.text(this.get_button_label()); + }, + + get_button_label() { + return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.'); }, set_language() { diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index eab09c1e10..eb70b255eb 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -99,7 +99,7 @@ frappe.ui.form.Sidebar = class { __("{0} edited this {1}", [ frappe.user.full_name(this.frm.doc.modified_by).bold(), "
    " + comment_when(this.frm.doc.modified), - ]) + ], "For example, 'Jon Doe edited this 5 minutes ago'.") ); this.sidebar .find(".created-by") @@ -107,7 +107,7 @@ frappe.ui.form.Sidebar = class { __("{0} created this {1}", [ frappe.user.full_name(this.frm.doc.owner).bold(), "
    " + comment_when(this.frm.doc.creation), - ]) + ], "For example, 'Jon Doe created this 5 minutes ago'.") ); this.refresh_like(); diff --git a/frappe/public/js/frappe/utils/user.js b/frappe/public/js/frappe/utils/user.js index 311f208750..64db23d306 100644 --- a/frappe/public/js/frappe/utils/user.js +++ b/frappe/public/js/frappe/utils/user.js @@ -55,7 +55,7 @@ $.extend(frappe.user, { name: 'Guest', full_name: function(uid) { return uid === frappe.session.user ? - __("You") : + __("You", null, "Name of the current user. For example: You edited this 5 hours ago.") : frappe.user_info(uid).fullname; }, image: function(uid) { diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js index abacc6f354..8ef003cc67 100644 --- a/frappe/public/js/frappe/widgets/onboarding_widget.js +++ b/frappe/public/js/frappe/widgets/onboarding_widget.js @@ -444,7 +444,7 @@ export default class OnboardingWidget extends Widget { set_actions() { this.action_area.empty(); const dismiss = $( - `
    ${__('Dismiss')}
    ` + `
    ${__('Dismiss', null, 'Stop showing the onboarding widget.')}
    ` ); dismiss.on("click", () => { let dismissed = JSON.parse( From 19c6e0218db9b1dd95132693a96a8174fac2dc94 Mon Sep 17 00:00:00 2001 From: snyk-bot Date: Wed, 20 Jan 2021 22:57:25 +0000 Subject: [PATCH 266/284] fix: requirements.txt to reduce vulnerabilities The following vulnerabilities are fixed by pinning transitive dependencies: - https://snyk.io/vuln/SNYK-PYTHON-PYYAML-590151 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3cc92264a2..e128790e45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ pypng==0.0.20 PyQRCode==1.2.1 python-dateutil==2.8.1 pytz==2019.3 -PyYAML==5.3.1 +PyYAML==5.4 rauth==0.7.3 redis==3.5.3 requests-oauthlib==1.3.0 From 33ea496a8bd5953714941cfde4d2c1da1982c0b3 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Jan 2021 13:12:42 +0530 Subject: [PATCH 267/284] feat: Added get_datetime_in_timezone in frappe.utils to get datetime in specific timezones * Added util in safe_exec to access via Server Scripts and System Console --- frappe/utils/data.py | 12 ++++++++++-- frappe/utils/safe_exec.py | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index c24b9f186e..c60e64b015 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -154,14 +154,22 @@ def get_time_zone(): return frappe.cache().get_value("time_zone", _get_time_zone) -def convert_utc_to_user_timezone(utc_timestamp): +def convert_utc_to_timezone(utc_timestamp, time_zone): from pytz import timezone, UnknownTimeZoneError utcnow = timezone('UTC').localize(utc_timestamp) try: - return utcnow.astimezone(timezone(get_time_zone())) + return utcnow.astimezone(timezone(time_zone)) except UnknownTimeZoneError: return utcnow +def get_datetime_in_timezone(time_zone): + utc_timestamp = datetime.datetime.utcnow() + return convert_utc_to_timezone(utc_timestamp, time_zone) + +def convert_utc_to_user_timezone(utc_timestamp): + time_zone = get_time_zone() + return convert_utc_to_timezone(utc_timestamp, time_zone) + def now(): """return current datetime as yyyy-mm-dd hh:mm:ss""" if frappe.flags.current_date: diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 2aacf5eda8..06a192c05e 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -222,6 +222,7 @@ VALID_UTILS = ( "get_last_day_of_week", "get_last_day", "get_time", +"get_datetime_in_timezone", "get_datetime_str", "get_date_str", "get_time_str", From 16f2b29cb3c1395e8fbaae50d3076b410b4555b7 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 21 Jan 2021 13:15:14 +0530 Subject: [PATCH 268/284] style: Trim extra whitespace --- frappe/utils/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index c60e64b015..da2c910e20 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -377,7 +377,7 @@ def format_duration(seconds, hide_days=False): example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float """ - + seconds = cint(seconds) total_duration = { From 4887dd1d5a20c952dea1fc8d4f5ce49aeb5136cb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 21 Jan 2021 18:48:25 +0530 Subject: [PATCH 269/284] chore: Rename 'Exclude Descendants' to 'Hide Descendants' - Rename 'Exclude Descendants' to 'Hide Descendants' - Rename js trigger to 'toggle_hide_descendants' --- .../user_permission/test_user_permission.py | 8 ++++---- .../doctype/user_permission/user_permission.js | 8 ++++---- .../user_permission/user_permission.json | 8 ++++---- .../doctype/user_permission/user_permission.py | 18 +++++++++--------- .../user_permission/user_permission_list.js | 6 +++--- frappe/permissions.py | 4 ++-- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 6773af004f..7e0b4a49c6 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -145,10 +145,10 @@ class TestUserPermission(unittest.TestCase): self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) self.assertTrue(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) - frappe.db.set_value("User Permission", {"allow": "Person", "for_value": parent_record.name}, "exclude_descendants", 1) + frappe.db.set_value("User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1) frappe.cache().delete_value("user_permissions") - # check if adding perm on a group record with exclude_descendants enabled, + # check if adding perm on a group record with hide_descendants enabled, # hides child records self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) self.assertFalse(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) @@ -164,7 +164,7 @@ def create_user(email, role="System Manager"): user.add_roles(role) return user -def get_params(user, doctype, docname, is_default=0, exclude_descendants=0, applicable=None): +def get_params(user, doctype, docname, is_default=0, hide_descendants=0, applicable=None): ''' Return param to insert ''' param = { "user": user.name, @@ -173,7 +173,7 @@ def get_params(user, doctype, docname, is_default=0, exclude_descendants=0, appl "is_default": is_default, "apply_to_all_doctypes": 1, "applicable_doctypes": [], - "exclude_descendants": exclude_descendants + "hide_descendants": hide_descendants } if applicable: param.update({"apply_to_all_doctypes": 0}) diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index 665d3c963c..4c3f5b4eb8 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -26,7 +26,7 @@ frappe.ui.form.on('User Permission', { () => frappe.set_route('query-report', 'Permitted Documents For User', { user: frm.doc.user })); frm.trigger('set_applicable_for_constraint'); - frm.trigger('show_exclude_descendants'); + frm.trigger('toggle_hide_descendants'); }, allow: frm => { @@ -34,7 +34,7 @@ frappe.ui.form.on('User Permission', { if (frm.doc.for_value) { frm.set_value('for_value', null); } - frm.trigger('show_exclude_descendants'); + frm.trigger('toggle_hide_descendants'); } }, @@ -49,9 +49,9 @@ frappe.ui.form.on('User Permission', { } }, - show_exclude_descendants: frm => { + toggle_hide_descendants: frm => { let show = frappe.boot.nested_set_doctypes.includes(frm.doc.allow); - frm.toggle_display('exclude_descendants', show); + frm.toggle_display('hide_descendants', show); } diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 6dc626ba04..9cea0856c9 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -15,7 +15,7 @@ "apply_to_all_doctypes", "applicable_for", "column_break_9", - "exclude_descendants" + "hide_descendants" ], "fields": [ { @@ -83,14 +83,14 @@ { "default": "0", "description": "Hide descendant records of For Value.", - "fieldname": "exclude_descendants", + "fieldname": "hide_descendants", "fieldtype": "Check", "hidden": 1, - "label": "Exclude Descendants" + "label": "Hide Descendants" } ], "links": [], - "modified": "2021-01-13 19:31:05.618273", + "modified": "2021-01-21 18:14:10.839381", "modified_by": "Administrator", "module": "Core", "name": "User Permission", diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index 6c926ba10f..e04000c0b3 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -50,7 +50,7 @@ class UserPermission(Document): }, or_filters={ 'applicable_for': cstr(self.applicable_for), 'apply_to_all_doctypes': 1, - 'exclude_descendants': cstr(self.exclude_descendants) + 'hide_descendants': cstr(self.hide_descendants) }, limit=1) if overlap_exists: ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) @@ -92,13 +92,13 @@ def get_user_permissions(user=None): try: for perm in frappe.get_all('User Permission', - fields=['allow', 'for_value', 'applicable_for', 'is_default', 'exclude_descendants'], + fields=['allow', 'for_value', 'applicable_for', 'is_default', 'hide_descendants'], filters=dict(user=user)): meta = frappe.get_meta(perm.allow) add_doc_to_perm(perm, perm.for_value, perm.is_default) - if meta.is_nested_set() and not perm.exclude_descendants: + if meta.is_nested_set() and not perm.hide_descendants: decendants = frappe.db.get_descendants(perm.allow, perm.for_value) for doc in decendants: add_doc_to_perm(perm, doc, False) @@ -195,7 +195,7 @@ def add_user_permissions(data): data = json.loads(data) data = frappe._dict(data) - # get all doctypes on whom this permission os applied + # get all doctypes on whom this permission is applied perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) exists = frappe.db.exists("User Permission", { "user": data.user, @@ -205,26 +205,26 @@ def add_user_permissions(data): }) if data.apply_to_all_doctypes == 1 and not exists: remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname) - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, apply_to_all=1) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, apply_to_all=1) return 1 elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: remove_apply_to_all(data.user, data.doctype, data.docname) update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname) for applicable in data.applicable_doctypes : if applicable not in perm_applied_docs: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, applicable=applicable) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) elif exists: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.exclude_descendants, applicable=applicable) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) return 1 return 0 -def insert_user_perm(user, doctype, docname, is_default=0, exclude_descendants=0, apply_to_all=None, applicable=None): +def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, apply_to_all=None, applicable=None): user_perm = frappe.new_doc("User Permission") user_perm.user = user user_perm.allow = doctype user_perm.for_value = docname user_perm.is_default = is_default - user_perm.exclude_descendants = exclude_descendants + user_perm.hide_descendants = hide_descendants if applicable: user_perm.applicable_for = applicable user_perm.apply_to_all_doctypes = 0 diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 49da74347f..49ec81984b 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -19,7 +19,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("is_default", "hidden", 1); dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); dialog.set_df_property("applicable_doctypes", "hidden", 1); - dialog.set_df_property("exclude_descendants", "hidden", 1); + dialog.set_df_property("hide_descendants", "hidden", 1); } }, { @@ -83,7 +83,7 @@ frappe.listview_settings['User Permission'] = { fieldtype: "Column Break" }, { - fieldname: 'exclude_descendants', + fieldname: 'hide_descendants', label: __('Exclude Descendants'), fieldtype: 'Check', hidden: 1 @@ -233,7 +233,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); dialog.set_value("apply_to_all_doctypes", "checked", 1); let show = frappe.boot.nested_set_doctypes.includes(dialog.get_value("doctype")); - dialog.set_df_property("exclude_descendants", "hidden", !show); + dialog.set_df_property("hide_descendants", "hidden", !show); dialog.refresh(); }, diff --git a/frappe/permissions.py b/frappe/permissions.py index ce551d0617..abb1f6653a 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -399,7 +399,7 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): add_user_permission(doctype, name, user) def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, - is_default=0, exclude_descendants=0): + is_default=0, hide_descendants=0): '''Add user permission''' from frappe.core.doctype.user_permission.user_permission import user_permission_exists @@ -414,7 +414,7 @@ def add_user_permission(doctype, name, user, ignore_permissions=False, applicabl for_value=name, is_default=is_default, applicable_for=applicable_for, - exclude_descendants=exclude_descendants, + hide_descendants=hide_descendants, )).insert(ignore_permissions=ignore_permissions) def remove_user_permission(doctype, name, user): From 5c9cc655cfd9b09a40a7f8b5ba555b94899a8225 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 22 Jan 2021 11:50:04 +0530 Subject: [PATCH 270/284] fix: Insufficient Permission for Leads > Dashboard Chart (#12243) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 2fa36b5514..b19f6cf9f0 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -73,7 +73,7 @@ def has_permission(doc, ptype, user): if doc.report_name in allowed_reports: return True else: - allowed_doctypes = [frappe.permissions.get_doctypes_with_read()] + allowed_doctypes = frappe.permissions.get_doctypes_with_read() if doc.document_type in allowed_doctypes: return True From aad9610b78ff91cc429f4b7532785e75b8b5e990 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 22 Jan 2021 12:18:25 +0530 Subject: [PATCH 271/284] fix: checkbox label in UP List View Dialog Co-authored-by: Rucha Mahabal --- frappe/core/doctype/user_permission/user_permission_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index 49ec81984b..5539a26438 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -84,7 +84,7 @@ frappe.listview_settings['User Permission'] = { }, { fieldname: 'hide_descendants', - label: __('Exclude Descendants'), + label: __('Hide Descendants'), fieldtype: 'Check', hidden: 1 }, @@ -267,4 +267,4 @@ frappe.listview_settings['User Permission'] = { } dialog.refresh_sections(); } -}; \ No newline at end of file +}; From 9b59b59e44211812fe24443ba66cfd56fd9e02cf Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 22 Jan 2021 13:13:50 +0530 Subject: [PATCH 272/284] test: Update TestNewsletter.test_unsubscribe * Update selecting email to unsubscribe email logic * style fixes --- .../doctype/newsletter/test_newsletter.py | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index ee7f123b7e..bd8fadc29c 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -2,58 +2,66 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, unittest -from frappe.utils import getdate, add_days - -from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email -from six.moves.urllib.parse import unquote +import unittest +from random import choice + +import frappe +from frappe.email.doctype.newsletter.newsletter import ( + confirmed_unsubscribe, + send_scheduled_email, +) +from frappe.email.doctype.newsletter.newsletter import get_newsletter_list +from frappe.email.queue import flush +from frappe.utils import add_days, getdate test_dependencies = ["Email Group"] +emails = [ + "test_subscriber1@example.com", + "test_subscriber2@example.com", + "test_subscriber3@example.com", + "test1@example.com", +] -emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", - "test_subscriber3@example.com", "test1@example.com"] class TestNewsletter(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") - frappe.db.sql('delete from `tabEmail Group Member`') + frappe.db.sql("delete from `tabEmail Group Member`") + + if not frappe.db.exists("Email Group", "_Test Email Group"): + frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert() - group_exist=frappe.db.exists("Email Group", "_Test Email Group") - if len(group_exist) == 0: + for email in emails: frappe.get_doc({ - "doctype": "Email Group", - "title": "_Test Email Group" + "doctype": "Email Group Member", + "email": email, + "email_group": "_Test Email Group" }).insert() - for email in emails: - frappe.get_doc({ - "doctype": "Email Group Member", - "email": email, - "email_group": "_Test Email Group" - }).insert() def test_send(self): - name = self.send_newsletter() + self.send_newsletter() - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] self.assertEqual(len(email_queue_list), 4) - recipients = [e.recipients[0].recipient for e in email_queue_list] - for email in emails: - self.assertTrue(email in recipients) + + recipients = set([e.recipients[0].recipient for e in email_queue_list]) + self.assertTrue(set(emails).issubset(recipients)) def test_unsubscribe(self): - # test unsubscribe name = self.send_newsletter() - from frappe.email.queue import flush + to_unsubscribe = choice(emails) + group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"]) + flush(from_test=True) - to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0]) - group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"]) confirmed_unsubscribe(to_unsubscribe, group[0].email_group) name = self.send_newsletter() - - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [ + frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue") + ] self.assertEqual(len(email_queue_list), 3) recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: if email != to_unsubscribe: self.assertTrue(email in recipients) @@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase): def test_portal(self): self.send_newsletter(1) frappe.set_user("test1@example.com") - from frappe.email.doctype.newsletter.newsletter import get_newsletter_list newsletters = get_newsletter_list("Newsletter", None, None, 0) self.assertEqual(len(newsletters), 1) @@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase): self.assertEqual(len(email_queue_list), 4) recipients = [e.recipients[0].recipient for e in email_queue_list] for email in emails: - self.assertTrue(email in recipients) \ No newline at end of file + self.assertTrue(email in recipients) From e9e99f20c3c16487964f9aed80c9965d45a45c4a Mon Sep 17 00:00:00 2001 From: pateljannat Date: Sat, 23 Jan 2021 19:11:45 +0530 Subject: [PATCH 273/284] fix: user mandatory in change user dialog --- frappe/desk/page/user_profile/user_profile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/desk/page/user_profile/user_profile.js b/frappe/desk/page/user_profile/user_profile.js index 5f91b376e8..1057cce2f3 100644 --- a/frappe/desk/page/user_profile/user_profile.js +++ b/frappe/desk/page/user_profile/user_profile.js @@ -76,6 +76,7 @@ class UserProfile { fieldname: 'user', options: 'User', label: __('User'), + reqd: 1 } ], primary_action_label: __('Go'), From a2fba77116609c089f281ee92c15cccb13c0e610 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 25 Jan 2021 15:37:37 +0530 Subject: [PATCH 274/284] fix: Skip translation of gender while creating it (#12260) --- frappe/desk/page/setup_wizard/install_fixtures.py | 4 ++-- frappe/utils/oauth.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 60e1f3242a..6d3aaee22b 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -18,14 +18,14 @@ def install(): @frappe.whitelist() def update_genders(): - default_genders = [_("Male"), _("Female"), _("Other"),_("Transgender"), _("Genderqueer"), _("Non-Conforming"),_("Prefer not to say")] + default_genders = ["Male", "Female", "Other","Transgender", "Genderqueer", "Non-Conforming","Prefer not to say"] records = [{'doctype': 'Gender', 'gender': d} for d in default_genders] for record in records: frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) @frappe.whitelist() def update_salutations(): - default_salutations = [_("Mr"), _("Ms"), _('Mx'), _("Dr"), _("Mrs"), _("Madam"), _("Miss"), _("Master"), _("Prof")] + default_salutations = ["Mr", "Ms", 'Mx', "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] records = [{'doctype': 'Salutation', 'salutation': d} for d in default_salutations] for record in records: doc = frappe.new_doc(record.get("doctype")) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index d090aabffc..e7672cedb3 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -230,12 +230,19 @@ def update_oauth_user(user, data, provider): save = True user = frappe.new_doc("User") + + gender = (data.get("gender") or "").title() + + if not frappe.db.exists("Gender", gender): + doc = frappe.new_doc("Gender", {"gender": gender}) + doc.insert(ignore_permissions=True) + user.update({ "doctype":"User", "first_name": get_first_name(data), "last_name": get_last_name(data), "email": get_email(data), - "gender": (data.get("gender") or "").title(), + "gender": gender, "enabled": 1, "new_password": frappe.generate_hash(get_email(data)), "location": data.get("location"), From a4396a83d080e7b07d3fca67159025c981f64562 Mon Sep 17 00:00:00 2001 From: Pierre-Louis Bonicoli Date: Mon, 25 Jan 2021 11:06:02 +0100 Subject: [PATCH 275/284] ci: fix a regex used in Travis config Fix this line in Procfile: redis_# socketio: redis-server config/redis_socketio.conf --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2331217363..23fb525138 100644 --- a/.travis.yml +++ b/.travis.yml @@ -104,11 +104,11 @@ install: - cd ./frappe-bench - - sed -i 's/watch:/# watch:/g' Procfile - - sed -i 's/schedule:/# schedule:/g' Procfile + - sed -i 's/^watch:/# watch:/g' Procfile + - sed -i 's/^schedule:/# schedule:/g' Procfile - - if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi - - if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi From b2fbcacfd489dbdae2458e7678f4a5e166516730 Mon Sep 17 00:00:00 2001 From: ollyboy Date: Tue, 26 Jan 2021 18:32:34 +0000 Subject: [PATCH 276/284] #12224 improve --- frappe/desk/calendar.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index 127194671a..ed227e1dc3 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -29,6 +29,7 @@ def get_event_conditions(doctype, filters=None): def get_events(doctype, start, end, field_map, filters=None, fields=None): field_map = frappe._dict(json.loads(field_map)) + fields = json.loads ( fields ) doc_meta = frappe.get_meta(doctype) for d in doc_meta.fields: @@ -44,8 +45,6 @@ def get_events(doctype, start, end, field_map, filters=None, fields=None): fields = [field_map.start, field_map.end, field_map.title, 'name'] if field_map.color: - if isinstance(fields, str): - fields = json.loads ( fields ) fields.append(field_map.color) start_date = "ifnull(%s, '0001-01-01 00:00:00')" % field_map.start From 36fcc1d55b365cad93af0f091555ee8e38088269 Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Thu, 28 Jan 2021 13:58:01 +0530 Subject: [PATCH 277/284] fix: use frappe.parse_json --- frappe/desk/calendar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index ed227e1dc3..064d870092 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -29,7 +29,7 @@ def get_event_conditions(doctype, filters=None): def get_events(doctype, start, end, field_map, filters=None, fields=None): field_map = frappe._dict(json.loads(field_map)) - fields = json.loads ( fields ) + fields = frappe.parse_json(fields) doc_meta = frappe.get_meta(doctype) for d in doc_meta.fields: From bcd1ad446d1651d6d6a20e9f243a76c65107ffe6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Jan 2021 14:58:49 +0530 Subject: [PATCH 278/284] feat: Module Profiles (#12148) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .../core/doctype/module_profile/__init__.py | 0 .../doctype/module_profile/module_profile.js | 19 ++++++ .../module_profile/module_profile.json | 60 +++++++++++++++++++ .../doctype/module_profile/module_profile.py | 12 ++++ .../module_profile/test_module_profile.py | 32 ++++++++++ frappe/core/doctype/user/user.js | 59 ++++++------------ frappe/core/doctype/user/user.json | 10 +++- frappe/core/doctype/user/user.py | 23 +++++-- frappe/public/build.json | 1 + frappe/public/js/frappe/module_editor.js | 39 ++++++++++++ 10 files changed, 209 insertions(+), 46 deletions(-) create mode 100644 frappe/core/doctype/module_profile/__init__.py create mode 100644 frappe/core/doctype/module_profile/module_profile.js create mode 100644 frappe/core/doctype/module_profile/module_profile.json create mode 100644 frappe/core/doctype/module_profile/module_profile.py create mode 100644 frappe/core/doctype/module_profile/test_module_profile.py create mode 100644 frappe/public/js/frappe/module_editor.js diff --git a/frappe/core/doctype/module_profile/__init__.py b/frappe/core/doctype/module_profile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js new file mode 100644 index 0000000000..9c92042dda --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.js @@ -0,0 +1,19 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Module Profile', { + refresh: function(frm) { + if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) { + if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) { + let module_area = $('
    ') + .appendTo(frm.fields_dict.module_html.wrapper); + + frm.module_editor = new frappe.ModuleEditor(frm, module_area); + } + } + + if (frm.module_editor) { + frm.module_editor.refresh(); + } + } +}); diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json new file mode 100644 index 0000000000..0e4e56962e --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "autoname": "field:module_profile_name", + "creation": "2020-12-22 22:00:30.614475", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "module_profile_name", + "module_html", + "block_modules" + ], + "fields": [ + { + "fieldname": "module_profile_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Module Profile Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "module_html", + "fieldtype": "HTML", + "label": "Module HTML" + }, + { + "fieldname": "block_modules", + "fieldtype": "Table", + "hidden": 1, + "label": "Block Modules", + "options": "Block Module", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-03 15:36:52.622696", + "modified_by": "Administrator", + "module": "Core", + "name": "Module Profile", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/module_profile/module_profile.py b/frappe/core/doctype/module_profile/module_profile.py new file mode 100644 index 0000000000..4f392353ac --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class ModuleProfile(Document): + def onload(self): + from frappe.config import get_modules_from_all_apps + self.set_onload('all_modules', + [m.get("module_name") for m in get_modules_from_all_apps()]) diff --git a/frappe/core/doctype/module_profile/test_module_profile.py b/frappe/core/doctype/module_profile/test_module_profile.py new file mode 100644 index 0000000000..400053d22c --- /dev/null +++ b/frappe/core/doctype/module_profile/test_module_profile.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest + +class TestModuleProfile(unittest.TestCase): + def test_make_new_module_profile(self): + if not frappe.db.get_value('Module Profile', '_Test Module Profile'): + frappe.get_doc({ + 'doctype': 'Module Profile', + 'module_profile_name': '_Test Module Profile', + 'block_modules': [ + {'module': 'Accounts'} + ] + }).insert() + + # add to user and check + if not frappe.db.get_value('User', 'test-for-module_profile@example.com'): + new_user = frappe.get_doc({ + 'doctype': 'User', + 'email':'test-for-module_profile@example.com', + 'first_name':'Test User' + }).insert() + else: + new_user = frappe.get_doc('User', 'test-for-module_profile@example.com') + + new_user.module_profile = '_Test Module Profile' + new_user.save() + + self.assertEqual(new_user.block_modules[0].module, 'Accounts') diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index b8e16bfe25..5493baf553 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -37,6 +37,25 @@ frappe.ui.form.on('User', { } }, + module_profile: function(frm) { + if (frm.doc.module_profile) { + frappe.call({ + "method": "frappe.core.doctype.user.user.get_module_profile", + args: { + module_profile: frm.doc.module_profile + }, + callback: function(data) { + frm.set_value("block_modules", []); + $.each(data.message || [], function(i, v) { + let d = frm.add_child("block_modules"); + d.module = v.module; + }); + frm.module_editor && frm.module_editor.refresh(); + } + }); + } + }, + onload: function(frm) { frm.can_edit_roles = has_access_to_edit_user(); @@ -255,43 +274,3 @@ function get_roles_for_editing_user() { .filter(perm => perm.permlevel >= 1 && perm.write) .map(perm => perm.role) || ['System Manager']; } - -frappe.ModuleEditor = Class.extend({ - init: function(frm, wrapper) { - this.wrapper = $('
    ').appendTo(wrapper); - this.frm = frm; - this.make(); - }, - make: function() { - var me = this; - this.frm.doc.__onload.all_modules.forEach(function(m) { - $(repl('
    \ -
    ', {module: m})).appendTo(me.wrapper); - }); - this.bind(); - }, - refresh: function() { - var me = this; - this.wrapper.find(".block-module-check").prop("checked", true); - $.each(this.frm.doc.block_modules, function(i, d) { - me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false); - }); - }, - bind: function() { - var me = this; - this.wrapper.on("change", ".block-module-check", function() { - var module = $(this).attr('data-module'); - if($(this).prop("checked")) { - // remove from block_modules - me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) { - if (d.module != module) { - return d; - } - }); - } else { - me.frm.add_child("block_modules", {"module": module}); - } - }); - } -}); diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 7d91e8cfe0..53e05bb916 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -51,9 +51,9 @@ "send_me_a_copy", "allowed_in_mentions", "email_signature", - "email_inbox", "user_emails", "sb_allow_modules", + "module_profile", "modules_html", "block_modules", "home_settings", @@ -577,6 +577,12 @@ "fieldtype": "Password", "label": "API Secret", "read_only": 1 + }, + { + "fieldname": "module_profile", + "fieldtype": "Link", + "label": "Module Profile", + "options": "Module Profile" } ], "icon": "fa fa-user", @@ -684,4 +690,4 @@ "sort_order": "DESC", "title_field": "full_name", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index da4026d8fd..dcca4f4a25 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -75,6 +75,7 @@ class User(Document): self.validate_user_email_inbox() ask_pass_update() self.validate_roles() + self.validate_allowed_modules() self.validate_user_image() if self.language == "Loading...": @@ -85,9 +86,18 @@ class User(Document): def validate_roles(self): if self.role_profile_name: - role_profile = frappe.get_doc('Role Profile', self.role_profile_name) - self.set('roles', []) - self.append_roles(*[role.role for role in role_profile.roles]) + role_profile = frappe.get_doc('Role Profile', self.role_profile_name) + self.set('roles', []) + self.append_roles(*[role.role for role in role_profile.roles]) + + def validate_allowed_modules(self): + if self.module_profile: + module_profile = frappe.get_doc('Module Profile', self.module_profile) + self.set('block_modules', []) + for d in module_profile.get('block_modules'): + self.append('block_modules', { + 'module': d.module + }) def validate_user_image(self): if self.user_image and len(self.user_image) > 2000: @@ -108,7 +118,7 @@ class User(Document): ) if self.name not in ('Administrator', 'Guest') and not self.user_image: frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now) - + # Set user selected timezone if self.time_zone: frappe.defaults.set_default("time_zone", self.time_zone, self.name) @@ -1042,6 +1052,11 @@ def get_role_profile(role_profile): roles = frappe.get_doc('Role Profile', {'role_profile': role_profile}) return roles.roles +@frappe.whitelist() +def get_module_profile(module_profile): + module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile}) + return module_profile.get('block_modules') + def update_roles(role_profile): users = frappe.get_all('User', filters={'role_profile_name': role_profile}) role_profile = frappe.get_doc('Role Profile', role_profile) diff --git a/frappe/public/build.json b/frappe/public/build.json index d744da98d1..ebc96e6f6b 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -161,6 +161,7 @@ "public/js/frappe/router_history.js", "public/js/frappe/defaults.js", "public/js/frappe/roles_editor.js", + "public/js/frappe/module_editor.js", "public/js/frappe/microtemplate.js", "public/js/frappe/ui/page.html", diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js new file mode 100644 index 0000000000..35037a3e62 --- /dev/null +++ b/frappe/public/js/frappe/module_editor.js @@ -0,0 +1,39 @@ +frappe.ModuleEditor = Class.extend({ + init: function(frm, wrapper) { + this.wrapper = $('
    ').appendTo(wrapper); + this.frm = frm; + this.make(); + }, + make: function() { + var me = this; + this.frm.doc.__onload.all_modules.forEach(function(m) { + $(repl('
    \ +
    ', {module: m})).appendTo(me.wrapper); + }); + this.bind(); + }, + refresh: function() { + var me = this; + this.wrapper.find(".block-module-check").prop("checked", true); + $.each(this.frm.doc.block_modules, function(i, d) { + me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false); + }); + }, + bind: function() { + var me = this; + this.wrapper.on("change", ".block-module-check", function() { + var module = $(this).attr('data-module'); + if ($(this).prop("checked")) { + // remove from block_modules + me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) { + if (d.module != module) { + return d; + } + }); + } else { + me.frm.add_child("block_modules", {"module": module}); + } + }); + } +}); \ No newline at end of file From 6f774d6d0c7f1163600b07238bebced3a7f392b1 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 28 Jan 2021 17:14:47 +0530 Subject: [PATCH 279/284] fix: Check for fieldlevel permission for report query (#12163) Co-authored-by: Prssanna Desai --- frappe/desk/reportview.py | 33 ++++++++++++--- frappe/model/meta.py | 19 +++++++++ frappe/tests/test_db_query.py | 80 ++++++++++++++++++++++++++++++++++- 3 files changed, 124 insertions(+), 8 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 9f5a5d84c8..36870d40bb 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -54,6 +54,12 @@ def get_form_params(): fields = data["fields"] + if ((isinstance(fields, string_types) and fields == "*") + or (isinstance(fields, (list, tuple)) and len(fields) == 1 and fields[0] == "*")): + parenttype = data.doctype + data["fields"] = frappe.db.get_table_columns(parenttype) + fields = data["fields"] + for field in fields: key = field.split(" as ")[0] @@ -61,21 +67,24 @@ def get_form_params(): if key.startswith('sum('): continue if key.startswith('avg('): continue - if "." in key: - parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") - else: - parenttype = data.doctype - fieldname = field.strip("`") + parenttype, fieldname = get_parent_dt_and_field(key, data) - df = frappe.get_meta(parenttype).get_field(fieldname) + if fieldname == "*": + # * inside list is not allowed with other fields + fields.remove(field) + + meta = frappe.get_meta(parenttype) + df = meta.get_field(fieldname) - fieldname = df.fieldname if df else None report_hide = df.report_hide if df else None # remove the field from the query if the report hide flag is set and current view is Report if report_hide and is_report: fields.remove(field) + if df and fieldname in [df.fieldname for df in meta.get_high_permlevel_fields()]: + if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype) and field in fields: + fields.remove(field) # queries must always be server side data.query = None @@ -83,6 +92,16 @@ def get_form_params(): return data +def get_parent_dt_and_field(field, data): + if "." in field: + parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`") + else: + parenttype = data.doctype + fieldname = field.strip("`") + + return parenttype, fieldname + + def compress(data, args = {}): """separate keys and values""" from frappe.desk.query_report import add_total_row diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 88ed1a7e78..5dc7ca2d4d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -450,6 +450,25 @@ class Meta(Document): return self.high_permlevel_fields + def get_permlevel_access(self, permission_type='read', parenttype=None): + has_access_to = [] + roles = frappe.get_roles() + for perm in self.get_permissions(parenttype): + if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type): + if perm.permlevel not in has_access_to: + has_access_to.append(perm.permlevel) + + return has_access_to + + def get_permissions(self, parenttype=None): + if self.istable and parenttype: + # use parent permissions + permissions = frappe.get_meta(parenttype).permissions + else: + permissions = self.get('permissions', []) + + return permissions + def get_dashboard_data(self): '''Returns dashboard setup related to this doctype. diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 4d2bef9479..836bb4bbf5 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -7,9 +7,14 @@ import frappe, unittest from frappe.model.db_query import DatabaseQuery from frappe.desk.reportview import get_filters_cond +from frappe.core.page.permission_manager.permission_manager import update, reset, add from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype +from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.handler import execute_cmd -test_dependencies = ['User', 'Blog Post'] +from frappe.utils.testutils import add_custom_field, clear_custom_fields + +test_dependencies = ['User', 'Blog Post', 'Blog Category', 'Blogger'] class TestReportview(unittest.TestCase): def test_basic(self): @@ -355,6 +360,79 @@ class TestReportview(unittest.TestCase): owners = DatabaseQuery("DocType").execute(filters={"name": "DocType"}, pluck="owner") self.assertEqual(owners, ["Administrator"]) + def test_reportview_get(self): + user = frappe.get_doc("User", "test@example.com") + add_child_table_to_blog_post() + + user_roles = frappe.get_roles() + user.remove_roles(*user_roles) + user.add_roles("Blogger") + + make_property_setter("Blog Post", "published", "permlevel", 1, "Int") + reset("Blog Post") + add("Blog Post", "Website Manager", 1) + update("Blog Post", "Website Manager", 1, "write", 1) + + frappe.set_user(user.name) + + frappe.local.request = frappe._dict() + frappe.local.request.method = "POST" + + frappe.local.form_dict = frappe._dict({ + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + }) + + # even if * is passed, fields which are not accessible should be filtered out + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ["title"]) + frappe.local.form_dict = frappe._dict({ + "doctype": "Blog Post", + "fields": ["*"], + }) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertNotIn("published", response["keys"]) + + frappe.set_user("Administrator") + user.add_roles("Website Manager") + frappe.set_user(user.name) + + frappe.set_user("Administrator") + + # Admin should be able to see access all fields + frappe.local.form_dict = frappe._dict({ + "doctype": "Blog Post", + "fields": ["published", "title", "`tabTest Child`.`test_field`"], + }) + + response = execute_cmd("frappe.desk.reportview.get") + self.assertListEqual(response["keys"], ['published', 'title', 'test_field']) + + # reset user roles + user.remove_roles("Blogger", "Website Manager") + user.add_roles(*user_roles) + + +def add_child_table_to_blog_post(): + child_table = frappe.get_doc({ + 'doctype': 'DocType', + 'istable': 1, + 'custom': 1, + 'name': 'Test Child', + 'module': 'Custom', + 'autoname': 'Prompt', + 'fields': [{ + 'fieldname': 'test_field', + 'fieldtype': 'Data', + 'permlevel': 1 + }], + }) + + child_table.insert(ignore_permissions=True, ignore_if_duplicate=True) + clear_custom_fields('Blog Post') + add_custom_field('Blog Post', 'child_table', 'Table', child_table.name) + def create_event(subject="_Test Event", starts_on=None): """ create a test event """ From cdf8114531b3edacf2bb6a5b98f4bb1cc8854660 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Mon, 1 Feb 2021 15:26:46 +0530 Subject: [PATCH 280/284] fix: validate user permissions while user has select perms (#12287) --- frappe/model/db_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index c799586d61..6f4e1fc53c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -597,7 +597,7 @@ class DatabaseQuery(object): self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype, frappe.db.escape(self.user, percent=False))) # add user permission only if role has read perm - elif role_permissions.get("read"): + elif role_permissions.get("read") or role_permissions.get("select"): # get user permissions user_permissions = frappe.permissions.get_user_permissions(self.user) self.add_user_permissions(user_permissions) From 6984f4949155d460372541da6f2619b43297de0a Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 1 Feb 2021 15:29:12 +0530 Subject: [PATCH 281/284] feat: ability to set default desk page (#12286) * feat: ability to set default desk page * chore: add description * fix: validate single default page * fix: error message Co-authored-by: Prssanna Desai Co-authored-by: Prssanna Desai --- frappe/desk/desktop.py | 15 ++++++++++++--- frappe/desk/doctype/desk_page/desk_page.json | 11 ++++++++++- frappe/desk/doctype/desk_page/desk_page.py | 7 ++++++- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 4dab313892..a476573b1a 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -108,9 +108,18 @@ class Workspace: 'extends': self.page_name, 'for_user': frappe.session.user } - pages = frappe.get_all("Desk Page", filters=filters, limit=1) - if pages: - return frappe.get_cached_doc("Desk Page", pages[0]) + user_pages = frappe.get_all("Desk Page", filters=filters, limit=1) + if user_pages: + return frappe.get_cached_doc("Desk Page", user_pages[0]) + + filters = { + 'extends_another_page': 1, + 'extends': self.page_name, + 'is_default': 1 + } + default_page = frappe.get_all("Desk Page", filters=filters, limit=1) + if default_page: + return frappe.get_cached_doc("Desk Page", default_page[0]) self.get_pages_to_extend() return frappe.get_cached_doc("Desk Page", self.page_name) diff --git a/frappe/desk/doctype/desk_page/desk_page.json b/frappe/desk/doctype/desk_page/desk_page.json index 2b8aea5e6c..016d6c89d4 100644 --- a/frappe/desk/doctype/desk_page/desk_page.json +++ b/frappe/desk/doctype/desk_page/desk_page.json @@ -16,6 +16,7 @@ "onboarding", "column_break_3", "extends_another_page", + "is_default", "is_standard", "developer_mode_only", "disable_user_customization", @@ -197,10 +198,18 @@ "fieldname": "hide_custom", "fieldtype": "Check", "label": "Hide Custom DocTypes and Reports" + }, + { + "default": "0", + "depends_on": "extends_another_page", + "description": "Sets the current page as default for all users", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" } ], "links": [], - "modified": "2020-05-18 19:17:27.206646", + "modified": "2021-01-21 12:09:36.156614", "modified_by": "Administrator", "module": "Desk", "name": "Desk Page", diff --git a/frappe/desk/doctype/desk_page/desk_page.py b/frappe/desk/doctype/desk_page/desk_page.py index e92844ac0b..fcc3c08135 100644 --- a/frappe/desk/doctype/desk_page/desk_page.py +++ b/frappe/desk/doctype/desk_page/desk_page.py @@ -15,6 +15,11 @@ class DeskPage(Document): if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): frappe.throw(_("You need to be in developer mode to edit this document")) + if self.is_default and self.name and frappe.db.exists("Desk Page", { + "name": ["!=", self.name], 'is_default': 1, 'extends': self.extends + }): + frappe.throw(_("You can only have one default page that extends a particular standard page.")) + def validate_cards_json(self): for card in self.cards: try: @@ -45,4 +50,4 @@ def disable_saving_as_standard(): frappe.flags.in_patch or \ frappe.flags.in_test or \ frappe.flags.in_fixtures or \ - frappe.flags.in_migrate \ No newline at end of file + frappe.flags.in_migrate From 8d413fc69594a660cbe3b368421ea147fe84da2d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 19:52:10 +0530 Subject: [PATCH 282/284] fix: cannot extend a desk page with developer mode off (bp #12294) (#12295) Co-authored-by: Saqib --- frappe/desk/doctype/desk_page/desk_page.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/desk/doctype/desk_page/desk_page.js b/frappe/desk/doctype/desk_page/desk_page.js index 503859eb61..86fea6df40 100644 --- a/frappe/desk/doctype/desk_page/desk_page.js +++ b/frappe/desk/doctype/desk_page/desk_page.js @@ -5,7 +5,6 @@ frappe.ui.form.on('Desk Page', { refresh: function(frm) { frm.enable_save(); frm.get_field("is_standard").toggle(frappe.boot.developer_mode); - frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode); frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode); if (frm.doc.for_user) { From f8dc985ef3dc52412ac41d20487c3ed8c8e45fb0 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 2 Feb 2021 07:02:17 +0530 Subject: [PATCH 283/284] chore: Add release notes for v13-beta-10 (#12296) --- frappe/change_log/v13/v13_0_0-beta_10.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 frappe/change_log/v13/v13_0_0-beta_10.md diff --git a/frappe/change_log/v13/v13_0_0-beta_10.md b/frappe/change_log/v13/v13_0_0-beta_10.md new file mode 100644 index 0000000000..ff98a8f9c2 --- /dev/null +++ b/frappe/change_log/v13/v13_0_0-beta_10.md @@ -0,0 +1,21 @@ +### Version 13.0.0 Beta 10 Release Notes + +#### Features and Enhancements + +- Option to hide child records for a nested DocType via User Permissions ([12209](https://github.com/frappe/frappe/pull/12209)) +- Added option to grant only `Select` access to document ([12063](https://github.com/frappe/frappe/pull/12063)) +- Introduced map view ([11202](https://github.com/frappe/frappe/pull/11202)) +- Enabled image rendering from links in Print View ([12101](https://github.com/frappe/frappe/pull/12101)) +- Introduced "Yesterday" and "Tomorrow" options for Timespan filter ([12179](https://github.com/frappe/frappe/pull/12179)) + +#### Fixes + +- Fixed HTML download of Auto Email Report that used to break in some cases ([12202](https://github.com/frappe/frappe/pull/12202)) +- Fixed reset customizations functionality ([12152](https://github.com/frappe/frappe/pull/12152)) +- Fixed the rendering of percentage stat in Dashboard Chart ([12090](https://github.com/frappe/frappe/pull/12090)) +- Fixed permission issues in Dashboard Chart ([12243](https://github.com/frappe/frappe/pull/12243)) +- Fixed an issue with grid row index ([12188](https://github.com/frappe/frappe/pull/12188)) +- Fixed an issue where fields used to get reordered after adding new columns ([12058](https://github.com/frappe/frappe/pull/12058)) +- Fixed currency formatting in Print Format ([11897](https://github.com/frappe/frappe/pull/11897)) +- Added a fieldlevel permission check for report data ([12163](https://github.com/frappe/frappe/pull/12163)) +- Fixed an issue with percent precision ([12010](https://github.com/frappe/frappe/pull/12010)) \ No newline at end of file From 195e9cb5db422db6af24f4bc1a48bbc0ac7ce2b7 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 2 Feb 2021 21:13:54 +0550 Subject: [PATCH 284/284] bumped to version 13.0.0-beta.10 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 663036b2c8..7929e62acb 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -23,7 +23,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '13.0.0-beta.9' +__version__ = '13.0.0-beta.10' __title__ = "Frappe Framework"