Merge branch 'develop' into wiki-based-desk

This commit is contained in:
Shariq Ansari 2021-05-25 21:20:06 +05:30 committed by GitHub
commit 8165961343
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
242 changed files with 10682 additions and 10010 deletions

View file

@ -149,6 +149,7 @@
"before": true,
"beforeEach": true,
"qz": true,
"localforage": true
"localforage": true,
"extend_cscript": true
}
}

View file

@ -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
View 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

View file

@ -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"

View file

@ -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])

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- version-13-hotfix
- version-13-pre-release
jobs:
semgrep:
name: Frappe Linter

View file

@ -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

View 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
View 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
View file

@ -9,6 +9,7 @@ locale
dist/
# build/
frappe/docs/current
frappe/public/dist
.vscode
node_modules
.kdev4/

View file

@ -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

View file

@ -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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
require("./esbuild");

29
esbuild/sass_options.js Normal file
View 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
View 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
};

View file

@ -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()

View file

@ -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

View file

@ -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(":")

View file

@ -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):

View 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))

View file

@ -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()

View file

@ -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
]

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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,

View file

@ -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:

View file

@ -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",
}
)

View file

@ -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)

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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()

View 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, [])

View file

@ -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:

View file

@ -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 {

View file

@ -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())

View file

@ -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"))

View file

@ -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",

View file

@ -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">\

View file

@ -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();
});
};
};

View file

@ -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",

View file

@ -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()

View file

@ -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):

View file

@ -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)

View file

@ -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

View file

@ -953,7 +953,7 @@
"currency_fraction_units": 100,
"smallest_currency_fraction_value": 0.01,
"currency_symbol": "\u20ac",
"number_format": "#,###.##",
"number_format": "#.###,##",
"timezones": [
"Europe/Berlin"
]

View file

@ -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",

View file

@ -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):

View file

@ -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",

View file

@ -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()
)

View file

@ -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
}

View file

@ -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:

View file

@ -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,

View file

@ -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

View file

@ -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}`

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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):

View file

@ -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>`
);
}

View file

@ -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({
}
});
}
});
};

View file

@ -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>

View file

@ -0,0 +1 @@
import "./frappe/barcode_scanner/quagga";

View 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');
}
}
};

View file

@ -0,0 +1 @@
import "./frappe/chat";

View file

@ -0,0 +1 @@
import "./integrations/razorpay";

View 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";

View file

@ -0,0 +1 @@
import "./frappe/data_import";

View 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";

View 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";

View 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";

View 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";

View file

@ -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;
}
};

View file

@ -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);
});

View 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,
` &gt; <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>

View 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>

View 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);
}

View file

@ -80,4 +80,4 @@ To subclass, use:
// export
global.Class = Class;
})(this);
})(window);

View file

@ -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;

View file

@ -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);
}
});
};

View file

@ -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/*'];
}
});
};

View file

@ -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;
}
});
};

View file

@ -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;
}
});
};

View file

@ -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;
}
}
});
};

View file

@ -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) || "&nbsp;";
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));
}
}
});
};

View file

@ -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("&nbsp;");
this.$input && this.$input.html(label);
}
});
};

View file

@ -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);
}
});
};

View file

@ -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;
}
});
};

View file

@ -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;
}
});
};

View file

@ -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);
}
});
};

View file

@ -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