diff --git a/.eslintrc b/.eslintrc
index a80d2910fa..cc7f555669 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -149,6 +149,7 @@
"before": true,
"beforeEach": true,
"qz": true,
- "localforage": true
+ "localforage": true,
+ "extend_cscript": true
}
}
diff --git a/.flake8 b/.flake8
index 399b176e1d..56c9b9a369 100644
--- a/.flake8
+++ b/.flake8
@@ -29,4 +29,5 @@ ignore =
B950,
W191,
-max-line-length = 200
\ No newline at end of file
+max-line-length = 200
+exclude=.github/helper/semgrep_rules
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
new file mode 100644
index 0000000000..4faece896a
--- /dev/null
+++ b/.git-blame-ignore-revs
@@ -0,0 +1,12 @@
+# Since version 2.23 (released in August 2019), git-blame has a feature
+# to ignore or bypass certain commits.
+#
+# This file contains a list of commits that are not likely what you
+# are looking for in a blame, such as mass reformatting or renaming.
+# You can set this file as a default ignore file for blame by running
+# the following command.
+#
+# $ git config blame.ignoreRevsFile .git-blame-ignore-revs
+
+# Replace use of Class.extend with native JS class
+fe20515c23a3ac41f1092bf0eaf0a0a452ec2e85
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
index 37889fbbb1..745e6463b8 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.py
+++ b/.github/helper/semgrep_rules/frappe_correctness.py
@@ -4,25 +4,61 @@ from frappe import _, flt
from frappe.model.document import Document
+# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
- # ruleid: frappe-modifying-after-submit
self.status = 'Submitted'
-def on_submit(self): # noqa
- if flt(self.per_billed) < 100:
- self.update_billing_status()
- else:
- # todook: frappe-modifying-after-submit
- self.status = "Completed"
- self.db_set("status", "Completed")
-class TestDoc(Document):
- pass
+# ok: frappe-modifying-but-not-comitting
+def on_submit(self):
+ if self.value_of_goods == 0:
+ frappe.throw(_('Value of goods cannot be 0'))
+ self.status = 'Submitted'
+ self.db_set('status', 'Submitted')
- def validate(self):
- #ruleid: frappe-modifying-child-tables-while-iterating
- for item in self.child_table:
- if item.value < 0:
- self.remove(item)
+# ok: frappe-modifying-but-not-comitting
+def on_submit(self):
+ if self.value_of_goods == 0:
+ frappe.throw(_('Value of goods cannot be 0'))
+ x = "y"
+ self.status = x
+ self.db_set('status', x)
+
+
+# ok: frappe-modifying-but-not-comitting
+def on_submit(self):
+ x = "y"
+ self.status = x
+ self.save()
+
+# ruleid: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+
+ def tainted_method(self):
+ self.status = "uptate"
+
+
+# ok: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+
+ def tainted_method(self):
+ self.status = "update"
+ self.db_set("status", "update")
+
+# ok: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+ self.save()
+
+ def tainted_method(self):
+ self.status = "uptate"
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
index 7b92fe2dff..9cdfb75d0b 100644
--- a/.github/helper/semgrep_rules/translate.js
+++ b/.github/helper/semgrep_rules/translate.js
@@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])
+
+// ok: frappe-translation-js-splitting
+__("Ctrl+Enter to add comment")
+
+// ruleid: frappe-translation-js-splitting
+__('You have {0} subscribers \
+ in your mailing list', [subscribers.length])
diff --git a/.github/helper/semgrep_rules/translate.py b/.github/helper/semgrep_rules/translate.py
index bd6cd9126c..9de6aa94f0 100644
--- a/.github/helper/semgrep_rules/translate.py
+++ b/.github/helper/semgrep_rules/translate.py
@@ -51,3 +51,11 @@ _(f"what" + f"this is also not cool")
_("")
# ruleid: frappe-translation-empty-string
_('')
+
+
+class Test:
+ # ok: frappe-translation-python-splitting
+ def __init__(
+ args
+ ):
+ pass
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
index 7754b52efc..5f03fb9fd0 100644
--- a/.github/helper/semgrep_rules/translate.yml
+++ b/.github/helper/semgrep_rules/translate.yml
@@ -44,8 +44,8 @@ rules:
pattern-either:
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- - pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
- - pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
+ - pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
+ - pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
@@ -54,8 +54,8 @@ rules:
- id: frappe-translation-js-splitting
pattern-either:
- - pattern-regex: '__\([^\)]*[\+\\]\s*'
- - pattern: __('...' + '...')
+ - pattern-regex: '__\([^\)]*[\\]\s+'
+ - pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
index 2a934a6795..a23885b508 100644
--- a/.github/workflows/publish-assets-develop.yml
+++ b/.github/workflows/publish-assets-develop.yml
@@ -15,11 +15,11 @@ jobs:
path: 'frappe'
- uses: actions/setup-node@v1
with:
- python-version: '12.x'
+ node-version: 14
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- - name: Set up bench for current push
+ - name: Set up bench and build assets
run: |
npm install -g yarn
pip3 install -U frappe-bench
@@ -29,7 +29,7 @@ jobs:
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
- tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
+ tar -cvpzf $GITHUB_WORKSPACE/build/$GITHUB_SHA.tar.gz ./frappe-bench/sites/assets/frappe/dist
- name: Publish assets to S3
uses: jakejarvis/s3-sync-action@master
diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml
index e86f884f35..a697517c23 100644
--- a/.github/workflows/publish-assets-releases.yml
+++ b/.github/workflows/publish-assets-releases.yml
@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-python@v2
with:
python-version: '3.6'
- - name: Set up bench for current push
+ - name: Set up bench and build assets
run: |
npm install -g yarn
pip3 install -U frappe-bench
@@ -32,7 +32,7 @@ jobs:
- name: Package assets
run: |
mkdir -p $GITHUB_WORKSPACE/build
- tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/js ./frappe-bench/sites/assets/css
+ tar -cvpzf $GITHUB_WORKSPACE/build/assets.tar.gz ./frappe-bench/sites/assets/frappe/dist
- name: Get release
id: get_release
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 5092bf4705..389524e968 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
+ - version-13-hotfix
+ - version-13-pre-release
jobs:
semgrep:
name: Frappe Linter
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/server-mariadb-tests.yml
similarity index 51%
rename from .github/workflows/ci-tests.yml
rename to .github/workflows/server-mariadb-tests.yml
index d2fbef528b..1742e813c6 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/server-mariadb-tests.yml
@@ -1,10 +1,8 @@
-name: CI
+name: Server
on:
pull_request:
- types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
- push:
jobs:
test:
@@ -13,23 +11,9 @@ jobs:
strategy:
fail-fast: false
matrix:
- include:
- - DB: "mariadb"
- TYPE: "server"
- JOB_NAME: "Python MariaDB"
- RUN_COMMAND: bench --site test_site run-tests --coverage
+ container: [1, 2]
- - DB: "postgres"
- TYPE: "server"
- JOB_NAME: "Python PostgreSQL"
- RUN_COMMAND: bench --site test_site run-tests --coverage
-
- - DB: "mariadb"
- TYPE: "ui"
- JOB_NAME: "UI MariaDB"
- RUN_COMMAND: bench --site test_site run-ui-tests frappe --headless
-
- name: ${{ matrix.JOB_NAME }}
+ name: Python Unit Tests (MariaDB)
services:
mysql:
@@ -40,18 +24,6 @@ jobs:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
- postgres:
- image: postgres:12.4
- env:
- POSTGRES_PASSWORD: travis
- options: >-
- --health-cmd pg_isready
- --health-interval 10s
- --health-timeout 5s
- --health-retries 5
- ports:
- - 5432:5432
-
steps:
- name: Clone
uses: actions/checkout@v2
@@ -63,7 +35,7 @@ jobs:
- uses: actions/setup-node@v2
with:
- node-version: '12'
+ node-version: 14
check-latest: true
- name: Add to Hosts
@@ -104,68 +76,54 @@ jobs:
restore-keys: |
${{ runner.os }}-yarn-
- - name: Cache cypress binary
- if: matrix.TYPE == 'ui'
- uses: actions/cache@v2
- with:
- path: ~/.cache
- key: ${{ runner.os }}-cypress-
- restore-keys: |
- ${{ runner.os }}-cypress-
- ${{ runner.os }}-
-
- name: Install Dependencies
run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
- TYPE: ${{ matrix.TYPE }}
+ TYPE: server
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
- DB: ${{ matrix.DB }}
- TYPE: ${{ matrix.TYPE }}
+ DB: mariadb
+ TYPE: server
- - name: Run Set-Up
- if: matrix.TYPE == 'ui'
- run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
- env:
- DB: ${{ matrix.DB }}
- TYPE: ${{ matrix.TYPE }}
-
- - name: Setup tmate session
- if: contains(github.event.pull_request.labels.*.name, 'debug-gha')
- uses: mxschmitt/action-tmate@v3
- name: Run Tests
- run: cd ~/frappe-bench/ && ${{ matrix.RUN_COMMAND }}
+ run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator --with-coverage
env:
- DB: ${{ matrix.DB }}
- TYPE: ${{ matrix.TYPE }}
+ CI_BUILD_ID: ${{ github.run_id }}
+ ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- - name: Coverage - Pull Request
- if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
+ - name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
- pip install coveralls==2.2.0
- pip install coverage==4.5.4
- coveralls --service=github
+ pip3 install coverage==5.5
+ pip3 install coveralls==3.0.1
+ coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
- COVERALLS_SERVICE_NAME: github
-
- - name: Coverage - Push
- if: matrix.TYPE == 'server' && github.event_name == 'push'
+ COVERALLS_FLAG_NAME: run-${{ matrix.container }}
+ COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
+ COVERALLS_PARALLEL: true
+
+ coveralls:
+ name: Coverage Wrap Up
+ needs: test
+ container: python:3-slim
+ runs-on: ubuntu-18.04
+ steps:
+ - name: Clone
+ uses: actions/checkout@v2
+
+ - name: Coveralls Finished
run: |
- cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
- pip install coveralls==2.2.0
- pip install coverage==4.5.4
- coveralls --service=github-actions
+ pip3 install coverage==5.5
+ pip3 install coveralls==3.0.1
+ coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
- COVERALLS_SERVICE_NAME: github-actions
diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml
new file mode 100644
index 0000000000..4325eebaad
--- /dev/null
+++ b/.github/workflows/server-postgres-tests.yml
@@ -0,0 +1,100 @@
+name: Server
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ fail-fast: false
+ matrix:
+ container: [1, 2]
+
+ name: Python Unit Tests (Postgres)
+
+ services:
+ postgres:
+ image: postgres:12.4
+ env:
+ POSTGRES_PASSWORD: travis
+ options: >-
+ --health-cmd pg_isready
+ --health-interval 10s
+ --health-timeout 5s
+ --health-retries 5
+ ports:
+ - 5432:5432
+
+ steps:
+ - name: Clone
+ uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: '14'
+ check-latest: true
+
+ - name: Add to Hosts
+ run: |
+ echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
+ echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
+
+ - name: Cache pip
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+
+ - name: Cache node modules
+ uses: actions/cache@v2
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Install Dependencies
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
+ env:
+ BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
+ AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
+ TYPE: server
+
+ - name: Install
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ DB: postgres
+ TYPE: server
+
+ - name: Run Tests
+ run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator
+ env:
+ CI_BUILD_ID: ${{ github.run_id }}
+ ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
new file mode 100644
index 0000000000..d9ccb07da0
--- /dev/null
+++ b/.github/workflows/ui-tests.yml
@@ -0,0 +1,105 @@
+name: UI
+
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ test:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ fail-fast: false
+ matrix:
+ containers: [1, 2]
+
+ name: UI Tests (Cypress)
+
+ services:
+ mysql:
+ image: mariadb:10.3
+ env:
+ MYSQL_ALLOW_EMPTY_PASSWORD: YES
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
+
+ steps:
+ - name: Clone
+ uses: actions/checkout@v2
+
+ - name: Setup Python
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.7
+
+ - uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ check-latest: true
+
+ - name: Add to Hosts
+ run: |
+ echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
+ echo "127.0.0.1 test_site_producer" | sudo tee -a /etc/hosts
+
+ - name: Cache pip
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache/pip
+ key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
+ restore-keys: |
+ ${{ runner.os }}-pip-
+ ${{ runner.os }}-
+
+ - name: Cache node modules
+ uses: actions/cache@v2
+ env:
+ cache-name: cache-node-modules
+ with:
+ path: ~/.npm
+ key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
+ restore-keys: |
+ ${{ runner.os }}-build-${{ env.cache-name }}-
+ ${{ runner.os }}-build-
+ ${{ runner.os }}-
+
+ - name: Get yarn cache directory path
+ id: yarn-cache-dir-path
+ run: echo "::set-output name=dir::$(yarn cache dir)"
+
+ - uses: actions/cache@v2
+ id: yarn-cache
+ with:
+ path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
+ key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-yarn-
+
+ - name: Cache cypress binary
+ uses: actions/cache@v2
+ with:
+ path: ~/.cache
+ key: ${{ runner.os }}-cypress-
+ restore-keys: |
+ ${{ runner.os }}-cypress-
+ ${{ runner.os }}-
+
+ - name: Install Dependencies
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
+ env:
+ BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
+ AFTER: ${{ env.GITHUB_EVENT_PATH.after }}
+ TYPE: ui
+
+ - name: Install
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ DB: mariadb
+ TYPE: ui
+
+ - name: Site Setup
+ run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
+
+ - name: UI Tests
+ run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
diff --git a/.gitignore b/.gitignore
index 766288fe2e..1ff3122d70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ locale
dist/
# build/
frappe/docs/current
+frappe/public/dist
.vscode
node_modules
.kdev4/
diff --git a/.mergify.yml b/.mergify.yml
index 82f710a5a8..c759c1e3ec 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -3,9 +3,12 @@ pull_request_rules:
conditions:
- status-success=Sider
- status-success=Semantic Pull Request
- - status-success=Python MariaDB
- - status-success=Python PostgreSQL
- - status-success=UI MariaDB
+ - status-success=Python Unit Tests (MariaDB) (1)
+ - status-success=Python Unit Tests (MariaDB) (2)
+ - status-success=Python Unit Tests (Postgres) (1)
+ - status-success=Python Unit Tests (Postgres) (2)
+ - status-success=UI Tests (Cypress) (1)
+ - status-success=UI Tests (Cypress) (2)
- status-success=security/snyk (frappe)
- label!=dont-merge
- label!=squash
@@ -16,9 +19,12 @@ pull_request_rules:
- name: Automatic squash on CI success and review
conditions:
- status-success=Sider
- - status-success=Python MariaDB
- - status-success=Python PostgreSQL
- - status-success=UI MariaDB
+ - status-success=Python Unit Tests (MariaDB) (1)
+ - status-success=Python Unit Tests (MariaDB) (2)
+ - status-success=Python Unit Tests (Postgres) (1)
+ - status-success=Python Unit Tests (Postgres) (2)
+ - status-success=UI Tests (Cypress) (1)
+ - status-success=UI Tests (Cypress) (2)
- status-success=security/snyk (frappe)
- label!=dont-merge
- label=squash
diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js
index d30cc3568c..5b7692d8ff 100644
--- a/cypress/integration/recorder.js
+++ b/cypress/integration/recorder.js
@@ -50,7 +50,7 @@ context('Recorder', () => {
cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get');
});
- it.only('Recorder View Request', () => {
+ it('Recorder View Request', () => {
cy.get('.primary-action').should('contain', 'Start').click();
cy.visit('/app/List/DocType/List');
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
new file mode 100644
index 0000000000..ecf0d49511
--- /dev/null
+++ b/esbuild/esbuild.js
@@ -0,0 +1,486 @@
+/* eslint-disable no-console */
+let path = require("path");
+let fs = require("fs");
+let glob = require("fast-glob");
+let esbuild = require("esbuild");
+let vue = require("esbuild-vue");
+let yargs = require("yargs");
+let cliui = require("cliui")();
+let chalk = require("chalk");
+let html_plugin = require("./frappe-html");
+let postCssPlugin = require("esbuild-plugin-postcss2").default;
+let ignore_assets = require("./ignore-assets");
+let sass_options = require("./sass_options");
+let {
+ app_list,
+ assets_path,
+ apps_path,
+ sites_path,
+ get_app_path,
+ get_public_path,
+ log,
+ log_warn,
+ log_error,
+ bench_path,
+ get_redis_subscriber
+} = require("./utils");
+
+let argv = yargs
+ .usage("Usage: node esbuild [options]")
+ .option("apps", {
+ type: "string",
+ description: "Run build for specific apps"
+ })
+ .option("skip_frappe", {
+ type: "boolean",
+ description: "Skip building frappe assets"
+ })
+ .option("files", {
+ type: "string",
+ description: "Run build for specified bundles"
+ })
+ .option("watch", {
+ type: "boolean",
+ description: "Run in watch mode and rebuild on file changes"
+ })
+ .option("production", {
+ type: "boolean",
+ description: "Run build in production mode"
+ })
+ .option("run-build-command", {
+ type: "boolean",
+ description: "Run build command for apps"
+ })
+ .example(
+ "node esbuild --apps frappe,erpnext",
+ "Run build only for frappe and erpnext"
+ )
+ .example(
+ "node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
+ "Run build only for specified bundles"
+ )
+ .version(false).argv;
+
+const APPS = (!argv.apps ? app_list : argv.apps.split(",")).filter(
+ app => !(argv.skip_frappe && app == "frappe")
+);
+const FILES_TO_BUILD = argv.files ? argv.files.split(",") : [];
+const WATCH_MODE = Boolean(argv.watch);
+const PRODUCTION = Boolean(argv.production);
+const RUN_BUILD_COMMAND = !WATCH_MODE && Boolean(argv["run-build-command"]);
+
+const TOTAL_BUILD_TIME = `${chalk.black.bgGreen(" DONE ")} Total Build Time`;
+const NODE_PATHS = [].concat(
+ // node_modules of apps directly importable
+ app_list
+ .map(app => path.resolve(get_app_path(app), "../node_modules"))
+ .filter(fs.existsSync),
+ // import js file of any app if you provide the full path
+ app_list
+ .map(app => path.resolve(get_app_path(app), ".."))
+ .filter(fs.existsSync)
+);
+
+execute()
+ .then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
+ .catch(e => console.error(e));
+
+if (WATCH_MODE) {
+ // listen for open files in editor event
+ open_in_editor();
+}
+
+async function execute() {
+ console.time(TOTAL_BUILD_TIME);
+ if (!FILES_TO_BUILD.length) {
+ await clean_dist_folders(APPS);
+ }
+
+ let result;
+ try {
+ result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
+ } catch (e) {
+ log_error("There were some problems during build");
+ log();
+ log(chalk.dim(e.stack));
+ return;
+ }
+
+ if (!WATCH_MODE) {
+ log_built_assets(result.metafile);
+ console.timeEnd(TOTAL_BUILD_TIME);
+ log();
+ } else {
+ log("Watching for changes...");
+ }
+ return await write_assets_json(result.metafile);
+}
+
+function build_assets_for_apps(apps, files) {
+ let { include_patterns, ignore_patterns } = files.length
+ ? get_files_to_build(files)
+ : get_all_files_to_build(apps);
+
+ return glob(include_patterns, { ignore: ignore_patterns }).then(files => {
+ let output_path = assets_path;
+
+ let file_map = {};
+ for (let file of files) {
+ let relative_app_path = path.relative(apps_path, file);
+ let app = relative_app_path.split(path.sep)[0];
+
+ let extension = path.extname(file);
+ let output_name = path.basename(file, extension);
+ if (
+ [".css", ".scss", ".less", ".sass", ".styl"].includes(extension)
+ ) {
+ output_name = path.join("css", output_name);
+ } else if ([".js", ".ts"].includes(extension)) {
+ output_name = path.join("js", output_name);
+ }
+ output_name = path.join(app, "dist", output_name);
+
+ if (Object.keys(file_map).includes(output_name)) {
+ log_warn(
+ `Duplicate output file ${output_name} generated from ${file}`
+ );
+ }
+
+ file_map[output_name] = file;
+ }
+
+ return build_files({
+ files: file_map,
+ outdir: output_path
+ });
+ });
+}
+
+function get_all_files_to_build(apps) {
+ let include_patterns = [];
+ let ignore_patterns = [];
+
+ for (let app of apps) {
+ let public_path = get_public_path(app);
+ include_patterns.push(
+ path.resolve(
+ public_path,
+ "**",
+ "*.bundle.{js,ts,css,sass,scss,less,styl}"
+ )
+ );
+ ignore_patterns.push(
+ path.resolve(public_path, "node_modules"),
+ path.resolve(public_path, "dist")
+ );
+ }
+
+ return {
+ include_patterns,
+ ignore_patterns
+ };
+}
+
+function get_files_to_build(files) {
+ // files: ['frappe/website.bundle.js', 'erpnext/main.bundle.js']
+ let include_patterns = [];
+ let ignore_patterns = [];
+
+ for (let file of files) {
+ let [app, bundle] = file.split("/");
+ let public_path = get_public_path(app);
+ include_patterns.push(path.resolve(public_path, "**", bundle));
+ ignore_patterns.push(
+ path.resolve(public_path, "node_modules"),
+ path.resolve(public_path, "dist")
+ );
+ }
+
+ return {
+ include_patterns,
+ ignore_patterns
+ };
+}
+
+function build_files({ files, outdir }) {
+ return esbuild.build({
+ entryPoints: files,
+ entryNames: "[dir]/[name].[hash]",
+ outdir,
+ sourcemap: true,
+ bundle: true,
+ metafile: true,
+ minify: PRODUCTION,
+ nodePaths: NODE_PATHS,
+ define: {
+ "process.env.NODE_ENV": JSON.stringify(
+ PRODUCTION ? "production" : "development"
+ )
+ },
+ plugins: [
+ html_plugin,
+ ignore_assets,
+ vue(),
+ postCssPlugin({
+ plugins: [require("autoprefixer")],
+ sassOptions: sass_options
+ })
+ ],
+ watch: get_watch_config()
+ });
+}
+
+function get_watch_config() {
+ if (WATCH_MODE) {
+ return {
+ async onRebuild(error, result) {
+ if (error) {
+ log_error("There was an error during rebuilding changes.");
+ log();
+ log(chalk.dim(error.stack));
+ notify_redis({ error });
+ } else {
+ let {
+ assets_json,
+ prev_assets_json
+ } = await write_assets_json(result.metafile);
+ if (prev_assets_json) {
+ log_rebuilt_assets(prev_assets_json, assets_json);
+ }
+ notify_redis({ success: true });
+ }
+ }
+ };
+ }
+ return null;
+}
+
+async function clean_dist_folders(apps) {
+ for (let app of apps) {
+ let public_path = get_public_path(app);
+ await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
+ recursive: true
+ });
+ await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
+ recursive: true
+ });
+ }
+}
+
+function log_built_assets(metafile) {
+ let column_widths = [60, 20];
+ cliui.div(
+ {
+ text: chalk.cyan.bold("File"),
+ width: column_widths[0]
+ },
+ {
+ text: chalk.cyan.bold("Size"),
+ width: column_widths[1]
+ }
+ );
+ cliui.div("");
+
+ let output_by_dist_path = {};
+ for (let outfile in metafile.outputs) {
+ if (outfile.endsWith(".map")) continue;
+ let data = metafile.outputs[outfile];
+ outfile = path.resolve(outfile);
+ outfile = path.relative(assets_path, outfile);
+ let filename = path.basename(outfile);
+ let dist_path = outfile.replace(filename, "");
+ output_by_dist_path[dist_path] = output_by_dist_path[dist_path] || [];
+ output_by_dist_path[dist_path].push({
+ name: filename,
+ size: (data.bytes / 1000).toFixed(2) + " Kb"
+ });
+ }
+
+ for (let dist_path in output_by_dist_path) {
+ let files = output_by_dist_path[dist_path];
+ cliui.div({
+ text: dist_path,
+ width: column_widths[0]
+ });
+
+ for (let i in files) {
+ let file = files[i];
+ let branch = "";
+ if (i < files.length - 1) {
+ branch = "├─ ";
+ } else {
+ branch = "└─ ";
+ }
+ let color = file.name.endsWith(".js") ? "green" : "blue";
+ cliui.div(
+ {
+ text: branch + chalk[color]("" + file.name),
+ width: column_widths[0]
+ },
+ {
+ text: file.size,
+ width: column_widths[1]
+ }
+ );
+ }
+ cliui.div("");
+ }
+ log(cliui.toString());
+}
+
+// to store previous build's assets.json for comparison
+let prev_assets_json;
+let curr_assets_json;
+
+async function write_assets_json(metafile) {
+ prev_assets_json = curr_assets_json;
+ let out = {};
+ for (let output in metafile.outputs) {
+ let info = metafile.outputs[output];
+ let asset_path = "/" + path.relative(sites_path, output);
+ if (info.entryPoint) {
+ out[path.basename(info.entryPoint)] = asset_path;
+ }
+ }
+
+ let assets_json_path = path.resolve(
+ assets_path,
+ "frappe",
+ "dist",
+ "assets.json"
+ );
+ let assets_json;
+ try {
+ assets_json = await fs.promises.readFile(assets_json_path, "utf-8");
+ } catch (error) {
+ assets_json = "{}";
+ }
+ assets_json = JSON.parse(assets_json);
+ // update with new values
+ assets_json = Object.assign({}, assets_json, out);
+ curr_assets_json = assets_json;
+
+ await fs.promises.writeFile(
+ assets_json_path,
+ JSON.stringify(assets_json, null, 4)
+ );
+ await update_assets_json_in_cache(assets_json);
+ return {
+ assets_json,
+ prev_assets_json
+ };
+}
+
+function update_assets_json_in_cache(assets_json) {
+ // update assets_json cache in redis, so that it can be read directly by python
+ return new Promise(resolve => {
+ let client = get_redis_subscriber("redis_cache");
+ // handle error event to avoid printing stack traces
+ client.on("error", _ => {
+ log_warn("Cannot connect to redis_cache to update assets_json");
+ });
+ client.set("assets_json", JSON.stringify(assets_json), err => {
+ client.unref();
+ resolve();
+ });
+ });
+}
+
+function run_build_command_for_apps(apps) {
+ let cwd = process.cwd();
+ let { execSync } = require("child_process");
+
+ for (let app of apps) {
+ if (app === "frappe") continue;
+
+ let root_app_path = path.resolve(get_app_path(app), "..");
+ let package_json = path.resolve(root_app_path, "package.json");
+ if (fs.existsSync(package_json)) {
+ let { scripts } = require(package_json);
+ if (scripts && scripts.build) {
+ log("\nRunning build command for", chalk.bold(app));
+ process.chdir(root_app_path);
+ execSync("yarn build", { encoding: "utf8", stdio: "inherit" });
+ }
+ }
+ }
+
+ process.chdir(cwd);
+}
+
+async function notify_redis({ error, success }) {
+ // notify redis which in turns tells socketio to publish this to browser
+ let subscriber = get_redis_subscriber("redis_socketio");
+ subscriber.on("error", _ => {
+ log_warn("Cannot connect to redis_socketio for browser events");
+ });
+
+ let payload = null;
+ if (error) {
+ let formatted = await esbuild.formatMessages(error.errors, {
+ kind: "error",
+ terminalWidth: 100
+ });
+ let stack = error.stack.replace(new RegExp(bench_path, "g"), "");
+ payload = {
+ error,
+ formatted,
+ stack
+ };
+ }
+ if (success) {
+ payload = {
+ success: true
+ };
+ }
+
+ subscriber.publish(
+ "events",
+ JSON.stringify({
+ event: "build_event",
+ message: payload
+ })
+ );
+}
+
+function open_in_editor() {
+ let subscriber = get_redis_subscriber("redis_socketio");
+ subscriber.on("error", _ => {
+ log_warn("Cannot connect to redis_socketio for open_in_editor events");
+ });
+ subscriber.on("message", (event, file) => {
+ if (event === "open_in_editor") {
+ file = JSON.parse(file);
+ let file_path = path.resolve(file.file);
+ log("Opening file in editor:", file_path);
+ let launch = require("launch-editor");
+ launch(`${file_path}:${file.line}:${file.column}`);
+ }
+ });
+ subscriber.subscribe("open_in_editor");
+}
+
+function log_rebuilt_assets(prev_assets, new_assets) {
+ let added_files = [];
+ let old_files = Object.values(prev_assets);
+ let new_files = Object.values(new_assets);
+
+ for (let filepath of new_files) {
+ if (!old_files.includes(filepath)) {
+ added_files.push(filepath);
+ }
+ }
+
+ log(
+ chalk.yellow(
+ `${new Date().toLocaleTimeString()}: Compiled ${
+ added_files.length
+ } files...`
+ )
+ );
+ for (let filepath of added_files) {
+ let filename = path.basename(filepath);
+ log(" " + filename);
+ }
+ log();
+}
diff --git a/esbuild/frappe-html.js b/esbuild/frappe-html.js
new file mode 100644
index 0000000000..8c4b7ca3d7
--- /dev/null
+++ b/esbuild/frappe-html.js
@@ -0,0 +1,43 @@
+module.exports = {
+ name: "frappe-html",
+ setup(build) {
+ let path = require("path");
+ let fs = require("fs/promises");
+
+ build.onResolve({ filter: /\.html$/ }, args => {
+ return {
+ path: path.join(args.resolveDir, args.path),
+ namespace: "frappe-html"
+ };
+ });
+
+ build.onLoad({ filter: /.*/, namespace: "frappe-html" }, args => {
+ let filepath = args.path;
+ let filename = path.basename(filepath).split(".")[0];
+
+ return fs
+ .readFile(filepath, "utf-8")
+ .then(content => {
+ content = scrub_html_template(content);
+ return {
+ contents: `\n\tfrappe.templates['${filename}'] = \`${content}\`;\n`
+ };
+ })
+ .catch(() => {
+ return {
+ contents: "",
+ warnings: [
+ {
+ text: `There was an error importing ${filepath}`
+ }
+ ]
+ };
+ });
+ });
+ }
+};
+
+function scrub_html_template(content) {
+ content = content.replace(/`/g, "\\`");
+ return content;
+}
diff --git a/esbuild/ignore-assets.js b/esbuild/ignore-assets.js
new file mode 100644
index 0000000000..5edfef2110
--- /dev/null
+++ b/esbuild/ignore-assets.js
@@ -0,0 +1,11 @@
+module.exports = {
+ name: "frappe-ignore-asset",
+ setup(build) {
+ build.onResolve({ filter: /^\/assets\// }, args => {
+ return {
+ path: args.path,
+ external: true
+ };
+ });
+ }
+};
diff --git a/esbuild/index.js b/esbuild/index.js
new file mode 100644
index 0000000000..2721673702
--- /dev/null
+++ b/esbuild/index.js
@@ -0,0 +1 @@
+require("./esbuild");
diff --git a/esbuild/sass_options.js b/esbuild/sass_options.js
new file mode 100644
index 0000000000..fcc7e04ccd
--- /dev/null
+++ b/esbuild/sass_options.js
@@ -0,0 +1,29 @@
+let path = require("path");
+let { get_app_path, app_list } = require("./utils");
+
+let node_modules_path = path.resolve(
+ get_app_path("frappe"),
+ "..",
+ "node_modules"
+);
+let app_paths = app_list
+ .map(get_app_path)
+ .map(app_path => path.resolve(app_path, ".."));
+
+module.exports = {
+ includePaths: [node_modules_path, ...app_paths],
+ importer: function(url) {
+ if (url.startsWith("~")) {
+ // strip ~ so that it can resolve from node_modules
+ url = url.slice(1);
+ }
+ if (url.endsWith(".css")) {
+ // strip .css from end of path
+ url = url.slice(0, -4);
+ }
+ // normal file, let it go
+ return {
+ file: url
+ };
+ }
+};
diff --git a/esbuild/utils.js b/esbuild/utils.js
new file mode 100644
index 0000000000..82490adb36
--- /dev/null
+++ b/esbuild/utils.js
@@ -0,0 +1,145 @@
+const path = require("path");
+const fs = require("fs");
+const chalk = require("chalk");
+
+const frappe_path = path.resolve(__dirname, "..");
+const bench_path = path.resolve(frappe_path, "..", "..");
+const sites_path = path.resolve(bench_path, "sites");
+const apps_path = path.resolve(bench_path, "apps");
+const assets_path = path.resolve(sites_path, "assets");
+const app_list = get_apps_list();
+
+const app_paths = app_list.reduce((out, app) => {
+ out[app] = path.resolve(apps_path, app, app);
+ return out;
+}, {});
+const public_paths = app_list.reduce((out, app) => {
+ out[app] = path.resolve(app_paths[app], "public");
+ return out;
+}, {});
+const public_js_paths = app_list.reduce((out, app) => {
+ out[app] = path.resolve(app_paths[app], "public/js");
+ return out;
+}, {});
+
+const bundle_map = app_list.reduce((out, app) => {
+ const public_js_path = public_js_paths[app];
+ if (fs.existsSync(public_js_path)) {
+ const all_files = fs.readdirSync(public_js_path);
+ const js_files = all_files.filter(file => file.endsWith(".js"));
+
+ for (let js_file of js_files) {
+ const filename = path.basename(js_file).split(".")[0];
+ out[path.join(app, "js", filename)] = path.resolve(
+ public_js_path,
+ js_file
+ );
+ }
+ }
+
+ return out;
+}, {});
+
+const get_public_path = app => public_paths[app];
+
+const get_build_json_path = app =>
+ path.resolve(get_public_path(app), "build.json");
+
+function get_build_json(app) {
+ try {
+ return require(get_build_json_path(app));
+ } catch (e) {
+ // build.json does not exist
+ return null;
+ }
+}
+
+function delete_file(path) {
+ if (fs.existsSync(path)) {
+ fs.unlinkSync(path);
+ }
+}
+
+function run_serially(tasks) {
+ let result = Promise.resolve();
+ tasks.forEach(task => {
+ if (task) {
+ result = result.then ? result.then(task) : Promise.resolve();
+ }
+ });
+ return result;
+}
+
+const get_app_path = app => app_paths[app];
+
+function get_apps_list() {
+ return fs
+ .readFileSync(path.resolve(sites_path, "apps.txt"), {
+ encoding: "utf-8"
+ })
+ .split("\n")
+ .filter(Boolean);
+}
+
+function get_cli_arg(name) {
+ let args = process.argv.slice(2);
+ let arg = `--${name}`;
+ let index = args.indexOf(arg);
+
+ let value = null;
+ if (index != -1) {
+ value = true;
+ }
+ if (value && args[index + 1]) {
+ value = args[index + 1];
+ }
+ return value;
+}
+
+function log_error(message, badge = "ERROR") {
+ badge = chalk.white.bgRed(` ${badge} `);
+ console.error(`${badge} ${message}`); // eslint-disable-line no-console
+}
+
+function log_warn(message, badge = "WARN") {
+ badge = chalk.black.bgYellowBright(` ${badge} `);
+ console.warn(`${badge} ${message}`); // eslint-disable-line no-console
+}
+
+function log(...args) {
+ console.log(...args); // eslint-disable-line no-console
+}
+
+function get_redis_subscriber(kind) {
+ // get redis subscriber that aborts after 10 connection attempts
+ let { get_redis_subscriber: get_redis } = require("../node_utils");
+ return get_redis(kind, {
+ retry_strategy: function(options) {
+ // abort after 10 connection attempts
+ if (options.attempt > 10) {
+ return undefined;
+ }
+ return Math.min(options.attempt * 100, 2000);
+ }
+ });
+}
+
+module.exports = {
+ app_list,
+ bench_path,
+ assets_path,
+ sites_path,
+ apps_path,
+ bundle_map,
+ get_public_path,
+ get_build_json_path,
+ get_build_json,
+ get_app_path,
+ delete_file,
+ run_serially,
+ get_cli_arg,
+ log,
+ log_warn,
+ log_error,
+ get_redis_subscriber
+};
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 02b8d71e40..9b208f7c2d 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -10,9 +10,16 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
+import os, warnings
+
+_dev_server = os.environ.get('DEV_SERVER', False)
+
+if _dev_server:
+ warnings.simplefilter('always', DeprecationWarning)
+ warnings.simplefilter('always', PendingDeprecationWarning)
from werkzeug.local import Local, release_local
-import os, sys, importlib, inspect, json, warnings
+import sys, importlib, inspect, json
import typing
from past.builtins import cmp
import click
@@ -31,8 +38,6 @@ __title__ = "Frappe Framework"
local = Local()
controllers = {}
-warnings.simplefilter('always', DeprecationWarning)
-warnings.simplefilter('always', PendingDeprecationWarning)
class _dict(dict):
"""dict like object that exposes keys as attributes"""
@@ -197,7 +202,7 @@ def init(site, sites_path=None, new_site=False):
local.meta_cache = {}
local.form_dict = _dict()
local.session = _dict()
- local.dev_server = os.environ.get('DEV_SERVER', False)
+ local.dev_server = _dev_server
setup_module_map()
diff --git a/frappe/app.py b/frappe/app.py
index a72f343532..64befdf531 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -99,17 +99,7 @@ def application(request):
frappe.monitor.stop(response)
frappe.recorder.dump()
- if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
- frappe.logger("frappe.web", allow_site=frappe.local.site).info({
- "site": get_site_name(request.host),
- "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
- "base_url": getattr(request, "base_url", "NOTFOUND"),
- "full_path": getattr(request, "full_path", "NOTFOUND"),
- "method": getattr(request, "method", "NOTFOUND"),
- "scheme": getattr(request, "scheme", "NOTFOUND"),
- "http_status_code": getattr(response, "status_code", "NOTFOUND")
- })
-
+ log_request(request, response)
process_response(response)
frappe.destroy()
@@ -137,6 +127,19 @@ def init_request(request):
if request.method != "OPTIONS":
frappe.local.http_request = frappe.auth.HTTPRequest()
+def log_request(request, response):
+ if hasattr(frappe.local, 'conf') and frappe.local.conf.enable_frappe_logger:
+ frappe.logger("frappe.web", allow_site=frappe.local.site).info({
+ "site": get_site_name(request.host),
+ "remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
+ "base_url": getattr(request, "base_url", "NOTFOUND"),
+ "full_path": getattr(request, "full_path", "NOTFOUND"),
+ "method": getattr(request, "method", "NOTFOUND"),
+ "scheme": getattr(request, "scheme", "NOTFOUND"),
+ "http_status_code": getattr(response, "status_code", "NOTFOUND")
+ })
+
+
def process_response(response):
if not response:
return
diff --git a/frappe/build.py b/frappe/build.py
index 5b541b4852..c970ae3a28 100644
--- a/frappe/build.py
+++ b/frappe/build.py
@@ -1,14 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-from __future__ import print_function, unicode_literals
-
import os
import re
import json
import shutil
-import warnings
-import tempfile
+import subprocess
+from tempfile import mkdtemp, mktemp
from distutils.spawn import find_executable
import frappe
@@ -16,8 +14,9 @@ from frappe.utils.minify import JavascriptMinify
import click
import psutil
-from six import iteritems, text_type
-from six.moves.urllib.parse import urlparse
+from urllib.parse import urlparse
+from simple_chalk import green
+from semantic_version import Version
timestamps = {}
@@ -39,35 +38,36 @@ def download_file(url, prefix):
def build_missing_files():
- # check which files dont exist yet from the build.json and tell build.js to build only those!
+ '''Check which files dont exist yet from the assets.json and run build for those files'''
+
missing_assets = []
current_asset_files = []
- frappe_build = os.path.join("..", "apps", "frappe", "frappe", "public", "build.json")
for type in ["css", "js"]:
- current_asset_files.extend(
- [
- "{0}/{1}".format(type, name)
- for name in os.listdir(os.path.join(sites_path, "assets", type))
- ]
- )
+ folder = os.path.join(sites_path, "assets", "frappe", "dist", type)
+ current_asset_files.extend(os.listdir(folder))
- with open(frappe_build) as f:
- all_asset_files = json.load(f).keys()
+ development = frappe.local.conf.developer_mode or frappe.local.dev_server
+ build_mode = "development" if development else "production"
- for asset in all_asset_files:
- if asset.replace("concat:", "") not in current_asset_files:
- missing_assets.append(asset)
+ assets_json = frappe.read_file(frappe.get_app_path('frappe', 'public', 'dist', 'assets.json'))
+ if assets_json:
+ assets_json = frappe.parse_json(assets_json)
- if missing_assets:
- from subprocess import check_call
- from shlex import split
+ for bundle_file, output_file in assets_json.items():
+ if not output_file.startswith('/assets/frappe'):
+ continue
- click.secho("\nBuilding missing assets...\n", fg="yellow")
- command = split(
- "node rollup/build.js --files {0} --no-concat".format(",".join(missing_assets))
- )
- check_call(command, cwd=os.path.join("..", "apps", "frappe"))
+ if os.path.basename(output_file) not in current_asset_files:
+ missing_assets.append(bundle_file)
+
+ if missing_assets:
+ click.secho("\nBuilding missing assets...\n", fg="yellow")
+ files_to_build = ["frappe/" + name for name in missing_assets]
+ bundle(build_mode, files=files_to_build)
+ else:
+ # no assets.json, run full build
+ bundle(build_mode, apps="frappe")
def get_assets_link(frappe_head):
@@ -75,8 +75,8 @@ def get_assets_link(frappe_head):
from requests import head
tag = getoutput(
- "cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
- " refs/tags/,,' -e 's/\^{}//'"
+ r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
+ r" refs/tags/,,' -e 's/\^{}//'"
% frappe_head
)
@@ -97,9 +97,7 @@ def download_frappe_assets(verbose=True):
commit HEAD.
Returns True if correctly setup else returns False.
"""
- from simple_chalk import green
from subprocess import getoutput
- from tempfile import mkdtemp
assets_setup = False
frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD")
@@ -166,7 +164,7 @@ def symlink(target, link_name, overwrite=False):
# Create link to target with temporary filename
while True:
- temp_link_name = tempfile.mktemp(dir=link_dir)
+ temp_link_name = mktemp(dir=link_dir)
# os.* functions mimic as closely as possible system functions
# The POSIX symlink() returns EEXIST if link_name already exists
@@ -193,7 +191,8 @@ def symlink(target, link_name, overwrite=False):
def setup():
- global app_paths
+ global app_paths, assets_path
+
pymodules = []
for app in frappe.get_all_apps(True):
try:
@@ -201,51 +200,54 @@ def setup():
except ImportError:
pass
app_paths = [os.path.dirname(pymodule.__file__) for pymodule in pymodules]
+ assets_path = os.path.join(frappe.local.sites_path, "assets")
-def get_node_pacman():
- exec_ = find_executable("yarn")
- if exec_:
- return exec_
- raise ValueError("Yarn not found")
-
-
-def bundle(no_compress, app=None, make_copy=False, restore=False, verbose=False, skip_frappe=False):
+def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, verbose=False, skip_frappe=False, files=None):
"""concat / minify js files"""
setup()
- make_asset_dirs(make_copy=make_copy, restore=restore)
+ make_asset_dirs(hard_link=hard_link)
- pacman = get_node_pacman()
- mode = "build" if no_compress else "production"
- command = "{pacman} run {mode}".format(pacman=pacman, mode=mode)
+ mode = "production" if mode == "production" else "build"
+ command = "yarn run {mode}".format(mode=mode)
- if app:
- command += " --app {app}".format(app=app)
+ if apps:
+ command += " --apps {apps}".format(apps=apps)
if skip_frappe:
command += " --skip_frappe"
- frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
- check_yarn()
+ if files:
+ command += " --files {files}".format(files=','.join(files))
+
+ command += " --run-build-command"
+
+ check_node_executable()
+ frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
-def watch(no_compress):
+def watch(apps=None):
"""watch and rebuild if necessary"""
setup()
- pacman = get_node_pacman()
+ command = "yarn run watch"
+ if apps:
+ command += " --apps {apps}".format(apps=apps)
- frappe_app_path = os.path.abspath(os.path.join(app_paths[0], ".."))
- check_yarn()
+ check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
- frappe.commands.popen("{pacman} run watch".format(pacman=pacman),
- cwd=frappe_app_path, env=get_node_env())
+ frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
-def check_yarn():
+def check_node_executable():
+ node_version = Version(subprocess.getoutput('node -v')[1:])
+ warn = '⚠️ '
+ if node_version.major < 14:
+ click.echo(f"{warn} Please update your node version to 14")
if not find_executable("yarn"):
- print("Please install yarn using below command and try again.\nnpm install -g yarn")
+ click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
+ click.echo()
def get_node_env():
node_env = {
@@ -266,75 +268,109 @@ def get_safe_max_old_space_size():
return safe_max_old_space_size
-def make_asset_dirs(make_copy=False, restore=False):
- # don't even think of making assets_path absolute - rm -rf ahead.
- assets_path = os.path.join(frappe.local.sites_path, "assets")
+def generate_assets_map():
+ symlinks = {}
- for dir_path in [os.path.join(assets_path, "js"), os.path.join(assets_path, "css")]:
- if not os.path.exists(dir_path):
- os.makedirs(dir_path)
+ for app_name in frappe.get_all_apps():
+ app_doc_path = None
- for app_name in frappe.get_all_apps(True):
pymodule = frappe.get_module(app_name)
app_base_path = os.path.abspath(os.path.dirname(pymodule.__file__))
-
- symlinks = []
app_public_path = os.path.join(app_base_path, "public")
- # app/public > assets/app
- symlinks.append([app_public_path, os.path.join(assets_path, app_name)])
- # app/node_modules > assets/app/node_modules
- if os.path.exists(os.path.abspath(app_public_path)):
- symlinks.append(
- [
- os.path.join(app_base_path, "..", "node_modules"),
- os.path.join(assets_path, app_name, "node_modules"),
- ]
- )
+ app_node_modules_path = os.path.join(app_base_path, "..", "node_modules")
+ app_docs_path = os.path.join(app_base_path, "docs")
+ app_www_docs_path = os.path.join(app_base_path, "www", "docs")
- app_doc_path = None
- if os.path.isdir(os.path.join(app_base_path, "docs")):
+ app_assets = os.path.abspath(app_public_path)
+ app_node_modules = os.path.abspath(app_node_modules_path)
+
+ # {app}/public > assets/{app}
+ if os.path.isdir(app_assets):
+ symlinks[app_assets] = os.path.join(assets_path, app_name)
+
+ # {app}/node_modules > assets/{app}/node_modules
+ if os.path.isdir(app_node_modules):
+ symlinks[app_node_modules] = os.path.join(assets_path, app_name, "node_modules")
+
+ # {app}/docs > assets/{app}_docs
+ if os.path.isdir(app_docs_path):
app_doc_path = os.path.join(app_base_path, "docs")
-
- elif os.path.isdir(os.path.join(app_base_path, "www", "docs")):
+ elif os.path.isdir(app_www_docs_path):
app_doc_path = os.path.join(app_base_path, "www", "docs")
-
if app_doc_path:
- symlinks.append([app_doc_path, os.path.join(assets_path, app_name + "_docs")])
+ app_docs = os.path.abspath(app_doc_path)
+ symlinks[app_docs] = os.path.join(assets_path, app_name + "_docs")
- for source, target in symlinks:
- source = os.path.abspath(source)
- if os.path.exists(source):
- if restore:
- if os.path.exists(target):
- if os.path.islink(target):
- os.unlink(target)
- else:
- shutil.rmtree(target)
- shutil.copytree(source, target)
- elif make_copy:
- if os.path.exists(target):
- warnings.warn("Target {target} already exists.".format(target=target))
- else:
- shutil.copytree(source, target)
- else:
- if os.path.exists(target):
- if os.path.islink(target):
- os.unlink(target)
- else:
- shutil.rmtree(target)
- try:
- symlink(source, target, overwrite=True)
- except OSError:
- print("Cannot link {} to {}".format(source, target))
- else:
- warnings.warn('Source {source} does not exist.'.format(source = source))
- pass
+ return symlinks
+
+
+def setup_assets_dirs():
+ for dir_path in (os.path.join(assets_path, x) for x in ("js", "css")):
+ os.makedirs(dir_path, exist_ok=True)
+
+
+def clear_broken_symlinks():
+ for path in os.listdir(assets_path):
+ path = os.path.join(assets_path, path)
+ if os.path.islink(path) and not os.path.exists(path):
+ os.remove(path)
+
+
+
+def unstrip(message: str) -> str:
+ """Pads input string on the right side until the last available column in the terminal
+ """
+ _len = len(message)
+ try:
+ max_str = os.get_terminal_size().columns
+ except Exception:
+ max_str = 80
+
+ if _len < max_str:
+ _rem = max_str - _len
+ else:
+ _rem = max_str % _len
+
+ return f"{message}{' ' * _rem}"
+
+
+def make_asset_dirs(hard_link=False):
+ setup_assets_dirs()
+ clear_broken_symlinks()
+ symlinks = generate_assets_map()
+
+ for source, target in symlinks.items():
+ start_message = unstrip(f"{'Copying assets from' if hard_link else 'Linking'} {source} to {target}")
+ fail_message = unstrip(f"Cannot {'copy' if hard_link else 'link'} {source} to {target}")
+
+ # Used '\r' instead of '\x1b[1K\r' to print entire lines in smaller terminal sizes
+ try:
+ print(start_message, end="\r")
+ link_assets_dir(source, target, hard_link=hard_link)
+ except Exception:
+ print(fail_message, end="\r")
+
+ print(unstrip(f"{green('✔')} Application Assets Linked") + "\n")
+
+
+def link_assets_dir(source, target, hard_link=False):
+ if not os.path.exists(source):
+ return
+
+ if os.path.exists(target):
+ if os.path.islink(target):
+ os.unlink(target)
+ else:
+ shutil.rmtree(target)
+
+ if hard_link:
+ shutil.copytree(source, target, dirs_exist_ok=True)
+ else:
+ symlink(source, target, overwrite=True)
def build(no_compress=False, verbose=False):
- assets_path = os.path.join(frappe.local.sites_path, "assets")
-
- for target, sources in iteritems(get_build_maps()):
+ for target, sources in get_build_maps().items():
pack(os.path.join(assets_path, target), sources, no_compress, verbose)
@@ -348,7 +384,7 @@ def get_build_maps():
if os.path.exists(path):
with open(path) as f:
try:
- for target, sources in iteritems(json.loads(f.read())):
+ for target, sources in (json.loads(f.read() or "{}")).items():
# update app path
source_paths = []
for source in sources:
@@ -381,7 +417,7 @@ def pack(target, sources, no_compress, verbose):
timestamps[f] = os.path.getmtime(f)
try:
with open(f, "r") as sourcefile:
- data = text_type(sourcefile.read(), "utf-8", errors="ignore")
+ data = str(sourcefile.read(), "utf-8", errors="ignore")
extn = f.rsplit(".", 1)[1]
@@ -396,7 +432,7 @@ def pack(target, sources, no_compress, verbose):
jsm.minify(tmpin, tmpout)
minified = tmpout.getvalue()
if minified:
- outtxt += text_type(minified or "", "utf-8").strip("\n") + ";"
+ outtxt += str(minified or "", "utf-8").strip("\n") + ";"
if verbose:
print("{0}: {1}k".format(f, int(len(minified) / 1024)))
@@ -426,16 +462,16 @@ def html_to_js_template(path, content):
def scrub_html_template(content):
"""Returns HTML content with removed whitespace and comments"""
# remove whitespace to a single space
- content = re.sub("\s+", " ", content)
+ content = re.sub(r"\s+", " ", content)
# strip comments
- content = re.sub("()", "", content)
+ content = re.sub(r"()", "", content)
return content.replace("'", "\'")
def files_dirty():
- for target, sources in iteritems(get_build_maps()):
+ for target, sources in get_build_maps().items():
for f in sources:
if ":" in f:
f, suffix = f.split(":")
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 4e0fe0cf44..7330c83102 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -13,6 +13,8 @@ common_default_keys = ["__default", "__global"]
doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map',
'milestone_tracker_map', 'event_consumer_document_type_map')
+bench_cache_keys = ('assets_json',)
+
global_cache_keys = ("app_hooks", "installed_apps", 'all_apps',
"app_modules", "module_app", "system_settings",
'scheduler_events', 'time_zone', 'webhooks', 'active_domains',
@@ -58,6 +60,7 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(global_cache_keys)
+ frappe.cache().delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
diff --git a/frappe/change_log/v13/v13_3_0.md b/frappe/change_log/v13/v13_3_0.md
new file mode 100644
index 0000000000..6ab181ef09
--- /dev/null
+++ b/frappe/change_log/v13/v13_3_0.md
@@ -0,0 +1,49 @@
+# Version 13.3.0 Release Notes
+
+### Features & Enhancements
+
+- Deletion Steps in Data Deletion Tool ([#13124](https://github.com/frappe/frappe/pull/13124))
+- Format Option for list-apps in bench CLI ([#13125](https://github.com/frappe/frappe/pull/13125))
+- Add password fieldtype option for Web Form ([#13093](https://github.com/frappe/frappe/pull/13093))
+- Add simple __repr__ for DocTypes ([#13151](https://github.com/frappe/frappe/pull/13151))
+- Switch theme with left/right keys ([#13077](https://github.com/frappe/frappe/pull/13077))
+- sourceURL for injected javascript ([#13022](https://github.com/frappe/frappe/pull/13022))
+
+### Fixes
+
+- Decode uri before importing file via weblink ([#13026](https://github.com/frappe/frappe/pull/13026))
+- Respond to /api requests as JSON by default ([#13053](https://github.com/frappe/frappe/pull/13053))
+- Disabled checkbox should be disabled ([#13021](https://github.com/frappe/frappe/pull/13021))
+- Moving Site folder across different FileSystems failed ([#13038](https://github.com/frappe/frappe/pull/13038))
+- Freeze screen till the background request is complete ([#13078](https://github.com/frappe/frappe/pull/13078))
+- Added conditional rendering for content field in split section w… ([#13075](https://github.com/frappe/frappe/pull/13075))
+- Show delete button on portal if user has permission to delete document ([#13149](https://github.com/frappe/frappe/pull/13149))
+- Dont disable dialog scroll on focusing a Link/Autocomplete field ([#13119](https://github.com/frappe/frappe/pull/13119))
+- Typo in RecorderDetail.vue ([#13086](https://github.com/frappe/frappe/pull/13086))
+- Error for bench drop-site. Added missing import. ([#13064](https://github.com/frappe/frappe/pull/13064))
+- Report column context ([#13090](https://github.com/frappe/frappe/pull/13090))
+- Different service name for push and pull request events ([#13094](https://github.com/frappe/frappe/pull/13094))
+- Moving Site folder across different FileSystems failed ([#13033](https://github.com/frappe/frappe/pull/13033))
+- Consistent checkboxes on all browsers ([#13042](https://github.com/frappe/frappe/pull/13042))
+- Changed shorcut widgets color picker to dropdown ([#13073](https://github.com/frappe/frappe/pull/13073))
+- Error while exporting reports with duration field ([#13118](https://github.com/frappe/frappe/pull/13118))
+- Add margin to download backup card ([#13079](https://github.com/frappe/frappe/pull/13079))
+- Move mention list generation logic to server-side ([#13074](https://github.com/frappe/frappe/pull/13074))
+- Make strings translatable ([#13046](https://github.com/frappe/frappe/pull/13046))
+- Don't evaluate dynamic properties to check if conflicts exist ([#13186](https://github.com/frappe/frappe/pull/13186))
+- Add __ function in vue global for translation in recorder ([#13089](https://github.com/frappe/frappe/pull/13089))
+- Make strings translatable ([#13076](https://github.com/frappe/frappe/pull/13076))
+- Show config in bench CLI ([#13128](https://github.com/frappe/frappe/pull/13128))
+- Add breadcrumbs for list view ([#13091](https://github.com/frappe/frappe/pull/13091))
+- Do not skip data in save while using shortcut ([#13182](https://github.com/frappe/frappe/pull/13182))
+- Use docfields from options if no docfields are returned from meta ([#13188](https://github.com/frappe/frappe/pull/13188))
+- Disable reloading files in `__pycache__` directory ([#13109](https://github.com/frappe/frappe/pull/13109))
+- RTL stylesheet route to load RTL style on demand. ([#13007](https://github.com/frappe/frappe/pull/13007))
+- Do not show messsage when exception is handled ([#13111](https://github.com/frappe/frappe/pull/13111))
+- Replace parseFloat by Number ([#13082](https://github.com/frappe/frappe/pull/13082))
+- Add margin to download backup card ([#13050](https://github.com/frappe/frappe/pull/13050))
+- Translate report column labels ([#13083](https://github.com/frappe/frappe/pull/13083))
+- Grid row color picker field not working ([#13040](https://github.com/frappe/frappe/pull/13040))
+- Improve oauthlib implementation ([#13045](https://github.com/frappe/frappe/pull/13045))
+- Replace filter_by like with full text filter ([#13126](https://github.com/frappe/frappe/pull/13126))
+- Focus jumps to first field ([#13067](https://github.com/frappe/frappe/pull/13067))
\ No newline at end of file
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index 61ee62d352..e521acc9ad 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -28,6 +28,10 @@ def pass_context(f):
except frappe.exceptions.SiteNotSpecifiedError as e:
click.secho(str(e), fg='yellow')
sys.exit(1)
+ except frappe.exceptions.IncorrectSitePath:
+ site = ctx.obj.get("sites", "")[0]
+ click.secho(f'Site {site} does not exist!', fg='yellow')
+ sys.exit(1)
if profile:
pr.disable()
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index b917126696..4da0f6bb78 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -16,33 +16,52 @@ from frappe.utils import get_bench_path, update_progress_bar, cint
@click.command('build')
@click.option('--app', help='Build assets for app')
-@click.option('--make-copy', is_flag=True, default=False, help='Copy the files instead of symlinking')
-@click.option('--restore', is_flag=True, default=False, help='Copy the files instead of symlinking with force')
+@click.option('--apps', help='Build assets for specific apps')
+@click.option('--hard-link', is_flag=True, default=False, help='Copy the files instead of symlinking')
+@click.option('--make-copy', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking')
+@click.option('--restore', is_flag=True, default=False, help='[DEPRECATED] Copy the files instead of symlinking with force')
+@click.option('--production', is_flag=True, default=False, help='Build assets in production mode')
@click.option('--verbose', is_flag=True, default=False, help='Verbose')
@click.option('--force', is_flag=True, default=False, help='Force build assets instead of downloading available')
-def build(app=None, make_copy=False, restore=False, verbose=False, force=False):
- "Minify + concatenate JS and CSS files, build translations"
- import frappe.build
+def build(app=None, apps=None, hard_link=False, make_copy=False, restore=False, production=False, verbose=False, force=False):
+ "Compile JS and CSS source files"
+ from frappe.build import bundle, download_frappe_assets
frappe.init('')
- # don't minify in developer_mode for faster builds
- no_compress = frappe.local.conf.developer_mode or False
+
+ if not apps and app:
+ apps = app
# dont try downloading assets if force used, app specified or running via CI
- if not (force or app or os.environ.get('CI')):
+ if not (force or apps or os.environ.get('CI')):
# skip building frappe if assets exist remotely
- skip_frappe = frappe.build.download_frappe_assets(verbose=verbose)
+ skip_frappe = download_frappe_assets(verbose=verbose)
else:
skip_frappe = False
- frappe.build.bundle(no_compress, app=app, make_copy=make_copy, restore=restore, verbose=verbose, skip_frappe=skip_frappe)
+ # don't minify in developer_mode for faster builds
+ development = frappe.local.conf.developer_mode or frappe.local.dev_server
+ mode = "development" if development else "production"
+ if production:
+ mode = "production"
+
+ if make_copy or restore:
+ hard_link = make_copy or restore
+ click.secho(
+ "bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
+ fg="yellow",
+ )
+
+ bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
+
@click.command('watch')
-def watch():
- "Watch and concatenate JS and CSS files as and when they change"
- import frappe.build
+@click.option('--apps', help='Watch assets for specific apps')
+def watch(apps=None):
+ "Watch and compile JS and CSS files as and when they change"
+ from frappe.build import watch
frappe.init('')
- frappe.build.watch(True)
+ watch(apps)
@click.command('clear-cache')
@@ -585,12 +604,29 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
if os.environ.get('CI'):
sys.exit(ret)
+@click.command('run-parallel-tests')
+@click.option('--app', help="For App", default='frappe')
+@click.option('--build-number', help="Build number", default=1)
+@click.option('--total-builds', help="Total number of builds", default=1)
+@click.option('--with-coverage', is_flag=True, help="Build coverage file")
+@click.option('--use-orchestrator', is_flag=True, help="Use orchestrator to run parallel tests")
+@pass_context
+def run_parallel_tests(context, app, build_number, total_builds, with_coverage=False, use_orchestrator=False):
+ site = get_site(context)
+ if use_orchestrator:
+ from frappe.parallel_test_runner import ParallelTestWithOrchestrator
+ ParallelTestWithOrchestrator(app, site=site, with_coverage=with_coverage)
+ else:
+ from frappe.parallel_test_runner import ParallelTestRunner
+ ParallelTestRunner(app, site=site, build_number=build_number, total_builds=total_builds, with_coverage=with_coverage)
@click.command('run-ui-tests')
@click.argument('app')
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
+@click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
+@click.option('--ci-build-id')
@pass_context
-def run_ui_tests(context, app, headless=False):
+def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
"Run UI tests"
site = get_site(context)
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
@@ -622,6 +658,12 @@ def run_ui_tests(context, app, headless=False):
command = '{site_env} {password_env} {cypress} {run_or_open}'
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
+ if parallel:
+ formatted_command += ' --parallel'
+
+ if ci_build_id:
+ formatted_command += ' --ci-build-id {}'.format(ci_build_id)
+
click.secho("Running Cypress...", fg="yellow")
frappe.commands.popen(formatted_command, cwd=app_base_path, raise_err=True)
@@ -797,5 +839,6 @@ commands = [
watch,
bulk_rename,
add_to_email_queue,
- rebuild_global_search
+ rebuild_global_search,
+ run_parallel_tests
]
diff --git a/frappe/contacts/doctype/contact/test_contact.py b/frappe/contacts/doctype/contact/test_contact.py
index 4929873dc4..b131428696 100644
--- a/frappe/contacts/doctype/contact/test_contact.py
+++ b/frappe/contacts/doctype/contact/test_contact.py
@@ -5,7 +5,8 @@ from __future__ import unicode_literals
import frappe
import unittest
-from frappe.exceptions import ValidationError
+
+test_dependencies = ['Contact', 'Salutation']
class TestContact(unittest.TestCase):
@@ -52,4 +53,4 @@ def create_contact(name, salutation, emails=None, phones=None, save=True):
if save:
doc.insert()
- return doc
\ No newline at end of file
+ return doc
diff --git a/frappe/core/doctype/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py
index 05ece76c7f..f33c7a1c85 100644
--- a/frappe/core/doctype/activity_log/test_activity_log.py
+++ b/frappe/core/doctype/activity_log/test_activity_log.py
@@ -90,4 +90,5 @@ class TestActivityLog(unittest.TestCase):
def update_system_settings(args):
doc = frappe.get_doc('System Settings')
doc.update(args)
+ doc.flags.ignore_mandatory = 1
doc.save()
diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py
index bec8cde7ea..5d600cc0db 100644
--- a/frappe/core/doctype/data_export/exporter.py
+++ b/frappe/core/doctype/data_export/exporter.py
@@ -282,7 +282,7 @@ class DataExporter:
try:
sflags = self.docs_to_export.get("flags", "I,U").upper()
flags = 0
- for a in re.split('\W+',sflags):
+ for a in re.split(r'\W+', sflags):
flags = flags | reflags.get(a,0)
c = re.compile(names, flags)
diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js
index e03c22a898..079bdaa09c 100644
--- a/frappe/core/doctype/data_import/data_import.js
+++ b/frappe/core/doctype/data_import/data_import.js
@@ -203,7 +203,7 @@ frappe.ui.form.on('Data Import', {
},
download_template(frm) {
- frappe.require('/assets/js/data_import_tools.min.js', () => {
+ frappe.require('data_import_tools.bundle.js', () => {
frm.data_exporter = new frappe.data_import.DataExporter(
frm.doc.reference_doctype,
frm.doc.import_type
@@ -287,7 +287,7 @@ frappe.ui.form.on('Data Import', {
return;
}
- frappe.require('/assets/js/data_import_tools.min.js', () => {
+ frappe.require('data_import_tools.bundle.js', () => {
frm.import_preview = new frappe.data_import.ImportPreview({
wrapper: frm.get_field('import_preview').$wrapper,
doctype: frm.doc.reference_doctype,
diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py
index 1c56f54303..7e8374a0a2 100644
--- a/frappe/core/doctype/data_import/data_import.py
+++ b/frappe/core/doctype/data_import/data_import.py
@@ -211,7 +211,12 @@ def export_json(
doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"
):
def post_process(out):
- del_keys = ("modified_by", "creation", "owner", "idx")
+ # Note on Tree DocTypes:
+ # The tree structure is maintained in the database via the fields "lft"
+ # and "rgt". They are automatically set and kept up-to-date. Importing
+ # them would destroy any existing tree structure. For this reason they
+ # are not exported as well.
+ del_keys = ("modified_by", "creation", "owner", "idx", "lft", "rgt")
for doc in out:
for key in del_keys:
if key in doc:
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 720fe1dda7..d3f981add4 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -641,7 +641,7 @@ 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)
+ is_valid_duration = re.match(r"^(?:(\d+d)?((^|\s)\d+h)?((^|\s)\d+m)?((^|\s)\d+s)?)$", value)
if not is_valid_duration:
self.warnings.append(
{
@@ -929,10 +929,7 @@ class Column:
self.warnings.append(
{
"col": self.column_number,
- "message": _(
- "Date format could not be determined from the values in"
- " this column. Defaulting to yyyy-mm-dd."
- ),
+ "message": _("Date format could not be determined from the values in this column. Defaulting to yyyy-mm-dd."),
"type": "info",
}
)
diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py
index d4ef1f92f8..9c424eb4d7 100644
--- a/frappe/core/doctype/docshare/test_docshare.py
+++ b/frappe/core/doctype/docshare/test_docshare.py
@@ -7,6 +7,8 @@ import frappe.share
import unittest
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
+test_dependencies = ['User']
+
class TestDocShare(unittest.TestCase):
def setUp(self):
self.user = "test@example.com"
@@ -112,4 +114,4 @@ class TestDocShare(unittest.TestCase):
self.assertTrue(frappe.has_permission(doctype, "read", doc=submittable_doc.name, user=self.user))
self.assertTrue(frappe.has_permission(doctype, "write", doc=submittable_doc.name, user=self.user))
- frappe.share.remove(doctype, submittable_doc.name, self.user)
\ No newline at end of file
+ frappe.share.remove(doctype, submittable_doc.name, self.user)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 6eef5a4023..f9dbeb0907 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -18,6 +18,7 @@ from frappe import _
from frappe.utils import now, cint
from frappe.model import no_value_fields, default_fields, data_fieldtypes, table_fields, data_field_options
from frappe.model.document import Document
+from frappe.model.base_document import get_controller
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.desk.notifications import delete_notification_count_for
@@ -107,8 +108,6 @@ class DocType(Document):
if self.name in core_doctypes:
return
- from frappe.model.base_document import get_controller
-
try:
controller = get_controller(self.name)
except ImportError:
@@ -123,6 +122,9 @@ class DocType(Document):
}
for docfield in self.get("fields") or []:
+ if docfield.fieldtype in no_value_fields:
+ continue
+
conflict_type = None
field = docfield.fieldname
field_label = docfield.label or docfield.fieldname
@@ -671,12 +673,12 @@ class DocType(Document):
flags = {"flags": re.ASCII} if six.PY3 else {}
# a DocType name should not start or end with an empty space
- if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
+ if re.search(r"^[ \t\n\r]+|[ \t\n\r]+$", name, **flags):
frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError)
# a DocType's name should not start with a number or underscore
# and should only contain letters, numbers and underscore
- if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags):
+ if not re.match(r"^(?![\W])[^\d_\s][\w ]+$", name, **flags):
frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError)
validate_route_conflict(self.doctype, self.name)
@@ -964,7 +966,7 @@ def validate_fields(meta):
for field in depends_on_fields:
depends_on = docfield.get(field, None)
if depends_on and ("=" in depends_on) and \
- re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", depends_on):
+ re.match(r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+', depends_on):
frappe.throw(_("Invalid {0} condition").format(frappe.unscrub(field)), frappe.ValidationError)
def check_table_multiselect_option(docfield):
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index bfa9d0ec8a..9c492d2c36 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -92,7 +92,7 @@ class TestDocType(unittest.TestCase):
fields=["parent", "depends_on", "collapsible_depends_on", "mandatory_depends_on",\
"read_only_depends_on", "fieldname", "fieldtype"])
- pattern = """[\w\.:_]+\s*={1}\s*[\w\.@'"]+"""
+ pattern = r'[\w\.:_]+\s*={1}\s*[\w\.@\'"]+'
for field in docfields:
for depends_on in ["depends_on", "collapsible_depends_on", "mandatory_depends_on", "read_only_depends_on"]:
condition = field.get(depends_on)
@@ -517,4 +517,4 @@ def new_doctype(name, unique=0, depends_on='', fields=None):
for f in fields:
doc.append('fields', f)
- return doc
\ No newline at end of file
+ return doc
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 017106e6f5..c4c37e6d13 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -498,7 +498,7 @@ class File(Document):
self.file_size = self.check_max_file_size()
if (
- self.content_type and "image" in self.content_type
+ self.content_type and self.content_type == "image/jpeg"
and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images")
):
self.content = strip_exif_data(self.content, self.content_type)
@@ -912,7 +912,7 @@ def extract_images_from_html(doc, content):
return ']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
+ content = re.sub(r'
]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
return content
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 2f8f437fc9..2596fe94d0 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -193,6 +193,7 @@ class TestSameContent(unittest.TestCase):
class TestFile(unittest.TestCase):
def setUp(self):
+ frappe.set_user('Administrator')
self.delete_test_data()
self.upload_file()
diff --git a/frappe/core/doctype/role_profile/test_role_profile.py b/frappe/core/doctype/role_profile/test_role_profile.py
index 624b85c315..975453e8d1 100644
--- a/frappe/core/doctype/role_profile/test_role_profile.py
+++ b/frappe/core/doctype/role_profile/test_role_profile.py
@@ -5,6 +5,8 @@ from __future__ import unicode_literals
import frappe
import unittest
+test_dependencies = ['Role']
+
class TestRoleProfile(unittest.TestCase):
def test_make_new_role_profile(self):
new_role_profile = frappe.get_doc(dict(doctype='Role Profile', role_profile='Test 1')).insert()
@@ -21,4 +23,4 @@ class TestRoleProfile(unittest.TestCase):
# clear roles
new_role_profile.roles = []
new_role_profile.save()
- self.assertEqual(new_role_profile.roles, [])
\ No newline at end of file
+ self.assertEqual(new_role_profile.roles, [])
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index d102526a9e..05aaca81de 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -42,7 +42,7 @@ class SystemSettings(Document):
def on_update(self):
for df in self.meta.get("fields"):
- if df.fieldtype not in no_value_fields:
+ if df.fieldtype not in no_value_fields and self.has_value_changed(df.fieldname):
frappe.db.set_default(df.fieldname, self.get(df.fieldname))
if self.language:
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
index fdca93e8b9..f1f74daf71 100644
--- a/frappe/core/page/recorder/recorder.js
+++ b/frappe/core/page/recorder/recorder.js
@@ -11,7 +11,7 @@ frappe.pages['recorder'].on_page_load = function(wrapper) {
frappe.recorder.show();
});
- frappe.require('/assets/js/frappe-recorder.min.js');
+ frappe.require('recorder.bundle.js');
};
class Recorder {
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index a87ca6bc02..ba4a44d7d7 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -360,15 +360,18 @@ def get_desktop_page(page):
Returns:
dict: dictionary of cards, charts and shortcuts to be displayed on website
"""
- wspace = Workspace(page)
- wspace.build_workspace()
- return {
- 'charts': wspace.charts,
- 'shortcuts': wspace.shortcuts,
- 'cards': wspace.cards,
- 'onboarding': wspace.onboarding,
- 'allow_customization': not wspace.doc.disable_user_customization
- }
+ try:
+ wspace = Workspace(page)
+ wspace.build_workspace()
+ return {
+ 'charts': wspace.charts,
+ 'shortcuts': wspace.shortcuts,
+ 'cards': wspace.cards,
+ 'onboarding': wspace.onboarding,
+ 'allow_customization': not wspace.doc.disable_user_customization
+ }
+ except DoesNotExistError:
+ return {}
@frappe.whitelist()
def get_desk_sidebar_items():
@@ -685,3 +688,4 @@ def merge_cards_based_on_label(cards):
cards_dict[label] = card
return list(cards_dict.values())
+
diff --git a/frappe/desk/doctype/todo/test_todo.py b/frappe/desk/doctype/todo/test_todo.py
index b767fd4aef..de5b6724a6 100644
--- a/frappe/desk/doctype/todo/test_todo.py
+++ b/frappe/desk/doctype/todo/test_todo.py
@@ -9,8 +9,7 @@ from frappe.model.db_query import DatabaseQuery
from frappe.permissions import add_permission, reset_perms
from frappe.core.doctype.doctype.doctype import clear_permissions_cache
-# test_records = frappe.get_test_records('ToDo')
-test_user_records = frappe.get_test_records('User')
+test_dependencies = ['User']
class TestToDo(unittest.TestCase):
def test_delete(self):
@@ -77,7 +76,7 @@ class TestToDo(unittest.TestCase):
frappe.set_user('test4@example.com')
#owner and assigned_by is test4
todo3 = create_new_todo('Test3', 'test4@example.com')
-
+
# user without any role to read or write todo document
self.assertFalse(todo1.has_permission("read"))
self.assertFalse(todo1.has_permission("write"))
diff --git a/frappe/desk/doctype/workspace_link/workspace_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json
index 010fb3f316..53dadad83d 100644
--- a/frappe/desk/doctype/workspace_link/workspace_link.json
+++ b/frappe/desk/doctype/workspace_link/workspace_link.json
@@ -8,13 +8,13 @@
"type",
"label",
"icon",
+ "only_for",
"hidden",
"link_details_section",
"link_type",
"link_to",
"column_break_7",
"dependencies",
- "only_for",
"onboard",
"is_query_report"
],
@@ -84,7 +84,7 @@
{
"fieldname": "only_for",
"fieldtype": "Link",
- "label": "Only for ",
+ "label": "Only for",
"options": "Country"
},
{
@@ -104,7 +104,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-12 13:13:12.379443",
+ "modified": "2021-05-13 13:10:18.128512",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace Link",
diff --git a/frappe/desk/page/activity/activity.js b/frappe/desk/page/activity/activity.js
index 39de414122..7b4e8ddc1a 100644
--- a/frappe/desk/page/activity/activity.js
+++ b/frappe/desk/page/activity/activity.js
@@ -67,8 +67,8 @@ frappe.pages['activity'].on_page_show = function () {
}
frappe.activity.last_feed_date = false;
-frappe.activity.Feed = Class.extend({
- init: function (row, data) {
+frappe.activity.Feed = class Feed {
+ constructor(row, data) {
this.scrub_data(data);
this.add_date_separator(row, data);
if (!data.add_class)
@@ -97,8 +97,9 @@ frappe.activity.Feed = Class.extend({
$(row)
.append(frappe.render_template("activity_row", data))
.find("a").addClass("grey");
- },
- scrub_data: function (data) {
+ }
+
+ scrub_data(data) {
data.by = frappe.user.full_name(data.owner);
data.avatar = frappe.avatar(data.owner);
@@ -113,9 +114,9 @@ frappe.activity.Feed = Class.extend({
data.when = comment_when(data.creation);
data.feed_type = data.comment_type || data.communication_medium;
- },
+ }
- add_date_separator: function (row, data) {
+ add_date_separator(row, data) {
var date = frappe.datetime.str_to_obj(data.creation);
var last = frappe.activity.last_feed_date;
@@ -137,7 +138,7 @@ frappe.activity.Feed = Class.extend({
}
frappe.activity.last_feed_date = date;
}
-});
+};
frappe.activity.render_heatmap = function (page) {
$('
' + time_zone; } } - this._super(); - }, - set_datepicker: function() { - this._super(); + super.set_description(); + } + set_datepicker() { + super.set_datepicker(); if (this.datepicker.opts.timeFormat.indexOf('s') == -1) { // No seconds in time format const $tp = this.datepicker.timepicker; @@ -36,4 +36,4 @@ frappe.ui.form.ControlDatetime = frappe.ui.form.ControlDate.extend({ $tp.$secondsText.prev().css('display', 'none'); } } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/duration.js b/frappe/public/js/frappe/form/controls/duration.js index e70afd6e65..361d10982e 100644 --- a/frappe/public/js/frappe/form/controls/duration.js +++ b/frappe/public/js/frappe/form/controls/duration.js @@ -1,10 +1,10 @@ -frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ - make_input: function() { - this._super(); +frappe.ui.form.ControlDuration = class ControlDuration extends frappe.ui.form.ControlData { + make_input() { + super.make_input(); this.make_picker(); - }, + } - make_picker: function() { + make_picker() { this.inputs = []; this.set_duration_options(); this.$picker = $( @@ -21,9 +21,9 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ this.$picker.hide(); this.bind_events(); this.refresh(); - }, + } - build_numeric_input: function(label, hidden, max) { + build_numeric_input(label, hidden, max) { let $duration_input = $(` `); @@ -47,13 +47,13 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ } $control.prepend($input); $control.appendTo(this.$picker.find(".picker-row")); - }, + } set_duration_options() { this.duration_options = frappe.utils.get_duration_options(this.df); - }, + } - set_duration_picker_value: function(value) { + set_duration_picker_value(value) { let total_duration = frappe.utils.seconds_to_duration(value, this.duration_options); if (this.$picker) { @@ -61,9 +61,9 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ this.inputs[duration].prop("value", total_duration[duration]); }); } - }, + } - bind_events: function() { + bind_events() { // flag to handle the display property of the picker let clicked = false; @@ -103,21 +103,21 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ this.$picker.hide(); } }); - }, + } get_value() { return cint(this.value); - }, + } - refresh_input: function() { - this._super(); + refresh_input() { + super.refresh_input(); this.set_duration_options(); this.set_duration_picker_value(this.value); - }, + } - format_for_input: function(value) { + format_for_input(value) { return frappe.utils.get_formatted_duration(value, this.duration_options); - }, + } get_duration() { // returns an object of days, hours, minutes and seconds from the inputs array @@ -138,7 +138,7 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ } } return total_duration; - }, + } is_duration_picker_set(inputs) { let is_set = false; @@ -149,4 +149,4 @@ frappe.ui.form.ControlDuration = frappe.ui.form.ControlData.extend({ }); return is_set; } -}); \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/form/controls/dynamic_link.js b/frappe/public/js/frappe/form/controls/dynamic_link.js index 00bb02a5fc..2c5661ca87 100644 --- a/frappe/public/js/frappe/form/controls/dynamic_link.js +++ b/frappe/public/js/frappe/form/controls/dynamic_link.js @@ -1,5 +1,5 @@ -frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ - get_options: function() { +frappe.ui.form.ControlDynamicLink = class ControlDynamicLink extends frappe.ui.form.ControlLink { + get_options() { let options = ''; if (this.df.get_options) { options = this.df.get_options(); @@ -28,5 +28,5 @@ frappe.ui.form.ControlDynamicLink = frappe.ui.form.ControlLink.extend({ } return options; - }, -}); + } +}; diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js index 306c05dc1b..89f8f23cc5 100644 --- a/frappe/public/js/frappe/form/controls/float.js +++ b/frappe/public/js/frappe/form/controls/float.js @@ -1,27 +1,27 @@ -frappe.ui.form.ControlFloat = frappe.ui.form.ControlInt.extend({ - parse: function(value) { +frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { + parse(value) { value = this.eval_expression(value); return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); - }, + } - format_for_input: function(value) { + format_for_input(value) { var number_format; if (this.df.fieldtype==="Float" && this.df.options && this.df.options.trim()) { number_format = this.get_number_format(); } var formatted_value = format_number(value, number_format, this.get_precision()); return isNaN(Number(value)) ? "" : formatted_value; - }, + } - get_number_format: function() { + get_number_format() { var currency = frappe.meta.get_field_currency(this.df, this.get_doc()); return get_number_format(currency); - }, + } - get_precision: function() { + get_precision() { // round based on field precision or float precision, else don't round return this.df.precision || cint(frappe.boot.sysdefaults.float_precision, null); } -}); +}; frappe.ui.form.ControlPercent = frappe.ui.form.ControlFloat; diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index dfd0f4d174..080a1cbb48 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -1,11 +1,11 @@ frappe.provide('frappe.utils.utils'); -frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ - horizontal: false, +frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.form.ControlData { + static horizontal = false make_wrapper() { // Create the elements for map area - this._super(); + super.make_wrapper(); let $input_wrapper = this.$wrapper.find('.control-input-wrapper'); this.map_id = frappe.dom.get_unique_id(); @@ -24,14 +24,14 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ this.make_map(); }); } - }, + } make_map() { this.bind_leaflet_map(); this.bind_leaflet_draw_control(); this.bind_leaflet_locate_control(); this.bind_leaflet_refresh_button(); - }, + } format_for_input(value) { if (!this.map) return; @@ -65,7 +65,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ } else if ((value===undefined) || (value == JSON.stringify(new L.FeatureGroup().toGeoJSON()))) { this.locate_control.start(); } - }, + } bind_leaflet_map() { var circleToGeoJSON = L.Circle.prototype.toGeoJSON; @@ -97,13 +97,13 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ L.tileLayer(frappe.utils.map_defaults.tiles, frappe.utils.map_defaults.options).addTo(this.map); - }, + } bind_leaflet_locate_control() { // To request location update and set location, sets current geolocation on load this.locate_control = L.control.locate({position:'topright'}); this.locate_control.addTo(this.map); - }, + } bind_leaflet_draw_control() { this.editableLayers = new L.FeatureGroup(); @@ -160,7 +160,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ this.editableLayers.removeLayer(layer); this.set_value(JSON.stringify(this.editableLayers.toGeoJSON())); }); - }, + } bind_leaflet_refresh_button() { L.easyButton({ @@ -177,7 +177,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ icon: 'fa fa-refresh' }] }).addTo(this.map); - }, + } add_non_group_layers(source_layer, target_group) { // https://gis.stackexchange.com/a/203773 @@ -189,11 +189,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ } else { target_group.addLayer(source_layer); } - }, + } clear_editable_layers() { this.editableLayers.eachLayer((l)=>{ this.editableLayers.removeLayer(l); }); } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/heading.js b/frappe/public/js/frappe/form/controls/heading.js index 7b9dd043f2..ccce412eaf 100644 --- a/frappe/public/js/frappe/form/controls/heading.js +++ b/frappe/public/js/frappe/form/controls/heading.js @@ -1,5 +1,5 @@ -frappe.ui.form.ControlHeading = frappe.ui.form.ControlHTML.extend({ - get_content: function() { +frappe.ui.form.ControlHeading = class ControlHeading extends frappe.ui.form.ControlHTML { + get_content() { return "
" + __(this.df.label) + "
"; } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/html.js b/frappe/public/js/frappe/form/controls/html.js index f8a3645705..b2f18d4ccc 100644 --- a/frappe/public/js/frappe/form/controls/html.js +++ b/frappe/public/js/frappe/form/controls/html.js @@ -1,13 +1,13 @@ -frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ - make: function() { - this._super(); +frappe.ui.form.ControlHTML = class ControlHTML extends frappe.ui.form.Control { + make() { + super.make(); this.disp_area = this.wrapper; - }, - refresh_input: function() { + } + refresh_input() { var content = this.get_content(); if(content) this.$wrapper.html(content); - }, - get_content: function() { + } + get_content() { var content = this.df.options || ""; content = __(content); try { @@ -15,11 +15,11 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ } catch (e) { return content; } - }, - html: function(html) { + } + html(html) { this.$wrapper.html(html || this.get_content()); - }, - set_value: function(html) { + } + set_value(html) { if(html.appendTo) { // jquery object html.appendTo(this.$wrapper.empty()); @@ -29,4 +29,4 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ this.html(html); } } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/html_editor.js b/frappe/public/js/frappe/form/controls/html_editor.js index f708bdbd98..7d43112e6a 100644 --- a/frappe/public/js/frappe/form/controls/html_editor.js +++ b/frappe/public/js/frappe/form/controls/html_editor.js @@ -1,13 +1,13 @@ -frappe.ui.form.ControlHTMLEditor = frappe.ui.form.ControlMarkdownEditor.extend({ - editor_class: 'html', +frappe.ui.form.ControlHTMLEditor = class ControlHTMLEditor extends frappe.ui.form.ControlMarkdownEditor { + static editor_class = 'html'; set_language() { this.df.options = 'HTML'; - this._super(); - }, + super.set_language(); + } update_preview() { if (!this.markdown_preview) return; let value = this.get_value() || ''; value = frappe.dom.remove_script_and_style(value); this.markdown_preview.html(value); } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/image.js b/frappe/public/js/frappe/form/controls/image.js index 90de22138a..d175330947 100644 --- a/frappe/public/js/frappe/form/controls/image.js +++ b/frappe/public/js/frappe/form/controls/image.js @@ -1,12 +1,12 @@ -frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({ - make: function() { - this._super(); +frappe.ui.form.ControlImage = class ControlImage extends frappe.ui.form.Control { + make() { + super.make(); this.$wrapper.css({"margin": "0px"}); this.$body = $("").appendTo(this.$wrapper) .css({"margin-bottom": "10px"}); $('').appendTo(this.$wrapper); - }, - refresh_input: function() { + } + refresh_input() { this.$body.empty(); var doc = this.get_doc(); @@ -19,4 +19,4 @@ frappe.ui.form.ControlImage = frappe.ui.form.Control.extend({ } return false; } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/int.js b/frappe/public/js/frappe/form/controls/int.js index aca3a85603..12652bf86e 100644 --- a/frappe/public/js/frappe/form/controls/int.js +++ b/frappe/public/js/frappe/form/controls/int.js @@ -1,13 +1,13 @@ -frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ - trigger_change_on_input_event: false, - make: function () { - this._super(); +frappe.ui.form.ControlInt = class ControlInt extends frappe.ui.form.ControlData { + static trigger_change_on_input_event = false + make () { + super.make(); // $(this.label_area).addClass('pull-right'); // $(this.disp_area).addClass('text-right'); - }, - make_input: function () { + } + make_input () { var me = this; - this._super(); + super.make_input(); this.$input // .addClass("text-right") .on("focus", function () { @@ -19,11 +19,11 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ }, 100); return false; }); - }, - validate: function (value) { + } + validate (value) { return this.parse(value); - }, - eval_expression: function (value) { + } + eval_expression (value) { if (typeof value === 'string') { if (value.match(/^[0-9+\-/* ]+$/)) { // If it is a string containing operators @@ -36,8 +36,8 @@ frappe.ui.form.ControlInt = frappe.ui.form.ControlData.extend({ } } return value; - }, - parse: function (value) { + } + parse (value) { return cint(this.eval_expression(value), null); } -}); +}; diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index c32c99f0ed..43bd7443ab 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -8,9 +8,9 @@ import Awesomplete from 'awesomplete'; frappe.ui.form.recent_link_validations = {}; -frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ - trigger_change_on_input_event: false, - make_input: function() { +frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlData { + static trigger_change_on_input_event = false + make_input() { var me = this; $(`