${abbr}
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 2e7ff4a812..5b4330ab6e 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -386,21 +386,18 @@ frappe.views.CommunicationComposer = Class.extend({
},
setup_print_language: function() {
- var me = this;
var doc = this.doc || cur_frm.doc;
var fields = this.dialog.fields_dict;
//Load default print language from doctype
this.lang_code = doc.language
- if (this.get_print_format().default_print_language) {
- var default_print_language_code = this.get_print_format().default_print_language;
- me.lang_code = default_print_language_code;
- } else {
- var default_print_language_code = null;
+ if (!this.lang_code && this.get_print_format().default_print_language) {
+ this.lang_code = this.get_print_format().default_print_language;
}
//On selection of language retrieve language code
+ var me = this;
$(fields.language_sel.input).change(function(){
me.lang_code = this.value
})
@@ -410,10 +407,8 @@ frappe.views.CommunicationComposer = Class.extend({
.empty()
.add_options(frappe.get_languages());
- if (default_print_language_code) {
- $(fields.language_sel.input).val(default_print_language_code);
- } else {
- $(fields.language_sel.input).val(doc.language);
+ if (this.lang_code) {
+ $(fields.language_sel.input).val(this.lang_code);
}
},
@@ -440,6 +435,7 @@ frappe.views.CommunicationComposer = Class.extend({
}
},
+
setup_attach: function() {
var fields = this.dialog.fields_dict;
var attach = $(fields.select_attachments.wrapper);
diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js
index 93c490884e..b69053eb26 100644
--- a/frappe/public/js/frappe/views/treeview.js
+++ b/frappe/public/js/frappe/views/treeview.js
@@ -172,6 +172,23 @@ frappe.views.TreeView = Class.extend({
this.post_render();
},
+ rebuild_tree: function() {
+ let me = this;
+
+ frappe.call({
+ "method": "frappe.utils.nestedset.rebuild_tree",
+ "args": {
+ 'doctype': me.doctype,
+ 'parent_field': "parent_"+me.doctype.toLowerCase().replace(/ /g, '_'),
+ },
+ "callback": function(r) {
+ if (!r.exc) {
+ me.make_tree();
+ }
+ }
+ });
+ },
+
post_render: function() {
var me = this;
me.opts.post_render && me.opts.post_render(me);
@@ -368,7 +385,7 @@ frappe.views.TreeView = Class.extend({
}, "add");
}
},
- set_menu_item: function(){
+ set_menu_item: function() {
var me = this;
this.menu_items = [
@@ -393,6 +410,17 @@ frappe.views.TreeView = Class.extend({
},
];
+ if (frappe.user.has_role('System Manager')) {
+ this.menu_items.push(
+ {
+ label: __('Rebuild Tree'),
+ action: function() {
+ me.rebuild_tree();
+ }
+ }
+ );
+ }
+
if (me.opts.menu_items) {
me.menu_items.push.apply(me.menu_items, me.opts.menu_items)
}
diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss
index cdb2c23558..91cc31c50d 100644
--- a/frappe/public/scss/common/global.scss
+++ b/frappe/public/scss/common/global.scss
@@ -162,3 +162,34 @@ html.firefox, html.safari {
-webkit-transform: translate(-50%, -50%);
}
+.hide {
+ display: none !important;
+}
+
+.btn-link {
+ box-shadow: none !important;
+ outline: none;
+ .icon, &:hover {
+ text-decoration: none !important;
+ }
+}
+
+.hidden {
+ @extend .d-none;
+}
+
+.margin {
+ margin: var(--margin-sm);
+}
+.margin-top {
+ margin-top: var(--margin-sm);
+}
+.margin-bottom {
+ margin-bottom: var(--margin-sm);
+}
+.margin-left {
+ margin-left: var(--margin-sm);
+}
+.margin-right {
+ margin-right: var(--margin-sm);
+}
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index 7c1ddde68e..b09d9146ae 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -105,10 +105,6 @@ pre {
color: var(--text-light)
}
-.hide {
- display: none !important;
-}
-
.col-xs-1 { @extend .col-1; }
.col-xs-2 { @extend .col-2; }
.col-xs-3 { @extend .col-3; }
@@ -150,23 +146,6 @@ footer {
float: right;
}
-// .border-${position} {
-// .border-{$position} {
-// border-{$position}: 1px solid var(--border-color);
-// }
-// }
-// .border-#{$position} {
-// .border-#{$position} {
-// border-#{$position}: 1px solid var(--border-color);
-// }
-// }
-
-// @include border-(top);
-// @include border-(bottom);
-// @include border-(left);
-// @include border-(right);
-
-
img {
max-width: 100%;
height: auto;
@@ -191,9 +170,6 @@ img {
}
}
-.hidden {
- @extend .d-none;
-}
.hide-control {
@extend .d-none;
@@ -224,10 +200,6 @@ p {
font-size: var(--text-sm);
}
-.fill-width {
- flex: 1
-}
-
h1 {
font-size: $font-size-3xl;
font-weight: 800;
@@ -280,6 +252,7 @@ select.input-xs {
/* popover */
.popover {
background-color: var(--popover-bg);
+ border: 0;
}
.bold {
@@ -386,31 +359,6 @@ kbd {
cursor: default;
}
-.btn-link {
- box-shadow: none !important;
- outline: none;
- .icon, &:hover {
- text-decoration: none !important;
- }
-}
-
-
-.margin {
- margin: var(--margin-sm);
-}
-.margin-top {
- margin-top: var(--margin-sm);
-}
-.margin-bottom {
- margin-bottom: var(--margin-sm);
-}
-.margin-left {
- margin-left: var(--margin-sm);
-}
-.margin-right {
- margin-right: var(--margin-sm);
-}
-
.standard-sidebar {
font-size: var(--text-base);
diff --git a/frappe/public/scss/desk/link_preview.scss b/frappe/public/scss/desk/link_preview.scss
index af14c4d8ef..b2dd3ec425 100644
--- a/frappe/public/scss/desk/link_preview.scss
+++ b/frappe/public/scss/desk/link_preview.scss
@@ -1,48 +1,61 @@
.link-preview-popover {
- border-radius: 0;
max-width: 100%;
+
.popover-content {
- padding: 0;
- .preview-popover-header {
- padding: var(--padding-md);
+ padding: var(--padding-md) 25px;
- .preview-header {
- display: inline-block;
- vertical-align: top;
- }
+ .preview-header {
+ @include flex(flex, null, center, column);
+ }
- .preview-image {
- width: 70px;
- height: 70px;
- object-fit: cover;
- margin-right: var(--margin-sm);
- border-radius: var(--border-radius);
- }
+ .preview-image {
+ margin-bottom: var(--margin-sm);
- .preview-name {
- display: block;
- }
+ .avatar {
+ width: 52px;
+ height: 52px;
- .preview-title {
- display: block;
+ .standard-image {
+ font-size: var(--text-lg);
+ }
}
}
- hr {
- margin: 0;
+ .preview-name {
+ font-size: var(--text-base);
+ font-weight: 500;
}
- .preview-table {
- padding: var(--padding-md);
- padding-bottom: var(--padding-xs);
- max-width: 330px;
- min-width: 200px;
- overflow-wrap: break-word;
+ .preview-title:not(:empty) {
+ margin-top: var(--margin-xs);
+ font-size: var(--text-md);
+ }
- .preview-field {
- padding-bottom: var(--padding-sm);
- .preview-label {
- padding-bottom: 4px;
+ .popover-body {
+ padding: 0;
+ .preview-table {
+ padding-bottom: var(--padding-xs);
+ max-width: 330px;
+ min-width: 200px;
+ overflow-wrap: break-word;
+
+ .preview-field {
+
+ .preview-label {
+ padding-bottom: 4px;
+ }
+
+ .preview-value {
+ font-weight: 500;
+ }
+
+ .ql-snow .ql-editor {
+ min-height: 0;
+ }
+
+ &:not(:last-child) {
+ margin-bottom: var(--margin-md);
+ }
}
}
}
diff --git a/frappe/public/scss/desk/variables.scss b/frappe/public/scss/desk/variables.scss
index 26af534c6f..4f43f22b9d 100644
--- a/frappe/public/scss/desk/variables.scss
+++ b/frappe/public/scss/desk/variables.scss
@@ -98,7 +98,7 @@ $mark-padding: 0;
$enable-shadows: true;
$popover-border-radius: var(--border-radius);
$popover-bg: var(--popover-bg);
-$popover-box-shadow: var(--shadow-md);
+$popover-box-shadow: 0px 2px 6px rgba(17, 43, 66, 0.08), 0px 1px 4px rgba(17, 43, 66, 0.1);
$popover-body-padding-x: var(--padding-md);
$popover-body-padding-y: var(--padding-md);
$popover-border-color: var(--dark-border-color);
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 0ddc8e8d0f..88f3b1db73 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -2,14 +2,14 @@
@import 'variables';
@import 'css_variables';
@import '~bootstrap/scss/bootstrap';
-@import "../common/mixins.scss";
-@import "../common/global.scss";
-@import "../common/icons.scss";
+@import "../common/mixins";
+@import "../common/global";
+@import "../common/icons";
@import 'base';
@import "../common/flex";
@import "../common/buttons";
@import "../common/modal";
-@import "../common/indicator.scss";
+@import "../common/indicator";
@import "../common/controls";
@import 'multilevel_dropdown';
@import 'website_image';
@@ -241,4 +241,4 @@ h5.modal-title {
}
.about-footer {
padding-top: 1rem;
-}
\ No newline at end of file
+}
diff --git a/frappe/public/scss/website/variables.scss b/frappe/public/scss/website/variables.scss
index 8fb4d0810c..fa68b57ad6 100644
--- a/frappe/public/scss/website/variables.scss
+++ b/frappe/public/scss/website/variables.scss
@@ -47,6 +47,10 @@ $font-sizes: (
}
}
+$border-radius: var(--border-radius);
+$border-radius-sm: var(--border-radius-sm);
+$border-radius-lg: var(--border-radius-lg);
+
$font-size-xs: 0.75rem !default;
$font-size-sm: 0.875rem !default;
$font-size-base: 1rem !default;
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 1ca1d4ee6f..3babf1db12 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -15,7 +15,6 @@ from frappe.utils import cint, cstr
import frappe.model.meta
import frappe.defaults
import frappe.translate
-from frappe.utils.change_log import get_change_log
import redis
from six.moves.urllib.parse import unquote
from six import text_type
@@ -117,6 +116,7 @@ def clear_expired_sessions():
def get():
"""get session boot info"""
from frappe.boot import get_bootinfo, get_unseen_notes
+ from frappe.utils.change_log import get_change_log
bootinfo = None
if not getattr(frappe.conf,'disable_session_cache', None):
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 9d2ca8103a..0186346840 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -140,7 +140,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
style="width: 12px; height: 12px; margin-top: 5px;">
- {% elif df.fieldtype=="Image" %}
+ {% elif df.fieldtype in ("Image", "Attach Image") and frappe.utils.is_image(doc[doc.meta.get_field(df.fieldname).options]) %}
.options] }})
@@ -151,7 +151,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% elif df.fieldtype=="Signature" %}

- {% elif df.fieldtype == "Attach" and doc[df.fieldname] and frappe.utils.is_image(doc[df.fieldname]) %}
+ {% elif df.fieldtype in ("Attach", "Attach Image") and frappe.utils.is_image(doc[df.fieldname]) %}

{% elif df.fieldtype=="HTML" %}
diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py
new file mode 100644
index 0000000000..c0f61b4863
--- /dev/null
+++ b/frappe/tests/test_auth.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import time
+import unittest
+from frappe.auth import LoginAttemptTracker
+
+class TestLoginAttemptTracker(unittest.TestCase):
+ def test_account_lock(self):
+ """Make sure that account locks after `n consecutive failures
+ """
+ tracker = LoginAttemptTracker(user_name='tester', max_consecutive_login_attempts=3, lock_interval=60)
+ # Clear the cache by setting attempt as success
+ tracker.add_success_attempt()
+
+ tracker.add_failure_attempt()
+ self.assertTrue(tracker.is_user_allowed())
+
+ tracker.add_failure_attempt()
+ self.assertTrue(tracker.is_user_allowed())
+
+ tracker.add_failure_attempt()
+ self.assertTrue(tracker.is_user_allowed())
+
+ tracker.add_failure_attempt()
+ self.assertFalse(tracker.is_user_allowed())
+
+ def test_account_unlock(self):
+ """Make sure that locked account gets unlocked after lock_interval of time.
+ """
+ lock_interval = 10 # In sec
+ tracker = LoginAttemptTracker(user_name='tester', max_consecutive_login_attempts=1, lock_interval=lock_interval)
+ # Clear the cache by setting attempt as success
+ tracker.add_success_attempt()
+
+ tracker.add_failure_attempt()
+ self.assertTrue(tracker.is_user_allowed())
+
+ tracker.add_failure_attempt()
+ self.assertFalse(tracker.is_user_allowed())
+
+ # Sleep for lock_interval of time, so that next request con unlock the user access.
+ time.sleep(lock_interval)
+
+ tracker.add_failure_attempt()
+ self.assertTrue(tracker.is_user_allowed())
diff --git a/frappe/translate.py b/frappe/translate.py
index 9601dfe2cc..b48884f4e8 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -17,7 +17,6 @@ from frappe.utils import cstr
import frappe, os, re, io, codecs, json
from frappe.model.utils import render_include, InvalidIncludePath
from frappe.utils import strip, strip_html_tags, is_html
-from jinja2 import TemplateError
import itertools, operator
def guess_language(lang_list=None):
@@ -526,6 +525,8 @@ def extract_messages_from_code(code):
:param code: code from which translatable files are to be extracted
:param is_py: include messages in triple quotes e.g. `_('''message''')`
"""
+ from jinja2 import TemplateError
+
try:
code = frappe.as_unicode(render_include(code))
except (TemplateError, ImportError, InvalidIncludePath, IOError):
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 414d3b930f..1ff9da0ca9 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -11,12 +11,12 @@ import os
import re
import sys
import traceback
+
from email.header import decode_header, make_header
from email.utils import formataddr, parseaddr
from gzip import GzipFile
from typing import Generator, Iterable
-import requests
from six import string_types, text_type
from six.moves.urllib.parse import quote
from werkzeug.test import Client
@@ -24,7 +24,6 @@ from werkzeug.test import Client
import frappe
# utility functions like cint, int, flt, etc.
from frappe.utils.data import *
-from frappe.utils.identicon import Identicon
from frappe.utils.html_utils import sanitize_html
@@ -170,6 +169,8 @@ def random_string(length):
def has_gravatar(email):
'''Returns gravatar url if user has set an avatar at gravatar.com'''
+ import requests
+
if (frappe.flags.in_import
or frappe.flags.in_install
or frappe.flags.in_test):
@@ -193,6 +194,8 @@ def get_gravatar_url(email):
return "https://secure.gravatar.com/avatar/{hash}?d=mm&s=200".format(hash=hashlib.md5(email.encode('utf-8')).hexdigest())
def get_gravatar(email):
+ from frappe.utils.identicon import Identicon
+
gravatar_url = has_gravatar(email)
if not gravatar_url:
@@ -457,6 +460,7 @@ def get_sites(sites_path=None):
return sorted(sites)
def get_request_session(max_retries=3):
+ import requests
from urllib3.util import Retry
session = requests.Session()
session.mount("http://", requests.adapters.HTTPAdapter(max_retries=Retry(total=5, status_forcelist=[500])))
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 21fa609b80..37d3dde054 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -4,18 +4,10 @@
from __future__ import unicode_literals
import frappe
-from dateutil.parser._parser import ParserError
import operator
import json
import re, datetime, math, time
-import babel.dates
-from babel.core import UnknownLocaleError
-from dateutil import parser
-from num2words import num2words
-from six.moves import html_parser as HTMLParser
from six.moves.urllib.parse import quote, urljoin
-from html2text import html2text
-from markdown2 import markdown as _markdown, MarkdownError
from six import iteritems, text_type, string_types, integer_types
from frappe.desk.utils import slug
@@ -34,6 +26,8 @@ def getdate(string_date=None):
Converts string date (yyyy-mm-dd) to datetime.date object.
If no input is provided, current date is returned.
"""
+ from dateutil import parser
+ from dateutil.parser._parser import ParserError
if not string_date:
return get_datetime().date()
@@ -53,6 +47,8 @@ def getdate(string_date=None):
), title=frappe._('Invalid Date'))
def get_datetime(datetime_str=None):
+ from dateutil import parser
+
if datetime_str is None:
return now_datetime()
@@ -74,6 +70,8 @@ def get_datetime(datetime_str=None):
return parser.parse(datetime_str)
def to_timedelta(time_str):
+ from dateutil import parser
+
if isinstance(time_str, string_types):
t = parser.parse(time_str)
return datetime.timedelta(hours=t.hour, minutes=t.minute, seconds=t.second, microseconds=t.microsecond)
@@ -83,6 +81,8 @@ def to_timedelta(time_str):
def add_to_date(date, years=0, months=0, weeks=0, days=0, hours=0, minutes=0, seconds=0, as_string=False, as_datetime=False):
"""Adds `days` to the given date"""
+ from dateutil import parser
+ from dateutil.parser._parser import ParserError
from dateutil.relativedelta import relativedelta
if date==None:
@@ -262,6 +262,8 @@ def get_year_ending(date):
return add_to_date(date, days=-1)
def get_time(time_str):
+ from dateutil import parser
+
if isinstance(time_str, datetime.datetime):
return time_str.time()
elif isinstance(time_str, datetime.time):
@@ -315,6 +317,8 @@ def format_date(string_date=None, format_string=None):
* mm-dd-yyyy
* dd/mm/yyyy
"""
+ import babel.dates
+ from babel.core import UnknownLocaleError
if not string_date:
return ''
@@ -343,6 +347,8 @@ def format_time(time_string=None, format_string=None):
* HH:mm:ss
* HH:mm
"""
+ import babel.dates
+ from babel.core import UnknownLocaleError
if not time_string:
return ''
@@ -367,6 +373,9 @@ def format_datetime(datetime_string, format_string=None):
* dd-mm-yyyy HH:mm:ss
* mm-dd-yyyy HH:mm
"""
+ import babel.dates
+ from babel.core import UnknownLocaleError
+
if not datetime_string:
return
@@ -488,6 +497,8 @@ def get_timespan_date_range(timespan):
def global_date_format(date, format="long"):
"""returns localized date in the form of January 1, 2012"""
+ import babel.dates
+
date = getdate(date)
formatted_date = babel.dates.format_date(date, locale=(frappe.local.lang or "en").replace("-", "_"), format=format)
return formatted_date
@@ -550,13 +561,13 @@ def flt(s, precision=None):
return num
-def cint(s):
+def cint(s, default=0):
"""Convert to integer
:param s: Number in string or other numeric format.
:returns: Converted number in python integer type.
- Returns 0 if input can not be converted to integer.
+ Returns default if input can not be converted to integer.
Examples:
>>> cint("100")
@@ -565,9 +576,10 @@ def cint(s):
0
"""
- try: num = int(float(s))
- except: num = 0
- return num
+ try:
+ return int(float(s))
+ except Exception:
+ return default
def floor(s):
"""
@@ -846,6 +858,8 @@ def in_words(integer, in_million=True):
"""
Returns string in words for the given integer.
"""
+ from num2words import num2words
+
locale = 'en_IN' if not in_million else frappe.local.lang
integer = int(integer)
try:
@@ -865,7 +879,7 @@ def is_image(filepath):
from mimetypes import guess_type
# filepath can be https://example.com/bed.jpg?v=129
- filepath = filepath.split('?')[0]
+ filepath = (filepath or "").split('?')[0]
return (guess_type(filepath)[0] or "").startswith("image/")
def get_thumbnail_base64_for_image(src):
@@ -1338,6 +1352,9 @@ def strip(val, chars=None):
return (val or "").replace("\ufeff", "").replace("\u200b", "").strip(chars)
def to_markdown(html):
+ from html2text import html2text
+ from six.moves import html_parser as HTMLParser
+
text = None
try:
text = html2text(html or '')
@@ -1347,6 +1364,8 @@ def to_markdown(html):
return text
def md_to_html(markdown_text):
+ from markdown2 import markdown as _markdown, MarkdownError
+
extras = {
'fenced-code-blocks': None,
'tables': None,
@@ -1361,7 +1380,7 @@ def md_to_html(markdown_text):
html = None
try:
- html = _markdown(markdown_text or '', extras=extras)
+ html = UnicodeWithAttrs(_markdown(markdown_text or '', extras=extras))
except MarkdownError:
pass
@@ -1471,3 +1490,9 @@ def get_user_info_for_avatar(user_id):
except Exception:
frappe.local.message_log = []
return user_info
+
+
+class UnicodeWithAttrs(text_type):
+ def __init__(self, text):
+ self.toc_html = text.toc_html
+ self.metadata = text.metadata
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index f605c3bf66..1c067d0146 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -8,7 +8,6 @@ import re
import redis
import json
import os
-from bs4 import BeautifulSoup
from frappe.utils import cint, strip_html_tags
from frappe.utils.html_utils import unescape_html
from frappe.model.base_document import get_controller
@@ -310,6 +309,7 @@ def get_routes_to_index():
def add_route_to_global_search(route):
+ from bs4 import BeautifulSoup
from frappe.website.render import render_page
from frappe.utils import set_request
frappe.set_user('Guest')
diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py
index bccdbd9441..81e5f2de3e 100644
--- a/frappe/utils/html_utils.py
+++ b/frappe/utils/html_utils.py
@@ -2,12 +2,12 @@ from __future__ import unicode_literals
import frappe
import json
import re
-import bleach
import bleach_whitelist.bleach_whitelist as bleach_whitelist
from six import string_types
-from bs4 import BeautifulSoup
def clean_html(html):
+ import bleach
+
if not isinstance(html, string_types):
return html
@@ -19,6 +19,8 @@ def clean_html(html):
strip=True, strip_comments=True)
def clean_email_html(html):
+ import bleach
+
if not isinstance(html, string_types):
return html
@@ -41,6 +43,8 @@ def clean_email_html(html):
def clean_script_and_style(html):
# remove script and style
+ from bs4 import BeautifulSoup
+
soup = BeautifulSoup(html, 'html5lib')
for s in soup(['script', 'style']):
s.decompose()
@@ -53,6 +57,9 @@ def sanitize_html(html, linkify=False):
Does not sanitize JSON, as it could lead to future problems
"""
+ import bleach
+ from bs4 import BeautifulSoup
+
if not isinstance(html, string_types):
return html
diff --git a/frappe/utils/nestedset.py b/frappe/utils/nestedset.py
index ab426d2ce4..531699db0c 100644
--- a/frappe/utils/nestedset.py
+++ b/frappe/utils/nestedset.py
@@ -137,10 +137,16 @@ def update_move_node(doc, parent_field):
frappe.db.sql("""update `tab{0}` set lft = -lft + %s, rgt = -rgt + %s, modified=%s
where lft < 0""".format(doc.doctype), (new_diff, new_diff, n))
+@frappe.whitelist()
def rebuild_tree(doctype, parent_field):
"""
call rebuild_node for all root nodes
"""
+
+ # Check for perm if called from client-side
+ if frappe.request and frappe.local.form_dict.cmd == 'rebuild_tree':
+ frappe.only_for('System Manager')
+
# get all roots
frappe.db.auto_commit_on_many_writes = 1
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index c9123412f0..b152d69d8d 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -17,7 +17,6 @@ from werkzeug.local import LocalProxy
from werkzeug.wsgi import wrap_file
from werkzeug.wrappers import Response
from werkzeug.exceptions import NotFound, Forbidden
-from frappe.website.render import render
from frappe.utils import cint
from six import text_type
from six.moves.urllib.parse import quote
@@ -150,6 +149,7 @@ def json_handler(obj):
def as_page():
"""print web page"""
+ from frappe.website.render import render
return render(frappe.response['route'], http_status_code=frappe.response.get("http_status_code"))
def redirect():
diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py
index 7546b4d363..89def9bf8d 100644
--- a/frappe/website/doctype/website_settings/website_settings.py
+++ b/frappe/website/doctype/website_settings/website_settings.py
@@ -2,7 +2,6 @@
# MIT License. See license.txt
from __future__ import unicode_literals
-import requests
import frappe
from frappe import _
from frappe.utils import get_request_site_address, encode
@@ -77,6 +76,8 @@ class WebsiteSettings(Document):
frappe.clear_cache()
def get_access_token(self):
+ import requests
+
google_settings = frappe.get_doc("Google Settings")
if not google_settings.enable:
diff --git a/frappe/website/render.py b/frappe/website/render.py
index 2f8bc59d6d..eb1d3d92a1 100644
--- a/frappe/website/render.py
+++ b/frappe/website/render.py
@@ -10,7 +10,6 @@ import os, mimetypes, json
import re
import six
-from bs4 import BeautifulSoup
from six import iteritems
from werkzeug.wrappers import Response
from werkzeug.routing import Rule
@@ -139,6 +138,8 @@ def build_response(path, data, http_status_code, headers=None):
def add_preload_headers(response):
+ from bs4 import BeautifulSoup
+
try:
preload = []
soup = BeautifulSoup(response.data, "lxml")
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 946c83811a..bd61fc1da3 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -7,8 +7,6 @@ import io
import os
import re
-import yaml
-
import frappe
from frappe.model.document import get_controller
from frappe.website.utils import can_cache, delete_page_cache, extract_comment_tag, extract_title
@@ -283,6 +281,7 @@ def get_frontmatter(string):
"""
Reference: https://github.com/jonbeebe/frontmatter
"""
+ import yaml
fmatter = ""
body = ""
diff --git a/package.json b/package.json
index 43cedc158a..5c93c24016 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"scripts": {
"build": "node rollup/build.js",
"production": "FRAPPE_ENV=production node rollup/build.js",
- "watch": "node rollup/watch.js",
+ "watch": "node --max_old_space_size=1280 rollup/watch.js",
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
},
diff --git a/yarn.lock b/yarn.lock
index 85983456fb..daca81cfda 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -961,15 +961,10 @@ caniuse-api@^3.0.0:
lodash.memoize "^4.1.2"
lodash.uniq "^4.5.0"
-caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939:
- version "1.0.30001116"
- resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001116.tgz"
- integrity sha512-f2lcYnmAI5Mst9+g0nkMIznFGsArRmZ0qU+dnq8l91hymdc2J3SFbiPhOJEeDqC1vtE8nc1qNQyklzB8veJefQ==
-
-caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
- version "1.0.30001118"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001118.tgz#116a9a670e5264aec895207f5e918129174c6f62"
- integrity sha512-RNKPLojZo74a0cP7jFMidQI7nvLER40HgNfgKQEJ2PFm225L0ectUungNQoK3Xk3StQcFbpBPNEvoWD59436Hg==
+caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000939, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001111:
+ version "1.0.30001191"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001191.tgz"
+ integrity sha512-xJJqzyd+7GCJXkcoBiQ1GuxEiOBCLQ0aVW9HMekifZsAVGdj5eJ4mFB9fEhSHipq9IOk/QXFJUiIr9lZT+EsGw==
caseless@~0.12.0:
version "0.12.0"