diff --git a/.eslintrc b/.eslintrc
index d123023a68..8a509f0df4 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -143,6 +143,7 @@
"Cypress": true,
"cy": true,
"it": true,
+ "describe": true,
"expect": true,
"context": true,
"before": true,
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
new file mode 100644
index 0000000000..37889fbbb1
--- /dev/null
+++ b/.github/helper/semgrep_rules/frappe_correctness.py
@@ -0,0 +1,28 @@
+import frappe
+from frappe import _, flt
+
+from frappe.model.document import Document
+
+
+def on_submit(self):
+ if self.value_of_goods == 0:
+ frappe.throw(_('Value of goods cannot be 0'))
+ # ruleid: frappe-modifying-after-submit
+ self.status = 'Submitted'
+
+def on_submit(self): # noqa
+ if flt(self.per_billed) < 100:
+ self.update_billing_status()
+ else:
+ # todook: frappe-modifying-after-submit
+ self.status = "Completed"
+ self.db_set("status", "Completed")
+
+class TestDoc(Document):
+ pass
+
+ def validate(self):
+ #ruleid: frappe-modifying-child-tables-while-iterating
+ for item in self.child_table:
+ if item.value < 0:
+ self.remove(item)
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
new file mode 100644
index 0000000000..faab3344a6
--- /dev/null
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -0,0 +1,135 @@
+# This file specifies rules for correctness according to how frappe doctype data model works.
+
+rules:
+- id: frappe-modifying-but-not-comitting
+ patterns:
+ - pattern: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ ...
+ self.db_set(..., self.$ATTR, ...)
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.db_set(..., $SOME_VAR, ...)
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.save()
+ - metavariable-regex:
+ metavariable: '$ATTR'
+ # this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
+ regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
+ - metavariable-regex:
+ metavariable: "$METHOD"
+ regex: "(on_submit|on_cancel)"
+ message: |
+ DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
+ languages: [python]
+ severity: ERROR
+
+- id: frappe-modifying-but-not-comitting-other-method
+ patterns:
+ - pattern: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ ...
+ self.db_set(..., self.$ATTR, ...)
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.db_set(..., $SOME_VAR, ...)
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+ self.save()
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ - metavariable-regex:
+ metavariable: "$METHOD"
+ regex: "(on_submit|on_cancel)"
+ message: |
+ self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
+ languages: [python]
+ severity: ERROR
+
+- id: frappe-print-function-in-doctypes
+ pattern: print(...)
+ message: |
+ Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
+ languages: [python]
+ severity: WARNING
+ paths:
+ exclude:
+ - test_*.py
+ include:
+ - "*/**/doctype/*"
+
+- id: frappe-modifying-child-tables-while-iterating
+ pattern-either:
+ - pattern: |
+ for $ROW in self.$TABLE:
+ ...
+ self.remove(...)
+ - pattern: |
+ for $ROW in self.$TABLE:
+ ...
+ self.append(...)
+ message: |
+ Child table being modified while iterating on it.
+ languages: [python]
+ severity: ERROR
+ paths:
+ include:
+ - "*/**/doctype/*"
+
+- id: frappe-same-key-assigned-twice
+ pattern-either:
+ - pattern: |
+ {..., $X: $A, ..., $X: $B, ...}
+ - pattern: |
+ dict(..., ($X, $A), ..., ($X, $B), ...)
+ - pattern: |
+ _dict(..., ($X, $A), ..., ($X, $B), ...)
+ message: |
+ key `$X` is uselessly assigned twice. This could be a potential bug.
+ languages: [python]
+ severity: ERROR
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
index 1937fc0e52..b2cc4b16fc 100644
--- a/.github/helper/semgrep_rules/security.yml
+++ b/.github/helper/semgrep_rules/security.yml
@@ -12,3 +12,18 @@ rules:
exclude:
- frappe/__init__.py
- frappe/commands/utils.py
+
+- id: frappe-sqli-format-strings
+ patterns:
+ - pattern-inside: |
+ @frappe.whitelist()
+ def $FUNC(...):
+ ...
+ - pattern-either:
+ - pattern: frappe.db.sql("..." % ...)
+ - pattern: frappe.db.sql(f"...", ...)
+ - pattern: frappe.db.sql("...".format(...), ...)
+ message: |
+ Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
+ languages: [python]
+ severity: WARNING
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
index 3737da5a7e..df55089b9f 100644
--- a/.github/helper/semgrep_rules/translate.yml
+++ b/.github/helper/semgrep_rules/translate.yml
@@ -44,7 +44,8 @@ rules:
pattern-either:
- pattern: _(...) + ... + _(...)
- pattern: _("..." + "...")
- - pattern-regex: '_\([^\)]*\\\s*'
+ - pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
+ - pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
diff --git a/.github/helper/semgrep_rules/ux.py b/.github/helper/semgrep_rules/ux.py
new file mode 100644
index 0000000000..4a74457435
--- /dev/null
+++ b/.github/helper/semgrep_rules/ux.py
@@ -0,0 +1,31 @@
+import frappe
+from frappe import msgprint, throw, _
+
+
+# ruleid: frappe-missing-translate-function
+throw("Error Occured")
+
+# ruleid: frappe-missing-translate-function
+frappe.throw("Error Occured")
+
+# ruleid: frappe-missing-translate-function
+frappe.msgprint("Useful message")
+
+# ruleid: frappe-missing-translate-function
+msgprint("Useful message")
+
+
+# ok: frappe-missing-translate-function
+translatedmessage = _("Hello")
+
+# ok: frappe-missing-translate-function
+throw(translatedmessage)
+
+# ok: frappe-missing-translate-function
+msgprint(translatedmessage)
+
+# ok: frappe-missing-translate-function
+msgprint(_("Helpful message"))
+
+# ok: frappe-missing-translate-function
+frappe.throw(_("Error occured"))
diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml
new file mode 100644
index 0000000000..ed06a6a80c
--- /dev/null
+++ b/.github/helper/semgrep_rules/ux.yml
@@ -0,0 +1,15 @@
+rules:
+- id: frappe-missing-translate-function
+ pattern-either:
+ - patterns:
+ - pattern: frappe.msgprint("...", ...)
+ - pattern-not: frappe.msgprint(_("..."), ...)
+ - pattern-not: frappe.msgprint(__("..."), ...)
+ - patterns:
+ - pattern: frappe.throw("...", ...)
+ - pattern-not: frappe.throw(_("..."), ...)
+ - pattern-not: frappe.throw(__("..."), ...)
+ message: |
+ All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
+ languages: [python, javascript, json]
+ severity: ERROR
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index bfe2002f69..e21a1f7ac6 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -151,7 +151,8 @@ jobs:
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
- coveralls
+ coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
+ COVERALLS_SERVICE_NAME: github
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 1d5694f521..5092bf4705 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -14,9 +14,19 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.8
- - name: Run semgrep
+
+ - name: Setup semgrep
run: |
python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
+
+ - name: Semgrep errors
+ run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --config=.github/helper/semgrep_rules --quiet --error $files
+ [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
+ semgrep --config="r/python.lang.correctness" --quiet --error $files
+
+ - name: Semgrep warnings
+ run: |
+ files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
+ [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
diff --git a/README.md b/README.md
index b00d291b96..e00bea7857 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,8 @@ Full-stack web application framework that uses Python and MariaDB on the server
### Installation
-[Install via Frappe Bench](https://github.com/frappe/bench)
+* [Install via Docker](https://github.com/frappe/frappe_docker)
+* [Install via Frappe Bench](https://github.com/frappe/bench)
## Contributing
diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js
index 80e6387d99..cbb0524c24 100644
--- a/cypress/integration/relative_time_filters.js
+++ b/cypress/integration/relative_time_filters.js
@@ -1,7 +1,4 @@
context('Relative Timeframe', () => {
- beforeEach(() => {
- cy.login();
- });
before(() => {
cy.login();
cy.visit('/app/website');
diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js
index faa72d63a5..25cab78ba2 100644
--- a/cypress/integration/table_multiselect.js
+++ b/cypress/integration/table_multiselect.js
@@ -1,5 +1,5 @@
context('Table MultiSelect', () => {
- beforeEach(() => {
+ before(() => {
cy.login();
});
diff --git a/frappe/__init__.py b/frappe/__init__.py
index cab9b0da76..5680ba86b5 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -34,7 +34,7 @@ if PY2:
reload(sys)
sys.setdefaultencoding("utf-8")
-__version__ = '13.0.0-dev'
+__version__ = '14.0.0-dev'
__title__ = "Frappe Framework"
@@ -975,7 +975,7 @@ def get_pymodule_path(modulename, *joins):
:param *joins: Join additional path elements using `os.path.join`."""
if not "public" in joins:
joins = [scrub(part) for part in joins]
- return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__), *joins)
+ return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ''), *joins)
def get_module_list(app_name):
"""Get list of modules for given all via `app/modules.txt`."""
diff --git a/frappe/api.py b/frappe/api.py
index 6a09b795b0..9039ae0e5f 100644
--- a/frappe/api.py
+++ b/frappe/api.py
@@ -1,12 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
import base64
import binascii
import json
-
-from six.moves.urllib.parse import urlencode, urlparse
+from urllib.parse import urlencode, urlparse
import frappe
import frappe.client
@@ -14,6 +12,7 @@ import frappe.handler
from frappe import _
from frappe.utils.response import build_response
+
def handle():
"""
Handler for `/api` methods
@@ -38,9 +37,6 @@ def handle():
`/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method
"""
-
- validate_auth()
-
parts = frappe.request.path[1:].split("/",3)
call = doctype = name = None
@@ -116,7 +112,7 @@ def handle():
frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields'])
frappe.local.form_dict.setdefault('limit_page_length', 20)
frappe.local.response.update({
- "data": frappe.call(
+ "data": frappe.call(
frappe.client.get_list,
doctype,
**frappe.local.form_dict
@@ -140,6 +136,7 @@ def handle():
return build_response("json")
+
def get_request_form_data():
if frappe.local.form_dict.data is None:
data = frappe.safe_decode(frappe.local.request.get_data())
@@ -148,25 +145,18 @@ def get_request_form_data():
return frappe.parse_json(data)
+
def validate_auth():
- if frappe.get_request_header("Authorization") is None:
- return
-
- VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token']
- VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title()
-
+ """
+ Authenticate and sets user for the request.
+ """
authorization_header = frappe.get_request_header("Authorization", str()).split(" ")
- authorization_type = authorization_header[0].lower()
- if len(authorization_header) == 1:
- frappe.throw(_('Invalid Authorization headers, add a token with a prefix from one of the following: {0}.').format(VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationHeader)
-
- if authorization_type == "bearer":
+ if len(authorization_header) == 2:
validate_oauth(authorization_header)
- elif authorization_type in VALID_AUTH_PREFIX_TYPES:
validate_auth_via_api_keys(authorization_header)
- else:
- frappe.throw(_('Invalid Authorization Type {0}, must be one of {1}.').format(authorization_type, VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationPrefix)
+
+ validate_auth_via_hooks()
def validate_oauth(authorization_header):
@@ -177,8 +167,8 @@ def validate_oauth(authorization_header):
authorization_header (list of str): The 'Authorization' header containing the prefix and token
"""
- from frappe.oauth import get_url_delimiter
from frappe.integrations.oauth2 import get_oauth_server
+ from frappe.oauth import get_url_delimiter
form_dict = frappe.local.form_dict
token = authorization_header[1]
@@ -187,19 +177,20 @@ def validate_oauth(authorization_header):
access_token = {"access_token": token}
uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token)
http_method = req.method
- body = req.get_data()
headers = req.headers
+ body = req.get_data()
+ if req.content_type and "multipart/form-data" in req.content_type:
+ body = None
try:
required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter())
+ valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
+ if valid:
+ frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
+ frappe.local.form_dict = form_dict
except AttributeError:
- frappe.throw(_("Invalid Bearer token, please provide a valid access token with prefix 'Bearer'."), frappe.InvalidAuthorizationToken)
+ pass
- valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes)
-
- if valid:
- frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user"))
- frappe.local.form_dict = form_dict
def validate_auth_via_api_keys(authorization_header):
@@ -222,8 +213,7 @@ def validate_auth_via_api_keys(authorization_header):
except binascii.Error:
frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken)
except (AttributeError, TypeError, ValueError):
- frappe.throw(_("Invalid token, please provide a valid token with prefix 'Basic' or 'Token'."), frappe.InvalidAuthorizationToken)
-
+ pass
def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None):
@@ -248,3 +238,8 @@ def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=Non
if frappe.local.login_manager.user in ('', 'Guest'):
frappe.set_user(user)
frappe.local.form_dict = form_dict
+
+
+def validate_auth_via_hooks():
+ for auth_hook in frappe.get_hooks('auth_hooks', []):
+ frappe.get_attr(auth_hook)()
diff --git a/frappe/app.py b/frappe/app.py
index 607479ad52..c9e993a853 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -56,6 +56,7 @@ def application(request):
frappe.recorder.record()
frappe.monitor.start()
frappe.rate_limiter.apply()
+ frappe.api.validate_auth()
if request.method == "OPTIONS":
response = Response()
diff --git a/frappe/auth.py b/frappe/auth.py
index ca97bbc17d..73cb8e8c15 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -120,6 +120,7 @@ class LoginManager:
self.make_session()
self.set_user_info()
+ @frappe.whitelist()
def login(self):
# clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr'))
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index bad879d2fa..4e0fe0cf44 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version',
'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts',
- 'sitemap_routes', 'db_tables') + doctype_map_keys
+ 'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys
user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang",
"defaults", "user_permissions", "home_page", "linked_with",
diff --git a/frappe/change_log/v13/v13_1_0.md b/frappe/change_log/v13/v13_1_0.md
new file mode 100644
index 0000000000..87c3bd0906
--- /dev/null
+++ b/frappe/change_log/v13/v13_1_0.md
@@ -0,0 +1,22 @@
+# Version 13.1.0 Release Notes
+
+### Features & Enhancements
+
+- Automated mail notifications will be shown in timeline ([#12693](https://github.com/frappe/frappe/pull/12693))
+- Introduced Client Script for List views ([#12590](https://github.com/frappe/frappe/pull/12590))
+- Introduced language switcher for guest users on website navbar ([#12813](https://github.com/frappe/frappe/pull/12813))
+- Option to give submit permission while sharing a document ([#12799](https://github.com/frappe/frappe/pull/12799))
+- Added option to set `autoname` in Customize Form ([#12413](https://github.com/frappe/frappe/pull/12413))
+- Virtual DocType ([#12121](https://github.com/frappe/frappe/pull/12121))
+
+### Fixes
+
+- Workspace fixes ([#12650](https://github.com/frappe/frappe/pull/12650)) ([#12655](https://github.com/frappe/frappe/pull/12655)) ([#12869](https://github.com/frappe/frappe/pull/12869))
+- Fixed an issue where select options were not getting updated in Grid ([#12839](https://github.com/frappe/frappe/pull/12839))
+- Webform Fixes ([#12630](https://github.com/frappe/frappe/pull/12630)) ([#12756](https://github.com/frappe/frappe/pull/12756)) ([#12819](https://github.com/frappe/frappe/pull/12819))
+- Fixed timespan filter for next and last timespans ([#12509](https://github.com/frappe/frappe/pull/12509))
+- System Notification fixes ([#12719](https://github.com/frappe/frappe/pull/12719))
+- Design Fixes ([#12669](https://github.com/frappe/frappe/pull/12669)) ([#12591](https://github.com/frappe/frappe/pull/12591)) ([#12557](https://github.com/frappe/frappe/pull/12557)) ([#12751](https://github.com/frappe/frappe/pull/12751)) ([#12864](https://github.com/frappe/frappe/pull/12864))
+- Fixed Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861))
+- Fixed grid validation ([#12744](https://github.com/frappe/frappe/pull/12744))
+- Fixed currency value formatting in dashboard chart ([#12613](https://github.com/frappe/frappe/pull/12613))
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index b9ae02e112..61ee62d352 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -62,11 +62,24 @@ def popen(command, *args, **kwargs):
if env:
env = dict(environ, **env)
+ def set_low_prio():
+ import psutil
+ if psutil.LINUX:
+ psutil.Process().nice(19)
+ psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE)
+ elif psutil.WINDOWS:
+ psutil.Process().nice(psutil.IDLE_PRIORITY_CLASS)
+ psutil.Process().ionice(psutil.IOPRIO_VERYLOW)
+ else:
+ psutil.Process().nice(19)
+ # ionice not supported
+
proc = subprocess.Popen(command,
stdout=None if output else subprocess.PIPE,
stderr=None if output else subprocess.PIPE,
shell=shell,
cwd=cwd,
+ preexec_fn=set_low_prio,
env=env
)
diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py
index bd9c9d2cb0..e9638800cd 100755
--- a/frappe/commands/scheduler.py
+++ b/frappe/commands/scheduler.py
@@ -18,22 +18,33 @@ def _is_scheduler_enabled():
return enable_scheduler
-@click.command('trigger-scheduler-event')
-@click.argument('event')
+
+@click.command("trigger-scheduler-event", help="Trigger a scheduler event")
+@click.argument("event")
@pass_context
def trigger_scheduler_event(context, event):
- "Trigger a scheduler event"
import frappe.utils.scheduler
+
+ exit_code = 0
+
for site in context.sites:
try:
frappe.init(site=site)
frappe.connect()
- frappe.utils.scheduler.trigger(site, event, now=True)
+ try:
+ frappe.get_doc("Scheduled Job Type", {"method": event}).execute()
+ except frappe.DoesNotExistError:
+ click.secho(f"Event {event} does not exist!", fg="red")
+ exit_code = 1
finally:
frappe.destroy()
+
if not context.sites:
raise SiteNotSpecifiedError
+ sys.exit(exit_code)
+
+
@click.command('enable-scheduler')
@pass_context
def enable_scheduler(context):
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 0fadf2a294..0102d3ac40 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -676,10 +676,8 @@ def start_ngrok(context):
frappe.init(site=site)
port = frappe.conf.http_port or frappe.conf.webserver_port
- public_url = ngrok.connect(port=port, options={
- 'host_header': site
- })
- print(f'Public URL: {public_url}')
+ tunnel = ngrok.connect(addr=str(port), host_header=site)
+ print(f'Public URL: {tunnel.public_url}')
print('Inspect logs at http://localhost:4040')
ngrok_process = ngrok.get_ngrok_process()
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 5ff66171fc..a203c8c6d9 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -11,7 +11,7 @@ import click
import frappe
from frappe.commands import get_site, pass_context
from frappe.exceptions import SiteNotSpecifiedError
-from frappe.utils import get_bench_path, update_progress_bar
+from frappe.utils import get_bench_path, update_progress_bar, cint
@click.command('build')
@@ -567,11 +567,14 @@ def run_ui_tests(context, app, headless=False):
node_bin = subprocess.getoutput("npm bin")
cypress_path = "{0}/cypress".format(node_bin)
- plugin_path = "{0}/cypress-file-upload".format(node_bin)
+ plugin_path = "{0}/../cypress-file-upload".format(node_bin)
# check if cypress in path...if not, install it.
- if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \
- or not subprocess.getoutput("npm view cypress version").startswith("6."):
+ if not (
+ os.path.exists(cypress_path)
+ and os.path.exists(plugin_path)
+ and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
+ ):
# install cypress
click.secho("Installing Cypress...", fg="yellow")
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile")
diff --git a/frappe/core/doctype/communication/communication_list.js b/frappe/core/doctype/communication/communication_list.js
index 454897b865..315b74a39c 100644
--- a/frappe/core/doctype/communication/communication_list.js
+++ b/frappe/core/doctype/communication/communication_list.js
@@ -20,6 +20,6 @@ frappe.listview_settings['Communication'] = {
},
primary_action: function() {
- new frappe.views.CommunicationComposer({ doc: {} });
+ new frappe.views.CommunicationComposer();
}
};
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 276ce7bee7..fe5038b841 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -56,6 +56,8 @@
"show_preview_popup",
"show_name_in_global_search",
"email_settings_sb",
+ "default_email_template",
+ "column_break_51",
"email_append_to",
"sender_field",
"subject_field",
@@ -535,6 +537,16 @@
"fieldname": "is_virtual",
"fieldtype": "Check",
"label": "Is Virtual"
+ },
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_51",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-bolt",
@@ -616,7 +628,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-02-17 20:18:06.212232",
+ "modified": "2021-04-16 12:26:41.031135",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index af2c4e5dc2..8a0f9a99f5 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -325,9 +325,8 @@ def get_group_by_field(args, doctype):
if args['aggregate_function'] == 'count':
group_by_field = 'count(*) as _aggregate_column'
else:
- group_by_field = '{0}(`tab{1}`.{2}) as _aggregate_column'.format(
+ group_by_field = '{0}({1}) as _aggregate_column'.format(
args.aggregate_function,
- doctype,
args.aggregate_on
)
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index 92493a593a..59089d12ad 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -2,14 +2,15 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
+import json
+from datetime import datetime
from typing import Dict, List
-import frappe, json
-from frappe.model.document import Document
-from frappe.utils import now_datetime, get_datetime
-from datetime import datetime
from croniter import croniter
+
+import frappe
+from frappe.model.document import Document
+from frappe.utils import get_datetime, now_datetime
from frappe.utils.background_jobs import enqueue, get_jobs
diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js
index 95a63780f8..dda39115bf 100644
--- a/frappe/core/doctype/server_script/server_script.js
+++ b/frappe/core/doctype/server_script/server_script.js
@@ -9,6 +9,12 @@ frappe.ui.form.on('Server Script', {
if (frm.doc.script_type != 'Scheduler Event') {
frm.dashboard.hide();
}
+
+ frm.call('get_autocompletion_items')
+ .then(r => r.message)
+ .then(items => {
+ frm.set_df_property('script', 'autocompletions', items);
+ });
},
setup_help(frm) {
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 8838d9e954..f80a067cf1 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -5,11 +5,12 @@
from __future__ import unicode_literals
import ast
+from types import FunctionType, MethodType, ModuleType
from typing import Dict, List
import frappe
from frappe.model.document import Document
-from frappe.utils.safe_exec import safe_exec
+from frappe.utils.safe_exec import get_safe_globals, safe_exec, NamespaceDict
from frappe import _
@@ -122,6 +123,51 @@ class ServerScript(Document):
if locals["conditions"]:
return locals["conditions"]
+ @frappe.whitelist()
+ def get_autocompletion_items(self):
+ """Generates a list of a autocompletion strings from the context dict
+ that is used while executing a Server Script.
+
+ Returns:
+ list: Returns list of autocompletion items.
+ For e.g., ["frappe.utils.cint", "frappe.db.get_all", ...]
+ """
+ def get_keys(obj):
+ out = []
+ for key in obj:
+ if key.startswith('_'):
+ continue
+ value = obj[key]
+ if isinstance(value, (NamespaceDict, dict)) and value:
+ if key == 'form_dict':
+ out.append(['form_dict', 7])
+ continue
+ for subkey, score in get_keys(value):
+ fullkey = f'{key}.{subkey}'
+ out.append([fullkey, score])
+ else:
+ if isinstance(value, type) and issubclass(value, Exception):
+ score = 0
+ elif isinstance(value, ModuleType):
+ score = 10
+ elif isinstance(value, (FunctionType, MethodType)):
+ score = 9
+ elif isinstance(value, type):
+ score = 8
+ elif isinstance(value, dict):
+ score = 7
+ else:
+ score = 6
+ out.append([key, score])
+ return out
+
+ items = frappe.cache().get_value('server_script_autocompletion_items')
+ if not items:
+ items = get_keys(get_safe_globals())
+ items = [{'value': d[0], 'score': d[1]} for d in items]
+ frappe.cache().set_value('server_script_autocompletion_items', items)
+ return items
+
@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index 77f62b3ec3..442b8dbb31 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -33,6 +33,8 @@
"show_preview_popup",
"image_view",
"email_settings_section",
+ "default_email_template",
+ "column_break_26",
"email_append_to",
"sender_field",
"subject_field",
@@ -264,6 +266,16 @@
"label": "Actions",
"options": "DocType Action"
},
+ {
+ "fieldname": "default_email_template",
+ "fieldtype": "Link",
+ "label": "Default Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
{
"collapsible": 1,
"fieldname": "naming_section",
@@ -283,7 +295,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-02-16 15:22:11.108256",
+ "modified": "2021-03-22 12:27:15.462727",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
@@ -304,4 +316,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 9f6996a660..be0dded99c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -491,6 +491,7 @@ doctype_properties = {
'allow_auto_repeat': 'Check',
'allow_import': 'Check',
'show_preview_popup': 'Check',
+ 'default_email_template': 'Data',
'email_append_to': 'Check',
'subject_field': 'Data',
'sender_field': 'Data',
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index f9997d1526..7d1d92408c 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -1,17 +1,13 @@
-from __future__ import unicode_literals
-
-import frappe
import warnings
import pymysql
-from pymysql.times import TimeDelta
-from pymysql.constants import ER, FIELD_TYPE
-from pymysql.converters import conversions
+from pymysql.constants import ER, FIELD_TYPE
+from pymysql.converters import conversions, escape_string
-from frappe.utils import get_datetime, cstr, UnicodeWithAttrs
+import frappe
from frappe.database.database import Database
-from six import PY2, binary_type, text_type, string_types
from frappe.database.mariadb.schema import MariaDBTable
+from frappe.utils import UnicodeWithAttrs, cstr, get_datetime
class MariaDBDatabase(Database):
@@ -72,22 +68,20 @@ class MariaDBDatabase(Database):
conversions.update({
FIELD_TYPE.NEWDECIMAL: float,
FIELD_TYPE.DATETIME: get_datetime,
- UnicodeWithAttrs: conversions[text_type]
+ UnicodeWithAttrs: conversions[str]
})
- if PY2:
- conversions.update({
- TimeDelta: conversions[binary_type]
- })
-
- if usessl:
- conn = pymysql.connect(self.host, self.user or '', self.password or '',
- port=self.port, charset='utf8mb4', use_unicode = True, ssl=ssl_params,
- conv = conversions, local_infile = frappe.conf.local_infile)
- else:
- conn = pymysql.connect(self.host, self.user or '', self.password or '',
- port=self.port, charset='utf8mb4', use_unicode = True, conv = conversions,
- local_infile = frappe.conf.local_infile)
+ conn = pymysql.connect(
+ user=self.user or '',
+ password=self.password or '',
+ host=self.host,
+ port=self.port,
+ charset='utf8mb4',
+ use_unicode=True,
+ ssl=ssl_params if usessl else None,
+ conv=conversions,
+ local_infile=frappe.conf.local_infile
+ )
# MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1
# # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF)
@@ -111,7 +105,7 @@ class MariaDBDatabase(Database):
def escape(s, percent=True):
"""Excape quotes and percent in given string."""
# pymysql expects unicode argument to escape_string with Python 3
- s = frappe.as_unicode(pymysql.escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
+ s = frappe.as_unicode(escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`")
# NOTE separating % escape, because % escape should only be done when using LIKE operator
# or when you use python format string to generate query that already has a %s
@@ -260,7 +254,7 @@ class MariaDBDatabase(Database):
ADD INDEX `%s`(%s)""" % (table_name, index_name, ", ".join(fields)))
def add_unique(self, doctype, fields, constraint_name=None):
- if isinstance(fields, string_types):
+ if isinstance(fields, str):
fields = [fields]
if not constraint_name:
constraint_name = "unique_" + "_".join(fields)
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js
index 88dc145be2..cc2fd95204 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.js
+++ b/frappe/desk/doctype/notification_settings/notification_settings.js
@@ -19,7 +19,7 @@ frappe.ui.form.on('Notification Settings', {
refresh: (frm) => {
if (frappe.user.has_role('System Manager')) {
- frm.add_custom_button('Go to Notification Settings List', () => {
+ frm.add_custom_button(__('Go to Notification Settings List'), () => {
frappe.set_route('List', 'Notification Settings');
});
}
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 3d04c171a7..86f8ec0aa7 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -126,13 +126,14 @@ def setup_group_by(data):
if data.group_by:
if data.aggregate_function.lower() not in ('count', 'sum', 'avg'):
frappe.throw(_('Invalid aggregate function'))
- if '`' in data.aggregate_on:
- raise_invalid_field(data.aggregate_on)
- data.fields.append('{aggregate_function}(`tab{doctype}`.`{aggregate_on}`) AS _aggregate_column'.format(**data))
- if data.aggregate_on:
- data.fields.append(data.aggregate_on)
- data.pop('aggregate_on')
+ if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field):
+ data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data))
+ else:
+ raise_invalid_field(data.aggregate_on_field)
+
+ data.pop('aggregate_on_doctype')
+ data.pop('aggregate_on_field')
data.pop('aggregate_function')
def raise_invalid_field(fieldname):
diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py
index 12fdb0dadc..6f0d7d3d5f 100644
--- a/frappe/desk/treeview.py
+++ b/frappe/desk/treeview.py
@@ -36,7 +36,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters):
return out
@frappe.whitelist()
-def get_children(doctype, parent=''):
+def get_children(doctype, parent='', **filters):
return _get_children(doctype, parent)
def _get_children(doctype, parent='', ignore_permissions=False):
@@ -66,7 +66,7 @@ def add_node():
doc.save()
def make_tree_args(**kwarg):
- del kwarg['cmd']
+ kwarg.pop('cmd', None)
doctype = kwarg['doctype']
parent_field = 'parent_' + doctype.lower().replace(' ', '_')
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index c792347c09..6412338e96 100755
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -24,6 +24,7 @@ class Newsletter(WebsiteGenerator):
if self.send_from:
validate_email_address(self.send_from, True)
+ @frappe.whitelist()
def test_send(self, doctype="Lead"):
self.recipients = frappe.utils.split_emails(self.test_email_id)
self.queue_all(test_email=True)
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index cf6c13ee76..949da4a343 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -1,18 +1,27 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import unicode_literals
-import six
-from six import iteritems, text_type
-from six.moves import range
-import time, _socket, poplib, imaplib, email, email.utils, datetime, chardet, re
-from email_reply_parser import EmailReplyParser
+import datetime
+import email
+import email.utils
+import imaplib
+import poplib
+import re
+import time
from email.header import decode_header
+
+import _socket
+import chardet
+import six
+from email_reply_parser import EmailReplyParser
+
import frappe
from frappe import _, safe_decode, safe_encode
-from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now,
- cint, cstr, strip, markdown, parse_addr)
-from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError
+from frappe.core.doctype.file.file import (MaxFileSizeReachedError,
+ get_random_filename)
+from frappe.utils import (cint, convert_utc_to_user_timezone, cstr,
+ extract_email_id, markdown, now, parse_addr, strip)
+
class EmailSizeExceededError(frappe.ValidationError): pass
class EmailTimeoutError(frappe.ValidationError): pass
@@ -337,7 +346,7 @@ class EmailServer:
return
self.imap.select("Inbox")
- for uid, operation in iteritems(uid_list):
+ for uid, operation in uid_list.items():
if not uid: continue
op = "+FLAGS" if operation == "Read" else "-FLAGS"
@@ -473,7 +482,7 @@ class Email:
self.html_content += markdown(text_content)
def get_charset(self, part):
- """Detect chartset."""
+ """Detect charset."""
charset = part.get_content_charset()
if not charset:
charset = chardet.detect(safe_encode(cstr(part)))['encoding']
@@ -484,7 +493,7 @@ class Email:
charset = self.get_charset(part)
try:
- return text_type(part.get_payload(decode=True), str(charset), "ignore")
+ return str(part.get_payload(decode=True), str(charset), "ignore")
except LookupError:
return part.get_payload()
diff --git a/frappe/handler.py b/frappe/handler.py
index c8c24a2d57..118566235c 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -9,7 +9,6 @@ import frappe
import frappe.utils
import frappe.sessions
from frappe.utils import cint
-from frappe.api import validate_auth
from frappe import _, is_whitelisted
from frappe.utils.response import build_response
from frappe.utils.csvutils import build_csv_response
@@ -24,7 +23,7 @@ ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/
def handle():
"""handle request"""
- validate_auth()
+
cmd = frappe.local.form_dict.cmd
data = None
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 74c538c5df..1c78d47755 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -130,6 +130,16 @@ has_website_permission = {
"Address": "frappe.contacts.doctype.address.address.has_website_permission"
}
+jinja = {
+ "methods": "frappe.utils.jinja_globals",
+ "filters": [
+ "frappe.utils.data.global_date_format",
+ "frappe.utils.markdown",
+ "frappe.website.utils.get_shade",
+ "frappe.website.utils.abs_url",
+ ]
+}
+
standard_queries = {
"User": "frappe.core.doctype.user.user.user_query"
}
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 09da1ecc42..53f0935c80 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -2,22 +2,23 @@
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import dropbox
import json
-import frappe
import os
-from frappe import _
-from frappe.model.document import Document
-from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size, get_chunk_site
-from frappe.integrations.utils import make_post_request
-from frappe.utils import (cint, get_request_site_address,
- get_files_path, get_backups_path, get_url, encode)
-from frappe.utils.backups import new_backup
-from frappe.utils.background_jobs import enqueue
-from six.moves.urllib.parse import urlparse, parse_qs
+from urllib.parse import parse_qs, urlparse
+
+import dropbox
from rq.timeouts import JobTimeoutException
-from six import text_type
+
+import frappe
+from frappe import _
+from frappe.integrations.offsite_backup_utils import (get_chunk_site,
+ get_latest_backup_file, send_email, validate_file_size)
+from frappe.integrations.utils import make_post_request
+from frappe.model.document import Document
+from frappe.utils import (cint, encode, get_backups_path, get_files_path,
+ get_request_site_address, get_url)
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.backups import new_backup
ignore_list = [".DS_Store"]
@@ -91,7 +92,10 @@ def backup_to_dropbox(upload_db_backup=True):
dropbox_settings['access_token'] = access_token['oauth2_token']
set_dropbox_access_token(access_token['oauth2_token'])
- dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None)
+ dropbox_client = dropbox.Dropbox(
+ oauth2_access_token=dropbox_settings['access_token'],
+ timeout=None
+ )
if upload_db_backup:
if frappe.flags.create_new_backup:
@@ -127,7 +131,7 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not
else:
response = frappe._dict({"entries": []})
- path = text_type(path)
+ path = str(path)
for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private,
"uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']):
@@ -286,11 +290,11 @@ def get_redirect_url():
def get_dropbox_authorize_url():
app_details = get_dropbox_settings(redirect_uri=True)
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
- app_details["app_key"],
- app_details["app_secret"],
- app_details["redirect_uri"],
- {},
- "dropbox-auth-csrf-token"
+ consumer_key=app_details["app_key"],
+ redirect_uri=app_details["redirect_uri"],
+ session={},
+ csrf_token_session_key="dropbox-auth-csrf-token",
+ consumer_secret=app_details["app_secret"]
)
auth_url = dropbox_oauth_flow.start()
@@ -307,13 +311,13 @@ def dropbox_auth_finish(return_access_token=False):
close = '
' + _('Please close this window') + '
'
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
- app_details["app_key"],
- app_details["app_secret"],
- app_details["redirect_uri"],
- {
+ consumer_key=app_details["app_key"],
+ redirect_uri=app_details["redirect_uri"],
+ session={
'dropbox-auth-csrf-token': callback.state
},
- "dropbox-auth-csrf-token"
+ csrf_token_session_key="dropbox-auth-csrf-token",
+ consumer_secret=app_details["app_secret"]
)
if callback.state or callback.code:
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py
index fbedd75029..f93be35aa7 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.py
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.py
@@ -2,22 +2,23 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import frappe
-import requests
-import googleapiclient.discovery
-import google.oauth2.credentials
-from frappe import _
-from frappe.model.document import Document
-from frappe.utils import get_request_site_address
-from googleapiclient.errors import HttpError
-from frappe.utils.password import set_encrypted_password
-from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone
-from dateutil import parser
from datetime import datetime, timedelta
-from six.moves.urllib.parse import quote
+from urllib.parse import quote
+
+import google.oauth2.credentials
+import requests
+from dateutil import parser
+from googleapiclient.discovery import build
+from googleapiclient.errors import HttpError
+
+import frappe
+from frappe import _
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
+from frappe.model.document import Document
+from frappe.utils import (add_days, add_to_date, get_datetime,
+ get_request_site_address, get_time_zone, get_weekdays, now_datetime)
+from frappe.utils.password import set_encrypted_password
SCOPES = "https://www.googleapis.com/auth/calendar"
@@ -171,7 +172,12 @@ def get_google_calendar_object(g_calendar):
}
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
- google_calendar = googleapiclient.discovery.build("calendar", "v3", credentials=credentials)
+ google_calendar = build(
+ serviceName="calendar",
+ version="v3",
+ credentials=credentials,
+ static_discovery=False
+ )
check_google_calendar(account, google_calendar)
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 4c8c3b67f6..1705f98e91 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -2,17 +2,17 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import frappe
-import requests
-import googleapiclient.discovery
-import google.oauth2.credentials
-from frappe.model.document import Document
-from frappe import _
+import google.oauth2.credentials
+import requests
+from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
-from frappe.utils import get_request_site_address
+
+import frappe
+from frappe import _
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
+from frappe.model.document import Document
+from frappe.utils import get_request_site_address
SCOPES = "https://www.googleapis.com/auth/contacts"
@@ -118,7 +118,12 @@ def get_google_contacts_object(g_contact):
}
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
- google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials)
+ google_contacts = build(
+ serviceName="people",
+ version="v1",
+ credentials=credentials,
+ static_discovery=False
+ )
return google_contacts, account
diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py
index 859c769018..93b6fa3f8d 100644
--- a/frappe/integrations/doctype/google_drive/google_drive.py
+++ b/frappe/integrations/doctype/google_drive/google_drive.py
@@ -2,27 +2,29 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import frappe
-import requests
-import googleapiclient.discovery
-import google.oauth2.credentials
import os
+from urllib.parse import quote
-from frappe import _
-from googleapiclient.errors import HttpError
-from frappe.model.document import Document
-from frappe.utils import get_request_site_address
-from frappe.utils.background_jobs import enqueue
-from six.moves.urllib.parse import quote
+import google.oauth2.credentials
+import requests
from apiclient.http import MediaFileUpload
-from frappe.utils import get_backups_path, get_bench_path
-from frappe.utils.backups import new_backup
+from googleapiclient.discovery import build
+from googleapiclient.errors import HttpError
+
+import frappe
+from frappe import _
from frappe.integrations.doctype.google_settings.google_settings import get_auth_url
-from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size
+from frappe.integrations.offsite_backup_utils import (get_latest_backup_file,
+ send_email, validate_file_size)
+from frappe.model.document import Document
+from frappe.utils import (get_backups_path, get_bench_path,
+ get_request_site_address)
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.backups import new_backup
SCOPES = "https://www.googleapis.com/auth/drive"
+
class GoogleDrive(Document):
def validate(self):
@@ -126,7 +128,12 @@ def get_google_drive_object():
}
credentials = google.oauth2.credentials.Credentials(**credentials_dict)
- google_drive = googleapiclient.discovery.build("drive", "v3", credentials=credentials)
+ google_drive = build(
+ serviceName="drive",
+ version="v3",
+ credentials=credentials,
+ static_discovery=False
+ )
return google_drive, account
diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py
index 73c9f38fce..7aa069647d 100644
--- a/frappe/integrations/doctype/token_cache/test_token_cache.py
+++ b/frappe/integrations/doctype/token_cache/test_token_cache.py
@@ -13,7 +13,7 @@ class TestTokenCache(unittest.TestCase):
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()
+ self.token_cache.save(ignore_permissions=True)
def test_get_auth_header(self):
self.token_cache.get_auth_header()
diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py
index c444964a16..3ebaaffcff 100644
--- a/frappe/integrations/oauth2.py
+++ b/frappe/integrations/oauth2.py
@@ -133,7 +133,7 @@ def get_token(*args, **kwargs):
}
id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header)
- out.update({"id_token": str(id_token_encoded)})
+ out.update({"id_token": frappe.safe_decode(id_token_encoded)})
frappe.local.response = out
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 516ddb6094..60c3112f4a 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -335,3 +335,4 @@ frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings
frappe.patches.v13_0.remove_twilio_settings
frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
+frappe.patches.v13_0.jinja_hook
diff --git a/frappe/patches/v13_0/jinja_hook.py b/frappe/patches/v13_0/jinja_hook.py
new file mode 100644
index 0000000000..84ed6e6cff
--- /dev/null
+++ b/frappe/patches/v13_0/jinja_hook.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from click import secho
+
+def execute():
+ if frappe.get_hooks('jenv'):
+ print()
+ secho('WARNING: The hook "jenv" is deprecated. Follow the migration guide to use the new "jinja" hook.', fg='yellow')
+ secho('https://github.com/frappe/frappe/wiki/Migrating-to-Version-13', fg='yellow')
+ print()
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 9981398b84..8c2c5c4338 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -159,9 +159,13 @@ frappe.ui.form.Control = Class.extend({
},
validate_and_set_in_model: function(value, e) {
var me = this;
- if(this.inside_change_event) {
+ let force_value_set = (this.doc && this.doc.__run_link_triggers);
+ let is_value_same = (this.get_model_value() === value);
+
+ if (this.inside_change_event || (!force_value_set && is_value_same)) {
return Promise.resolve();
}
+
this.inside_change_event = true;
var set = function(value) {
me.inside_change_event = false;
diff --git a/frappe/public/js/frappe/form/controls/button.js b/frappe/public/js/frappe/form/controls/button.js
index b44c9d9dcd..d09e9c3a95 100644
--- a/frappe/public/js/frappe/form/controls/button.js
+++ b/frappe/public/js/frappe/form/controls/button.js
@@ -6,7 +6,10 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
make_input: function() {
var me = this;
const btn_type = this.df.primary ? 'btn-primary': 'btn-default';
- this.$input = $(`