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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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": [], - "modified": "2020-07-14 18:52:06.041273", + "links": [ + { + "link_doctype": "Token Cache", + "link_fieldname": "connected_app" + } + ], + "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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] refactor: flatten callback --- .../doctype/connected_app/connected_app.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 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 state != token_cache.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() - 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"] = '/desk' - else: + if len(path) != 4 or not path[3]: frappe.throw(_('Invalid Parameter(s)')) + + 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 state != token_cache.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() + 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/184] 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/184] 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/184] 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/184] refactor: move mothods to token cache --- .../doctype/connected_app/connected_app.js | 2 +- .../doctype/connected_app/connected_app.py | 89 +++++++------------ .../doctype/token_cache/token_cache.json | 8 +- .../doctype/token_cache/token_cache.py | 33 +++++++ 4 files changed, 69 insertions(+), 63 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,47 +53,6 @@ class ConnectedApp(Document): 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 - - try: - token = self.get_stored_user_token(user) - token = self.check_validity(token) - except frappe.exceptions.DoesNotExistError: - redirect = self.initiate_web_application_flow(user, redirect_to) - 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: - 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) @@ -113,7 +63,36 @@ class ConnectedApp(Document): client_secret=self.get_password('client_secret') ) - return self.update_stored_client_token(token) + 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 + + 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" + frappe.local.response["location"] = redirect + return redirect + + 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') @@ -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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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/184] :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} -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']: + 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_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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] :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/184] 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/184] 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/184] 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/184] 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/184] refactor: rename Endpoint to URI --- .../doctype/connected_app/connected_app.json | 62 +++++++++---------- .../doctype/connected_app/connected_app.py | 4 +- .../connected_app/test_connected_app.py | 4 +- .../doctype/token_cache/token_cache.py | 6 +- 4 files changed, 38 insertions(+), 38 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": [ { @@ -90,35 +90,10 @@ "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" - }, { "fieldname": "scopes", "fieldtype": "Table", @@ -129,6 +104,31 @@ "fieldname": "base_url", "fieldtype": "Data", "label": "Base URL" + }, + { + "fieldname": "authorization_uri", + "fieldtype": "Data", + "label": "Authorization URI" + }, + { + "fieldname": "token_uri", + "fieldtype": "Data", + "label": "Token URI" + }, + { + "fieldname": "revocation_uri", + "fieldtype": "Data", + "label": "Revocation URI" + }, + { + "fieldname": "userinfo_uri", + "fieldtype": "Data", + "label": "Userinfo URI" + }, + { + "fieldname": "introspection_uri", + "fieldtype": "Data", + "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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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/184] 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 33813fda881639e6fcaa05de48d08e777fde5cff Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Wed, 28 Oct 2020 14:11:50 +0530 Subject: [PATCH 087/184] 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 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 088/184] 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 089/184] 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 090/184] 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 091/184] 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 092/184] 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 093/184] 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 094/184] 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 095/184] refactor: frappe.db.exists instead of try/except --- .../doctype/connected_app/connected_app.py | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 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 + if not token_cache: + token_cache = frappe.new_doc('Token Cache') + token_cache.user = user + token_cache.connected_app = self.name - token.success_uri = success_uri - token.state = state - token.save(ignore_permissions=True) + 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 - return token + redirect = self.initiate_web_application_flow(user, success_uri) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect + return redirect - def get_stored_user_token(self, user): - return frappe.get_doc('Token Cache', self.name + '-' + user) + def get_token_cache(self, user): + token_cache = None + token_cache_name = self.name + '-' + user + + if frappe.db.exists('Token Cache', token_cache_name): + token_cache = frappe.get_doc('Token Cache', token_cache_name) + + 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 096/184] 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 2c4b5c67b03c199c572cd1c0ae48b48fff9417b5 Mon Sep 17 00:00:00 2001 From: conncampbell Date: Sun, 8 Nov 2020 10:02:35 -0700 Subject: [PATCH 097/184] 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 098/184] 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 099/184] 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 100/184] 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 101/184] 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 102/184] 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 103/184] 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 c5767b818facb2e3b9e61a8cede7c7653dca5a67 Mon Sep 17 00:00:00 2001 From: Saurabh Date: Tue, 1 Dec 2020 18:04:12 +0530 Subject: [PATCH 104/184] 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 105/184] 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 585215ccee981cdc0b2a986b443d8cbdd7ad1f15 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 8 Dec 2020 13:38:12 +0530 Subject: [PATCH 106/184] 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 107/184] 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 568426668fb60bc6c9ac044dd50930825274db11 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 11 Dec 2020 13:55:31 +0530 Subject: [PATCH 108/184] 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 109/184] 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 110/184] 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 e04b40aab4279300b3d26bd8f0199150167390ca Mon Sep 17 00:00:00 2001 From: Ishan Loya Date: Thu, 17 Dec 2020 12:29:35 +0530 Subject: [PATCH 111/184] 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 112/184] 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 ebd6de1ae14c5a5c8cac11db6e38d7ace4c821cb Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 18 Dec 2020 12:04:50 +0530 Subject: [PATCH 113/184] 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 6d989531914dc6b6a0d0c9b9e653aaab6bb520be Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 18 Dec 2020 14:35:00 +0530 Subject: [PATCH 114/184] 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 115/184] 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 116/184] 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 05770275b0292ebf37e175cd99503de69cfc43ec Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 21 Dec 2020 12:33:15 +0530 Subject: [PATCH 117/184] 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 118/184] 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 119/184] 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 120/184] 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 121/184] 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 122/184] 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 123/184] 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 124/184] 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 125/184] 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 126/184] 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 127/184] 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 128/184] 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 129/184] 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 130/184] 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 131/184] 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 132/184] 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 133/184] 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 134/184] 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 135/184] 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 136/184] 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 137/184] 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 138/184] 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 139/184] 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 140/184] 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 141/184] 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 142/184] 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 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 143/184] 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 144/184] 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 145/184] 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 146/184] 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 147/184] 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 148/184] 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 149/184] 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 51d8046da15b6539f9badecdff506ccb32eee26b Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Mon, 4 Jan 2021 15:30:28 +0530 Subject: [PATCH 150/184] 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 151/184] 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 152/184] 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 153/184] 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 154/184] 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 155/184] 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 156/184] 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 157/184] 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 158/184] 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 213744aa4949279c13f8f92c95700bab9c711be6 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 6 Jan 2021 17:34:48 +0530 Subject: [PATCH 159/184] 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 160/184] 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 161/184] 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 54f9b894edd2c6c6973468f1b8db8ba054b36dd2 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 7 Jan 2021 21:18:42 +0530 Subject: [PATCH 162/184] 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 163/184] 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 164/184] 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 165/184] 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 166/184] 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 167/184] 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 168/184] 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 0964f07ee47aad676236977fc7a2d5473c8a8ef4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 12 Jan 2021 09:25:52 +0530 Subject: [PATCH 169/184] 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 170/184] 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 171/184] 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 172/184] 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 173/184] 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 174/184] 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 175/184] 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 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 176/184] 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 177/184] 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 178/184] 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 179/184] 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 f23d88be5e0209cb7e4cf60aed1ca217f1f3065a Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Fri, 15 Jan 2021 13:48:53 +0530 Subject: [PATCH 180/184] 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 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 181/184] test: fix connected app --- .../connected_app/test_connected_app.py | 120 +++++++++++++++--- 1 file changed, 100 insertions(+), 20 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') - - 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() - + 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') - 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() + 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 + }) + self.oauth_client.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 182/184] 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 183/184] 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 184/184] 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)