Merge branch 'develop' into wiki-based-desk
This commit is contained in:
commit
8165961343
242 changed files with 10682 additions and 10010 deletions
|
|
@ -149,6 +149,7 @@
|
|||
"before": true,
|
||||
"beforeEach": true,
|
||||
"qz": true,
|
||||
"localforage": true
|
||||
"localforage": true,
|
||||
"extend_cscript": true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
.flake8
3
.flake8
|
|
@ -29,4 +29,5 @@ ignore =
|
|||
B950,
|
||||
W191,
|
||||
|
||||
max-line-length = 200
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
|
|
|||
12
.git-blame-ignore-revs
Normal file
12
.git-blame-ignore-revs
Normal file
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
7
.github/helper/semgrep_rules/translate.js
vendored
7
.github/helper/semgrep_rules/translate.js
vendored
|
|
@ -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])
|
||||
|
|
|
|||
8
.github/helper/semgrep_rules/translate.py
vendored
8
.github/helper/semgrep_rules/translate.py
vendored
|
|
@ -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
|
||||
|
|
|
|||
8
.github/helper/semgrep_rules/translate.yml
vendored
8
.github/helper/semgrep_rules/translate.yml
vendored
|
|
@ -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.
|
||||
|
|
|
|||
6
.github/workflows/publish-assets-develop.yml
vendored
6
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/semgrep.yml
vendored
2
.github/workflows/semgrep.yml
vendored
|
|
@ -4,6 +4,8 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
|
|
|
|||
|
|
@ -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
|
||||
100
.github/workflows/server-postgres-tests.yml
vendored
Normal file
100
.github/workflows/server-postgres-tests.yml
vendored
Normal file
|
|
@ -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
|
||||
105
.github/workflows/ui-tests.yml
vendored
Normal file
105
.github/workflows/ui-tests.yml
vendored
Normal file
|
|
@ -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
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -9,6 +9,7 @@ locale
|
|||
dist/
|
||||
# build/
|
||||
frappe/docs/current
|
||||
frappe/public/dist
|
||||
.vscode
|
||||
node_modules
|
||||
.kdev4/
|
||||
|
|
|
|||
18
.mergify.yml
18
.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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
486
esbuild/esbuild.js
Normal file
486
esbuild/esbuild.js
Normal file
|
|
@ -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();
|
||||
}
|
||||
43
esbuild/frappe-html.js
Normal file
43
esbuild/frappe-html.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
11
esbuild/ignore-assets.js
Normal file
11
esbuild/ignore-assets.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
name: "frappe-ignore-asset",
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^\/assets\// }, args => {
|
||||
return {
|
||||
path: args.path,
|
||||
external: true
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
1
esbuild/index.js
Normal file
1
esbuild/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
require("./esbuild");
|
||||
29
esbuild/sass_options.js
Normal file
29
esbuild/sass_options.js
Normal file
|
|
@ -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
|
||||
};
|
||||
}
|
||||
};
|
||||
145
esbuild/utils.js
Normal file
145
esbuild/utils.js
Normal file
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
274
frappe/build.py
274
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(":")
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
49
frappe/change_log/v13/v13_3_0.md
Normal file
49
frappe/change_log/v13/v13_3_0.md
Normal file
|
|
@ -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))
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
return doc
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
frappe.share.remove(doctype, submittable_doc.name, self.user)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
return doc
|
||||
|
|
|
|||
|
|
@ -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 '<img src="{file_url}"'.format(file_url=file_url)
|
||||
|
||||
if content and isinstance(content, string_types):
|
||||
content = re.sub('<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
|
||||
content = re.sub(r'<img[^>]*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content)
|
||||
|
||||
return content
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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, [])
|
||||
self.assertEqual(new_role_profile.roles, [])
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
$('<div class="heatmap-container" style="text-align:center">\
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
frappe.pages['user-profile'].on_page_load = function (wrapper) {
|
||||
frappe.require('assets/js/user_profile_controller.min.js', () => {
|
||||
frappe.require('user_profile_controller.bundle.js', () => {
|
||||
let user_profile = new frappe.ui.UserProfile(wrapper);
|
||||
user_profile.show();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@
|
|||
"unreplied_for_mins": 20,
|
||||
"send_notification_to": "test_unreplied@example.com",
|
||||
"pop3_server": "pop.test.example.com",
|
||||
"no_remaining":"0"
|
||||
"no_remaining":"0",
|
||||
"track_email_status": 1
|
||||
},
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class EmailQueue(Document):
|
|||
|
||||
message = ctx.build_message(recipient.recipient)
|
||||
if not frappe.flags.in_test:
|
||||
ctx.smtp_session.sendmail(recipient.recipient, self.sender, message)
|
||||
ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
|
||||
ctx.add_to_sent_list(recipient)
|
||||
|
||||
if frappe.flags.in_test:
|
||||
|
|
@ -218,7 +218,7 @@ class SendMailContext:
|
|||
'<img src="https://{}/api/method/frappe.core.doctype.communication.email.mark_email_as_seen?name={}"/>'
|
||||
|
||||
message = ''
|
||||
if frappe.conf.use_ssl and self.queue_doc.track_email_status:
|
||||
if frappe.conf.use_ssl and self.email_account_doc.track_email_status:
|
||||
message = quopri.encodestring(
|
||||
tracker_url_html.format(frappe.local.site, self.queue_doc.communication).encode()
|
||||
).decode()
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import frappe, frappe.utils, frappe.utils.scheduler
|
|||
from frappe.desk.form import assign_to
|
||||
import unittest
|
||||
|
||||
test_records = frappe.get_test_records('Notification')
|
||||
|
||||
test_dependencies = ["User"]
|
||||
test_dependencies = ["User", "Notification"]
|
||||
|
||||
class TestNotification(unittest.TestCase):
|
||||
def setUp(self):
|
||||
|
|
|
|||
|
|
@ -292,18 +292,12 @@ def inline_style_in_html(html):
|
|||
''' Convert email.css and html to inline-styled html
|
||||
'''
|
||||
from premailer import Premailer
|
||||
from frappe.utils.jinja_globals import bundled_asset
|
||||
|
||||
apps = frappe.get_installed_apps()
|
||||
|
||||
# add frappe email css file
|
||||
css_files = ['assets/css/email.css']
|
||||
if 'frappe' in apps:
|
||||
apps.remove('frappe')
|
||||
|
||||
for app in apps:
|
||||
path = 'assets/{0}/css/email.css'.format(app)
|
||||
css_files.append(path)
|
||||
|
||||
# get email css files from hooks
|
||||
css_files = frappe.get_hooks('email_css')
|
||||
css_files = [bundled_asset(path) for path in css_files]
|
||||
css_files = [path.lstrip('/') for path in css_files]
|
||||
css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))]
|
||||
|
||||
p = Premailer(html=html, external_styles=css_files, strip_important=False)
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ class EmailServer:
|
|||
|
||||
flags = []
|
||||
for flag in imaplib.ParseFlags(flag_string) or []:
|
||||
pattern = re.compile("\w+")
|
||||
pattern = re.compile(r"\w+")
|
||||
match = re.search(pattern, frappe.as_unicode(flag))
|
||||
flags.append(match.group(0))
|
||||
|
||||
|
|
@ -555,7 +555,7 @@ class Email:
|
|||
|
||||
def get_thread_id(self):
|
||||
"""Extract thread ID from `[]`"""
|
||||
l = re.findall('(?<=\[)[\w/-]+', self.subject)
|
||||
l = re.findall(r'(?<=\[)[\w/-]+', self.subject)
|
||||
return l and l[0] or None
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -953,7 +953,7 @@
|
|||
"currency_fraction_units": 100,
|
||||
"smallest_currency_fraction_value": 0.01,
|
||||
"currency_symbol": "\u20ac",
|
||||
"number_format": "#,###.##",
|
||||
"number_format": "#.###,##",
|
||||
"timezones": [
|
||||
"Europe/Berlin"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -29,16 +29,16 @@ page_js = {
|
|||
|
||||
# website
|
||||
app_include_js = [
|
||||
"/assets/js/libs.min.js",
|
||||
"/assets/js/desk.min.js",
|
||||
"/assets/js/list.min.js",
|
||||
"/assets/js/form.min.js",
|
||||
"/assets/js/control.min.js",
|
||||
"/assets/js/report.min.js",
|
||||
"libs.bundle.js",
|
||||
"desk.bundle.js",
|
||||
"list.bundle.js",
|
||||
"form.bundle.js",
|
||||
"controls.bundle.js",
|
||||
"report.bundle.js",
|
||||
]
|
||||
app_include_css = [
|
||||
"/assets/css/desk.min.css",
|
||||
"/assets/css/report.min.css",
|
||||
"desk.bundle.css",
|
||||
"report.bundle.css",
|
||||
]
|
||||
|
||||
doctype_js = {
|
||||
|
|
@ -52,6 +52,8 @@ web_include_js = [
|
|||
|
||||
web_include_css = []
|
||||
|
||||
email_css = ['email.bundle.css']
|
||||
|
||||
website_route_rules = [
|
||||
{"from_route": "/blog/<category>", "to_route": "Blog Post"},
|
||||
{"from_route": "/kb/<category>", "to_route": "Help Article"},
|
||||
|
|
@ -226,7 +228,6 @@ scheduler_events = {
|
|||
"frappe.desk.doctype.event.event.send_event_digest",
|
||||
"frappe.sessions.clear_expired_sessions",
|
||||
"frappe.email.doctype.notification.notification.trigger_daily_alerts",
|
||||
"frappe.realtime.remove_old_task_logs",
|
||||
"frappe.utils.scheduler.restrict_scheduler_events_if_dormant",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
|
||||
|
|
|
|||
|
|
@ -390,19 +390,16 @@ def get_conf_params(db_name=None, db_password=None):
|
|||
|
||||
|
||||
def make_site_dirs():
|
||||
site_public_path = os.path.join(frappe.local.site_path, 'public')
|
||||
site_private_path = os.path.join(frappe.local.site_path, 'private')
|
||||
for dir_path in (
|
||||
os.path.join(site_private_path, 'backups'),
|
||||
os.path.join(site_public_path, 'files'),
|
||||
os.path.join(site_private_path, 'files'),
|
||||
os.path.join(frappe.local.site_path, 'logs'),
|
||||
os.path.join(frappe.local.site_path, 'task-logs')):
|
||||
if not os.path.exists(dir_path):
|
||||
os.makedirs(dir_path)
|
||||
locks_dir = frappe.get_site_path('locks')
|
||||
if not os.path.exists(locks_dir):
|
||||
os.makedirs(locks_dir)
|
||||
for dir_path in [
|
||||
os.path.join("public", "files"),
|
||||
os.path.join("private", "backups"),
|
||||
os.path.join("private", "files"),
|
||||
"error-snapshots",
|
||||
"locks",
|
||||
"logs",
|
||||
]:
|
||||
path = frappe.get_site_path(dir_path)
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
|
||||
def add_module_defs(app):
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"fieldname": "client_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Client Id"
|
||||
"label": "Client Id",
|
||||
"mandatory_depends_on": "eval:doc.redirect_uri"
|
||||
},
|
||||
{
|
||||
"fieldname": "redirect_uri",
|
||||
|
|
@ -96,12 +97,14 @@
|
|||
{
|
||||
"fieldname": "authorization_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Authorization URI"
|
||||
"label": "Authorization URI",
|
||||
"mandatory_depends_on": "eval:doc.redirect_uri"
|
||||
},
|
||||
{
|
||||
"fieldname": "token_uri",
|
||||
"fieldtype": "Data",
|
||||
"label": "Token URI"
|
||||
"label": "Token URI",
|
||||
"mandatory_depends_on": "eval:doc.redirect_uri"
|
||||
},
|
||||
{
|
||||
"fieldname": "revocation_uri",
|
||||
|
|
@ -136,7 +139,7 @@
|
|||
"link_fieldname": "connected_app"
|
||||
}
|
||||
],
|
||||
"modified": "2020-11-16 16:29:50.277405",
|
||||
"modified": "2021-05-10 05:03:06.296863",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Connected App",
|
||||
|
|
|
|||
|
|
@ -26,20 +26,27 @@ class ConnectedApp(Document):
|
|||
self.redirect_uri = urljoin(base_url, callback_path)
|
||||
|
||||
def get_oauth2_session(self, user=None, init=False):
|
||||
"""Return an auto-refreshing OAuth2 session which is an extension of a requests.Session()"""
|
||||
token = None
|
||||
token_updater = None
|
||||
auto_refresh_kwargs = None
|
||||
|
||||
if not init:
|
||||
user = user or frappe.session.user
|
||||
token_cache = self.get_user_token(user)
|
||||
token = token_cache.get_json()
|
||||
token_updater = token_cache.update_data
|
||||
auto_refresh_kwargs = {'client_id': self.client_id}
|
||||
client_secret = self.get_password('client_secret')
|
||||
if client_secret:
|
||||
auto_refresh_kwargs['client_secret'] = client_secret
|
||||
|
||||
return OAuth2Session(
|
||||
client_id=self.client_id,
|
||||
token=token,
|
||||
token_updater=token_updater,
|
||||
auto_refresh_url=self.token_uri,
|
||||
auto_refresh_kwargs=auto_refresh_kwargs,
|
||||
redirect_uri=self.redirect_uri,
|
||||
scope=self.get_scopes()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,126 +1,61 @@
|
|||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:webhook_name",
|
||||
"beta": 0,
|
||||
"creation": "2018-05-22 13:20:51.450815",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"autoname": "field:webhook_name",
|
||||
"creation": "2018-05-22 13:20:51.450815",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"webhook_name",
|
||||
"webhook_url",
|
||||
"show_document_link"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "webhook_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "webhook_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "webhook_url",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Webhook URL",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldname": "webhook_url",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Webhook URL",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_in_quick_entry": 1,
|
||||
"default": "1",
|
||||
"fieldname": "show_document_link",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show link to document"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-05-22 13:25:24.621129",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Slack Webhook URL",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-05-12 18:24:37.810235",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Slack Webhook URL",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -25,22 +25,27 @@ class SlackWebhookURL(Document):
|
|||
|
||||
|
||||
def send_slack_message(webhook_url, message, reference_doctype, reference_name):
|
||||
slack_url = frappe.db.get_value("Slack Webhook URL", webhook_url, "webhook_url")
|
||||
doc_url = get_url_to_form(reference_doctype, reference_name)
|
||||
attachments = [
|
||||
{
|
||||
data = {"text": message, "attachments": []}
|
||||
|
||||
slack_url, show_link = frappe.db.get_value(
|
||||
"Slack Webhook URL", webhook_url, ["webhook_url", "show_document_link"]
|
||||
)
|
||||
|
||||
if show_link:
|
||||
doc_url = get_url_to_form(reference_doctype, reference_name)
|
||||
link_to_doc = {
|
||||
"fallback": _("See the document at {0}").format(doc_url),
|
||||
"actions": [
|
||||
{
|
||||
"type": "button",
|
||||
"text": _("Go to the document"),
|
||||
"url": doc_url,
|
||||
"style": "primary"
|
||||
"style": "primary",
|
||||
}
|
||||
]
|
||||
],
|
||||
}
|
||||
]
|
||||
data = {"text": message, "attachments": attachments}
|
||||
data["attachments"].append(link_to_doc)
|
||||
|
||||
r = requests.post(slack_url, data=json.dumps(data))
|
||||
|
||||
if not r.ok:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import json
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from oauthlib.oauth2 import FatalClientError, OAuth2Error
|
||||
from oauthlib.openid.connect.core.endpoints.pre_configured import (
|
||||
Server as WebApplicationServer,
|
||||
|
|
|
|||
|
|
@ -870,7 +870,7 @@ class BaseDocument(object):
|
|||
from frappe.model.meta import get_default_df
|
||||
df = get_default_df(fieldname)
|
||||
|
||||
if not currency:
|
||||
if not currency and df:
|
||||
currency = self.get(df.get("options"))
|
||||
if not frappe.db.exists('Currency', currency, cache=True):
|
||||
currency = None
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
|
|||
filters.update({fieldname: value})
|
||||
exists = frappe.db.exists(doctype, filters)
|
||||
|
||||
regex = "^{value}{separator}\d+$".format(value=re.escape(value), separator=separator)
|
||||
regex = "^{value}{separator}\\d+$".format(value=re.escape(value), separator=separator)
|
||||
|
||||
if exists:
|
||||
last = frappe.db.sql("""SELECT `{fieldname}` FROM `tab{doctype}`
|
||||
|
|
|
|||
|
|
@ -107,6 +107,15 @@ def import_doc(docdict, force=False, data_import=False, pre_process=None,
|
|||
|
||||
doc = frappe.get_doc(docdict)
|
||||
|
||||
# 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.
|
||||
if getattr(doc.meta, 'is_tree', None) and any([doc.lft, doc.rgt]):
|
||||
print('Ignoring values of `lft` and `rgt` for {} "{}"'.format(doc.doctype, doc.name))
|
||||
doc.lft = None
|
||||
doc.rgt = None
|
||||
|
||||
doc.run_method("before_import")
|
||||
|
||||
doc.flags.ignore_version = ignore_version
|
||||
|
|
|
|||
|
|
@ -4,11 +4,9 @@ import hashlib
|
|||
import re
|
||||
from http import cookies
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
import jwt
|
||||
import pytz
|
||||
from oauthlib.openid import RequestValidator
|
||||
|
||||
import frappe
|
||||
from frappe.auth import LoginManager
|
||||
|
||||
|
|
|
|||
282
frappe/parallel_test_runner.py
Normal file
282
frappe/parallel_test_runner.py
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import unittest
|
||||
import click
|
||||
import frappe
|
||||
import requests
|
||||
|
||||
from .test_runner import (SLOW_TEST_THRESHOLD, make_test_records, set_test_email_config)
|
||||
|
||||
click_ctx = click.get_current_context(True)
|
||||
if click_ctx:
|
||||
click_ctx.color = True
|
||||
|
||||
class ParallelTestRunner():
|
||||
def __init__(self, app, site, build_number=1, total_builds=1, with_coverage=False):
|
||||
self.app = app
|
||||
self.site = site
|
||||
self.with_coverage = with_coverage
|
||||
self.build_number = frappe.utils.cint(build_number) or 1
|
||||
self.total_builds = frappe.utils.cint(total_builds)
|
||||
self.setup_test_site()
|
||||
self.run_tests()
|
||||
|
||||
def setup_test_site(self):
|
||||
frappe.init(site=self.site)
|
||||
if not frappe.db:
|
||||
frappe.connect()
|
||||
|
||||
frappe.flags.in_test = True
|
||||
frappe.clear_cache()
|
||||
frappe.utils.scheduler.disable_scheduler()
|
||||
set_test_email_config()
|
||||
self.before_test_setup()
|
||||
|
||||
def before_test_setup(self):
|
||||
start_time = time.time()
|
||||
for fn in frappe.get_hooks("before_tests", app_name=self.app):
|
||||
frappe.get_attr(fn)()
|
||||
|
||||
test_module = frappe.get_module(f'{self.app}.tests')
|
||||
|
||||
if hasattr(test_module, "global_test_dependencies"):
|
||||
for doctype in test_module.global_test_dependencies:
|
||||
make_test_records(doctype)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
elapsed = click.style(f' ({elapsed:.03}s)', fg='red')
|
||||
click.echo(f'Before Test {elapsed}')
|
||||
|
||||
def run_tests(self):
|
||||
self.test_result = ParallelTestResult(stream=sys.stderr, descriptions=True, verbosity=2)
|
||||
|
||||
self.start_coverage()
|
||||
|
||||
for test_file_info in self.get_test_file_list():
|
||||
self.run_tests_for_file(test_file_info)
|
||||
|
||||
self.save_coverage()
|
||||
self.print_result()
|
||||
|
||||
def run_tests_for_file(self, file_info):
|
||||
if not file_info: return
|
||||
|
||||
frappe.set_user('Administrator')
|
||||
path, filename = file_info
|
||||
module = self.get_module(path, filename)
|
||||
self.create_test_dependency_records(module, path, filename)
|
||||
test_suite = unittest.TestSuite()
|
||||
module_test_cases = unittest.TestLoader().loadTestsFromModule(module)
|
||||
test_suite.addTest(module_test_cases)
|
||||
test_suite(self.test_result)
|
||||
|
||||
def create_test_dependency_records(self, module, path, filename):
|
||||
if hasattr(module, "test_dependencies"):
|
||||
for doctype in module.test_dependencies:
|
||||
make_test_records(doctype)
|
||||
|
||||
if os.path.basename(os.path.dirname(path)) == "doctype":
|
||||
# test_data_migration_connector.py > data_migration_connector.json
|
||||
test_record_filename = re.sub('^test_', '', filename).replace(".py", ".json")
|
||||
test_record_file_path = os.path.join(path, test_record_filename)
|
||||
if os.path.exists(test_record_file_path):
|
||||
with open(test_record_file_path, 'r') as f:
|
||||
doc = json.loads(f.read())
|
||||
doctype = doc["name"]
|
||||
make_test_records(doctype)
|
||||
|
||||
def get_module(self, path, filename):
|
||||
app_path = frappe.get_pymodule_path(self.app)
|
||||
relative_path = os.path.relpath(path, app_path)
|
||||
if relative_path == '.':
|
||||
module_name = self.app
|
||||
else:
|
||||
relative_path = relative_path.replace('/', '.')
|
||||
module_name = os.path.splitext(filename)[0]
|
||||
module_name = f'{self.app}.{relative_path}.{module_name}'
|
||||
|
||||
return frappe.get_module(module_name)
|
||||
|
||||
def print_result(self):
|
||||
self.test_result.printErrors()
|
||||
click.echo(self.test_result)
|
||||
if self.test_result.failures or self.test_result.errors:
|
||||
if os.environ.get('CI'):
|
||||
sys.exit(1)
|
||||
|
||||
def start_coverage(self):
|
||||
if self.with_coverage:
|
||||
from coverage import Coverage
|
||||
from frappe.utils import get_bench_path
|
||||
|
||||
# Generate coverage report only for app that is being tested
|
||||
source_path = os.path.join(get_bench_path(), 'apps', self.app)
|
||||
omit=['*.html', '*.js', '*.xml', '*.css', '*.less', '*.scss',
|
||||
'*.vue', '*/doctype/*/*_dashboard.py', '*/patches/*']
|
||||
|
||||
if self.app == 'frappe':
|
||||
omit.append('*/commands/*')
|
||||
|
||||
self.coverage = Coverage(source=[source_path], omit=omit)
|
||||
self.coverage.start()
|
||||
|
||||
def save_coverage(self):
|
||||
if not self.with_coverage:
|
||||
return
|
||||
self.coverage.stop()
|
||||
self.coverage.save()
|
||||
|
||||
def get_test_file_list(self):
|
||||
test_list = get_all_tests(self.app)
|
||||
split_size = frappe.utils.ceil(len(test_list) / self.total_builds)
|
||||
# [1,2,3,4,5,6] to [[1,2], [3,4], [4,6]] if split_size is 2
|
||||
test_chunks = [test_list[x:x+split_size] for x in range(0, len(test_list), split_size)]
|
||||
return test_chunks[self.build_number - 1]
|
||||
|
||||
|
||||
class ParallelTestResult(unittest.TextTestResult):
|
||||
def startTest(self, test):
|
||||
self._started_at = time.time()
|
||||
super(unittest.TextTestResult, self).startTest(test)
|
||||
test_class = unittest.util.strclass(test.__class__)
|
||||
if not hasattr(self, 'current_test_class') or self.current_test_class != test_class:
|
||||
click.echo(f"\n{unittest.util.strclass(test.__class__)}")
|
||||
self.current_test_class = test_class
|
||||
|
||||
def getTestMethodName(self, test):
|
||||
return test._testMethodName if hasattr(test, '_testMethodName') else str(test)
|
||||
|
||||
def addSuccess(self, test):
|
||||
super(unittest.TextTestResult, self).addSuccess(test)
|
||||
elapsed = time.time() - self._started_at
|
||||
threshold_passed = elapsed >= SLOW_TEST_THRESHOLD
|
||||
elapsed = click.style(f' ({elapsed:.03}s)', fg='red') if threshold_passed else ''
|
||||
click.echo(f" {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}{elapsed}")
|
||||
|
||||
def addError(self, test, err):
|
||||
super(unittest.TextTestResult, self).addError(test, err)
|
||||
click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")
|
||||
|
||||
def addFailure(self, test, err):
|
||||
super(unittest.TextTestResult, self).addFailure(test, err)
|
||||
click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")
|
||||
|
||||
def addSkip(self, test, reason):
|
||||
super(unittest.TextTestResult, self).addSkip(test, reason)
|
||||
click.echo(f" {click.style(' = ', fg='white')} {self.getTestMethodName(test)}")
|
||||
|
||||
def addExpectedFailure(self, test, err):
|
||||
super(unittest.TextTestResult, self).addExpectedFailure(test, err)
|
||||
click.echo(f" {click.style(' ✖ ', fg='red')} {self.getTestMethodName(test)}")
|
||||
|
||||
def addUnexpectedSuccess(self, test):
|
||||
super(unittest.TextTestResult, self).addUnexpectedSuccess(test)
|
||||
click.echo(f" {click.style(' ✔ ', fg='green')} {self.getTestMethodName(test)}")
|
||||
|
||||
def printErrors(self):
|
||||
click.echo('\n')
|
||||
self.printErrorList(' ERROR ', self.errors, 'red')
|
||||
self.printErrorList(' FAIL ', self.failures, 'red')
|
||||
|
||||
def printErrorList(self, flavour, errors, color):
|
||||
for test, err in errors:
|
||||
click.echo(self.separator1)
|
||||
click.echo(f"{click.style(flavour, bg=color)} {self.getDescription(test)}")
|
||||
click.echo(self.separator2)
|
||||
click.echo(err)
|
||||
|
||||
def __str__(self):
|
||||
return f"Tests: {self.testsRun}, Failing: {len(self.failures)}, Errors: {len(self.errors)}"
|
||||
|
||||
def get_all_tests(app):
|
||||
test_file_list = []
|
||||
for path, folders, files in os.walk(frappe.get_pymodule_path(app)):
|
||||
for dontwalk in ('locals', '.git', 'public', '__pycache__'):
|
||||
if dontwalk in folders:
|
||||
folders.remove(dontwalk)
|
||||
|
||||
# for predictability
|
||||
folders.sort()
|
||||
files.sort()
|
||||
|
||||
if os.path.sep.join(["doctype", "doctype", "boilerplate"]) in path:
|
||||
# in /doctype/doctype/boilerplate/
|
||||
continue
|
||||
|
||||
for filename in files:
|
||||
if filename.startswith("test_") and filename.endswith(".py") \
|
||||
and filename != 'test_runner.py':
|
||||
test_file_list.append([path, filename])
|
||||
|
||||
return test_file_list
|
||||
|
||||
|
||||
class ParallelTestWithOrchestrator(ParallelTestRunner):
|
||||
'''
|
||||
This can be used to balance-out test time across multiple instances
|
||||
This is dependent on external orchestrator which returns next test to run
|
||||
|
||||
orchestrator endpoints
|
||||
- register-instance (<build_id>, <instance_id>, test_spec_list)
|
||||
- get-next-test-spec (<build_id>, <instance_id>)
|
||||
- test-completed (<build_id>, <instance_id>)
|
||||
'''
|
||||
def __init__(self, app, site, with_coverage=False):
|
||||
self.orchestrator_url = os.environ.get('ORCHESTRATOR_URL')
|
||||
if not self.orchestrator_url:
|
||||
click.echo('ORCHESTRATOR_URL environment variable not found!')
|
||||
click.echo('Pass public URL after hosting https://github.com/frappe/test-orchestrator')
|
||||
sys.exit(1)
|
||||
|
||||
self.ci_build_id = os.environ.get('CI_BUILD_ID')
|
||||
self.ci_instance_id = os.environ.get('CI_INSTANCE_ID') or frappe.generate_hash(length=10)
|
||||
if not self.ci_build_id:
|
||||
click.echo('CI_BUILD_ID environment variable not found!')
|
||||
sys.exit(1)
|
||||
|
||||
ParallelTestRunner.__init__(self, app, site, with_coverage=with_coverage)
|
||||
|
||||
def run_tests(self):
|
||||
self.test_status = 'ongoing'
|
||||
self.register_instance()
|
||||
super().run_tests()
|
||||
|
||||
def get_test_file_list(self):
|
||||
while self.test_status == 'ongoing':
|
||||
yield self.get_next_test()
|
||||
|
||||
def register_instance(self):
|
||||
test_spec_list = get_all_tests(self.app)
|
||||
response_data = self.call_orchestrator('register-instance', data={
|
||||
'test_spec_list': test_spec_list
|
||||
})
|
||||
self.is_master = response_data.get('is_master')
|
||||
|
||||
def get_next_test(self):
|
||||
response_data = self.call_orchestrator('get-next-test-spec')
|
||||
self.test_status = response_data.get('status')
|
||||
return response_data.get('next_test')
|
||||
|
||||
def print_result(self):
|
||||
self.call_orchestrator('test-completed')
|
||||
return super().print_result()
|
||||
|
||||
def call_orchestrator(self, endpoint, data={}):
|
||||
# add repo token header
|
||||
# build id in header
|
||||
headers = {
|
||||
'CI-BUILD-ID': self.ci_build_id,
|
||||
'CI-INSTANCE-ID': self.ci_instance_id,
|
||||
'REPO-TOKEN': '2948288382838DE'
|
||||
}
|
||||
url = f'{self.orchestrator_url}/{endpoint}'
|
||||
res = requests.get(url, json=data, headers=headers)
|
||||
res.raise_for_status()
|
||||
response_data = {}
|
||||
if 'application/json' in res.headers.get('content-type'):
|
||||
response_data = res.json()
|
||||
|
||||
return response_data
|
||||
|
|
@ -33,8 +33,7 @@ def execute():
|
|||
def scrub_relative_urls(html):
|
||||
"""prepend a slash before a relative url"""
|
||||
try:
|
||||
return re.sub("""src[\s]*=[\s]*['"]files/([^'"]*)['"]""", 'src="/files/\g<1>"', html)
|
||||
# return re.sub("""(src|href)[^\w'"]*['"](?!http|ftp|mailto|/|#|%|{|cid:|\.com/www\.)([^'" >]+)['"]""", '\g<1>="/\g<2>"', html)
|
||||
return re.sub(r'src[\s]*=[\s]*[\'"]files/([^\'"]*)[\'"]', r'src="/files/\g<1>"', html)
|
||||
except:
|
||||
print("Error", html)
|
||||
raise
|
||||
|
|
|
|||
|
|
@ -12,13 +12,13 @@ class TestPrintFormat(unittest.TestCase):
|
|||
def test_print_user(self, style=None):
|
||||
print_html = frappe.get_print("User", "Administrator", style=style)
|
||||
self.assertTrue("<label>First Name: </label>" in print_html)
|
||||
self.assertTrue(re.findall('<div class="col-xs-[^"]*">[\s]*administrator[\s]*</div>', print_html))
|
||||
self.assertTrue(re.findall(r'<div class="col-xs-[^"]*">[\s]*administrator[\s]*</div>', print_html))
|
||||
return print_html
|
||||
|
||||
def test_print_user_standard(self):
|
||||
print_html = self.test_print_user("Standard")
|
||||
self.assertTrue(re.findall('\.print-format {[\s]*font-size: 9pt;', print_html))
|
||||
self.assertFalse(re.findall('th {[\s]*background-color: #eee;[\s]*}', print_html))
|
||||
self.assertTrue(re.findall(r'\.print-format {[\s]*font-size: 9pt;', print_html))
|
||||
self.assertFalse(re.findall(r'th {[\s]*background-color: #eee;[\s]*}', print_html))
|
||||
self.assertFalse("font-family: serif;" in print_html)
|
||||
|
||||
def test_print_user_modern(self):
|
||||
|
|
|
|||
|
|
@ -408,14 +408,17 @@ frappe.ui.form.PrintView = class {
|
|||
|
||||
setup_print_format_dom(out, $print_format) {
|
||||
this.print_wrapper.find('.print-format-skeleton').remove();
|
||||
let base_url = frappe.urllib.get_base_url();
|
||||
let print_css = frappe.assets.bundled_asset('print.bundle.css');
|
||||
this.$print_format_body.find('head').html(
|
||||
`<style type="text/css">${out.style}</style>
|
||||
<link href="${frappe.urllib.get_base_url()}/assets/css/printview.css" rel="stylesheet">`
|
||||
<link href="${base_url}${print_css}" rel="stylesheet">`
|
||||
);
|
||||
|
||||
if (frappe.utils.is_rtl(this.lang_code)) {
|
||||
let rtl_css = frappe.assets.bundled_asset('frappe-rtl.bundle.css');
|
||||
this.$print_format_body.find('head').append(
|
||||
`<link type="text/css" rel="stylesheet" href="${frappe.urllib.get_base_url()}/assets/css/frappe-rtl.css"></link>`
|
||||
`<link type="text/css" rel="stylesheet" href="${base_url}${rtl_css}"></link>`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,13 +23,13 @@ frappe.pages['print-format-builder'].on_page_show = function(wrapper) {
|
|||
}
|
||||
}
|
||||
|
||||
frappe.PrintFormatBuilder = Class.extend({
|
||||
init: function(parent) {
|
||||
frappe.PrintFormatBuilder = class PrintFormatBuilder {
|
||||
constructor(parent) {
|
||||
this.parent = parent;
|
||||
this.make();
|
||||
this.refresh();
|
||||
},
|
||||
refresh: function() {
|
||||
}
|
||||
refresh() {
|
||||
this.custom_html_count = 0;
|
||||
if(!this.print_format) {
|
||||
this.show_start();
|
||||
|
|
@ -37,8 +37,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
this.page.set_title(this.print_format.name);
|
||||
this.setup_print_format();
|
||||
}
|
||||
},
|
||||
make: function() {
|
||||
}
|
||||
make() {
|
||||
this.page = frappe.ui.make_app_page({
|
||||
parent: this.parent,
|
||||
title: __("Print Format Builder"),
|
||||
|
|
@ -56,15 +56,15 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
this.setup_edit_custom_html();
|
||||
// $(this.page.sidebar).css({"position": 'fixed'});
|
||||
// $(this.page.main).parent().css({"margin-left": '16.67%'});
|
||||
},
|
||||
show_start: function() {
|
||||
}
|
||||
show_start() {
|
||||
this.page.main.html(frappe.render_template("print_format_builder_start", {}));
|
||||
this.page.clear_actions();
|
||||
this.page.set_title(__("Print Format Builder"));
|
||||
this.start_edit_print_format();
|
||||
this.start_new_print_format();
|
||||
},
|
||||
start_edit_print_format: function() {
|
||||
}
|
||||
start_edit_print_format() {
|
||||
// print format control
|
||||
var me = this;
|
||||
this.print_format_input = frappe.ui.form.make_control({
|
||||
|
|
@ -89,8 +89,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
frappe.set_route('print-format-builder', name);
|
||||
});
|
||||
});
|
||||
},
|
||||
start_new_print_format: function() {
|
||||
}
|
||||
start_new_print_format() {
|
||||
var me = this;
|
||||
this.doctype_input = frappe.ui.form.make_control({
|
||||
parent: this.page.main.find(".doctype-selector"),
|
||||
|
|
@ -125,8 +125,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
me.setup_new_print_format(doctype, name);
|
||||
|
||||
});
|
||||
},
|
||||
setup_new_print_format: function(doctype, name, based_on) {
|
||||
}
|
||||
setup_new_print_format(doctype, name, based_on) {
|
||||
frappe.call({
|
||||
method: 'frappe.printing.page.print_format_builder.print_format_builder.create_custom_format',
|
||||
args: {
|
||||
|
|
@ -143,8 +143,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
setup_print_format: function() {
|
||||
}
|
||||
setup_print_format() {
|
||||
var me = this;
|
||||
frappe.model.with_doctype(this.print_format.doc_type, function(doctype) {
|
||||
me.meta = frappe.get_meta(me.print_format.doc_type);
|
||||
|
|
@ -163,23 +163,23 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
frappe.set_route("Form", "Print Format", me.print_format.name);
|
||||
});
|
||||
});
|
||||
},
|
||||
setup_sidebar: function() {
|
||||
}
|
||||
setup_sidebar() {
|
||||
// prepend custom HTML field
|
||||
var fields = [this.get_custom_html_field()].concat(this.meta.fields);
|
||||
this.page.sidebar.html(
|
||||
$(frappe.render_template("print_format_builder_sidebar", {fields: fields}))
|
||||
);
|
||||
this.setup_field_filter();
|
||||
},
|
||||
get_custom_html_field: function() {
|
||||
}
|
||||
get_custom_html_field() {
|
||||
return {
|
||||
fieldtype: "Custom HTML",
|
||||
fieldname: "_custom_html",
|
||||
label: __("Custom HTML")
|
||||
}
|
||||
},
|
||||
render_layout: function() {
|
||||
};
|
||||
}
|
||||
render_layout() {
|
||||
this.page.main.empty();
|
||||
this.prepare_data();
|
||||
$(frappe.render_template("print_format_builder_layout", {
|
||||
|
|
@ -190,8 +190,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
this.setup_edit_heading();
|
||||
this.setup_field_settings();
|
||||
this.setup_html_data();
|
||||
},
|
||||
prepare_data: function() {
|
||||
}
|
||||
prepare_data() {
|
||||
this.print_heading_template = null;
|
||||
this.data = JSON.parse(this.print_format.format_data || "[]");
|
||||
if(!this.data.length) {
|
||||
|
|
@ -280,22 +280,22 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
this.layout_data = $.map(this.layout_data, function(s) {
|
||||
return s.has_fields ? s : null
|
||||
});
|
||||
},
|
||||
get_new_section: function() {
|
||||
}
|
||||
get_new_section() {
|
||||
return {columns: [], no_of_columns: 0, label:''};
|
||||
},
|
||||
get_new_column: function() {
|
||||
}
|
||||
get_new_column() {
|
||||
return {fields: []}
|
||||
},
|
||||
add_table_properties: function(f) {
|
||||
}
|
||||
add_table_properties(f) {
|
||||
// build table columns and widths in a dict
|
||||
// visible_columns
|
||||
var me = this;
|
||||
if(!f.visible_columns) {
|
||||
me.init_visible_columns(f);
|
||||
}
|
||||
},
|
||||
init_visible_columns: function(f) {
|
||||
}
|
||||
init_visible_columns(f) {
|
||||
f.visible_columns = []
|
||||
$.each(frappe.get_meta(f.options).fields, function(i, _f) {
|
||||
if(!in_list(["Section Break", "Column Break"], _f.fieldtype) &&
|
||||
|
|
@ -306,8 +306,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
print_width: (_f.width || ""), print_hide:0});
|
||||
}
|
||||
});
|
||||
},
|
||||
setup_sortable: function() {
|
||||
}
|
||||
setup_sortable() {
|
||||
var me = this;
|
||||
|
||||
// drag from fields library
|
||||
|
|
@ -332,8 +332,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
Sortable.create(this.page.main.find(".print-format-builder-layout").get(0),
|
||||
{ handle: ".print-format-builder-section-head" }
|
||||
);
|
||||
},
|
||||
setup_sortable_for_column: function(col) {
|
||||
}
|
||||
setup_sortable_for_column(col) {
|
||||
var me = this;
|
||||
Sortable.create(col, {
|
||||
group: {
|
||||
|
|
@ -363,8 +363,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
}
|
||||
});
|
||||
|
||||
},
|
||||
setup_field_filter: function() {
|
||||
}
|
||||
setup_field_filter() {
|
||||
var me = this;
|
||||
this.page.sidebar.find(".filter-fields").on("keyup", function() {
|
||||
var text = $(this).val();
|
||||
|
|
@ -373,8 +373,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
$(this).parent().toggle(show);
|
||||
})
|
||||
});
|
||||
},
|
||||
setup_section_settings: function() {
|
||||
}
|
||||
setup_section_settings() {
|
||||
var me = this;
|
||||
this.page.main.on("click", ".section-settings", function() {
|
||||
var section = $(this).parent().parent();
|
||||
|
|
@ -431,8 +431,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
setup_field_settings: function() {
|
||||
}
|
||||
setup_field_settings() {
|
||||
this.page.main.find(".field-settings").on("click", e => {
|
||||
const field = $(e.currentTarget).parent();
|
||||
// new dialog
|
||||
|
|
@ -482,8 +482,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
setup_html_data: function() {
|
||||
}
|
||||
setup_html_data() {
|
||||
// set JQuery `data` for Custom HTML fields, since editing the HTML
|
||||
// directly causes problem becuase of HTML reformatting
|
||||
//
|
||||
|
|
@ -496,8 +496,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
var html = me.custom_html_dict[parseInt(content.attr('data-custom-html-id'))].options;
|
||||
content.data('content', html);
|
||||
})
|
||||
},
|
||||
update_columns_in_section: function(section, no_of_columns, new_no_of_columns) {
|
||||
}
|
||||
update_columns_in_section(section, no_of_columns, new_no_of_columns) {
|
||||
var col_size = 12 / new_no_of_columns,
|
||||
me = this,
|
||||
resize = function() {
|
||||
|
|
@ -539,8 +539,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
resize();
|
||||
}
|
||||
|
||||
},
|
||||
setup_add_section: function() {
|
||||
}
|
||||
setup_add_section() {
|
||||
var me = this;
|
||||
this.page.main.find(".print-format-builder-add-section").on("click", function() {
|
||||
// boostrap new section info
|
||||
|
|
@ -554,8 +554,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
|
||||
me.setup_sortable_for_column($section.find(".print-format-builder-column").get(0));
|
||||
});
|
||||
},
|
||||
setup_edit_heading: function() {
|
||||
}
|
||||
setup_edit_heading() {
|
||||
var me = this;
|
||||
var $heading = this.page.main.find(".print-format-builder-print-heading");
|
||||
|
||||
|
|
@ -565,8 +565,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
this.page.main.find(".edit-heading").on("click", function() {
|
||||
var d = me.get_edit_html_dialog(__("Edit Heading"), __("Heading"), $heading);
|
||||
})
|
||||
},
|
||||
setup_column_selector: function() {
|
||||
}
|
||||
setup_column_selector() {
|
||||
var me = this;
|
||||
this.page.main.on("click", ".select-columns", function() {
|
||||
var parent = $(this).parents(".print-format-builder-field:first"),
|
||||
|
|
@ -657,24 +657,24 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
|
||||
return false;
|
||||
});
|
||||
},
|
||||
get_visible_columns_string: function(f) {
|
||||
}
|
||||
get_visible_columns_string(f) {
|
||||
if(!f.visible_columns) {
|
||||
this.init_visible_columns(f);
|
||||
}
|
||||
return $.map(f.visible_columns, function(v) { return v.fieldname + "|" + (v.print_width || "") }).join(",");
|
||||
},
|
||||
get_no_content: function() {
|
||||
}
|
||||
get_no_content() {
|
||||
return __("Edit to add content")
|
||||
},
|
||||
setup_edit_custom_html: function() {
|
||||
}
|
||||
setup_edit_custom_html() {
|
||||
var me = this;
|
||||
this.page.main.on("click", ".edit-html", function() {
|
||||
me.get_edit_html_dialog(__("Edit Custom HTML"), __("Custom HTML"),
|
||||
$(this).parents(".print-format-builder-field:first").find(".html-content"));
|
||||
});
|
||||
},
|
||||
get_edit_html_dialog: function(title, label, $content) {
|
||||
}
|
||||
get_edit_html_dialog(title, label, $content) {
|
||||
var me = this;
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
|
|
@ -710,8 +710,8 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
d.show();
|
||||
|
||||
return d;
|
||||
},
|
||||
save_print_format: function() {
|
||||
}
|
||||
save_print_format() {
|
||||
var data = [],
|
||||
me = this;
|
||||
|
||||
|
|
@ -789,4 +789,4 @@ frappe.PrintFormatBuilder = Class.extend({
|
|||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
<title>{{ title }}</title>
|
||||
<link href="{{ base_url }}/assets/css/printview.css" rel="stylesheet">
|
||||
<link href="{{ base_url }}{{ frappe.assets.bundled_asset('print.bundle.css') }}" rel="stylesheet">
|
||||
<style>
|
||||
{{ print_css }}
|
||||
</style>
|
||||
|
|
|
|||
1
frappe/public/js/barcode_scanner.bundle.js
Normal file
1
frappe/public/js/barcode_scanner.bundle.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "./frappe/barcode_scanner/quagga";
|
||||
64
frappe/public/js/bootstrap-4-web.bundle.js
vendored
Normal file
64
frappe/public/js/bootstrap-4-web.bundle.js
vendored
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
|
||||
// multilevel dropdown
|
||||
$('.dropdown-menu a.dropdown-toggle').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
if (!$(this).next().hasClass('show')) {
|
||||
$(this).parents('.dropdown-menu').first().find('.show').removeClass("show");
|
||||
}
|
||||
var $subMenu = $(this).next(".dropdown-menu");
|
||||
$subMenu.toggleClass('show');
|
||||
|
||||
|
||||
$(this).parents('li.nav-item.dropdown.show').on('hidden.bs.dropdown', function () {
|
||||
$('.dropdown-submenu .show').removeClass("show");
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
frappe.get_modal = function (title, content) {
|
||||
return $(
|
||||
`<div class="modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-scrollable" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${title}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
${frappe.utils.icon('close-alt', 'sm', 'close-alt')}
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${content}
|
||||
</div>
|
||||
<div class="modal-footer hidden">
|
||||
<button type="button" class="btn btn-default btn-sm btn-modal-close" data-dismiss="modal">
|
||||
<i class="octicon octicon-x visible-xs" style="padding: 1px 0px;"></i>
|
||||
<span class="hidden-xs">${__("Close")}</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary hidden"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
);
|
||||
};
|
||||
|
||||
frappe.ui.Dialog = class Dialog extends frappe.ui.Dialog {
|
||||
get_primary_btn() {
|
||||
return this.$wrapper.find(".modal-footer .btn-primary");
|
||||
}
|
||||
|
||||
set_primary_action(label, click) {
|
||||
this.$wrapper.find('.modal-footer').removeClass('hidden');
|
||||
return super.set_primary_action(label, click)
|
||||
.removeClass('hidden');
|
||||
}
|
||||
|
||||
make() {
|
||||
super.make();
|
||||
if (this.fields) {
|
||||
this.$wrapper.find('.section-body').addClass('w-100');
|
||||
}
|
||||
}
|
||||
};
|
||||
1
frappe/public/js/chat.bundle.js
Normal file
1
frappe/public/js/chat.bundle.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "./frappe/chat";
|
||||
1
frappe/public/js/checkout.bundle.js
Normal file
1
frappe/public/js/checkout.bundle.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "./integrations/razorpay";
|
||||
18
frappe/public/js/controls.bundle.js
Normal file
18
frappe/public/js/controls.bundle.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import "air-datepicker/dist/js/datepicker.min.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.cs.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.da.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.de.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.en.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.es.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.fi.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.fr.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.hu.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.nl.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.pl.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.pt-BR.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.pt.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.ro.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.sk.js";
|
||||
import "air-datepicker/dist/js/i18n/datepicker.zh.js";
|
||||
import "./frappe/ui/capture.js";
|
||||
import "./frappe/form/controls/control.js";
|
||||
1
frappe/public/js/data_import_tools.bundle.js
Normal file
1
frappe/public/js/data_import_tools.bundle.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import "./frappe/data_import";
|
||||
105
frappe/public/js/desk.bundle.js
Normal file
105
frappe/public/js/desk.bundle.js
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import "./frappe/translate.js";
|
||||
import "./frappe/class.js";
|
||||
import "./frappe/polyfill.js";
|
||||
import "./frappe/provide.js";
|
||||
import "./frappe/assets.js";
|
||||
import "./frappe/format.js";
|
||||
import "./frappe/form/formatters.js";
|
||||
import "./frappe/dom.js";
|
||||
import "./frappe/ui/messages.js";
|
||||
import "./frappe/ui/keyboard.js";
|
||||
import "./frappe/ui/colors.js";
|
||||
import "./frappe/ui/sidebar.js";
|
||||
import "./frappe/ui/link_preview.js";
|
||||
|
||||
import "./frappe/request.js";
|
||||
import "./frappe/socketio_client.js";
|
||||
import "./frappe/utils/utils.js";
|
||||
import "./frappe/event_emitter.js";
|
||||
import "./frappe/router.js";
|
||||
import "./frappe/router_history.js";
|
||||
import "./frappe/defaults.js";
|
||||
import "./frappe/roles_editor.js";
|
||||
import "./frappe/module_editor.js";
|
||||
import "./frappe/microtemplate.js";
|
||||
|
||||
import "./frappe/ui/page.html";
|
||||
import "./frappe/ui/page.js";
|
||||
import "./frappe/ui/slides.js";
|
||||
// import "./frappe/ui/onboarding_dialog.js";
|
||||
import "./frappe/ui/find.js";
|
||||
import "./frappe/ui/iconbar.js";
|
||||
import "./frappe/form/layout.js";
|
||||
import "./frappe/ui/field_group.js";
|
||||
import "./frappe/form/link_selector.js";
|
||||
import "./frappe/form/multi_select_dialog.js";
|
||||
import "./frappe/ui/dialog.js";
|
||||
import "./frappe/ui/capture.js";
|
||||
import "./frappe/ui/app_icon.js";
|
||||
import "./frappe/ui/theme_switcher.js";
|
||||
|
||||
import "./frappe/model/model.js";
|
||||
import "./frappe/db.js";
|
||||
import "./frappe/model/meta.js";
|
||||
import "./frappe/model/sync.js";
|
||||
import "./frappe/model/create_new.js";
|
||||
import "./frappe/model/perm.js";
|
||||
import "./frappe/model/workflow.js";
|
||||
import "./frappe/model/user_settings.js";
|
||||
|
||||
import "./frappe/utils/user.js";
|
||||
import "./frappe/utils/common.js";
|
||||
import "./frappe/utils/urllib.js";
|
||||
import "./frappe/utils/pretty_date.js";
|
||||
import "./frappe/utils/tools.js";
|
||||
import "./frappe/utils/datetime.js";
|
||||
import "./frappe/utils/number_format.js";
|
||||
import "./frappe/utils/help.js";
|
||||
import "./frappe/utils/help_links.js";
|
||||
import "./frappe/utils/address_and_contact.js";
|
||||
import "./frappe/utils/preview_email.js";
|
||||
import "./frappe/utils/file_manager.js";
|
||||
|
||||
import "./frappe/upload.js";
|
||||
import "./frappe/ui/tree.js";
|
||||
|
||||
import "./frappe/views/container.js";
|
||||
import "./frappe/views/breadcrumbs.js";
|
||||
import "./frappe/views/factory.js";
|
||||
import "./frappe/views/pageview.js";
|
||||
|
||||
import "./frappe/ui/toolbar/awesome_bar.js";
|
||||
// import "./frappe/ui/toolbar/energy_points_notifications.js";
|
||||
import "./frappe/ui/notifications/notifications.js";
|
||||
import "./frappe/ui/toolbar/search.js";
|
||||
import "./frappe/ui/toolbar/tag_utils.js";
|
||||
import "./frappe/ui/toolbar/search.html";
|
||||
import "./frappe/ui/toolbar/search_utils.js";
|
||||
import "./frappe/ui/toolbar/about.js";
|
||||
import "./frappe/ui/toolbar/navbar.html";
|
||||
import "./frappe/ui/toolbar/toolbar.js";
|
||||
// import "./frappe/ui/toolbar/notifications.js";
|
||||
import "./frappe/views/communication.js";
|
||||
import "./frappe/views/translation_manager.js";
|
||||
import "./frappe/views/workspace/workspace.js";
|
||||
|
||||
import "./frappe/widgets/widget_group.js";
|
||||
|
||||
import "./frappe/ui/sort_selector.html";
|
||||
import "./frappe/ui/sort_selector.js";
|
||||
|
||||
import "./frappe/change_log.html";
|
||||
import "./frappe/ui/workspace_loading_skeleton.html";
|
||||
import "./frappe/desk.js";
|
||||
import "./frappe/query_string.js";
|
||||
|
||||
// import "./frappe/ui/comment.js";
|
||||
|
||||
import "./frappe/chat.js";
|
||||
import "./frappe/utils/energy_point_utils.js";
|
||||
import "./frappe/utils/dashboard_utils.js";
|
||||
import "./frappe/ui/chart.js";
|
||||
import "./frappe/ui/datatable.js";
|
||||
import "./frappe/ui/driver.js";
|
||||
import "./frappe/ui/plyr.js";
|
||||
import "./frappe/barcode_scanner/index.js";
|
||||
7
frappe/public/js/dialog.bundle.js
Normal file
7
frappe/public/js/dialog.bundle.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import "./frappe/dom.js";
|
||||
import "./frappe/form/formatters.js";
|
||||
import "./frappe/form/layout.js";
|
||||
import "./frappe/ui/field_group.js";
|
||||
import "./frappe/form/link_selector.js";
|
||||
import "./frappe/form/multi_select_dialog.js";
|
||||
import "./frappe/ui/dialog.js";
|
||||
17
frappe/public/js/form.bundle.js
Normal file
17
frappe/public/js/form.bundle.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import "./frappe/form/templates/address_list.html";
|
||||
import "./frappe/form/templates/contact_list.html";
|
||||
import "./frappe/form/templates/form_dashboard.html";
|
||||
import "./frappe/form/templates/form_footer.html";
|
||||
import "./frappe/form/templates/form_links.html";
|
||||
import "./frappe/form/templates/form_sidebar.html";
|
||||
import "./frappe/form/templates/print_layout.html";
|
||||
import "./frappe/form/templates/report_links.html";
|
||||
import "./frappe/form/templates/set_sharing.html";
|
||||
import "./frappe/form/templates/timeline_message_box.html";
|
||||
import "./frappe/form/templates/users_in_sidebar.html";
|
||||
|
||||
import "./frappe/form/controls/control.js";
|
||||
import "./frappe/views/formview.js";
|
||||
import "./frappe/form/form.js";
|
||||
import "./frappe/meta_tag.js";
|
||||
|
||||
26
frappe/public/js/frappe-web.bundle.js
Normal file
26
frappe/public/js/frappe-web.bundle.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import "./jquery-bootstrap";
|
||||
import "./frappe/class.js";
|
||||
import "./frappe/polyfill.js";
|
||||
import "./lib/md5.min.js";
|
||||
import "./frappe/provide.js";
|
||||
import "./frappe/format.js";
|
||||
import "./frappe/utils/number_format.js";
|
||||
import "./frappe/utils/utils.js";
|
||||
import "./frappe/utils/common.js";
|
||||
import "./frappe/ui/messages.js";
|
||||
import "./frappe/translate.js";
|
||||
import "./frappe/utils/pretty_date.js";
|
||||
import "./frappe/microtemplate.js";
|
||||
import "./frappe/query_string.js";
|
||||
|
||||
import "./frappe/upload.js";
|
||||
|
||||
import "./frappe/model/meta.js";
|
||||
import "./frappe/model/model.js";
|
||||
import "./frappe/model/perm.js";
|
||||
|
||||
import "./bootstrap-4-web.bundle";
|
||||
|
||||
|
||||
import "../../website/js/website.js";
|
||||
import "./frappe/socketio_client.js";
|
||||
|
|
@ -9,7 +9,14 @@ frappe.require = function(items, callback) {
|
|||
if(typeof items === "string") {
|
||||
items = [items];
|
||||
}
|
||||
frappe.assets.execute(items, callback);
|
||||
items = items.map(item => frappe.assets.bundled_asset(item));
|
||||
|
||||
return new Promise(resolve => {
|
||||
frappe.assets.execute(items, () => {
|
||||
resolve();
|
||||
callback && callback();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
frappe.assets = {
|
||||
|
|
@ -160,4 +167,11 @@ frappe.assets = {
|
|||
frappe.dom.set_style(txt);
|
||||
}
|
||||
},
|
||||
|
||||
bundled_asset(path) {
|
||||
if (!path.startsWith('/assets') && path.includes('.bundle.')) {
|
||||
return frappe.boot.assets_json[path] || path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ frappe.barcode.scan_barcode = function() {
|
|||
}
|
||||
}, reject);
|
||||
} else {
|
||||
frappe.require('/assets/js/barcode_scanner.min.js', () => {
|
||||
frappe.require('barcode_scanner.bundle.js', () => {
|
||||
frappe.barcode.get_barcode().then(barcode => {
|
||||
resolve(barcode);
|
||||
});
|
||||
|
|
|
|||
111
frappe/public/js/frappe/build_events/BuildError.vue
Normal file
111
frappe/public/js/frappe/build_events/BuildError.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<template>
|
||||
<div class="build-error-overlay" @click.self="data = null" v-show="data">
|
||||
<div class="window" v-if="data">
|
||||
<div v-for="(error, i) in data.formatted" :key="i">
|
||||
<!-- prettier-ignore -->
|
||||
<pre class="frame"><component :is="error_component(error, i)" /></pre>
|
||||
</div>
|
||||
<pre class="stack">{{ data.stack }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BuildError",
|
||||
data() {
|
||||
return {
|
||||
data: null
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show(data) {
|
||||
this.data = data;
|
||||
},
|
||||
hide() {
|
||||
this.data = null;
|
||||
},
|
||||
open_in_editor(location) {
|
||||
frappe.socketio.socket.emit("open_in_editor", location);
|
||||
},
|
||||
error_component(error, i) {
|
||||
let location = this.data.error.errors[i].location;
|
||||
let location_string = `${location.file}:${location.line}:${
|
||||
location.column
|
||||
}`;
|
||||
let template = error.replace(
|
||||
" > " + location_string,
|
||||
` > <a class="file-link" @click="open">${location_string}</a>`
|
||||
);
|
||||
|
||||
return {
|
||||
template: `<div>${template}</div>`,
|
||||
methods: {
|
||||
open() {
|
||||
frappe.socketio.socket.emit("open_in_editor", location);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.build-error-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
margin: 0;
|
||||
background: rgba(0, 0, 0, 0.66);
|
||||
--monospace: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier,
|
||||
monospace;
|
||||
--dim: var(--gray-400);
|
||||
}
|
||||
.window {
|
||||
font-family: var(--monospace);
|
||||
line-height: 1.5;
|
||||
width: 800px;
|
||||
color: #d8d8d8;
|
||||
margin: 30px auto;
|
||||
padding: 25px 40px;
|
||||
position: relative;
|
||||
background: #181818;
|
||||
border-radius: 6px 6px 8px 8px;
|
||||
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.3), 0 15px 12px rgba(0, 0, 0, 0.22);
|
||||
overflow: hidden;
|
||||
border-top: 8px solid var(--red);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: var(--monospace);
|
||||
font-size: 13px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1em;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
code {
|
||||
font-size: 13px;
|
||||
font-family: var(--monospace);
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.message {
|
||||
line-height: 1.3;
|
||||
font-weight: 600;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.frame {
|
||||
color: var(--yellow);
|
||||
}
|
||||
.stack {
|
||||
font-size: 13px;
|
||||
color: var(--dim);
|
||||
}
|
||||
.file-link {
|
||||
text-decoration: underline !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
52
frappe/public/js/frappe/build_events/BuildSuccess.vue
Normal file
52
frappe/public/js/frappe/build_events/BuildSuccess.vue
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<template>
|
||||
<div
|
||||
v-if="is_shown"
|
||||
class="flex justify-between build-success-message align-center"
|
||||
>
|
||||
<div class="mr-4">Compiled successfully</div>
|
||||
<a class="text-white underline" href="/" @click.prevent="reload">
|
||||
Refresh
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: "BuildSuccess",
|
||||
data() {
|
||||
return {
|
||||
is_shown: false
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.is_shown = true;
|
||||
if (this.timeout) {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
this.timeout = setTimeout(() => {
|
||||
this.hide();
|
||||
}, 10000);
|
||||
},
|
||||
hide() {
|
||||
this.is_shown = false;
|
||||
},
|
||||
reload() {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.build-success-message {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
margin: 1rem;
|
||||
}
|
||||
</style>
|
||||
48
frappe/public/js/frappe/build_events/build_events.bundle.js
Normal file
48
frappe/public/js/frappe/build_events/build_events.bundle.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import BuildError from "./BuildError.vue";
|
||||
import BuildSuccess from "./BuildSuccess.vue";
|
||||
|
||||
let $container = $("#build-events-overlay");
|
||||
let success = null;
|
||||
let error = null;
|
||||
|
||||
frappe.realtime.on("build_event", data => {
|
||||
if (data.success) {
|
||||
show_build_success(data);
|
||||
} else if (data.error) {
|
||||
show_build_error(data);
|
||||
}
|
||||
});
|
||||
|
||||
function show_build_success() {
|
||||
if (error) {
|
||||
error.hide();
|
||||
}
|
||||
if (!success) {
|
||||
let target = $('<div class="build-success-container">')
|
||||
.appendTo($container)
|
||||
.get(0);
|
||||
let vm = new Vue({
|
||||
el: target,
|
||||
render: h => h(BuildSuccess)
|
||||
});
|
||||
success = vm.$children[0];
|
||||
}
|
||||
success.show();
|
||||
}
|
||||
|
||||
function show_build_error(data) {
|
||||
if (success) {
|
||||
success.hide();
|
||||
}
|
||||
if (!error) {
|
||||
let target = $('<div class="build-error-container">')
|
||||
.appendTo($container)
|
||||
.get(0);
|
||||
let vm = new Vue({
|
||||
el: target,
|
||||
render: h => h(BuildError)
|
||||
});
|
||||
error = vm.$children[0];
|
||||
}
|
||||
error.show(data);
|
||||
}
|
||||
|
|
@ -80,4 +80,4 @@ To subclass, use:
|
|||
|
||||
// export
|
||||
global.Class = Class;
|
||||
})(this);
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -24,12 +24,12 @@ $(document).ready(function() {
|
|||
frappe.start_app();
|
||||
});
|
||||
|
||||
frappe.Application = Class.extend({
|
||||
init: function() {
|
||||
frappe.Application = class Application {
|
||||
constructor() {
|
||||
this.startup();
|
||||
},
|
||||
}
|
||||
|
||||
startup: function() {
|
||||
startup() {
|
||||
frappe.socketio.init();
|
||||
frappe.model.init();
|
||||
|
||||
|
|
@ -115,7 +115,7 @@ frappe.Application = Class.extend({
|
|||
});
|
||||
|
||||
// listen to build errors
|
||||
this.setup_build_error_listener();
|
||||
this.setup_build_events();
|
||||
|
||||
if (frappe.sys_defaults.email_user_password) {
|
||||
var email_list = frappe.sys_defaults.email_user_password.split(',');
|
||||
|
|
@ -160,7 +160,7 @@ frappe.Application = Class.extend({
|
|||
}, 600000); // check every 10 minutes
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
set_route() {
|
||||
frappe.flags.setting_original_route = true;
|
||||
|
|
@ -175,14 +175,14 @@ frappe.Application = Class.extend({
|
|||
frappe.router.on('change', () => {
|
||||
$(".tooltip").hide();
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
setup_frappe_vue() {
|
||||
Vue.prototype.__ = window.__;
|
||||
Vue.prototype.frappe = window.frappe;
|
||||
},
|
||||
}
|
||||
|
||||
set_password: function(user) {
|
||||
set_password(user) {
|
||||
var me=this;
|
||||
frappe.call({
|
||||
method: 'frappe.core.doctype.user.user.get_email_awaiting',
|
||||
|
|
@ -199,9 +199,9 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
email_password_prompt: function(email_account,user,i) {
|
||||
email_password_prompt(email_account,user,i) {
|
||||
var me = this;
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Password missing in Email Account'),
|
||||
|
|
@ -255,8 +255,8 @@ frappe.Application = Class.extend({
|
|||
});
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
load_bootinfo: function() {
|
||||
}
|
||||
load_bootinfo() {
|
||||
if(frappe.boot) {
|
||||
this.setup_workspaces();
|
||||
frappe.model.sync(frappe.boot.docs);
|
||||
|
|
@ -278,7 +278,7 @@ frappe.Application = Class.extend({
|
|||
} else {
|
||||
this.set_as_guest();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_workspaces() {
|
||||
frappe.modules = {};
|
||||
|
|
@ -289,26 +289,26 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
if (!frappe.workspaces['home']) {
|
||||
// default workspace is settings for Frappe
|
||||
frappe.workspaces['home'] = frappe.workspaces['build'];
|
||||
frappe.workspaces['home'] = frappe.workspaces[Object.keys(frappe.workspaces)[0]];
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
load_user_permissions: function() {
|
||||
load_user_permissions() {
|
||||
frappe.defaults.update_user_permissions();
|
||||
|
||||
frappe.realtime.on('update_user_permissions', frappe.utils.debounce(() => {
|
||||
frappe.defaults.update_user_permissions();
|
||||
}, 500));
|
||||
},
|
||||
}
|
||||
|
||||
check_metadata_cache_status: function() {
|
||||
check_metadata_cache_status() {
|
||||
if(frappe.boot.metadata_version != localStorage.metadata_version) {
|
||||
frappe.assets.clear_local_storage();
|
||||
frappe.assets.init_local_storage();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
set_globals: function() {
|
||||
set_globals() {
|
||||
frappe.session.user = frappe.boot.user.name;
|
||||
frappe.session.logged_in_user = frappe.boot.user.name;
|
||||
frappe.session.user_email = frappe.boot.user.email;
|
||||
|
|
@ -360,8 +360,8 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
sync_pages: function() {
|
||||
}
|
||||
sync_pages() {
|
||||
// clear cached pages if timestamp is not found
|
||||
if(localStorage["page_info"]) {
|
||||
frappe.boot.allowed_pages = [];
|
||||
|
|
@ -376,8 +376,8 @@ frappe.Application = Class.extend({
|
|||
frappe.boot.allowed_pages = Object.keys(frappe.boot.page_info);
|
||||
}
|
||||
localStorage["page_info"] = JSON.stringify(frappe.boot.page_info);
|
||||
},
|
||||
set_as_guest: function() {
|
||||
}
|
||||
set_as_guest() {
|
||||
frappe.session.user = 'Guest';
|
||||
frappe.session.user_email = '';
|
||||
frappe.session.user_fullname = 'Guest';
|
||||
|
|
@ -385,23 +385,23 @@ frappe.Application = Class.extend({
|
|||
frappe.user_defaults = {};
|
||||
frappe.user_roles = ['Guest'];
|
||||
frappe.sys_defaults = {};
|
||||
},
|
||||
make_page_container: function() {
|
||||
}
|
||||
make_page_container() {
|
||||
if ($("#body").length) {
|
||||
$(".splash").remove();
|
||||
frappe.temp_container = $("<div id='temp-container' style='display: none;'>")
|
||||
.appendTo("body");
|
||||
frappe.container = new frappe.views.Container();
|
||||
}
|
||||
},
|
||||
make_nav_bar: function() {
|
||||
}
|
||||
make_nav_bar() {
|
||||
// toolbar
|
||||
if(frappe.boot && frappe.boot.home_page!=='setup-wizard') {
|
||||
frappe.frappe_toolbar = new frappe.ui.toolbar.Toolbar();
|
||||
}
|
||||
|
||||
},
|
||||
logout: function() {
|
||||
}
|
||||
logout() {
|
||||
var me = this;
|
||||
me.logged_out = true;
|
||||
return frappe.call({
|
||||
|
|
@ -413,8 +413,8 @@ frappe.Application = Class.extend({
|
|||
me.redirect_to_login();
|
||||
}
|
||||
});
|
||||
},
|
||||
handle_session_expired: function() {
|
||||
}
|
||||
handle_session_expired() {
|
||||
if(!frappe.app.session_expired_dialog) {
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __('Session Expired'),
|
||||
|
|
@ -464,16 +464,16 @@ frappe.Application = Class.extend({
|
|||
'background-color': '#4B4C9D'
|
||||
});
|
||||
}
|
||||
},
|
||||
redirect_to_login: function() {
|
||||
}
|
||||
redirect_to_login() {
|
||||
window.location.href = '/';
|
||||
},
|
||||
set_favicon: function() {
|
||||
}
|
||||
set_favicon() {
|
||||
var link = $('link[type="image/x-icon"]').remove().attr("href");
|
||||
$('<link rel="shortcut icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
$('<link rel="icon" href="' + link + '" type="image/x-icon">').appendTo("head");
|
||||
},
|
||||
trigger_primary_action: function() {
|
||||
}
|
||||
trigger_primary_action() {
|
||||
// to trigger change event on active input before triggering primary action
|
||||
$(document.activeElement).blur();
|
||||
// wait for possible JS validations triggered after blur (it might change primary button)
|
||||
|
|
@ -487,20 +487,20 @@ frappe.Application = Class.extend({
|
|||
frappe.container.page.save_action();
|
||||
}
|
||||
}, 100);
|
||||
},
|
||||
}
|
||||
|
||||
set_rtl: function() {
|
||||
set_rtl() {
|
||||
if (frappe.utils.is_rtl()) {
|
||||
var ls = document.createElement('link');
|
||||
ls.rel="stylesheet";
|
||||
ls.type = "text/css";
|
||||
ls.href= "/assets/css/frappe-rtl.css";
|
||||
ls.href= frappe.assets.bundled_asset("frappe-rtl.bundle.css");
|
||||
document.getElementsByTagName('head')[0].appendChild(ls);
|
||||
$('body').addClass('frappe-rtl');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
show_change_log: function() {
|
||||
show_change_log() {
|
||||
var me = this;
|
||||
let change_log = frappe.boot.change_log;
|
||||
|
||||
|
|
@ -531,15 +531,15 @@ frappe.Application = Class.extend({
|
|||
});
|
||||
me.show_notes();
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
show_update_available: () => {
|
||||
show_update_available() {
|
||||
frappe.call({
|
||||
"method": "frappe.utils.change_log.show_update_popup"
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
setup_analytics: function() {
|
||||
setup_analytics() {
|
||||
if(window.mixpanel) {
|
||||
window.mixpanel.identify(frappe.session.user);
|
||||
window.mixpanel.people.set({
|
||||
|
|
@ -549,17 +549,17 @@ frappe.Application = Class.extend({
|
|||
"$email": frappe.session.user
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
add_browser_class() {
|
||||
$('html').addClass(frappe.utils.get_browser().name.toLowerCase());
|
||||
},
|
||||
}
|
||||
|
||||
set_fullwidth_if_enabled() {
|
||||
frappe.ui.toolbar.set_fullwidth_if_enabled();
|
||||
},
|
||||
}
|
||||
|
||||
show_notes: function() {
|
||||
show_notes() {
|
||||
var me = this;
|
||||
if(frappe.boot.notes.length) {
|
||||
frappe.boot.notes.forEach(function(note) {
|
||||
|
|
@ -586,21 +586,19 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_build_error_listener() {
|
||||
setup_build_events() {
|
||||
if (frappe.boot.developer_mode) {
|
||||
frappe.realtime.on('build_error', (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
frappe.require("build_events.bundle.js");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
setup_energy_point_listeners() {
|
||||
frappe.realtime.on('energy_point_alert', (message) => {
|
||||
frappe.show_alert(message);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
setup_copy_doc_listener() {
|
||||
$('body').on('paste', (e) => {
|
||||
|
|
@ -634,7 +632,7 @@ frappe.Application = Class.extend({
|
|||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
frappe.get_module = function(m, default_module) {
|
||||
var module = frappe.modules[m] || default_module;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
||||
make_input: function() {
|
||||
frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.ControlData {
|
||||
make_input() {
|
||||
let me = this;
|
||||
this.$input = $('<button class="btn btn-default btn-sm btn-attach">')
|
||||
.html(__("Attach"))
|
||||
|
|
@ -26,8 +26,8 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
|
||||
frappe.utils.bind_actions_with_object(this.$value, this);
|
||||
this.toggle_reload_button();
|
||||
},
|
||||
clear_attachment: function() {
|
||||
}
|
||||
clear_attachment() {
|
||||
let me = this;
|
||||
if(this.frm) {
|
||||
me.parse_validate_and_set_in_model(null);
|
||||
|
|
@ -44,16 +44,16 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
this.parse_validate_and_set_in_model(null);
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
}
|
||||
reload_attachment() {
|
||||
if (this.file_uploader) {
|
||||
this.file_uploader.uploader.upload_files();
|
||||
}
|
||||
},
|
||||
}
|
||||
on_attach_click() {
|
||||
this.set_upload_options();
|
||||
this.file_uploader = new frappe.ui.FileUploader(this.upload_options);
|
||||
},
|
||||
}
|
||||
set_upload_options() {
|
||||
let options = {
|
||||
allow_multiple: false,
|
||||
|
|
@ -73,9 +73,9 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
Object.assign(options, this.df.options);
|
||||
}
|
||||
this.upload_options = options;
|
||||
},
|
||||
}
|
||||
|
||||
set_input: function(value, dataurl) {
|
||||
set_input(value, dataurl) {
|
||||
this.value = value;
|
||||
if(this.value) {
|
||||
this.$input.toggle(false);
|
||||
|
|
@ -94,23 +94,23 @@ frappe.ui.form.ControlAttach = frappe.ui.form.ControlData.extend({
|
|||
this.$input.toggle(true);
|
||||
this.$value.toggle(false);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_value: function() {
|
||||
get_value() {
|
||||
return this.value || null;
|
||||
},
|
||||
}
|
||||
|
||||
on_upload_complete: function(attachment) {
|
||||
on_upload_complete(attachment) {
|
||||
if(this.frm) {
|
||||
this.parse_validate_and_set_in_model(attachment.file_url);
|
||||
this.frm.attachments.update_attachment(attachment);
|
||||
this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save();
|
||||
}
|
||||
this.set_value(attachment.file_url);
|
||||
},
|
||||
}
|
||||
|
||||
toggle_reload_button() {
|
||||
this.$value.find('[data-action="reload_attachment"]')
|
||||
.toggle(this.file_uploader && this.file_uploader.uploader.files.length > 0);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
|
||||
frappe.ui.form.ControlAttachImage = class ControlAttachImage extends frappe.ui.form.ControlAttach {
|
||||
make_input() {
|
||||
this._super();
|
||||
super.make_input();
|
||||
|
||||
let $file_link = this.$value.find('.attached-file-link');
|
||||
$file_link.popover({
|
||||
|
|
@ -16,10 +16,10 @@ frappe.ui.form.ControlAttachImage = frappe.ui.form.ControlAttach.extend({
|
|||
},
|
||||
html: true
|
||||
});
|
||||
},
|
||||
}
|
||||
set_upload_options() {
|
||||
this._super();
|
||||
super.set_upload_options();
|
||||
this.upload_options.restrictions = {};
|
||||
this.upload_options.restrictions.allowed_file_types = ['image/*'];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
import Awesomplete from 'awesomplete';
|
||||
|
||||
frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
||||
trigger_change_on_input_event: false,
|
||||
frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui.form.ControlData {
|
||||
static trigger_change_on_input_event = false
|
||||
make_input() {
|
||||
this._super();
|
||||
super.make_input();
|
||||
this.setup_awesomplete();
|
||||
this.set_options();
|
||||
},
|
||||
}
|
||||
|
||||
set_options() {
|
||||
if (this.df.options) {
|
||||
let options = this.df.options || [];
|
||||
this._data = this.parse_options(options);
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_awesomplete_settings() {
|
||||
var me = this;
|
||||
|
|
@ -63,7 +63,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
return 0;
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
setup_awesomplete() {
|
||||
this.awesomplete = new Awesomplete(
|
||||
|
|
@ -100,7 +100,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
this.$input.on('awesomplete-selectcomplete', () => {
|
||||
this.$input.trigger('change');
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (this.df.ignore_validation) {
|
||||
|
|
@ -115,7 +115,7 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
parse_options(options) {
|
||||
if (typeof options === 'string') {
|
||||
|
|
@ -125,11 +125,11 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
options = options.map(o => ({ label: o, value: o }));
|
||||
}
|
||||
return options;
|
||||
},
|
||||
}
|
||||
|
||||
get_data() {
|
||||
return this._data || [];
|
||||
},
|
||||
}
|
||||
|
||||
set_data(data) {
|
||||
data = this.parse_options(data);
|
||||
|
|
@ -138,4 +138,4 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({
|
|||
}
|
||||
this._data = data;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import JsBarcode from 'jsbarcode';
|
||||
|
||||
frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
||||
frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.ControlData {
|
||||
make_wrapper() {
|
||||
// Create the elements for barcode area
|
||||
this._super();
|
||||
super.make_wrapper();
|
||||
|
||||
this.default_svg = '<svg height=80></svg>';
|
||||
let $input_wrapper = this.$wrapper.find('.control-input-wrapper');
|
||||
|
|
@ -11,7 +11,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
`<div class="barcode-wrapper">${this.default_svg}</div>`
|
||||
);
|
||||
this.barcode_area.appendTo($input_wrapper);
|
||||
},
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
// Parse raw value
|
||||
|
|
@ -22,7 +22,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
return this.get_barcode_html(value);
|
||||
}
|
||||
return '';
|
||||
},
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
// Set values to display
|
||||
|
|
@ -40,7 +40,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
|
||||
this.$input.val(barcode_value || value);
|
||||
this.barcode_area.html(svg || this.default_svg);
|
||||
},
|
||||
}
|
||||
|
||||
get_barcode_html(value) {
|
||||
if (value) {
|
||||
|
|
@ -51,7 +51,7 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
$(svg).attr('width', '100%');
|
||||
return this.barcode_area.html();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_options(value) {
|
||||
// get JsBarcode options
|
||||
|
|
@ -76,4 +76,4 @@ frappe.ui.form.ControlBarcode = frappe.ui.form.ControlData.extend({
|
|||
}
|
||||
return options;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
frappe.ui.form.Control = Class.extend({
|
||||
init: function(opts) {
|
||||
frappe.ui.form.Control = class BaseControl {
|
||||
constructor(opts) {
|
||||
$.extend(this, opts);
|
||||
this.make();
|
||||
|
||||
|
|
@ -11,31 +11,31 @@ frappe.ui.form.Control = Class.extend({
|
|||
if(this.render_input) {
|
||||
this.refresh();
|
||||
}
|
||||
},
|
||||
make: function() {
|
||||
}
|
||||
make() {
|
||||
this.make_wrapper();
|
||||
this.$wrapper
|
||||
.attr("data-fieldtype", this.df.fieldtype)
|
||||
.attr("data-fieldname", this.df.fieldname);
|
||||
this.wrapper = this.$wrapper.get(0);
|
||||
this.wrapper.fieldobj = this; // reference for event handlers
|
||||
},
|
||||
}
|
||||
|
||||
make_wrapper: function() {
|
||||
make_wrapper() {
|
||||
this.$wrapper = $("<div class='frappe-control'></div>").appendTo(this.parent);
|
||||
|
||||
// alias
|
||||
this.wrapper = this.$wrapper;
|
||||
},
|
||||
}
|
||||
|
||||
toggle: function(show) {
|
||||
toggle(show) {
|
||||
this.df.hidden = show ? 0 : 1;
|
||||
this.refresh();
|
||||
},
|
||||
}
|
||||
|
||||
// returns "Read", "Write" or "None"
|
||||
// as strings based on permissions
|
||||
get_status: function(explain) {
|
||||
get_status(explain) {
|
||||
if (this.df.get_status) {
|
||||
return this.df.get_status(this);
|
||||
}
|
||||
|
|
@ -93,8 +93,8 @@ frappe.ui.form.Control = Class.extend({
|
|||
}
|
||||
|
||||
return status;
|
||||
},
|
||||
refresh: function() {
|
||||
}
|
||||
refresh() {
|
||||
this.disp_status = this.get_status();
|
||||
this.$wrapper
|
||||
&& this.$wrapper.toggleClass("hide-control", this.disp_status=="None")
|
||||
|
|
@ -104,7 +104,7 @@ frappe.ui.form.Control = Class.extend({
|
|||
var value = this.get_value();
|
||||
|
||||
this.show_translatable_button(value);
|
||||
},
|
||||
}
|
||||
show_translatable_button(value) {
|
||||
// Disable translation non-string fields or special string fields
|
||||
if (!frappe.model
|
||||
|
|
@ -138,26 +138,26 @@ frappe.ui.form.Control = Class.extend({
|
|||
}
|
||||
});
|
||||
|
||||
},
|
||||
get_doc: function() {
|
||||
}
|
||||
get_doc() {
|
||||
return this.doctype && this.docname
|
||||
&& locals[this.doctype] && locals[this.doctype][this.docname] || {};
|
||||
},
|
||||
get_model_value: function() {
|
||||
}
|
||||
get_model_value() {
|
||||
if(this.doc) {
|
||||
return this.doc[this.df.fieldname];
|
||||
}
|
||||
},
|
||||
set_value: function(value) {
|
||||
}
|
||||
set_value(value) {
|
||||
return this.validate_and_set_in_model(value);
|
||||
},
|
||||
parse_validate_and_set_in_model: function(value, e) {
|
||||
}
|
||||
parse_validate_and_set_in_model(value, e) {
|
||||
if(this.parse) {
|
||||
value = this.parse(value);
|
||||
}
|
||||
return this.validate_and_set_in_model(value, e);
|
||||
},
|
||||
validate_and_set_in_model: function(value, e) {
|
||||
}
|
||||
validate_and_set_in_model(value, e) {
|
||||
var me = this;
|
||||
let force_value_set = (this.doc && this.doc.__run_link_triggers);
|
||||
let is_value_same = (this.get_model_value() === value);
|
||||
|
|
@ -193,8 +193,8 @@ frappe.ui.form.Control = Class.extend({
|
|||
// all clear
|
||||
return set(value);
|
||||
}
|
||||
},
|
||||
get_value: function() {
|
||||
}
|
||||
get_value() {
|
||||
if(this.get_status()==='Write') {
|
||||
return this.get_input_value ?
|
||||
(this.parse ? this.parse(this.get_input_value()) : this.get_input_value()) :
|
||||
|
|
@ -202,8 +202,8 @@ frappe.ui.form.Control = Class.extend({
|
|||
} else {
|
||||
return this.value || undefined;
|
||||
}
|
||||
},
|
||||
set_model_value: function(value) {
|
||||
}
|
||||
set_model_value(value) {
|
||||
if(this.frm) {
|
||||
this.last_value = value;
|
||||
return frappe.model.set_value(this.doctype, this.docname, this.df.fieldname,
|
||||
|
|
@ -215,11 +215,11 @@ frappe.ui.form.Control = Class.extend({
|
|||
this.set_input(value);
|
||||
return Promise.resolve();
|
||||
}
|
||||
},
|
||||
set_focus: function() {
|
||||
}
|
||||
set_focus() {
|
||||
if(this.$input) {
|
||||
this.$input.get(0).focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
||||
horizontal: true,
|
||||
make: function() {
|
||||
frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control {
|
||||
static horizontal = true
|
||||
make() {
|
||||
// parent element
|
||||
this._super();
|
||||
super.make();
|
||||
this.set_input_areas();
|
||||
|
||||
// set description
|
||||
this.set_max_width();
|
||||
},
|
||||
make_wrapper: function() {
|
||||
}
|
||||
make_wrapper() {
|
||||
if(this.only_input) {
|
||||
this.$wrapper = $('<div class="form-group frappe-control">').appendTo(this.parent);
|
||||
} else {
|
||||
|
|
@ -25,14 +25,14 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
</div>\
|
||||
</div>').appendTo(this.parent);
|
||||
}
|
||||
},
|
||||
toggle_label: function(show) {
|
||||
}
|
||||
toggle_label(show) {
|
||||
this.$wrapper.find(".control-label").toggleClass("hide", !show);
|
||||
},
|
||||
toggle_description: function(show) {
|
||||
}
|
||||
toggle_description(show) {
|
||||
this.$wrapper.find(".help-box").toggleClass("hide", !show);
|
||||
},
|
||||
set_input_areas: function() {
|
||||
}
|
||||
set_input_areas() {
|
||||
if(this.only_input) {
|
||||
this.input_area = this.wrapper;
|
||||
} else {
|
||||
|
|
@ -43,17 +43,17 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
// like links, currencies, HTMLs etc.
|
||||
this.disp_area = this.$wrapper.find(".control-value").get(0);
|
||||
}
|
||||
},
|
||||
set_max_width: function() {
|
||||
if(this.horizontal) {
|
||||
}
|
||||
set_max_width() {
|
||||
if(this.constructor.horizontal) {
|
||||
this.$wrapper.addClass("input-max-width");
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// update input value, label, description
|
||||
// display (show/hide/read-only),
|
||||
// mandatory style on refresh
|
||||
refresh_input: function() {
|
||||
refresh_input() {
|
||||
var me = this;
|
||||
var make_input = function() {
|
||||
if (!me.has_input) {
|
||||
|
|
@ -106,13 +106,13 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
me.set_bold();
|
||||
me.set_required();
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
can_write() {
|
||||
return this.disp_status == "Write";
|
||||
},
|
||||
}
|
||||
|
||||
set_disp_area: function(value) {
|
||||
set_disp_area(value) {
|
||||
if(in_list(["Currency", "Int", "Float"], this.df.fieldtype)
|
||||
&& (this.value === 0 || value === 0)) {
|
||||
// to set the 0 value in readonly for currency, int, float field
|
||||
|
|
@ -126,8 +126,8 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
let doc = this.doc || (this.frm && this.frm.doc);
|
||||
let display_value = frappe.format(value, this.df, { no_icon: true, inline: true }, doc);
|
||||
this.disp_area && $(this.disp_area).html(display_value);
|
||||
},
|
||||
set_label: function(label) {
|
||||
}
|
||||
set_label(label) {
|
||||
if(label) this.df.label = label;
|
||||
|
||||
if(this.only_input || this.df.label==this._label)
|
||||
|
|
@ -137,8 +137,8 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
this.label_span.innerHTML = (icon ? '<i class="'+icon+'"></i> ' : "") +
|
||||
__(this.df.label) || " ";
|
||||
this._label = this.df.label;
|
||||
},
|
||||
set_description: function(description) {
|
||||
}
|
||||
set_description(description) {
|
||||
if (description !== undefined) {
|
||||
this.df.description = description;
|
||||
}
|
||||
|
|
@ -151,17 +151,17 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
this.set_empty_description();
|
||||
}
|
||||
this._description = this.df.description;
|
||||
},
|
||||
set_new_description: function(description) {
|
||||
}
|
||||
set_new_description(description) {
|
||||
this.$wrapper.find(".help-box").html(description);
|
||||
},
|
||||
set_empty_description: function() {
|
||||
}
|
||||
set_empty_description() {
|
||||
this.$wrapper.find(".help-box").html("");
|
||||
},
|
||||
set_mandatory: function(value) {
|
||||
}
|
||||
set_mandatory(value) {
|
||||
this.$wrapper.toggleClass("has-error", Boolean(this.df.reqd && is_null(value)));
|
||||
},
|
||||
set_invalid: function () {
|
||||
}
|
||||
set_invalid () {
|
||||
let invalid = !!this.df.invalid;
|
||||
if (this.grid) {
|
||||
this.$wrapper.parents('.grid-static-col').toggleClass('invalid', invalid);
|
||||
|
|
@ -170,11 +170,11 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
} else {
|
||||
this.$wrapper.toggleClass('has-error', invalid);
|
||||
}
|
||||
},
|
||||
}
|
||||
set_required() {
|
||||
this.label_area && $(this.label_area).toggleClass('reqd', Boolean(this.df.reqd));
|
||||
},
|
||||
set_bold: function() {
|
||||
}
|
||||
set_bold() {
|
||||
if(this.$input) {
|
||||
this.$input.toggleClass("bold", !!(this.df.bold || this.df.reqd));
|
||||
}
|
||||
|
|
@ -182,4 +182,4 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
|
|||
$(this.disp_area).toggleClass("bold", !!(this.df.bold || this.df.reqd));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
||||
frappe.ui.form.ControlButton = class ControlButton extends frappe.ui.form.ControlData {
|
||||
can_write() {
|
||||
// should be always true in case of button
|
||||
return true;
|
||||
},
|
||||
make_input: function() {
|
||||
}
|
||||
make_input() {
|
||||
var me = this;
|
||||
const btn_type = this.df.primary ? 'btn-primary': 'btn-default';
|
||||
const btn_size = this.df.btn_size
|
||||
|
|
@ -18,8 +18,8 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
this.set_input_attributes();
|
||||
this.has_input = true;
|
||||
this.toggle_label(false);
|
||||
},
|
||||
onclick: function() {
|
||||
}
|
||||
onclick() {
|
||||
if (this.frm && this.frm.doc) {
|
||||
if (this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
|
||||
this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
|
||||
|
|
@ -31,8 +31,8 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
} else if (this.df.click) {
|
||||
this.df.click();
|
||||
}
|
||||
},
|
||||
run_server_script: function() {
|
||||
}
|
||||
run_server_script() {
|
||||
// DEPRECATE
|
||||
var me = this;
|
||||
if(this.frm && this.frm.docname) {
|
||||
|
|
@ -47,18 +47,18 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
hide() {
|
||||
this.$input.hide();
|
||||
},
|
||||
set_input_areas: function() {
|
||||
this._super();
|
||||
}
|
||||
set_input_areas() {
|
||||
super.set_input_areas();
|
||||
$(this.disp_area).removeClass().addClass("hide");
|
||||
},
|
||||
set_empty_description: function() {
|
||||
}
|
||||
set_empty_description() {
|
||||
this.$wrapper.find(".help-box").empty().toggle(false);
|
||||
},
|
||||
set_label: function(label) {
|
||||
}
|
||||
set_label(label) {
|
||||
if (label) {
|
||||
this.df.label = label;
|
||||
}
|
||||
|
|
@ -66,4 +66,4 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
|
|||
$(this.label_span).html(" ");
|
||||
this.$input && this.$input.html(label);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
|
||||
input_type: "checkbox",
|
||||
make_wrapper: function() {
|
||||
frappe.ui.form.ControlCheck = class ControlCheck extends frappe.ui.form.ControlData {
|
||||
static html_element = "input"
|
||||
static input_type = "checkbox"
|
||||
make_wrapper() {
|
||||
this.$wrapper = $(`<div class="form-group frappe-control">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
|
|
@ -11,23 +12,23 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
|
|||
<p class="help-box small text-muted"></p>
|
||||
</div>
|
||||
</div>`).appendTo(this.parent);
|
||||
},
|
||||
set_input_areas: function() {
|
||||
}
|
||||
set_input_areas() {
|
||||
this.label_area = this.label_span = this.$wrapper.find(".label-area").get(0);
|
||||
this.input_area = this.$wrapper.find(".input-area").get(0);
|
||||
this.disp_area = this.$wrapper.find(".disp-area").get(0);
|
||||
},
|
||||
make_input: function() {
|
||||
this._super();
|
||||
}
|
||||
make_input() {
|
||||
super.make_input();
|
||||
this.$input.removeClass("form-control");
|
||||
},
|
||||
get_input_value: function() {
|
||||
}
|
||||
get_input_value() {
|
||||
return this.input && this.input.checked ? 1 : 0;
|
||||
},
|
||||
validate: function(value) {
|
||||
}
|
||||
validate(value) {
|
||||
return cint(value);
|
||||
},
|
||||
set_input: function(value) {
|
||||
}
|
||||
set_input(value) {
|
||||
value = cint(value);
|
||||
if(this.input) {
|
||||
this.input.checked = (value ? 1 : 0);
|
||||
|
|
@ -36,4 +37,4 @@ frappe.ui.form.ControlCheck = frappe.ui.form.ControlData.extend({
|
|||
this.set_mandatory(value);
|
||||
this.set_disp_area(value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
||||
frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlText {
|
||||
make_input() {
|
||||
if (this.editor) return;
|
||||
this.load_lib().then(() => this.make_ace_editor());
|
||||
},
|
||||
}
|
||||
|
||||
make_ace_editor() {
|
||||
if (this.editor) return;
|
||||
|
|
@ -34,6 +34,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
|
||||
// setup autocompletion when it is set the first time
|
||||
Object.defineProperty(this.df, 'autocompletions', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this._autocompletions || [];
|
||||
},
|
||||
|
|
@ -42,7 +43,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
this.df._autocompletions = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
setup_autocompletion() {
|
||||
if (this._autocompletion_setup) return;
|
||||
|
|
@ -82,20 +83,20 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
});
|
||||
});
|
||||
this._autocompletion_setup = true;
|
||||
},
|
||||
}
|
||||
|
||||
refresh_height() {
|
||||
this.ace_editor_target.css('height', this.expanded ? 600 : 300);
|
||||
this.editor.resize();
|
||||
},
|
||||
}
|
||||
|
||||
toggle_label() {
|
||||
this.$expand_button && this.$expand_button.text(this.get_button_label());
|
||||
},
|
||||
}
|
||||
|
||||
get_button_label() {
|
||||
return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.');
|
||||
},
|
||||
}
|
||||
|
||||
set_language() {
|
||||
const language_map = {
|
||||
|
|
@ -122,14 +123,14 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
const ace_language_mode = language_map[language] || '';
|
||||
this.editor.session.setMode(ace_language_mode);
|
||||
this.editor.setKeyboardHandler('ace/keyboard/vscode');
|
||||
},
|
||||
}
|
||||
|
||||
parse(value) {
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
return value;
|
||||
},
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
return this.load_lib().then(() => {
|
||||
|
|
@ -138,11 +139,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
if (value === this.get_input_value()) return;
|
||||
this.editor.session.setValue(value);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
get_input_value() {
|
||||
return this.editor ? this.editor.session.getValue() : '';
|
||||
},
|
||||
}
|
||||
|
||||
load_lib() {
|
||||
if (this.library_loaded) return this.library_loaded;
|
||||
|
|
@ -162,4 +163,4 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({
|
|||
|
||||
return this.library_loaded;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import Picker from '../../color_picker/color_picker';
|
||||
|
||||
frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
||||
make_input: function () {
|
||||
frappe.ui.form.ControlColor = class ControlColor extends frappe.ui.form.ControlData {
|
||||
make_input() {
|
||||
this.df.placeholder = this.df.placeholder || __('Choose a color');
|
||||
this._super();
|
||||
super.make_input();
|
||||
this.make_color_input();
|
||||
},
|
||||
make_color_input: function () {
|
||||
}
|
||||
|
||||
make_color_input() {
|
||||
let picker_wrapper = $('<div>');
|
||||
this.picker = new Picker({
|
||||
parent: picker_wrapper[0],
|
||||
|
|
@ -73,27 +74,31 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
this.$wrapper.popover('hide');
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this._super();
|
||||
super.refresh();
|
||||
let color = this.get_color();
|
||||
if (this.picker && this.picker.color !== color) {
|
||||
this.picker.color = color;
|
||||
this.picker.refresh();
|
||||
}
|
||||
},
|
||||
set_formatted_input: function(value) {
|
||||
this._super(value);
|
||||
}
|
||||
|
||||
set_formatted_input(value) {
|
||||
super.set_formatted_input(value);
|
||||
this.$input.val(value);
|
||||
this.selected_color.css({
|
||||
"background-color": value || 'transparent',
|
||||
});
|
||||
this.selected_color.toggleClass('no-value', !value);
|
||||
},
|
||||
}
|
||||
|
||||
get_color() {
|
||||
return this.validate(this.get_value());
|
||||
},
|
||||
validate: function (value) {
|
||||
}
|
||||
|
||||
validate(value) {
|
||||
if (value === '') {
|
||||
return '';
|
||||
}
|
||||
|
|
@ -103,4 +108,4 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({
|
|||
}
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import Mention from './quill-mention/quill.mention';
|
|||
|
||||
Quill.register('modules/mention', Mention, true);
|
||||
|
||||
frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
||||
frappe.ui.form.ControlComment = class ControlComment extends frappe.ui.form.ControlTextEditor {
|
||||
make_wrapper() {
|
||||
this.comment_wrapper = !this.no_wrapper ? $(`
|
||||
<div class="comment-input-wrapper">
|
||||
|
|
@ -32,10 +32,10 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
this.wrapper = this.$wrapper;
|
||||
|
||||
this.button = this.comment_wrapper.find('.btn-comment');
|
||||
},
|
||||
}
|
||||
|
||||
bind_events() {
|
||||
this._super();
|
||||
super.bind_events();
|
||||
|
||||
this.button.click(() => {
|
||||
this.submit();
|
||||
|
|
@ -52,11 +52,11 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
this.quill.on('text-change', frappe.utils.debounce(() => {
|
||||
this.update_state();
|
||||
}, 300));
|
||||
},
|
||||
}
|
||||
|
||||
submit() {
|
||||
this.on_submit && this.on_submit(this.get_value());
|
||||
},
|
||||
}
|
||||
|
||||
update_state() {
|
||||
const value = this.get_value();
|
||||
|
|
@ -65,17 +65,17 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
} else {
|
||||
this.button.addClass('btn-default').removeClass('btn-primary');
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
get_quill_options() {
|
||||
const options = this._super();
|
||||
const options = super.get_quill_options();
|
||||
return Object.assign(options, {
|
||||
theme: 'bubble',
|
||||
modules: Object.assign(options.modules, {
|
||||
mention: this.get_mention_options()
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
get_mention_options() {
|
||||
if (!this.enable_mentions) {
|
||||
|
|
@ -98,7 +98,7 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`;
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
get_toolbar_options() {
|
||||
return [
|
||||
|
|
@ -108,19 +108,19 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({
|
|||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
['clean']
|
||||
];
|
||||
},
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.quill.setText('');
|
||||
},
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.quill.disable();
|
||||
this.button.prop('disabled', true);
|
||||
},
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.quill.enable();
|
||||
this.button.prop('disabled', false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
|
||||
format_for_input: function(value) {
|
||||
frappe.ui.form.ControlCurrency = class ControlCurrency extends frappe.ui.form.ControlFloat {
|
||||
format_for_input(value) {
|
||||
var formatted_value = format_number(value, this.get_number_format(), this.get_precision());
|
||||
return isNaN(Number(value)) ? "" : formatted_value;
|
||||
},
|
||||
}
|
||||
|
||||
get_precision: function() {
|
||||
get_precision() {
|
||||
// always round based on field precision or currency's precision
|
||||
// this method is also called in this.parse()
|
||||
if (!this.df.precision) {
|
||||
|
|
@ -17,4 +17,4 @@ frappe.ui.form.ControlCurrency = frappe.ui.form.ControlFloat.extend({
|
|||
|
||||
return this.df.precision;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue