From 6a2248eef315fd5efe38d8a50ebd385725d4a5ce Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Fri, 7 Aug 2020 20:12:33 +0530 Subject: [PATCH 001/295] fix: move node-sass to dependencies from devDependencies node-sass is required in production for Website Theme yarn --prod will install only dependencies and reduce size fixes #11219 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d6bb3ccda4..07cc91e011 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "jsbarcode": "^3.9.0", "moment": "^2.20.1", "moment-timezone": "^0.5.28", + "node-sass": "^4.13.1", "quagga": "^0.12.1", "quill": "2.0.0-dev.4", "qz-tray": "^2.0.8", @@ -56,7 +57,6 @@ "cypress-file-upload": "^3.1.0", "graphlib": "^2.1.8", "less": "^3.11.1", - "node-sass": "^4.13.1", "rollup": "^1.2.2", "rollup-plugin-buble": "^0.19.2", "rollup-plugin-commonjs": "^8.3.0", From e7d21a813c579078cdd61f13313c109dad032a6d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 27 Aug 2020 18:05:45 +0200 Subject: [PATCH 002/295] feat: hook for website_theme_scss --- frappe/utils/boilerplate.py | 3 +++ frappe/website/doctype/website_theme/website_theme.py | 7 ++++--- .../doctype/website_theme/website_theme_template.scss | 4 ++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index d3b206559f..550302774c 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -157,6 +157,9 @@ app_license = "{app_license}" # web_include_css = "/assets/{app_name}/css/{app_name}.css" # web_include_js = "/assets/{app_name}/js/{app_name}.js" +# include custom scss in every website theme (without file extension ".scss") +# website_theme_scss = "{app_name}/public/scss/website" + # include js, css files in header of web form # webform_include_js = {{"doctype": "public/js/doctype.js"}} # webform_include_css = {{"doctype": "public/css/doctype.css"}} diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index c2c4a6e2c8..6e95c7901b 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -73,7 +73,7 @@ class WebsiteTheme(Document): file_name = frappe.scrub(self.name) + '_' + suffix + '.css' output_path = join_path(folder_path, file_name) - content = get_scss(self) + self.theme_scss = content = get_scss(self) content = content.replace('\n', '\\n') command = ['node', 'generate_bootstrap_theme.js', output_path, content] @@ -128,5 +128,6 @@ def get_active_theme(): def get_scss(doc): - return frappe.render_template('frappe/website/doctype/website_theme/website_theme_template.scss', doc.as_dict()) - + opts = doc.as_dict() + opts['website_theme_scss'] = frappe.get_hooks('website_theme_scss') + return frappe.render_template('frappe/website/doctype/website_theme/website_theme_template.scss', opts) diff --git a/frappe/website/doctype/website_theme/website_theme_template.scss b/frappe/website/doctype/website_theme/website_theme_template.scss index b46184361b..43fdea354c 100644 --- a/frappe/website/doctype/website_theme/website_theme_template.scss +++ b/frappe/website/doctype/website_theme/website_theme_template.scss @@ -26,3 +26,7 @@ body { // Custom Theme {{ custom_scss or '' }} + +{% for app_scss in website_theme_scss %} +@import "{{ app_scss }}" +{% endfor %} From 65ae9036ae069690893e77f8f567c6e2c96114e8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 28 Aug 2020 14:47:19 +0200 Subject: [PATCH 003/295] feat: allow to ignore standard app themes --- frappe/hooks.py | 1 + .../doctype/website_theme/website_theme.json | 10 +++++++++- .../doctype/website_theme/website_theme.py | 6 +++++- .../website_theme/website_theme_template.scss | 18 ++++++++++-------- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index 7ecc199814..b139788c65 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -48,6 +48,7 @@ web_include_js = [ ] web_include_css = [] +website_theme_scss = "frappe/public/scss/website" website_route_rules = [ {"from_route": "/blog/", "to_route": "Blog Post"}, diff --git a/frappe/website/doctype/website_theme/website_theme.json b/frappe/website/doctype/website_theme/website_theme.json index 7af6c91281..e02f4e9cc2 100644 --- a/frappe/website/doctype/website_theme/website_theme.json +++ b/frappe/website/doctype/website_theme/website_theme.json @@ -26,6 +26,7 @@ "stylesheet_section", "custom_overrides", "custom_scss", + "imports_to_ignore", "theme_scss", "theme_url", "custom_js_section", @@ -167,10 +168,17 @@ "fieldtype": "Code", "label": "Custom Overrides", "options": "SCSS" + }, + { + "description": "Comma-separated list of import paths", + "fieldname": "imports_to_ignore", + "fieldtype": "Data", + "label": "Imports To Ignore" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-05-16 18:36:22.203519", + "modified": "2020-08-28 13:31:32.846474", "modified_by": "Administrator", "module": "Website", "name": "Website Theme", diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index 6e95c7901b..5491bdd83c 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -128,6 +128,10 @@ def get_active_theme(): def get_scss(doc): + def trim_list(list_of_strings): + return [s.strip() for s in list_of_strings] + opts = doc.as_dict() - opts['website_theme_scss'] = frappe.get_hooks('website_theme_scss') + opts['website_theme_scss'] = trim_list(frappe.get_hooks('website_theme_scss', [])) + opts['imports_to_ignore'] = trim_list((opts.get('imports_to_ignore') or '').split(',')) return frappe.render_template('frappe/website/doctype/website_theme/website_theme_template.scss', opts) diff --git a/frappe/website/doctype/website_theme/website_theme_template.scss b/frappe/website/doctype/website_theme/website_theme_template.scss index 43fdea354c..0174996683 100644 --- a/frappe/website/doctype/website_theme/website_theme_template.scss +++ b/frappe/website/doctype/website_theme/website_theme_template.scss @@ -1,7 +1,8 @@ {% if google_font %} -@import url('https://fonts.googleapis.com/css2?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap'); -$font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; - +@import url("https://fonts.googleapis.com/css2?family={{ google_font.replace(' ', '+') }}:{{ font_properties }}&display=swap"); +$font-family-sans-serif: "{{ google_font }}", -apple-system, BlinkMacSystemFont, + "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", + "Droid Sans", "Helvetica Neue", sans-serif; {% endif -%} {% if primary_color %}$primary: {{ frappe.db.get_value('Color', primary_color, 'color') }};{% endif -%} @@ -16,7 +17,12 @@ $enable-rounded: {{ button_rounded_corners and "true" or "false" }}; // Bootstrap Variable Overrides {{ custom_overrides or '' }} -@import "frappe/public/scss/website"; +// Import themes from installed apps +{% for import_path in website_theme_scss -%} +{% if import_path not in imports_to_ignore -%} +@import "{{ import_path }}"; +{%- endif %} +{% endfor %} {% if font_size -%} body { @@ -26,7 +32,3 @@ body { // Custom Theme {{ custom_scss or '' }} - -{% for app_scss in website_theme_scss %} -@import "{{ app_scss }}" -{% endfor %} From 0139de6c4e8fc526de44e67cd25224fd661a06fd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 28 Aug 2020 14:48:01 +0200 Subject: [PATCH 004/295] tests: create and update test cases --- .../doctype/website_theme/test_records.json | 1 - .../website_theme/test_website_theme.py | 33 ++++--- package.json | 4 +- yarn.lock | 92 +++++++------------ 4 files changed, 56 insertions(+), 74 deletions(-) delete mode 100644 frappe/website/doctype/website_theme/test_records.json diff --git a/frappe/website/doctype/website_theme/test_records.json b/frappe/website/doctype/website_theme/test_records.json deleted file mode 100644 index fe51488c70..0000000000 --- a/frappe/website/doctype/website_theme/test_records.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/frappe/website/doctype/website_theme/test_website_theme.py b/frappe/website/doctype/website_theme/test_website_theme.py index 0402ec1e52..ef3a924905 100644 --- a/frappe/website/doctype/website_theme/test_website_theme.py +++ b/frappe/website/doctype/website_theme/test_website_theme.py @@ -7,25 +7,30 @@ import os import frappe import unittest -test_records = frappe.get_test_records('Website Theme') - class TestWebsiteTheme(unittest.TestCase): - def test_website_theme(self): - if os.environ.get('CI'): - # no node-sass on travis (?) - return + def test_website_theme(self): + frappe.delete_doc_if_exists('Website Theme', 'test-theme') + theme = frappe.get_doc(dict( + doctype='Website Theme', + theme='test-theme', + google_font='Inter', + custom_scss='body { font-size: 16.5px; }' # this will get minified! + )).insert() + + theme_path = frappe.get_site_path('public', theme.theme_url[1:]) + with open(theme_path) as theme_file: + css = theme_file.read() + + self.assertTrue('body{font-size:16.5px}' in css) + self.assertTrue('fonts.googleapis.com' in css) + + def test_imports_to_ignore(self): frappe.delete_doc_if_exists('Website Theme', 'test-theme') theme = frappe.get_doc(dict( doctype = 'Website Theme', theme = 'test-theme', - google_font = 'Inter', - custom_scss = 'body { font-size: 16.5px; }' + imports_to_ignore = 'frappe/public/scss/website' )).insert() - with open(theme.theme_url[1:]) as f: - css = f.read() - - self.assertTrue(theme.custom_scss in css) - self.assertTrue('fonts.googleapis.com' in css) - + self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss) diff --git a/package.json b/package.json index f893d03ad3..2a8a88823a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,8 @@ "superagent": "^3.8.2", "touch": "^3.1.0", "vue": "^2.6.11", - "vue-router": "^2.0.0" + "vue-router": "^2.0.0", + "node-sass": "^4.14.1" }, "devDependencies": { "babel-runtime": "^6.26.0", @@ -57,7 +58,6 @@ "cypress-file-upload": "^3.1.0", "graphlib": "^2.1.8", "less": "^3.11.1", - "node-sass": "^4.13.1", "rollup": "^1.2.2", "rollup-plugin-buble": "^0.19.2", "rollup-plugin-commonjs": "^8.3.0", diff --git a/yarn.lock b/yarn.lock index c3808f680a..6b72e0981a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1178,11 +1178,6 @@ camelcase@^2.0.0, camelcase@^2.0.1: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -1330,7 +1325,7 @@ clipanion@^2.4.2: resolved "https://registry.yarnpkg.com/clipanion/-/clipanion-2.4.4.tgz#d70244c6f60feb3f4cbd509d2fcbe829fc619061" integrity sha512-KjyCBz8xplftHjIK/nOqq/9b3hPlXbAAo/AxoITrO4yySpQ6a9QSJDAfOx9PfcRUHteeqbdNxZKSPfeFqQ7plg== -cliui@^3.0.3, cliui@^3.2.0: +cliui@^3.0.3: version "3.2.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= @@ -2837,11 +2832,6 @@ generic-names@^1.0.2, generic-names@^1.0.3: dependencies: loader-utils "^0.2.16" -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4823,10 +4813,10 @@ node-releases@^1.1.8: dependencies: semver "^5.3.0" -node-sass@^4.13.1: - version "4.13.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" - integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== +node-sass@^4.14.1: + version "4.14.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" + integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -4842,7 +4832,7 @@ node-sass@^4.13.1: node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" - sass-graph "^2.2.4" + sass-graph "2.2.5" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -6304,11 +6294,6 @@ require-from-string@^2.0.1: resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -6557,15 +6542,15 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= +sass-graph@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" + integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== dependencies: glob "^7.0.0" lodash "^4.0.0" scss-tokenizer "^0.2.3" - yargs "^7.0.0" + yargs "^13.3.2" sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: version "1.2.4" @@ -7367,7 +7352,7 @@ string-hash@^1.1.0, string-hash@^1.1.1: resolved "https://registry.yarnpkg.com/string-hash/-/string-hash-1.1.3.tgz#e8aafc0ac1855b4666929ed7dd1275df5d6c811b" integrity sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs= -string-width@^1.0.1, string-width@^1.0.2: +string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -8135,11 +8120,6 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -8279,7 +8259,7 @@ xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.0, y18n@^3.2.1: +y18n@^3.2.0: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= @@ -8309,6 +8289,14 @@ yaml@^1.9.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + yargs-parser@^15.0.0: version "15.0.0" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.0.tgz#cdd7a97490ec836195f59f3f4dbe5ea9e8f75f08" @@ -8317,12 +8305,21 @@ yargs-parser@^15.0.0: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== dependencies: - camelcase "^3.0.0" + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" yargs@^14.2: version "14.2.2" @@ -8354,25 +8351,6 @@ yargs@^3.19.0: window-size "^0.1.4" y18n "^3.2.0" -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" - yauzl@2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From c19bc430280392fa9426340bc5c8c9210ceddfcd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 28 Aug 2020 15:54:13 +0200 Subject: [PATCH 005/295] test: fix spacing --- frappe/website/doctype/website_theme/test_website_theme.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/website/doctype/website_theme/test_website_theme.py b/frappe/website/doctype/website_theme/test_website_theme.py index ef3a924905..03fab88da3 100644 --- a/frappe/website/doctype/website_theme/test_website_theme.py +++ b/frappe/website/doctype/website_theme/test_website_theme.py @@ -28,9 +28,9 @@ class TestWebsiteTheme(unittest.TestCase): def test_imports_to_ignore(self): frappe.delete_doc_if_exists('Website Theme', 'test-theme') theme = frappe.get_doc(dict( - doctype = 'Website Theme', - theme = 'test-theme', - imports_to_ignore = 'frappe/public/scss/website' + doctype='Website Theme', + theme='test-theme', + imports_to_ignore='frappe/public/scss/website' )).insert() self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss) From 14d70b274317a703c9a97ab351151c3c4ed7b8e9 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 31 Aug 2020 20:51:25 +0530 Subject: [PATCH 006/295] fix: Refresh datatable when sidebar is toggled --- frappe/public/js/frappe/views/reports/report_view.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index c7d001ed94..aec24d9d13 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -48,6 +48,10 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { setup_view() { this.setup_columns(); super.setup_new_doc_event(); + // refresh datatable when sidebar is toggled to accomodate extra space + $(document.body) + .off('toggleListSidebar.report_view') + .on('toggleListSidebar.report_view', () => this.render(true)); } setup_result_area() { From b5252d11b95c8808dd825168ddead90826517281 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Sep 2020 15:58:58 +0530 Subject: [PATCH 007/295] feat: Compress private and public files during backup --- frappe/commands/site.py | 11 ++++++----- frappe/installer.py | 7 +++++-- frappe/utils/backups.py | 35 ++++++++++++++++++++++++----------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index d343d10126..40149582cb 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -112,7 +112,7 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N @pass_context def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" - from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade + from frappe.installer import extract_sql_gzip, extract_files, is_downgrade force = context.force or force # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file @@ -148,12 +148,12 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas # Extract public and/or private files to the restored site, if user has given the path if with_public_files: with_public_files = os.path.join(base_path, with_public_files) - public = extract_tar_files(site, with_public_files, 'public') + public = extract_files(site, with_public_files, 'public') os.remove(public) if with_private_files: with_private_files = os.path.join(base_path, with_private_files) - private = extract_tar_files(site, with_private_files, 'private') + private = extract_files(site, with_private_files, 'private') os.remove(private) # Removing temporarily created file @@ -388,10 +388,11 @@ def use(site, sites_path='.'): @click.command('backup') @click.option('--with-files', default=False, is_flag=True, help="Take backup with files") +@click.option('--compress', default=False, is_flag=True, help="Compress private and public files") @click.option('--verbose', default=False, is_flag=True) @pass_context def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, quiet=False, verbose=False): + backup_path_private_files=None, quiet=False, verbose=False, compress=False): "Backup" from frappe.utils.backups import scheduled_backup verbose = verbose or context.verbose @@ -400,7 +401,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non try: frappe.init(site=site) frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose, compress=compress) except Exception as e: if verbose: print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) diff --git a/frappe/installer.py b/frappe/installer.py index 4994646890..e7f24288b1 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -328,7 +328,7 @@ def extract_sql_gzip(sql_gz_path): return decompressed_file -def extract_tar_files(site_name, file_path, folder_name): +def extract_files(site_name, file_path, folder_name): # Need to do frappe.init to maintain the site locals frappe.init(site=site_name) abs_site_path = os.path.abspath(frappe.get_site_path()) @@ -341,7 +341,10 @@ def extract_tar_files(site_name, file_path, folder_name): tar_path = os.path.join(abs_site_path, tar_name) try: - subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path) + if file_path.endswith(".tar"): + subprocess.check_output(['tar', 'xvf', tar_path, '--strip', '2'], cwd=abs_site_path) + elif file_path.endswith(".tgz"): + subprocess.check_output(['tar', 'zxvf', tar_path, '--strip', '2'], cwd=abs_site_path) except: raise finally: diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 86d7ad5a06..e4e43c057f 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -17,6 +17,7 @@ from frappe.utils import cstr, get_url, now_datetime # backup variable for backwards compatibility verbose = False +compress = False _verbose = verbose @@ -29,8 +30,9 @@ class BackupGenerator: """ def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False, - db_type='mariadb'): + db_type='mariadb', backup_path_conf=None, compress_files=False): global _verbose + self.compress_files = compress_files or compress self.db_host = db_host self.db_port = db_port self.db_name = db_name @@ -89,7 +91,7 @@ class BackupGenerator: self.take_dump() self.copy_site_config() if not ignore_files: - self.zip_files() + self.backup_files() else: self.backup_path_files = last_file @@ -100,8 +102,10 @@ class BackupGenerator: def set_backup_file_name(self): #Generate a random name using today's date and a 8 digit random number for_db = self.todays_date + "-" + self.site_slug + "-database.sql.gz" - for_public_files = self.todays_date + "-" + self.site_slug + "-files.tar" - for_private_files = self.todays_date + "-" + self.site_slug + "-private-files.tar" + ext = "tgz" if self.compress_files else "tar" + + for_public_files = self.todays_date + "-" + self.site_slug + "-files." + ext + for_private_files = self.todays_date + "-" + self.site_slug + "-private-files." + ext backup_path = get_backup_path() if not self.backup_path_db: @@ -155,15 +159,23 @@ class BackupGenerator: ) def zip_files(self): + # For backwards compatibility - pre v13 + return backup_files(self) + + def backup_files(self): for folder in ("public", "private"): files_path = frappe.get_site_path(folder, "files") backup_path = self.backup_path_files if folder=="public" else self.backup_path_private_files - cmd_string = """tar -cf %s %s""" % (backup_path, files_path) - err, out = frappe.utils.execute_in_shell(cmd_string) + if self.compress_files: + cmd_string = "tar cf - {1} | gzip -v > {0}" + else: + cmd_string = "tar -cf {0} {1}" + import subprocess + output = subprocess.check_output(cmd_string.format(backup_path, files_path), shell=True) if self.verbose: - print('Backed up files', os.path.abspath(backup_path)) + print('{0}\nBacked up file: {1}'.format(output or "", os.path.abspath(backup_path))) def copy_site_config(self): site_config_backup_path = os.path.join( @@ -273,14 +285,14 @@ def fetch_latest_backups(): } -def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False): +def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False, compress=False): """this function is called from scheduler deletes backups older than 7 days takes backup""" - odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force, verbose=verbose) + odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force, verbose=verbose, compress=compress) return odb -def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False): +def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False, compress=False): delete_temp_backups(older_than = frappe.conf.keep_backups_for_hours or 24) odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\ frappe.conf.db_password, @@ -289,7 +301,8 @@ def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_pat db_host = frappe.db.host, db_port = frappe.db.port, db_type = frappe.conf.db_type, - verbose=verbose) + verbose=verbose, + compress_files=compress) odb.get_backup(older_than, ignore_files, force=force) return odb From 4792556cc523f33b3a5c668bb1f2e48ba6fc7e66 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Sep 2020 16:12:26 +0530 Subject: [PATCH 008/295] fix: Sider --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 443335b034..6e039443a6 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -164,7 +164,7 @@ class BackupGenerator: def zip_files(self): # For backwards compatibility - pre v13 - return backup_files(self) + return self.backup_files() def backup_files(self): for folder in ("public", "private"): From bf6baff2e40e7cc4276e2dda30728b79cf353dea Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Sep 2020 20:16:07 +0530 Subject: [PATCH 009/295] fix: Pass conf path via backup command --- frappe/commands/site.py | 4 ++-- frappe/utils/backups.py | 14 ++++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 40149582cb..1138021f80 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -392,7 +392,7 @@ def use(site, sites_path='.'): @click.option('--verbose', default=False, is_flag=True) @pass_context def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, quiet=False, verbose=False, compress=False): + backup_path_private_files=None, backup_path_conf=None, quiet=False, verbose=False, compress=False): "Backup" from frappe.utils.backups import scheduled_backup verbose = verbose or context.verbose @@ -401,7 +401,7 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non try: frappe.init(site=site) frappe.connect() - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, force=True, verbose=verbose, compress=compress) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=True, verbose=verbose, compress=compress) except Exception as e: if verbose: print("Backup failed for {0}. Database or site_config.json may be corrupted".format(site)) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 6e039443a6..c530fc441a 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -281,19 +281,21 @@ def fetch_latest_backups(): } -def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False, compress=False): +def scheduled_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, force=False, verbose=False, compress=False): """this function is called from scheduler deletes backups older than 7 days takes backup""" - odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=force, verbose=verbose, compress=compress) + odb = new_backup(older_than, ignore_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=force, verbose=verbose, compress=compress) return odb -def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, force=False, verbose=False, compress=False): +def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, force=False, verbose=False, compress=False): delete_temp_backups(older_than = frappe.conf.keep_backups_for_hours or 24) odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\ frappe.conf.db_password, - backup_path_db=backup_path_db, backup_path_files=backup_path_files, + backup_path_db=backup_path_db, + backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, + backup_path_conf=backup_path_conf, db_host = frappe.db.host, db_port = frappe.db.port, db_type = frappe.conf.db_type, @@ -343,9 +345,9 @@ def get_backup_path(): backup_path = frappe.utils.get_site_path(conf.get("backup_path", "private/backups")) return backup_path -def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet=False): +def backup(with_files=False, backup_path_db=None, backup_path_files=None, backup_path_private_files=None, backup_path_conf=None, quiet=False): "Backup" - odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, force=True) + odb = scheduled_backup(ignore_files=not with_files, backup_path_db=backup_path_db, backup_path_files=backup_path_files, backup_path_private_files=backup_path_private_files, backup_path_conf=backup_path_conf, force=True) return { "backup_path_db": odb.backup_path_db, "backup_path_files": odb.backup_path_files, From 2339165d75fbdf99191b99f4ec764e350f1ef34c Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 1 Sep 2020 20:51:13 +0530 Subject: [PATCH 010/295] fix: Add command options and description --- frappe/commands/site.py | 17 ++++++++++------- frappe/utils/backups.py | 9 +++++---- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 1138021f80..b15925e8d4 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -388,11 +388,15 @@ def use(site, sites_path='.'): @click.command('backup') @click.option('--with-files', default=False, is_flag=True, help="Take backup with files") -@click.option('--compress', default=False, is_flag=True, help="Compress private and public files") +@click.option('--backup-path-db', default=None, help="Compress private and public files") +@click.option('--backup-path-files', default=None, help="Compress private and public files") +@click.option('--backup-path-private-files', default=None, help="Compress private and public files") +@click.option('--backup-path-conf', default=None, help="Compress private and public files") @click.option('--verbose', default=False, is_flag=True) +@click.option('--compress', default=False, is_flag=True, help="Compress private and public files") @pass_context def backup(context, with_files=False, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, backup_path_conf=None, quiet=False, verbose=False, compress=False): + backup_path_private_files=None, backup_path_conf=None, verbose=False, compress=False): "Backup" from frappe.utils.backups import scheduled_backup verbose = verbose or context.verbose @@ -410,12 +414,11 @@ def backup(context, with_files=False, backup_path_db=None, backup_path_files=Non if verbose: from frappe.utils import now - summary_title = "Backup Summary at {0}".format(now()) - print(summary_title + "\n" + "-" * len(summary_title)) - print("Database backup:", odb.backup_path_db) + summary = "Backup Summary at {0}".format(now()) + summary_title = summary + "\n" + "-" * len(summary) + "\n" + print(summary_title + "Config file: {0}\nDatabase file: {1}".format(odb.backup_path_conf, odb.backup_path_db)) if with_files: - print("Public files: ", odb.backup_path_files) - print("Private files: ", odb.backup_path_private_files) + print("Public file: {0}\nPrivate file: {1}".format(odb.backup_path_files, odb.backup_path_private_files)) frappe.destroy() if not context.sites: diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index c530fc441a..8622091e72 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -59,16 +59,17 @@ class BackupGenerator: _verbose = verbose def setup_backup_directory(self): - specified = self.backup_path_db or self.backup_path_files or self.backup_path_private_files + specified = self.backup_path_db or self.backup_path_files or self.backup_path_private_files or self.backup_path_conf if not specified: backups_folder = get_backup_path() if not os.path.exists(backups_folder): os.makedirs(backups_folder) else: - for file_path in [self.backup_path_files, self.backup_path_db, self.backup_path_private_files]: - dir = os.path.dirname(file_path) - os.makedirs(dir, exist_ok=True) + for file_path in set([self.backup_path_files, self.backup_path_db, self.backup_path_private_files, self.backup_path_conf]): + if file_path: + dir = os.path.dirname(file_path) + os.makedirs(dir, exist_ok=True) def get_backup(self, older_than=24, ignore_files=False, force=False): From c92b5d01660d47f3c145f9f5333ec7c58d019871 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 24 Aug 2020 19:12:26 +0530 Subject: [PATCH 011/295] fix: Handle duration fieldtype during export --- frappe/core/doctype/data_export/exporter.py | 4 ++- frappe/desk/query_report.py | 31 +++++++++++++++++++-- frappe/desk/reportview.py | 27 +++++++++++++++++- 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index e4d2ff2af6..bec8cde7ea 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -8,7 +8,7 @@ from frappe import _ import frappe.permissions import re, csv, os from frappe.utils.csvutils import UnicodeWriter -from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint +from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration from frappe.core.doctype.data_import_legacy.importer import get_data_keys from six import string_types from frappe.core.doctype.access_log.access_log import make_access_log @@ -330,6 +330,8 @@ class DataExporter: value = formatdate(value) elif fieldtype == "Datetime": value = format_datetime(value) + elif fieldtype == "Duration": + value = format_duration(value, df.hide_days) row[_column_start_end.start + i + 1] = value diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index a1cfd02132..8e1c98c2af 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,14 +8,13 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, get_url_to_form +from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview from frappe.permissions import get_role_permissions from six import string_types, iteritems from datetime import timedelta -from frappe.utils import gzip_decompress from frappe.core.utils import ljust_list def get_report_doc(report_name): @@ -83,6 +82,8 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns) + result = handle_duration_fieldtype_values(columns, result) + return { "result": result, "columns": columns, @@ -266,6 +267,32 @@ def get_columns_from_dict(columns, result): return reordered_result + +def handle_duration_fieldtype_values(columns, result, meta=None): + for i, col in enumerate(columns): + fieldtype, fieldname = None, None + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + fieldname = col.get("fieldname") + + if fieldtype == "Duration": + for entry in range(0, len(result)): + val_in_seconds = result[entry][i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + result[entry][i] = duration_val + return result + + def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 6102be61ce..340e447f19 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -11,7 +11,7 @@ from frappe.model.db_query import DatabaseQuery from frappe import _ from six import string_types, StringIO from frappe.core.doctype.access_log.access_log import make_access_log -from frappe.utils import cstr +from frappe.utils import cstr, format_duration @frappe.whitelist() @@ -166,6 +166,8 @@ def export_query(): for i, row in enumerate(ret): data.append([i+1] + list(row)) + data = handle_duration_fieldtype_values(doctype, data, db_query.fields) + if file_format_type == "CSV": # convert to csv @@ -235,6 +237,29 @@ def get_labels(fields, doctype): return labels +def handle_duration_fieldtype_values(doctype, data, fields): + for field in fields: + key = field.split(" as ")[0] + + if key.startswith(('count(', 'sum(', 'avg(')): continue + + if "." in key: + parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") + else: + parenttype = doctype + fieldname = field.strip("`") + + df = frappe.get_meta(parenttype).get_field(fieldname) + + if df and df.fieldtype == 'Duration': + index = fields.index(field) + 1 + for i in range(1, len(data)): + val_in_seconds = data[i][index] + if val_in_seconds: + duration_val = format_duration(val_in_seconds, df.hide_days) + data[i][index] = duration_val + return data + @frappe.whitelist() def delete_items(): """delete selected items""" From c0b4532ea510c3e905ab3052d63d867badf87edd Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 24 Aug 2020 19:12:59 +0530 Subject: [PATCH 012/295] fix: Handle duration fieldtype for Data Import --- frappe/core/doctype/data_import/exporter.py | 5 +++ frappe/core/doctype/data_import/importer.py | 4 ++- .../doctype/data_import_legacy/importer.py | 5 +-- frappe/utils/data.py | 33 +++++++++++++++++++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 3eef6ce016..2ef206f56a 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -8,6 +8,7 @@ from frappe.model import ( no_value_fields, table_fields as table_fieldtypes, ) +from frappe.utils import flt, format_duration from frappe.utils.csvutils import build_csv_response from frappe.utils.xlsxutils import build_xlsx_response @@ -148,6 +149,10 @@ class Exporter: continue row[i] = doc.get(df.fieldname, "") + if df.fieldtype == "Duration": + value = flt(doc.get(df.fieldname, 0)) + row[i] = format_duration(value, df.hide_days) + return rows def get_data_as_docs(self): diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 2c10c6b0a5..301c356e45 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -9,7 +9,7 @@ import timeit import json from datetime import datetime, date from frappe import _ -from frappe.utils import cint, flt, update_progress_bar, cstr +from frappe.utils import cint, flt, update_progress_bar, cstr, duration_to_seconds from frappe.utils.csvutils import read_csv_content, get_csv_content_from_google_sheets from frappe.utils.xlsxutils import ( read_xlsx_file_from_attached_file, @@ -692,6 +692,8 @@ class Row: value = flt(value) elif df.fieldtype in ["Date", "Datetime"]: value = self.get_date(value, col) + elif df.fieldtype == "Duration": + value = duration_to_seconds(value) return value diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py index 5bd0daf32b..f7f196da61 100644 --- a/frappe/core/doctype/data_import_legacy/importer.py +++ b/frappe/core/doctype/data_import_legacy/importer.py @@ -15,7 +15,7 @@ from frappe import _ from frappe.utils.csvutils import getlink from frappe.utils.dateutils import parse_date -from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url +from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds from six import string_types @@ -164,7 +164,8 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, d[fieldname] = get_datetime(_date + " " + _time) else: d[fieldname] = None - + elif df.fieldtype == "Duration": + d[fieldname] = duration_to_seconds(cstr(d[fieldname])) elif fieldtype in ("Image", "Attach Image", "Attach"): # added file to attachments list attachments.append(d[fieldname]) diff --git a/frappe/utils/data.py b/frappe/utils/data.py index fd5c838b57..2a5ab8d5a3 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -346,6 +346,11 @@ def format_datetime(datetime_string, format_string=None): return formatted_datetime def format_duration(seconds, hide_days=False): + """Converts the given duration value in float(seconds) to duration format + + example: converts 12885 to '3h 34m 45s' where 12885 = seconds in float + """ + total_duration = { 'days': math.floor(seconds / (3600 * 24)), 'hours': math.floor(seconds % (3600 * 24) / 3600), @@ -373,6 +378,34 @@ def format_duration(seconds, hide_days=False): return duration +def duration_to_seconds(duration): + """Converts the given duration formatted value to duration value in seconds + + example: converts '3h 34m 45s' to 12885 (value in seconds) + """ + value = 0 + if 'd' in duration: + val = duration.split('d') + days = val[0] + value += cint(days) * 24 * 60 * 60 + duration = val[1] + if 'h' in duration: + val = duration.split('h') + hours = val[0] + value += cint(hours) * 60 * 60 + duration = val[1] + if 'm' in duration: + val = duration.split('m') + mins = val[0] + value += cint(mins) * 60 + duration = val[1] + if 's' in duration: + val = duration.split('s') + secs = val[0] + value += cint(secs) + + return value + def get_weekdays(): return ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] From 22a8cc2ddaee2db053dfc7255dc92d72a9454269 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 2 Sep 2020 16:31:02 +0530 Subject: [PATCH 013/295] fix: Uninstall App even if it doesn't exist on bench --- frappe/commands/site.py | 2 +- frappe/installer.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index d343d10126..f85a9b2474 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -452,7 +452,7 @@ def uninstall(context, app, dry_run, yes, no_backup, force): try: frappe.init(site=site) frappe.connect() - remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose) finally: frappe.destroy() if not context.sites: diff --git a/frappe/installer.py b/frappe/installer.py index 4994646890..53a8d878c7 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals, print_function from six.moves import input -import os, json, subprocess, shutil +import os, json, subprocess, shutil, sys import click import frappe import frappe.database @@ -119,9 +119,12 @@ def remove_from_installed_apps(app_name): if frappe.flags.in_install: post_install() -def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False): +def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, verbose=True): """Remove app and all linked to the app's module with the app from a site.""" + if not (verbose or dry_run): + sys.stdout = open(os.devnull, "w") + # dont allow uninstall app if not installed unless forced if not force: if app_name not in frappe.get_installed_apps(): @@ -143,11 +146,12 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) frappe.flags.in_uninstall = True drop_doctypes = [] - # remove modules, doctypes, roles - for module_name in frappe.get_module_list(app_name): - for doctype in frappe.get_list("DocType", filters={"module": module_name}, - fields=["name", "issingle"]): - print("removing DocType {0}...".format(doctype.name)) + modules = (x.name for x in frappe.get_all("Module Def", filters={"app_name": app_name})) + for module_name in modules: + print("Deleting Module '{0}'".format(module_name)) + + for doctype in frappe.get_list("DocType", filters={"module": module_name}, fields=["name", "issingle"]): + print("* removing DocType '{0}'...".format(doctype.name)) if not dry_run: frappe.delete_doc("DocType", doctype.name) @@ -155,24 +159,22 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) if not doctype.issingle: drop_doctypes.append(doctype.name) - linked_doctypes = frappe.get_all("DocField", filters={"fieldtype": "Link", "options": "Module Def"}, fields=['parent']) ordered_doctypes = ["Desk Page", "Report", "Page", "Web Form"] doctypes_with_linked_modules = ordered_doctypes + [doctype.parent for doctype in linked_doctypes if doctype.parent not in ordered_doctypes] for doctype in doctypes_with_linked_modules: for record in frappe.get_list(doctype, filters={"module": module_name}): - print("removing {0} {1}...".format(doctype, record.name)) + print("* removing {0} '{1}'...".format(doctype, record.name)) if not dry_run: frappe.delete_doc(doctype, record.name) - print("removing Module {0}...".format(module_name)) + print("* removing Module Def '{0}'...".format(module_name)) if not dry_run: frappe.delete_doc("Module Def", module_name) - remove_from_installed_apps(app_name) - if not dry_run: + remove_from_installed_apps(app_name) # drop tables after a commit frappe.db.commit() From dbea31943a6f58582c116423f4a170f0b0935f94 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 2 Sep 2020 17:08:00 +0530 Subject: [PATCH 014/295] fix: Add verbosity --- frappe/commands/site.py | 5 +++-- frappe/installer.py | 7 ++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index f85a9b2474..9de06b3723 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -444,15 +444,16 @@ def remove_from_installed_apps(context, app): @click.option('--dry-run', help='List all doctypes that will be deleted', is_flag=True, default=False) @click.option('--no-backup', help='Do not backup the site', is_flag=True, default=False) @click.option('--force', help='Force remove app from site', is_flag=True, default=False) +@click.option('--verbose', help='Add verbosity', is_flag=True, default=False) @pass_context -def uninstall(context, app, dry_run, yes, no_backup, force): +def uninstall(context, app, dry_run, yes, no_backup, force, verbose): "Remove app and linked modules from site" from frappe.installer import remove_app for site in context.sites: try: frappe.init(site=site) frappe.connect() - remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose) + remove_app(app_name=app, dry_run=dry_run, yes=yes, no_backup=no_backup, force=force, verbose=context.verbose or verbose) finally: frappe.destroy() if not context.sites: diff --git a/frappe/installer.py b/frappe/installer.py index 53a8d878c7..d0b6db77d4 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -122,9 +122,6 @@ def remove_from_installed_apps(app_name): def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, verbose=True): """Remove app and all linked to the app's module with the app from a site.""" - if not (verbose or dry_run): - sys.stdout = open(os.devnull, "w") - # dont allow uninstall app if not installed unless forced if not force: if app_name not in frappe.get_installed_apps(): @@ -138,6 +135,9 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, if not confirm: return + if not (verbose or dry_run): + sys.stdout = open(os.devnull, "w") + if not no_backup: from frappe.utils.backups import scheduled_backup print("Backing up...") @@ -181,6 +181,7 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False, for doctype in set(drop_doctypes): frappe.db.sql("drop table `tab{0}`".format(doctype)) + sys.stdout = sys.__stdout__ click.secho("Uninstalled App {0} from Site {1}".format(app_name, frappe.local.site), fg="green") frappe.flags.in_uninstall = False From 5c01c7145a75f84474e05ea826dceacd905bea70 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 17:38:21 +0530 Subject: [PATCH 015/295] fix: handle total for duration fieldtype --- frappe/desk/query_report.py | 32 ++------------------------ frappe/public/js/frappe/model/model.js | 2 +- 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 8e1c98c2af..fa1df349c3 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -8,7 +8,7 @@ import os, json from frappe import _ from frappe.modules import scrub, get_module_path -from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration +from frappe.utils import flt, cint, get_html_format, get_url_to_form, gzip_decompress, format_duration, cstr from frappe.model.utils import render_include from frappe.translate import send_translations import frappe.desk.reportview @@ -82,8 +82,6 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns) - result = handle_duration_fieldtype_values(columns, result) - return { "result": result, "columns": columns, @@ -267,32 +265,6 @@ def get_columns_from_dict(columns, result): return reordered_result - -def handle_duration_fieldtype_values(columns, result, meta=None): - for i, col in enumerate(columns): - fieldtype, fieldname = None, None - if isinstance(col, string_types): - col = col.split(":") - if len(col) > 1: - if col[1]: - fieldtype = col[1] - if "/" in fieldtype: - fieldtype, options = fieldtype.split("/") - else: - fieldtype = "Data" - else: - fieldtype = col.get("fieldtype") - fieldname = col.get("fieldname") - - if fieldtype == "Duration": - for entry in range(0, len(result)): - val_in_seconds = result[entry][i] - if val_in_seconds: - duration_val = format_duration(val_in_seconds) - result[entry][i] = duration_val - return result - - def get_prepared_report_result(report, filters, dn="", user=None): latest_report_data = {} doc = None @@ -454,7 +426,7 @@ def add_total_row(result, columns, meta = None): if i >= len(row): continue cell = row.get(fieldname) if isinstance(row, dict) else row[i] - if fieldtype in ["Currency", "Int", "Float", "Percent"] and flt(cell): + if fieldtype in ["Currency", "Int", "Float", "Percent", "Duration"] and flt(cell): total_row[i] = flt(total_row[i]) + flt(cell) if fieldtype == "Percent" and i not in has_percent: diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 663850d08c..308d9bd5f8 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -31,7 +31,7 @@ $.extend(frappe.model, { {fieldname:'docstatus', fieldtype:'Int', label:__('Document Status')}, ], - numeric_fieldtypes: ["Int", "Float", "Currency", "Percent"], + numeric_fieldtypes: ["Int", "Float", "Currency", "Percent", "Duration"], std_fields_table: [ {fieldname:'parent', fieldtype:'Data', label:__('Parent')}, From 0888fb48a73d42cd3f2eaae2eb77bbd4a9947ebe Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 19:01:42 +0530 Subject: [PATCH 016/295] feat: validate duration format in data import --- frappe/core/doctype/data_import/importer.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 301c356e45..5271690527 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -664,6 +664,20 @@ class Row: } ) return + elif df.fieldtype == "Duration": + import re + is_valid_duration = re.match("^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value) + if not is_valid_duration: + self.warnings.append( + { + "row": self.row_number, + "col": col.column_number, + "field": df_as_json(df), + "message": _("Value {0} must be in the valid duration format: d h m s").format( + frappe.bold(value) + ) + } + ) return value From 32e864bb6c55d5f84f3cf7be56294510e8d12b30 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 2 Sep 2020 19:21:56 +0530 Subject: [PATCH 017/295] test: duration fieldtype import --- .../data_import/fixtures/sample_import_file.csv | 10 +++++----- frappe/core/doctype/data_import/test_importer.py | 7 ++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv index ef5b96df58..693f400878 100644 --- a/frappe/core/doctype/data_import/fixtures/sample_import_file.csv +++ b/frappe/core/doctype/data_import/fixtures/sample_import_file.csv @@ -1,5 +1,5 @@ -Title ,Description ,Number ,another_number ,ID (Table Field 1) ,Child Title (Table Field 1) ,Child Description (Table Field 1) ,Child 2 Title (Table Field 2) ,Child 2 Date (Table Field 2) ,Child 2 Number (Table Field 2) ,Child Title (Table Field 1 Again) ,Child Date (Table Field 1 Again) ,Child Number (Table Field 1 Again) ,table_field_1_again.child_another_number -Test ,test description ,1 ,2 ,"" ,child title ,child description ,child title ,14-08-2019 ,4 ,child title again ,22-09-2020 ,5 , 7 - , , , , ,child title 2 ,child description 2 ,title child ,30-10-2019 ,5 ,child title again 2 ,22-09-2021 , , -Test 2 ,test description 2 ,1 ,2 , ,child mandatory title , ,title child man , , ,child mandatory again , , , -Test 3 ,test description 3 ,4 ,5 ,"" ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019 ,6 ,child title again asdf ,22-09-2022 ,9 , 71 +Title ,Description ,Number ,Duration,another_number ,ID (Table Field 1),Child Title (Table Field 1),Child Description (Table Field 1),Child 2 Title (Table Field 2),Child 2 Date (Table Field 2),Child 2 Number (Table Field 2),Child Title (Table Field 1 Again),Child Date (Table Field 1 Again),Child Number (Table Field 1 Again),table_field_1_again.child_another_number +Test ,test description ,1,3h,2, ,child title ,child description ,child title ,14-08-2019,4,child title again ,22-09-2020,5,7 + , , ,, , ,child title 2,child description 2,title child ,30-10-2019,5,child title again 2,22-09-2021, , +Test 2,test description 2,1,4d 3h,2, ,child mandatory title , ,title child man , , ,child mandatory again , , , +Test 3,test description 3,4,5d 5h 45m,5, ,child title asdf ,child description asdf ,child title asdf adsf ,15-08-2019,6,child title again asdf ,22-09-2022,9,71 \ No newline at end of file diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index bdadad7890..3b573e64a1 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import getdate +from frappe.utils import getdate, format_duration doctype_name = 'DocType for Import' @@ -24,6 +24,7 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.description, 'test description') self.assertEqual(doc1.number, 1) + self.assertEqual(format_duration(doc1.duration), '3h') self.assertEqual(doc1.table_field_1[0].child_title, 'child title') self.assertEqual(doc1.table_field_1[0].child_description, 'child description') @@ -40,7 +41,10 @@ class TestImporter(unittest.TestCase): self.assertEqual(doc1.table_field_1_again[1].child_date, getdate('2021-09-22')) self.assertEqual(doc2.description, 'test description 2') + self.assertEqual(format_duration(doc2.duration), '4d 3h') + self.assertEqual(doc3.another_number, 5) + self.assertEqual(format_duration(doc3.duration), '5d 5h 45m') def test_data_import_preview(self): import_file = get_import_file('sample_import_file') @@ -146,6 +150,7 @@ def create_doctype_if_not_exists(doctype_name, force=False): {'label': 'Title', 'fieldname': 'title', 'reqd': 1, 'fieldtype': 'Data'}, {'label': 'Description', 'fieldname': 'description', 'fieldtype': 'Small Text'}, {'label': 'Date', 'fieldname': 'date', 'fieldtype': 'Date'}, + {'label': 'Duration', 'fieldname': 'duration', 'fieldtype': 'Duration'}, {'label': 'Number', 'fieldname': 'number', 'fieldtype': 'Int'}, {'label': 'Number', 'fieldname': 'another_number', 'fieldtype': 'Int'}, {'label': 'Table Field 1', 'fieldname': 'table_field_1', 'fieldtype': 'Table', 'options': table_1_name}, From 1359c1c8b6aaa2cef6c4fc784ac563a5adfbb206 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 3 Sep 2020 12:35:12 +0530 Subject: [PATCH 018/295] fix: add correct slack webhook url link --- frappe/email/doctype/notification/notification.js | 1 + frappe/email/doctype/notification/notification.json | 4 ++-- .../integrations/doctype/twilio_settings/twilio_settings.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 454514f922..02fc6b31ca 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -151,6 +151,7 @@ frappe.ui.form.on('Notification', { }, refresh: function(frm) { frappe.notification.setup_fieldname_select(frm); + frappe.notification.setup_example_message(frm); frm.get_field('is_standard').toggle(frappe.boot.developer_mode); frm.trigger('event'); }, diff --git a/frappe/email/doctype/notification/notification.json b/frappe/email/doctype/notification/notification.json index 95f218ad73..1e3c0d5b14 100644 --- a/frappe/email/doctype/notification/notification.json +++ b/frappe/email/doctype/notification/notification.json @@ -66,7 +66,7 @@ }, { "depends_on": "eval:doc.channel=='Slack'", - "description": "To use Slack Channel, add a Slack Webhook URL.", + "description": "To use Slack Channel, add a Slack Webhook URL.", "fieldname": "slack_webhook_url", "fieldtype": "Link", "label": "Slack Channel", @@ -281,7 +281,7 @@ ], "icon": "fa fa-envelope", "links": [], - "modified": "2020-08-11 19:24:35.479373", + "modified": "2020-09-03 08:45:21.289300", "modified_by": "Administrator", "module": "Email", "name": "Notification", diff --git a/frappe/integrations/doctype/twilio_settings/twilio_settings.py b/frappe/integrations/doctype/twilio_settings/twilio_settings.py index 6c698d719a..80c5162987 100644 --- a/frappe/integrations/doctype/twilio_settings/twilio_settings.py +++ b/frappe/integrations/doctype/twilio_settings/twilio_settings.py @@ -11,7 +11,7 @@ from frappe.utils.password import get_decrypted_password from six import string_types class TwilioSettings(Document): - def validate(self): + def on_update(self): self.validate_twilio_credentials() def validate_twilio_credentials(self): From 21d58f3acd15ea45347e0da17c41a9880e00ea7a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 3 Sep 2020 12:47:00 +0530 Subject: [PATCH 019/295] fix: update message to be more concised --- frappe/email/doctype/notification/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 02fc6b31ca..24ebf8d01b 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -87,7 +87,7 @@ frappe.notification = {
Message Example
-Your {{ doc.name }} order of {{ doc.total }} has shipped and should be delivered on {{ doc.date }}. Details : {{doc.customer}}
+Your appointment is coming up on {{ doc.date }} at {{ doc.time }}
 
`; } else if (frm.doc.channel === 'Email') { template = `
Message Example
From 5f51c201773bee983c6f36869eefcfed120e4677 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 3 Sep 2020 13:36:30 +0530 Subject: [PATCH 020/295] fix(reports): handle duration fieldtype during export --- .../doctype/report_column/report_column.json | 4 +-- frappe/desk/query_report.py | 29 +++++++++++++++++++ frappe/public/js/frappe/utils/utils.js | 8 ++++- .../js/frappe/views/reports/query_report.js | 3 ++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/report_column/report_column.json b/frappe/core/doctype/report_column/report_column.json index 53b5dff9b6..2e6a22d29a 100644 --- a/frappe/core/doctype/report_column/report_column.json +++ b/frappe/core/doctype/report_column/report_column.json @@ -31,7 +31,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Check\nCurrency\nData\nDate\nDatetime\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", + "options": "Check\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nInt\nLink\nSelect\nTime", "reqd": 1 }, { @@ -48,7 +48,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 14:32:17.174796", + "modified": "2020-09-03 10:52:03.895817", "modified_by": "Administrator", "module": "Core", "name": "Report Column", diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index fa1df349c3..dc42228f81 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -359,6 +359,7 @@ def export_query(): columns = get_columns_dict(data.columns) from frappe.utils.xlsxutils import make_xlsx + data['result'] = handle_duration_fieldtype_values(data.get('result'), data.get('columns')) xlsx_data = build_xlsx_data(columns, data, visible_idx, include_indentation) xlsx_file = make_xlsx(xlsx_data, "Query Report") @@ -366,6 +367,30 @@ def export_query(): frappe.response['filecontent'] = xlsx_file.getvalue() frappe.response['type'] = 'binary' +def handle_duration_fieldtype_values(result, columns): + for i, col in enumerate(columns): + fieldtype, fieldname = None, None + if isinstance(col, string_types): + col = col.split(":") + if len(col) > 1: + if col[1]: + fieldtype = col[1] + if "/" in fieldtype: + fieldtype, options = fieldtype.split("/") + else: + fieldtype = "Data" + else: + fieldtype = col.get("fieldtype") + fieldname = col.get("fieldname") + + if fieldtype == "Duration": + for entry in range(0, len(result)): + val_in_seconds = result[entry][i] + if val_in_seconds: + duration_val = format_duration(val_in_seconds) + result[entry][i] = duration_val + + return result def build_xlsx_data(columns, data, visible_idx, include_indentation): result = [[]] @@ -385,7 +410,11 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation): for idx in range(len(data.columns)): label = columns[idx]["label"] fieldname = columns[idx]["fieldname"] + fieldtype = columns[idx]["fieldtype"] cell_value = row.get(fieldname, row.get(label, "")) + if fieldtype == "Duration": + cell_value = format_duration(value) + if cint(include_indentation) and 'indent' in row and idx == 0: cell_value = (' ' * cint(row['indent'])) + cell_value row_data.append(cell_value) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 38c22c9c9f..d64be06869 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -824,8 +824,14 @@ Object.assign(frappe.utils, { }; }, - get_formatted_duration(value, duration_options) { + get_formatted_duration(value, duration_options=null) { let duration = ''; + if (!duration_options) { + duration_options = { + hide_days: 0, + hide_seconds: 0 + } + } if (value) { let total_duration = frappe.utils.seconds_to_duration(value, duration_options); diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1bec65e460..0817d8cfa5 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1322,6 +1322,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return row .slice(standard_column_count) .map((cell, i) => { + if (cell.column.fieldtype === "Duration") { + cell.content = frappe.utils.get_formatted_duration(cell.content) + } if (include_indentation && i===0) { cell.content = ' '.repeat(row.meta.indent) + (cell.content || ''); } From 14af05037abe8e24c66a1907eda37f15d09e2890 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 13:15:00 +0530 Subject: [PATCH 021/295] feat: Section with Collapsible Content --- frappe/public/scss/page-builder.scss | 38 ++++++++++++++ .../section_with_collapsible_content.html | 21 ++++++++ .../section_with_collapsible_content.json | 51 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html create mode 100644 frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index f6446a9ba9..28db0b5a85 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -409,3 +409,41 @@ } } } + + +/* Section with Collapsible Content */ + +.collapsible-items { + max-width: 46rem; +} + +.collapsible-item { + padding: 1.75rem 0; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } +} + +.collapsible-item a { + text-decoration: none; +} + +.collapsible-item h3 { + margin-bottom: 0; +} + +.collapsible-content { + margin-top: 1rem; + margin-bottom: 0; + color: $gray-700; +} + +.section-with-collapsible-content.align-center { + .section-title, .section-description { + text-align: center; + } + .section-description, .collapsible-items { + margin: 0 auto; + } +} diff --git a/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html new file mode 100644 index 0000000000..2b86e7f992 --- /dev/null +++ b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.html @@ -0,0 +1,21 @@ +
+

{{ title }}

+ {%- if subtitle -%} +

{{ subtitle }}

+ {%- endif -%} + +
+ {%- for item in items -%} +
+ {%- set collapse_id = 'id-' + frappe.utils.generate_hash('Collapse', 12) -%} + +
+ {{ frappe.utils.md_to_html(item.content) }} +
+
+ {%- endfor -%} +
+
diff --git a/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json new file mode 100644 index 0000000000..f35b8d793e --- /dev/null +++ b/frappe/website/web_template/section_with_collapsible_content/section_with_collapsible_content.json @@ -0,0 +1,51 @@ +{ + "creation": "2020-08-07 16:27:38.265089", + "docstatus": 0, + "doctype": "Web Template", + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "subtitle", + "fieldtype": "Data", + "label": "Subtitle", + "reqd": 0 + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nCenter", + "reqd": 0 + }, + { + "fieldname": "items", + "fieldtype": "Table Break", + "label": "Items", + "reqd": 0 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 0 + }, + { + "fieldname": "content", + "fieldtype": "Markdown Editor", + "label": "Content", + "reqd": 0 + } + ], + "idx": 0, + "modified": "2020-08-13 15:51:23.728803", + "modified_by": "Administrator", + "name": "Section with Collapsible Content", + "owner": "Administrator", + "standard": 1, + "template": "" +} \ No newline at end of file From c0734921de5d30e335c9dedd6282dbcfff321abe Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 13:23:03 +0530 Subject: [PATCH 022/295] feat: Section with Image align center --- frappe/public/scss/page-builder.scss | 14 +++++++++++-- .../section_with_image.html | 16 +++++++------- .../section_with_image.json | 21 ++++++++++++++----- 3 files changed, 37 insertions(+), 14 deletions(-) diff --git a/frappe/public/scss/page-builder.scss b/frappe/public/scss/page-builder.scss index 28db0b5a85..81c06420bc 100644 --- a/frappe/public/scss/page-builder.scss +++ b/frappe/public/scss/page-builder.scss @@ -1,6 +1,6 @@ .hero-content { .btn-primary { - margin-top: 1rem; + margin-top: 1rem; margin-right: 0.5rem; @include media-breakpoint-up(lg) { @@ -35,6 +35,15 @@ } } +.section-with-image.align-center { + text-align: center; + + .section-description, .section-image { + margin-left: auto; + margin-right: auto; + } +} + .section-image { margin-top: 2rem; border-radius: 0.75rem; @@ -444,6 +453,7 @@ text-align: center; } .section-description, .collapsible-items { - margin: 0 auto; + margin-left: auto; + margin-right: auto; } } diff --git a/frappe/website/web_template/section_with_image/section_with_image.html b/frappe/website/web_template/section_with_image/section_with_image.html index ffa47d089e..cfd98064ac 100644 --- a/frappe/website/web_template/section_with_image/section_with_image.html +++ b/frappe/website/web_template/section_with_image/section_with_image.html @@ -1,8 +1,10 @@ -

{{ title }}

-

{{ subtitle }}

+
+

{{ title }}

+

{{ subtitle }}

-{{ frappe.render_template('templates/includes/image_with_blur.html', { - "src": image, - "alt": image_description, - "class": "section-image" -}) }} + {{ frappe.render_template('templates/includes/image_with_blur.html', { + "src": image, + "alt": image_description, + "class": "section-image" + }) }} +
diff --git a/frappe/website/web_template/section_with_image/section_with_image.json b/frappe/website/web_template/section_with_image/section_with_image.json index 5f610e5e2f..46169a8cc3 100644 --- a/frappe/website/web_template/section_with_image/section_with_image.json +++ b/frappe/website/web_template/section_with_image/section_with_image.json @@ -6,26 +6,37 @@ { "fieldname": "title", "fieldtype": "Data", - "label": "Title" + "label": "Title", + "reqd": 0 }, { "fieldname": "subtitle", "fieldtype": "Small Text", - "label": "Subtitle" + "label": "Subtitle", + "reqd": 0 }, { "fieldname": "image", "fieldtype": "Attach Image", - "label": "Image" + "label": "Image", + "reqd": 0 }, { "fieldname": "image_description", "fieldtype": "Data", - "label": "Image Description" + "label": "Image Description", + "reqd": 0 + }, + { + "fieldname": "align", + "fieldtype": "Select", + "label": "Align", + "options": "Left\nCenter", + "reqd": 0 } ], "idx": 0, - "modified": "2020-04-17 19:31:33.474017", + "modified": "2020-08-06 16:08:12.005764", "modified_by": "Administrator", "name": "Section with Image", "owner": "Administrator", From 40a0c69255b7a3db0f5f1a8807ac90e576aaffb8 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 17 Aug 2020 17:00:26 +0530 Subject: [PATCH 023/295] feat: Footer - Split footer in files - Footer grouped links styling - Put footer logo and extension in one row - Delete unused footer_items.html - Uncheck Right when adding Footer Items in Website Settings --- frappe/public/scss/footer.scss | 77 +++++++++++++++++++ frappe/public/scss/website.scss | 63 +-------------- frappe/templates/includes/footer/footer.html | 44 ++--------- .../includes/footer/footer_grouped_links.html | 56 ++++++++------ .../includes/footer/footer_info.html | 19 +++++ .../includes/footer/footer_items.html | 28 ------- .../includes/footer/footer_links.html | 8 +- .../footer/footer_logo_extension.html | 16 ++++ .../website_settings/website_settings.js | 6 +- 9 files changed, 158 insertions(+), 159 deletions(-) create mode 100644 frappe/public/scss/footer.scss create mode 100644 frappe/templates/includes/footer/footer_info.html delete mode 100644 frappe/templates/includes/footer/footer_items.html create mode 100644 frappe/templates/includes/footer/footer_logo_extension.html diff --git a/frappe/public/scss/footer.scss b/frappe/public/scss/footer.scss new file mode 100644 index 0000000000..9214907fbb --- /dev/null +++ b/frappe/public/scss/footer.scss @@ -0,0 +1,77 @@ +.web-footer { + padding: 5rem 0; + min-height: 140px; +} + +.footer-logo { + min-width: 5rem; + height: 1.5rem; + object-fit: contain; + object-position: left; +} + +.footer-child-item { + margin-top: 0.5rem; +} + +.footer-link, .footer-child-item a { + font-size: $font-size-sm; + font-weight: 500; + color: $gray-700; + + &:hover { + color: $primary; + text-decoration: none; + } +} + +.footer-col-left, .footer-col-right { + padding-top: 0.8rem; + padding-bottom: 1rem; + line-height: 2; + + &:empty { + padding: 0; + } +} + +.footer-col-right { + @include media-breakpoint-up(sm) { + text-align: right; + } +} + +.footer-col-left .footer-link { + margin-right: 1rem; +} + +.footer-col-right .footer-link { + margin-right: 1rem; + @include media-breakpoint-up(sm) { + margin-right: 0; + margin-left: 1rem; + } +} + +.footer-group { + margin-top: 2rem; +} + +.footer-group-label { + color: $text-muted; + font-size: $font-size-sm; + margin-bottom: 0.5rem; +} + +.footer-group-links { + display: flex; + flex-direction: column; + flex-wrap: wrap; + max-height: 10rem; +} + +.footer-info { + border-top: 1px solid $border-color; + color: $text-muted; + font-size: $font-size-sm; +} diff --git a/frappe/public/scss/website.scss b/frappe/public/scss/website.scss index e64c090ea8..59bbe4f19d 100644 --- a/frappe/public/scss/website.scss +++ b/frappe/public/scss/website.scss @@ -12,6 +12,7 @@ @import 'portal'; @import 'search'; @import 'doc'; +@import 'footer'; .ql-editor.read-mode { padding: 0; @@ -162,68 +163,6 @@ a.card { color: #d1d8dd !important; } -// footer - -.web-footer { - padding: 5rem 0; - min-height: 140px; -} - -.footer-logo { - width: 5rem; - height: 2rem; - object-fit: contain; - object-position: left; -} - -.footer-link, .footer-child-item a { - font-weight: 500; - color: $gray-700; - - &:hover { - color: $primary; - text-decoration: none; - } -} - -.footer-col-left, .footer-col-right { - padding-top: 0.8rem; - padding-bottom: 1rem; - line-height: 2; -} - -.footer-col-right { - @include media-breakpoint-up(sm) { - text-align: right; - } -} - -.footer-col-left .footer-link { - margin-right: 1rem; -} - -.footer-col-right .footer-link { - margin-right: 1rem; - @include media-breakpoint-up(sm) { - margin-right: 0; - margin-left: 1rem; - } -} - -.footer-group-label { - color: $text-muted; -} - -.footer-parent-item { - margin-bottom: 0.5rem; -} - -.footer-info { - border-top: 1px solid $border-color; - color: $text-muted; - font-size: $font-size-sm; -} - .no-underline { text-decoration: none !important; } diff --git a/frappe/templates/includes/footer/footer.html b/frappe/templates/includes/footer/footer.html index 671e928d32..2016c7e3d9 100644 --- a/frappe/templates/includes/footer/footer.html +++ b/frappe/templates/includes/footer/footer.html @@ -1,46 +1,12 @@
- {%- if footer_logo -%} -
- -
- {%- endif -%} -
-
- {% if footer_items -%} -
- {% include ["templates/includes/footer/footer_grouped_links.html", "templates/includes/footer/footer_items.html"] %} -
- {% endif %} -
+ {% include "templates/includes/footer/footer_logo_extension.html" %} -
- {% block extension %} - {% include "templates/includes/footer/footer_extension.html" %} - {% endblock %} -
-
+ {% if footer_items -%} + {% include "templates/includes/footer/footer_grouped_links.html" %} + {% endif %} {% include "templates/includes/footer/footer_links.html" %} - - + {% include "templates/includes/footer/footer_info.html" %}
diff --git a/frappe/templates/includes/footer/footer_grouped_links.html b/frappe/templates/includes/footer/footer_grouped_links.html index 6e20c51279..22cdb10824 100644 --- a/frappe/templates/includes/footer/footer_grouped_links.html +++ b/frappe/templates/includes/footer/footer_grouped_links.html @@ -1,28 +1,34 @@ -{% for page in footer_items if page.child_items %} -