Merge branch 'develop' into copy-config-to-new-app
This commit is contained in:
commit
ce50162213
123 changed files with 3640 additions and 2611 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -66,6 +66,6 @@ bench --site test_site reinstall --yes
|
||||||
|
|
||||||
if [ "$TYPE" == "server" ]
|
if [ "$TYPE" == "server" ]
|
||||||
then
|
then
|
||||||
# wait till assets are built succesfully
|
# wait till assets are built successfully
|
||||||
wait $build_pid
|
wait $build_pid
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
7
.github/helper/roulette.py
vendored
7
.github/helper/roulette.py
vendored
|
|
@ -73,8 +73,9 @@ def has_label(pr_number, label, repo="frappe/frappe"):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def is_py(file):
|
def is_server_side_code(file):
|
||||||
return file.endswith("py")
|
"""File exclusively affects server side code"""
|
||||||
|
return file.endswith("py") or file.endswith(".po")
|
||||||
|
|
||||||
|
|
||||||
def is_ci(file):
|
def is_ci(file):
|
||||||
|
|
@ -112,7 +113,7 @@ if __name__ == "__main__":
|
||||||
ci_files_changed = any(f for f in files_list if is_ci(f))
|
ci_files_changed = any(f for f in files_list if is_ci(f))
|
||||||
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
only_docs_changed = len(list(filter(is_docs, files_list))) == len(files_list)
|
||||||
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
only_frontend_code_changed = len(list(filter(is_frontend_code, files_list))) == len(files_list)
|
||||||
updated_py_file_count = len(list(filter(is_py, files_list)))
|
updated_py_file_count = len(list(filter(is_server_side_code, files_list)))
|
||||||
only_py_changed = updated_py_file_count == len(files_list)
|
only_py_changed = updated_py_file_count == len(files_list)
|
||||||
|
|
||||||
if has_skip_ci_label(pr_number, repo):
|
if has_skip_ci_label(pr_number, repo):
|
||||||
|
|
|
||||||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -19,7 +19,7 @@ jobs:
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
- name: Setup dependencies
|
- name: Setup dependencies
|
||||||
run: |
|
run: |
|
||||||
npm install @semantic-release/git @semantic-release/exec --no-save
|
npm install @semantic-release/git @semantic-release/exec --no-save
|
||||||
|
|
|
||||||
8
.github/workflows/linters.yml
vendored
8
.github/workflows/linters.yml
vendored
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
|
python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
name: 'Frappe Linter'
|
name: 'Semgrep Rules'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
|
|
||||||
|
|
@ -62,7 +62,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
cache: pip
|
cache: pip
|
||||||
- uses: pre-commit/action@v3.0.0
|
|
||||||
|
|
||||||
- name: Download Semgrep rules
|
- name: Download Semgrep rules
|
||||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||||
|
|
@ -84,7 +83,7 @@ jobs:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||||
|
|
@ -96,5 +95,4 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
pip install pip-audit
|
pip install pip-audit
|
||||||
cd ${GITHUB_WORKSPACE}
|
cd ${GITHUB_WORKSPACE}
|
||||||
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
|
pip-audit --desc on .
|
||||||
pip-audit --desc on --ignore-vuln GHSA-4xqq-73wg-5mjp .
|
|
||||||
|
|
|
||||||
4
.github/workflows/patch-mariadb-tests.yml
vendored
4
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -76,7 +76,7 @@ jobs:
|
||||||
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||||
|
|
@ -88,7 +88,7 @@ jobs:
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
|
|
||||||
26
.github/workflows/pre-commit.yml
vendored
Normal file
26
.github/workflows/pre-commit.yml
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
name: Pre-commit
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: precommit-frappe-${{ github.event_name }}-${{ github.event.number }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
linter:
|
||||||
|
name: 'precommit'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.event_name == 'pull_request'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
cache: pip
|
||||||
|
- uses: pre-commit/action@v3.0.0
|
||||||
4
.github/workflows/server-tests.yml
vendored
4
.github/workflows/server-tests.yml
vendored
|
|
@ -104,7 +104,7 @@ jobs:
|
||||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||||
|
|
@ -116,7 +116,7 @@ jobs:
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
|
|
||||||
6
.github/workflows/ui-tests.yml
vendored
6
.github/workflows/ui-tests.yml
vendored
|
|
@ -87,7 +87,7 @@ jobs:
|
||||||
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
|
||||||
|
|
||||||
- name: Cache pip
|
- name: Cache pip
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pip
|
path: ~/.cache/pip
|
||||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||||
|
|
@ -99,7 +99,7 @@ jobs:
|
||||||
id: yarn-cache-dir-path
|
id: yarn-cache-dir-path
|
||||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v4
|
||||||
id: yarn-cache
|
id: yarn-cache
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||||
|
|
@ -108,7 +108,7 @@ jobs:
|
||||||
${{ runner.os }}-yarn-ui-
|
${{ runner.os }}-yarn-ui-
|
||||||
|
|
||||||
- name: Cache cypress binary
|
- name: Cache cypress binary
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/Cypress
|
path: ~/.cache/Cypress
|
||||||
key: ${{ runner.os }}-cypress
|
key: ${{ runner.os }}-cypress
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,11 @@ const argv = yargs
|
||||||
description:
|
description:
|
||||||
"Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
|
"Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
|
||||||
})
|
})
|
||||||
|
.option("using-cached", {
|
||||||
|
type: "boolean",
|
||||||
|
description:
|
||||||
|
"Skips build and uses cached build artifacts to update assets.json (used by Bench)",
|
||||||
|
})
|
||||||
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
|
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
|
||||||
.example(
|
.example(
|
||||||
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
|
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
|
||||||
|
|
@ -88,6 +93,7 @@ const NODE_PATHS = [].concat(
|
||||||
// import js file of any app if you provide the full path
|
// 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)
|
app_list.map((app) => path.resolve(get_app_path(app), "..")).filter(fs.existsSync)
|
||||||
);
|
);
|
||||||
|
const USING_CACHED = Boolean(argv["using-cached"]);
|
||||||
|
|
||||||
execute().catch((e) => {
|
execute().catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -101,6 +107,12 @@ if (WATCH_MODE) {
|
||||||
|
|
||||||
async function execute() {
|
async function execute() {
|
||||||
console.time(TOTAL_BUILD_TIME);
|
console.time(TOTAL_BUILD_TIME);
|
||||||
|
if (USING_CACHED) {
|
||||||
|
await update_assets_json_from_built_assets(APPS);
|
||||||
|
await update_assets_json_in_cache();
|
||||||
|
console.timeEnd(TOTAL_BUILD_TIME);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
let results;
|
let results;
|
||||||
try {
|
try {
|
||||||
|
|
@ -131,6 +143,44 @@ async function execute() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function update_assets_json_from_built_assets(apps) {
|
||||||
|
const assets = await get_assets_json_path_and_obj(false);
|
||||||
|
const assets_rtl = await get_assets_json_path_and_obj(true);
|
||||||
|
|
||||||
|
for (const app in apps) {
|
||||||
|
await update_assets_obj(app, assets.obj, assets_rtl.obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { obj, path } of [assets, assets_rtl]) {
|
||||||
|
const data = JSON.stringify(obj, null, 4);
|
||||||
|
await fs.promises.writeFile(path, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update_assets_obj(app, assets, assets_rtl) {
|
||||||
|
const app_path = path.join(apps_path, app, app);
|
||||||
|
const dist_path = path.join(app_path, "public, dist");
|
||||||
|
const files = await glob("**/*.bundle.*.{js,css}", { cwd: dist_path });
|
||||||
|
const prefix = path.join("/", "assets", app, "dist");
|
||||||
|
|
||||||
|
// eg: "js/marketplace.bundle.6SCSPSGQ.js"
|
||||||
|
for (const file of files) {
|
||||||
|
// eg: [ "marketplace", "bundle", "6SCSPSGQ", "js" ]
|
||||||
|
const parts = path.parse(file).base.split(".");
|
||||||
|
|
||||||
|
// eg: "marketplace.bundle.js"
|
||||||
|
const key = [...parts.slice(0, -2), parts.at(-1)].join(".");
|
||||||
|
|
||||||
|
// eg: "js/marketplace.bundle.6SCSPSGQ.js"
|
||||||
|
const value = path.join(prefix, file);
|
||||||
|
if (file.includes("-rtl")) {
|
||||||
|
assets_rtl[`rtl_${key}`] = value;
|
||||||
|
} else {
|
||||||
|
assets[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function build_assets_for_apps(apps, files) {
|
function build_assets_for_apps(apps, files) {
|
||||||
let { include_patterns, ignore_patterns } = files.length
|
let { include_patterns, ignore_patterns } = files.length
|
||||||
? get_files_to_build(files)
|
? get_files_to_build(files)
|
||||||
|
|
@ -393,14 +443,7 @@ async function write_assets_json(metafile) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let assets_json_path = path.resolve(assets_path, `assets${rtl ? "-rtl" : ""}.json`);
|
let { obj: assets_json, path: assets_json_path } = await get_assets_json_path_and_obj(rtl);
|
||||||
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
|
// update with new values
|
||||||
let new_assets_json = Object.assign({}, assets_json, out);
|
let new_assets_json = Object.assign({}, assets_json, out);
|
||||||
curr_assets_json = new_assets_json;
|
curr_assets_json = new_assets_json;
|
||||||
|
|
@ -434,6 +477,19 @@ async function update_assets_json_in_cache() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function get_assets_json_path_and_obj(is_rtl) {
|
||||||
|
const file_name = is_rtl ? "assets-rtl.json" : "assets.json";
|
||||||
|
const assets_json_path = path.resolve(assets_path, file_name);
|
||||||
|
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);
|
||||||
|
return { obj: assets_json, path: assets_json_path };
|
||||||
|
}
|
||||||
|
|
||||||
function run_build_command_for_apps(apps) {
|
function run_build_command_for_apps(apps) {
|
||||||
let cwd = process.cwd();
|
let cwd = process.cwd();
|
||||||
let { execSync } = require("child_process");
|
let { execSync } = require("child_process");
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,16 @@
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const chalk = require("chalk");
|
const chalk = require("chalk");
|
||||||
|
let bench_path;
|
||||||
|
if (process.env.FRAPPE_BENCH_ROOT) {
|
||||||
|
bench_path = process.env.FRAPPE_BENCH_ROOT;
|
||||||
|
} else {
|
||||||
|
const frappe_path = path.resolve(__dirname, "..");
|
||||||
|
bench_path = path.resolve(frappe_path, "..", "..");
|
||||||
|
}
|
||||||
|
|
||||||
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 apps_path = path.resolve(bench_path, "apps");
|
||||||
|
const sites_path = path.resolve(bench_path, "sites");
|
||||||
const assets_path = path.resolve(sites_path, "assets");
|
const assets_path = path.resolve(sites_path, "assets");
|
||||||
const app_list = get_apps_list();
|
const app_list = get_apps_list();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -202,8 +202,19 @@ if TYPE_CHECKING: # pragma: no cover
|
||||||
# end: static analysis hack
|
# end: static analysis hack
|
||||||
|
|
||||||
|
|
||||||
def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) -> None:
|
def init(
|
||||||
"""Initialize frappe for the current site. Reset thread locals `frappe.local`"""
|
site: str, sites_path: str = ".", new_site: bool = False, force=False, site_ready: bool = True
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Initialize frappe for the current site. Reset thread locals `frappe.local`
|
||||||
|
|
||||||
|
:param site: Site name.
|
||||||
|
:param sites_path: Path to sites directory.
|
||||||
|
:param new_site: Sets a flag to indicate a new site.
|
||||||
|
:param force: Force initialization if already previously run.
|
||||||
|
:param site_ready: Any init during site installation should set this to False.
|
||||||
|
|
||||||
|
"""
|
||||||
if getattr(local, "initialised", None) and not force:
|
if getattr(local, "initialised", None) and not force:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -261,34 +272,47 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
|
||||||
local.qb = get_query_builder(local.conf.db_type)
|
local.qb = get_query_builder(local.conf.db_type)
|
||||||
local.qb.get_query = get_query
|
local.qb.get_query = get_query
|
||||||
setup_redis_cache_connection()
|
setup_redis_cache_connection()
|
||||||
setup_module_map()
|
|
||||||
|
|
||||||
if not _qb_patched.get(local.conf.db_type):
|
if not _qb_patched.get(local.conf.db_type):
|
||||||
patch_query_execute()
|
patch_query_execute()
|
||||||
patch_query_aggregation()
|
patch_query_aggregation()
|
||||||
|
|
||||||
|
if site:
|
||||||
|
setup_module_map(site_ready)
|
||||||
|
|
||||||
local.initialised = True
|
local.initialised = True
|
||||||
|
|
||||||
|
# Set the user as database name if not set in config
|
||||||
|
if local.conf and local.conf.db_name is not None and local.conf.db_user is None:
|
||||||
|
local.conf.db_user = local.conf.db_name
|
||||||
|
|
||||||
|
|
||||||
def connect(
|
def connect(
|
||||||
site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True
|
site: str | None = None, db_name: str | None = None, set_admin_as_user: bool = True
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Connect to site database instance.
|
"""Connect to site database instance.
|
||||||
|
|
||||||
:param site: If site is given, calls `frappe.init`.
|
:param site: (Deprecated) If site is given, calls `frappe.init`.
|
||||||
:param db_name: Optional. Will use from `site_config.json`.
|
:param db_name: Optional. Will use from `site_config.json`.
|
||||||
:param set_admin_as_user: Set Administrator as current user.
|
:param set_admin_as_user: Set Administrator as current user.
|
||||||
"""
|
"""
|
||||||
from frappe.database import get_db
|
from frappe.database import get_db
|
||||||
|
|
||||||
if site:
|
if site:
|
||||||
|
from frappe.utils.deprecations import deprecation_warning
|
||||||
|
|
||||||
|
deprecation_warning(
|
||||||
|
"Calling frappe.connect with the site argument is deprecated and will be removed in next major version. "
|
||||||
|
"Instead, explicitly invoke frappe.init(site) prior to calling frappe.connect(), if initializing the site is necessary."
|
||||||
|
)
|
||||||
init(site)
|
init(site)
|
||||||
|
|
||||||
local.db = get_db(
|
local.db = get_db(
|
||||||
host=local.conf.db_host,
|
host=local.conf.db_host,
|
||||||
port=local.conf.db_port,
|
port=local.conf.db_port,
|
||||||
user=db_name or local.conf.db_name,
|
user=local.conf.db_user or db_name or local.conf.db_name,
|
||||||
password=None,
|
password=local.conf.db_password,
|
||||||
|
cur_db_name=db_name or local.conf.db_name,
|
||||||
)
|
)
|
||||||
if set_admin_as_user:
|
if set_admin_as_user:
|
||||||
set_user("Administrator")
|
set_user("Administrator")
|
||||||
|
|
@ -300,15 +324,21 @@ def connect_replica() -> bool:
|
||||||
if local and hasattr(local, "replica_db") and hasattr(local, "primary_db"):
|
if local and hasattr(local, "replica_db") and hasattr(local, "primary_db"):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
user = local.conf.db_name
|
user = local.conf.db_user
|
||||||
password = local.conf.db_password
|
password = local.conf.db_password
|
||||||
port = local.conf.replica_db_port
|
port = local.conf.replica_db_port
|
||||||
|
|
||||||
if local.conf.different_credentials_for_replica:
|
if local.conf.different_credentials_for_replica:
|
||||||
user = local.conf.replica_db_name
|
user = local.conf.replica_db_user or local.conf.replica_db_name
|
||||||
password = local.conf.replica_db_password
|
password = local.conf.replica_db_password
|
||||||
|
|
||||||
local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port)
|
local.replica_db = get_db(
|
||||||
|
host=local.conf.replica_host,
|
||||||
|
port=port,
|
||||||
|
user=user,
|
||||||
|
password=password,
|
||||||
|
cur_db_name=local.conf.db_name,
|
||||||
|
)
|
||||||
|
|
||||||
# swap db connections
|
# swap db connections
|
||||||
local.primary_db = local.db
|
local.primary_db = local.db
|
||||||
|
|
@ -325,8 +355,10 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
|
||||||
sites_path = sites_path or getattr(local, "sites_path", None)
|
sites_path = sites_path or getattr(local, "sites_path", None)
|
||||||
site_path = site_path or getattr(local, "site_path", None)
|
site_path = site_path or getattr(local, "site_path", None)
|
||||||
|
|
||||||
|
common_config = get_common_site_config(sites_path)
|
||||||
|
|
||||||
if sites_path:
|
if sites_path:
|
||||||
config.update(get_common_site_config(sites_path))
|
config.update(common_config)
|
||||||
|
|
||||||
if site_path:
|
if site_path:
|
||||||
site_config = os.path.join(site_path, "site_config.json")
|
site_config = os.path.join(site_path, "site_config.json")
|
||||||
|
|
@ -337,7 +369,15 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
|
||||||
click.secho(f"{local.site}/site_config.json is invalid", fg="red")
|
click.secho(f"{local.site}/site_config.json is invalid", fg="red")
|
||||||
print(error)
|
print(error)
|
||||||
elif local.site and not local.flags.new_site:
|
elif local.site and not local.flags.new_site:
|
||||||
raise IncorrectSitePath(f"{local.site} does not exist")
|
error_msg = f"{local.site} does not exist."
|
||||||
|
if common_config.developer_mode:
|
||||||
|
from frappe.utils import get_sites
|
||||||
|
|
||||||
|
all_sites = get_sites()
|
||||||
|
error_msg += "\n\nSites on this bench:\n"
|
||||||
|
error_msg += "\n".join(f"* {site}" for site in all_sites)
|
||||||
|
|
||||||
|
raise IncorrectSitePath(error_msg)
|
||||||
|
|
||||||
# Generalized env variable overrides and defaults
|
# Generalized env variable overrides and defaults
|
||||||
def db_default_ports(db_type):
|
def db_default_ports(db_type):
|
||||||
|
|
@ -1609,18 +1649,32 @@ def append_hook(target, key, value):
|
||||||
target[key].extend(value)
|
target[key].extend(value)
|
||||||
|
|
||||||
|
|
||||||
def setup_module_map():
|
def setup_module_map(site_ready: bool = True):
|
||||||
"""Rebuild map of all modules (internal)."""
|
"""
|
||||||
|
Rebuild map of all modules (internal).
|
||||||
|
|
||||||
|
:param site_ready: If the site isn't fully ready yet - install is still going on, we can't
|
||||||
|
fetch apps from site DB. Fallback to fetching all apps on bench for module map temporarily.
|
||||||
|
"""
|
||||||
if conf.db_name:
|
if conf.db_name:
|
||||||
local.app_modules = cache.get_value("app_modules")
|
local.app_modules = cache.get_value("app_modules")
|
||||||
local.module_app = cache.get_value("module_app")
|
local.module_app = cache.get_value("module_app")
|
||||||
|
|
||||||
if not (local.app_modules and local.module_app):
|
if not (local.app_modules and local.module_app):
|
||||||
local.module_app, local.app_modules = {}, {}
|
local.module_app, local.app_modules = {}, {}
|
||||||
for app in get_all_apps(with_internal_apps=True):
|
|
||||||
|
if site_ready:
|
||||||
|
apps = get_installed_apps(_ensure_on_bench=True)
|
||||||
|
else:
|
||||||
|
apps = get_all_apps()
|
||||||
|
|
||||||
|
for app in apps:
|
||||||
local.app_modules.setdefault(app, [])
|
local.app_modules.setdefault(app, [])
|
||||||
for module in get_module_list(app):
|
for module in get_module_list(app):
|
||||||
module = scrub(module)
|
module = scrub(module)
|
||||||
|
if module in local.module_app:
|
||||||
|
print(f"WARNING: module `{module}` found in apps `{local.module_app[module]}` and `{app}`")
|
||||||
|
|
||||||
local.module_app[module] = app
|
local.module_app[module] = app
|
||||||
local.app_modules[app].append(module)
|
local.app_modules[app].append(module)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,15 @@ import base64
|
||||||
import binascii
|
import binascii
|
||||||
from urllib.parse import quote, urlencode, urlparse
|
from urllib.parse import quote, urlencode, urlparse
|
||||||
|
|
||||||
|
from werkzeug.wrappers import Response
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.database
|
import frappe.database
|
||||||
import frappe.utils
|
import frappe.utils
|
||||||
import frappe.utils.user
|
import frappe.utils.user
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
|
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
|
||||||
from frappe.sessions import Session, clear_sessions, delete_session
|
from frappe.sessions import Session, clear_sessions, delete_session, get_expiry_in_seconds
|
||||||
from frappe.translate import get_language
|
from frappe.translate import get_language
|
||||||
from frappe.twofactor import (
|
from frappe.twofactor import (
|
||||||
authenticate_for_2factor,
|
authenticate_for_2factor,
|
||||||
|
|
@ -356,12 +358,19 @@ class CookieManager:
|
||||||
if not frappe.local.session.get("sid"):
|
if not frappe.local.session.get("sid"):
|
||||||
return
|
return
|
||||||
|
|
||||||
# sid expires in 3 days
|
|
||||||
expires = datetime.datetime.now() + datetime.timedelta(days=3)
|
|
||||||
if frappe.session.sid:
|
if frappe.session.sid:
|
||||||
self.set_cookie("sid", frappe.session.sid, expires=expires, httponly=True)
|
self.set_cookie("sid", frappe.session.sid, max_age=get_expiry_in_seconds(), httponly=True)
|
||||||
|
|
||||||
def set_cookie(self, key, value, expires=None, secure=False, httponly=False, samesite="Lax"):
|
def set_cookie(
|
||||||
|
self,
|
||||||
|
key,
|
||||||
|
value,
|
||||||
|
expires=None,
|
||||||
|
secure=False,
|
||||||
|
httponly=False,
|
||||||
|
samesite="Lax",
|
||||||
|
max_age=None,
|
||||||
|
):
|
||||||
if not secure and hasattr(frappe.local, "request"):
|
if not secure and hasattr(frappe.local, "request"):
|
||||||
secure = frappe.local.request.scheme == "https"
|
secure = frappe.local.request.scheme == "https"
|
||||||
|
|
||||||
|
|
@ -371,6 +380,7 @@ class CookieManager:
|
||||||
"secure": secure,
|
"secure": secure,
|
||||||
"httponly": httponly,
|
"httponly": httponly,
|
||||||
"samesite": samesite,
|
"samesite": samesite,
|
||||||
|
"max_age": max_age,
|
||||||
}
|
}
|
||||||
|
|
||||||
def delete_cookie(self, to_delete):
|
def delete_cookie(self, to_delete):
|
||||||
|
|
@ -379,7 +389,7 @@ class CookieManager:
|
||||||
|
|
||||||
self.to_delete.extend(to_delete)
|
self.to_delete.extend(to_delete)
|
||||||
|
|
||||||
def flush_cookies(self, response):
|
def flush_cookies(self, response: Response):
|
||||||
for key, opts in self.cookies.items():
|
for key, opts in self.cookies.items():
|
||||||
response.set_cookie(
|
response.set_cookie(
|
||||||
key,
|
key,
|
||||||
|
|
@ -388,6 +398,7 @@ class CookieManager:
|
||||||
secure=opts.get("secure"),
|
secure=opts.get("secure"),
|
||||||
httponly=opts.get("httponly"),
|
httponly=opts.get("httponly"),
|
||||||
samesite=opts.get("samesite"),
|
samesite=opts.get("samesite"),
|
||||||
|
max_age=opts.get("max_age"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# expires yesterday!
|
# expires yesterday!
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from frappe.cache_manager import clear_doctype_map, get_doctype_map
|
||||||
from frappe.desk.form import assign_to
|
from frappe.desk.form import assign_to
|
||||||
from frappe.model import log_types
|
from frappe.model import log_types
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils.data import comma_and
|
||||||
|
|
||||||
|
|
||||||
class AssignmentRule(Document):
|
class AssignmentRule(Document):
|
||||||
|
|
@ -55,14 +56,10 @@ class AssignmentRule(Document):
|
||||||
|
|
||||||
def validate_assignment_days(self):
|
def validate_assignment_days(self):
|
||||||
assignment_days = self.get_assignment_days()
|
assignment_days = self.get_assignment_days()
|
||||||
|
|
||||||
if len(set(assignment_days)) != len(assignment_days):
|
if len(set(assignment_days)) != len(assignment_days):
|
||||||
repeated_days = get_repeated(assignment_days)
|
|
||||||
plural = "s" if len(repeated_days) > 1 else ""
|
|
||||||
|
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Assignment Day{0} {1} has been repeated.").format(
|
_("The following Assignment Days have been repeated: {0}").format(
|
||||||
plural, frappe.bold(", ".join(repeated_days))
|
comma_and([_(day) for day in get_repeated(assignment_days)], add_quotes=False)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -229,6 +229,7 @@ def bundle(
|
||||||
skip_frappe=False,
|
skip_frappe=False,
|
||||||
files=None,
|
files=None,
|
||||||
save_metafiles=False,
|
save_metafiles=False,
|
||||||
|
using_cached=False,
|
||||||
):
|
):
|
||||||
"""concat / minify js files"""
|
"""concat / minify js files"""
|
||||||
setup()
|
setup()
|
||||||
|
|
@ -246,7 +247,10 @@ def bundle(
|
||||||
if files:
|
if files:
|
||||||
command += " --files {files}".format(files=",".join(files))
|
command += " --files {files}".format(files=",".join(files))
|
||||||
|
|
||||||
command += " --run-build-command"
|
if using_cached:
|
||||||
|
command += " --using-cached"
|
||||||
|
else:
|
||||||
|
command += " --run-build-command"
|
||||||
|
|
||||||
if save_metafiles:
|
if save_metafiles:
|
||||||
command += " --save-metafiles"
|
command += " --save-metafiles"
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ from frappe.exceptions import SiteNotSpecifiedError
|
||||||
default=True,
|
default=True,
|
||||||
help="Create user and database in mariadb/postgres; only bootstrap if false",
|
help="Create user and database in mariadb/postgres; only bootstrap if false",
|
||||||
)
|
)
|
||||||
|
@click.option("--db-user", help="Database user if you already have one")
|
||||||
def new_site(
|
def new_site(
|
||||||
site,
|
site,
|
||||||
db_root_username=None,
|
db_root_username=None,
|
||||||
|
|
@ -68,13 +69,14 @@ def new_site(
|
||||||
db_type=None,
|
db_type=None,
|
||||||
db_host=None,
|
db_host=None,
|
||||||
db_port=None,
|
db_port=None,
|
||||||
|
db_user=None,
|
||||||
set_default=False,
|
set_default=False,
|
||||||
setup_db=True,
|
setup_db=True,
|
||||||
):
|
):
|
||||||
"Create a new site"
|
"Create a new site"
|
||||||
from frappe.installer import _new_site
|
from frappe.installer import _new_site
|
||||||
|
|
||||||
frappe.init(site=site, new_site=True)
|
frappe.init(site=site, new_site=True, site_ready=False)
|
||||||
|
|
||||||
_new_site(
|
_new_site(
|
||||||
db_name,
|
db_name,
|
||||||
|
|
@ -91,6 +93,7 @@ def new_site(
|
||||||
db_type=db_type,
|
db_type=db_type,
|
||||||
db_host=db_host,
|
db_host=db_host,
|
||||||
db_port=db_port,
|
db_port=db_port,
|
||||||
|
db_user=db_user,
|
||||||
setup_db=setup_db,
|
setup_db=setup_db,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -319,7 +322,7 @@ def restore_backup(
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print(err.args[1])
|
print(err)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,7 +342,7 @@ def partial_restore(context, sql_file_path, verbose, encryption_key=None):
|
||||||
site = get_site(context)
|
site = get_site(context)
|
||||||
verbose = context.verbose or verbose
|
verbose = context.verbose or verbose
|
||||||
frappe.init(site=site)
|
frappe.init(site=site)
|
||||||
frappe.connect(site=site)
|
frappe.connect()
|
||||||
err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True)
|
err, out = frappe.utils.execute_in_shell(f"file {sql_file_path}", check_exit_code=True)
|
||||||
if err:
|
if err:
|
||||||
click.secho("Failed to detect type of backup file", fg="red")
|
click.secho("Failed to detect type of backup file", fg="red")
|
||||||
|
|
@ -414,7 +417,7 @@ def _reinstall(
|
||||||
if not yes:
|
if not yes:
|
||||||
click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True)
|
click.confirm("This will wipe your database. Are you sure you want to reinstall?", abort=True)
|
||||||
try:
|
try:
|
||||||
frappe.init(site=site)
|
frappe.init(site=site, site_ready=False)
|
||||||
frappe.connect()
|
frappe.connect()
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
installed = frappe.get_installed_apps()
|
installed = frappe.get_installed_apps()
|
||||||
|
|
@ -426,7 +429,7 @@ def _reinstall(
|
||||||
frappe.db.close()
|
frappe.db.close()
|
||||||
frappe.destroy()
|
frappe.destroy()
|
||||||
|
|
||||||
frappe.init(site=site)
|
frappe.init(site=site, site_ready=False)
|
||||||
|
|
||||||
_new_site(
|
_new_site(
|
||||||
frappe.conf.db_name,
|
frappe.conf.db_name,
|
||||||
|
|
@ -535,7 +538,8 @@ def add_db_index(context, doctype, column):
|
||||||
|
|
||||||
columns = column # correct naming
|
columns = column # correct naming
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
frappe.connect(site=site)
|
frappe.init(site=site)
|
||||||
|
frappe.connect()
|
||||||
try:
|
try:
|
||||||
frappe.db.add_index(doctype, columns)
|
frappe.db.add_index(doctype, columns)
|
||||||
if len(columns) == 1:
|
if len(columns) == 1:
|
||||||
|
|
@ -577,7 +581,8 @@ def describe_database_table(context, doctype, column):
|
||||||
import json
|
import json
|
||||||
|
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
frappe.connect(site=site)
|
frappe.init(site=site)
|
||||||
|
frappe.connect()
|
||||||
try:
|
try:
|
||||||
data = _extract_table_stats(doctype, column)
|
data = _extract_table_stats(doctype, column)
|
||||||
# NOTE: Do not print anything else in this to avoid clobbering the output.
|
# NOTE: Do not print anything else in this to avoid clobbering the output.
|
||||||
|
|
@ -663,7 +668,8 @@ def add_system_manager(context, email, first_name, last_name, send_welcome_email
|
||||||
import frappe.utils.user
|
import frappe.utils.user
|
||||||
|
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
frappe.connect(site=site)
|
frappe.init(site=site)
|
||||||
|
frappe.connect()
|
||||||
try:
|
try:
|
||||||
frappe.utils.user.add_system_manager(email, first_name, last_name, send_welcome_email, password)
|
frappe.utils.user.add_system_manager(email, first_name, last_name, send_welcome_email, password)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
@ -689,7 +695,8 @@ def add_user_for_sites(
|
||||||
import frappe.utils.user
|
import frappe.utils.user
|
||||||
|
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
frappe.connect(site=site)
|
frappe.init(site=site)
|
||||||
|
frappe.connect()
|
||||||
try:
|
try:
|
||||||
add_new_user(email, first_name, last_name, user_type, send_welcome_email, password, add_role)
|
add_new_user(email, first_name, last_name, user_type, send_welcome_email, password, add_role)
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
@ -719,7 +726,6 @@ def disable_user(context, email):
|
||||||
@pass_context
|
@pass_context
|
||||||
def migrate(context, skip_failing=False, skip_search_index=False):
|
def migrate(context, skip_failing=False, skip_search_index=False):
|
||||||
"Run patches, sync schema and rebuild files/translations"
|
"Run patches, sync schema and rebuild files/translations"
|
||||||
from traceback_with_variables import activate_by_import
|
|
||||||
|
|
||||||
from frappe.migrate import SiteMigration
|
from frappe.migrate import SiteMigration
|
||||||
|
|
||||||
|
|
@ -1058,7 +1064,11 @@ def _drop_site(
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
click.secho("Dropping site database and user", fg="green")
|
click.secho("Dropping site database and user", fg="green")
|
||||||
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
|
|
||||||
|
frappe.flags.root_login = db_root_username
|
||||||
|
frappe.flags.root_password = db_root_password
|
||||||
|
|
||||||
|
drop_user_and_database(frappe.conf.db_name, frappe.conf.db_user)
|
||||||
|
|
||||||
archived_sites_path = archived_sites_path or os.path.join(
|
archived_sites_path = archived_sites_path or os.path.join(
|
||||||
frappe.utils.get_bench_path(), "archived", "sites"
|
frappe.utils.get_bench_path(), "archived", "sites"
|
||||||
|
|
@ -1336,7 +1346,6 @@ def build_search_index(context):
|
||||||
@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table")
|
@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table")
|
||||||
@pass_context
|
@pass_context
|
||||||
def clear_log_table(context, doctype, days, no_backup):
|
def clear_log_table(context, doctype, days, no_backup):
|
||||||
|
|
||||||
"""If any logtype table grows too large then clearing it with DELETE query
|
"""If any logtype table grows too large then clearing it with DELETE query
|
||||||
is not feasible in reasonable time. This command copies recent data to new
|
is not feasible in reasonable time. This command copies recent data to new
|
||||||
table and replaces current table with new smaller table.
|
table and replaces current table with new smaller table.
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,8 @@ def new_language(context, lang_code, app):
|
||||||
raise Exception("--site is required")
|
raise Exception("--site is required")
|
||||||
|
|
||||||
# init site
|
# init site
|
||||||
frappe.connect(site=context["sites"][0])
|
frappe.init(site=context["sites"][0])
|
||||||
|
frappe.connect()
|
||||||
frappe.translate.write_translations_file(app, lang_code)
|
frappe.translate.write_translations_file(app, lang_code)
|
||||||
|
|
||||||
print(
|
print(
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,13 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
|
||||||
default=False,
|
default=False,
|
||||||
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
|
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--using-cached",
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
envvar="USING_CACHED",
|
||||||
|
help="Skips build and uses cached build artifacts (cache is set by Bench). Ignored if developer_mode enabled.",
|
||||||
|
)
|
||||||
def build(
|
def build(
|
||||||
app=None,
|
app=None,
|
||||||
apps=None,
|
apps=None,
|
||||||
|
|
@ -44,6 +51,7 @@ def build(
|
||||||
verbose=False,
|
verbose=False,
|
||||||
force=False,
|
force=False,
|
||||||
save_metafiles=False,
|
save_metafiles=False,
|
||||||
|
using_cached=False,
|
||||||
):
|
):
|
||||||
"Compile JS and CSS source files"
|
"Compile JS and CSS source files"
|
||||||
from frappe.build import bundle, download_frappe_assets
|
from frappe.build import bundle, download_frappe_assets
|
||||||
|
|
@ -69,6 +77,9 @@ def build(
|
||||||
if production:
|
if production:
|
||||||
mode = "production"
|
mode = "production"
|
||||||
|
|
||||||
|
if development:
|
||||||
|
using_cached = False
|
||||||
|
|
||||||
bundle(
|
bundle(
|
||||||
mode,
|
mode,
|
||||||
apps=apps,
|
apps=apps,
|
||||||
|
|
@ -76,6 +87,7 @@ def build(
|
||||||
verbose=verbose,
|
verbose=verbose,
|
||||||
skip_frappe=skip_frappe,
|
skip_frappe=skip_frappe,
|
||||||
save_metafiles=save_metafiles,
|
save_metafiles=save_metafiles,
|
||||||
|
using_cached=using_cached,
|
||||||
)
|
)
|
||||||
|
|
||||||
if apps and isinstance(apps, str):
|
if apps and isinstance(apps, str):
|
||||||
|
|
@ -108,7 +120,8 @@ def clear_cache(context):
|
||||||
|
|
||||||
for site in context.sites:
|
for site in context.sites:
|
||||||
try:
|
try:
|
||||||
frappe.connect(site)
|
frappe.init(site=site)
|
||||||
|
frappe.connect()
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
clear_website_cache()
|
clear_website_cache()
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -601,7 +614,7 @@ def console(context, autoreload=False):
|
||||||
all_apps = frappe.get_installed_apps()
|
all_apps = frappe.get_installed_apps()
|
||||||
failed_to_import = []
|
failed_to_import = []
|
||||||
|
|
||||||
for app in all_apps:
|
for app in list(all_apps):
|
||||||
try:
|
try:
|
||||||
locals()[app] = __import__(app)
|
locals()[app] = __import__(app)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,15 @@ class TestContact(FrappeTestCase):
|
||||||
|
|
||||||
def test_check_default_phone_and_mobile(self):
|
def test_check_default_phone_and_mobile(self):
|
||||||
phones = [
|
phones = [
|
||||||
{"phone": "+91 0000000000", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
{"phone": "+91 0000000010", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||||
{"phone": "+91 0000000001", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
{"phone": "+91 0000000011", "is_primary_phone": 0, "is_primary_mobile_no": 0},
|
||||||
{"phone": "+91 0000000002", "is_primary_phone": 1, "is_primary_mobile_no": 0},
|
{"phone": "+91 0000000012", "is_primary_phone": 1, "is_primary_mobile_no": 0},
|
||||||
{"phone": "+91 0000000003", "is_primary_phone": 0, "is_primary_mobile_no": 1},
|
{"phone": "+91 0000000013", "is_primary_phone": 0, "is_primary_mobile_no": 1},
|
||||||
]
|
]
|
||||||
contact = create_contact("Phone", "Mr", phones=phones)
|
contact = create_contact("Phone", "Mr", phones=phones)
|
||||||
|
|
||||||
self.assertEqual(contact.phone, "+91 0000000002")
|
self.assertEqual(contact.phone, "+91 0000000012")
|
||||||
self.assertEqual(contact.mobile_no, "+91 0000000003")
|
self.assertEqual(contact.mobile_no, "+91 0000000013")
|
||||||
|
|
||||||
def test_get_full_name(self):
|
def test_get_full_name(self):
|
||||||
self.assertEqual(get_full_name(first="John"), "John")
|
self.assertEqual(get_full_name(first="John"), "John")
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@
|
||||||
],
|
],
|
||||||
"phone_nos": [
|
"phone_nos": [
|
||||||
{
|
{
|
||||||
"phone": "+91 0000000000",
|
"phone": "+91 0000000001",
|
||||||
"is_primary_phone": 1
|
"is_primary_phone": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ def create_linked_contact(link_list, address):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
contact.add_email("test_contact@example.com", is_primary=True)
|
contact.add_email("test_contact@example.com", is_primary=True)
|
||||||
contact.add_phone("+91 0000000000", is_primary_phone=True)
|
contact.add_phone("+91 0000000020", is_primary_phone=True)
|
||||||
|
|
||||||
for name in link_list:
|
for name in link_list:
|
||||||
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
|
contact.append("links", {"link_doctype": "Test Custom Doctype", "link_name": name})
|
||||||
|
|
@ -105,7 +105,7 @@ class TestAddressesAndContacts(FrappeTestCase):
|
||||||
"_Test First Name",
|
"_Test First Name",
|
||||||
"_Test Last Name",
|
"_Test Last Name",
|
||||||
"_Test Address-Billing",
|
"_Test Address-Billing",
|
||||||
"+91 0000000000",
|
"+91 0000000020",
|
||||||
"",
|
"",
|
||||||
"test_contact@example.com",
|
"test_contact@example.com",
|
||||||
1,
|
1,
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class Comment(Document):
|
||||||
|
|
||||||
def remove_comment_from_cache(self):
|
def remove_comment_from_cache(self):
|
||||||
_comments = get_comments_from_parent(self)
|
_comments = get_comments_from_parent(self)
|
||||||
for c in _comments:
|
for c in list(_comments):
|
||||||
if c.get("name") == self.name:
|
if c.get("name") == self.name:
|
||||||
_comments.remove(c)
|
_comments.remove(c)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ const can_export = (frm) => {
|
||||||
if (!doctype) {
|
if (!doctype) {
|
||||||
frappe.msgprint(__("Please select the Document Type."));
|
frappe.msgprint(__("Please select the Document Type."));
|
||||||
} else if (!parent_multicheck_options.length) {
|
} else if (!parent_multicheck_options.length) {
|
||||||
frappe.msgprint(__("Atleast one field of Parent Document Type is mandatory"));
|
frappe.msgprint(__("At least one field of Parent Document Type is mandatory"));
|
||||||
} else {
|
} else {
|
||||||
is_valid_form = true;
|
is_valid_form = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,10 @@
|
||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# imports - standard imports
|
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
# imports - module imports
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.cache_manager import clear_controller_cache, clear_user_cache
|
from frappe.cache_manager import clear_controller_cache, clear_user_cache
|
||||||
|
|
@ -1614,7 +1611,6 @@ def validate_fields(meta):
|
||||||
|
|
||||||
check_illegal_characters(d.fieldname)
|
check_illegal_characters(d.fieldname)
|
||||||
check_invalid_fieldnames(meta.get("name"), d.fieldname)
|
check_invalid_fieldnames(meta.get("name"), d.fieldname)
|
||||||
check_unique_fieldname(meta.get("name"), d.fieldname)
|
|
||||||
check_fieldname_length(d.fieldname)
|
check_fieldname_length(d.fieldname)
|
||||||
check_hidden_and_mandatory(meta.get("name"), d)
|
check_hidden_and_mandatory(meta.get("name"), d)
|
||||||
check_unique_and_text(meta.get("name"), d)
|
check_unique_and_text(meta.get("name"), d)
|
||||||
|
|
@ -1624,6 +1620,7 @@ def validate_fields(meta):
|
||||||
validate_data_field_type(d)
|
validate_data_field_type(d)
|
||||||
|
|
||||||
if not frappe.flags.in_migrate:
|
if not frappe.flags.in_migrate:
|
||||||
|
check_unique_fieldname(meta.get("name"), d.fieldname)
|
||||||
check_link_table_options(meta.get("name"), d)
|
check_link_table_options(meta.get("name"), d)
|
||||||
check_illegal_mandatory(meta.get("name"), d)
|
check_illegal_mandatory(meta.get("name"), d)
|
||||||
check_dynamic_link_options(d)
|
check_dynamic_link_options(d)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,19 @@ frappe.listview_settings["DocType"] = {
|
||||||
this.new_doctype_dialog();
|
this.new_doctype_dialog();
|
||||||
},
|
},
|
||||||
|
|
||||||
new_doctype_dialog() {
|
new_doctype_dialog(args) {
|
||||||
|
const {
|
||||||
|
doctype_name = "",
|
||||||
|
doctype_module = "",
|
||||||
|
is_submittable = 0,
|
||||||
|
is_child = 0,
|
||||||
|
is_virtual = 0,
|
||||||
|
is_single = 0,
|
||||||
|
is_tree = 0,
|
||||||
|
is_custom = 0,
|
||||||
|
editable_grid = 1,
|
||||||
|
} = args || {};
|
||||||
|
|
||||||
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
|
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
|
||||||
let fields = [
|
let fields = [
|
||||||
{
|
{
|
||||||
|
|
@ -11,6 +23,7 @@ frappe.listview_settings["DocType"] = {
|
||||||
fieldname: "name",
|
fieldname: "name",
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
default: doctype_name,
|
||||||
},
|
},
|
||||||
{ fieldtype: "Column Break" },
|
{ fieldtype: "Column Break" },
|
||||||
{
|
{
|
||||||
|
|
@ -19,6 +32,7 @@ frappe.listview_settings["DocType"] = {
|
||||||
fieldtype: "Link",
|
fieldtype: "Link",
|
||||||
options: "Module Def",
|
options: "Module Def",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
default: doctype_module,
|
||||||
},
|
},
|
||||||
{ fieldtype: "Section Break" },
|
{ fieldtype: "Section Break" },
|
||||||
{
|
{
|
||||||
|
|
@ -29,6 +43,7 @@ frappe.listview_settings["DocType"] = {
|
||||||
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
|
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
|
||||||
),
|
),
|
||||||
depends_on: "eval:!doc.istable && !doc.issingle",
|
depends_on: "eval:!doc.istable && !doc.issingle",
|
||||||
|
default: is_submittable,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Is Child Table"),
|
label: __("Is Child Table"),
|
||||||
|
|
@ -36,13 +51,14 @@ frappe.listview_settings["DocType"] = {
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
description: __("Child Tables are shown as a Grid in other DocTypes"),
|
description: __("Child Tables are shown as a Grid in other DocTypes"),
|
||||||
depends_on: "eval:!doc.is_submittable && !doc.issingle",
|
depends_on: "eval:!doc.is_submittable && !doc.issingle",
|
||||||
|
default: is_child,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Editable Grid"),
|
label: __("Editable Grid"),
|
||||||
fieldname: "editable_grid",
|
fieldname: "editable_grid",
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
depends_on: "istable",
|
depends_on: "istable",
|
||||||
default: 1,
|
default: editable_grid,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __("Is Single"),
|
label: __("Is Single"),
|
||||||
|
|
@ -52,12 +68,13 @@ frappe.listview_settings["DocType"] = {
|
||||||
"Single Types have only one record no tables associated. Values are stored in tabSingles"
|
"Single Types have only one record no tables associated. Values are stored in tabSingles"
|
||||||
),
|
),
|
||||||
depends_on: "eval:!doc.istable && !doc.is_submittable",
|
depends_on: "eval:!doc.istable && !doc.is_submittable",
|
||||||
|
default: is_single,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Is Tree",
|
label: "Is Tree",
|
||||||
fieldname: "is_tree",
|
fieldname: "is_tree",
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: "0",
|
default: is_tree,
|
||||||
depends_on: "eval:!doc.istable",
|
depends_on: "eval:!doc.istable",
|
||||||
description: "Tree structures are implemented using Nested Set",
|
description: "Tree structures are implemented using Nested Set",
|
||||||
},
|
},
|
||||||
|
|
@ -65,7 +82,7 @@ frappe.listview_settings["DocType"] = {
|
||||||
label: __("Custom?"),
|
label: __("Custom?"),
|
||||||
fieldname: "custom",
|
fieldname: "custom",
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: non_developer,
|
default: non_developer || is_custom,
|
||||||
read_only: non_developer,
|
read_only: non_developer,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
@ -75,7 +92,7 @@ frappe.listview_settings["DocType"] = {
|
||||||
label: "Is Virtual",
|
label: "Is Virtual",
|
||||||
fieldname: "is_virtual",
|
fieldname: "is_virtual",
|
||||||
fieldtype: "Check",
|
fieldtype: "Check",
|
||||||
default: "0",
|
default: is_virtual,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,8 +40,8 @@ class TestNamingSeries(FrappeTestCase):
|
||||||
|
|
||||||
def get_valid_serieses(self):
|
def get_valid_serieses(self):
|
||||||
VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""]
|
VALID_SERIES = ["SINV-", "SI-.{field}.", "SI-#.###", ""]
|
||||||
exisiting_series = self.dns.get_transactions_and_prefixes()["prefixes"]
|
existing_series = self.dns.get_transactions_and_prefixes()["prefixes"]
|
||||||
return VALID_SERIES + exisiting_series
|
return VALID_SERIES + existing_series
|
||||||
|
|
||||||
def test_naming_preview(self):
|
def test_naming_preview(self):
|
||||||
self.dns.transaction_type = self.ns_doctype
|
self.dns.transaction_type = self.ns_doctype
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ class RoleProfile(Document):
|
||||||
self.name = self.role_profile
|
self.name = self.role_profile
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
self.queue_action("update_all_users", now=frappe.flags.in_test)
|
self.queue_action("update_all_users", now=frappe.flags.in_test, enqueue_after_commit=True)
|
||||||
|
|
||||||
def update_all_users(self):
|
def update_all_users(self):
|
||||||
"""Changes in role_profile reflected across all its user"""
|
"""Changes in role_profile reflected across all its user"""
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ class TestRQJob(FrappeTestCase):
|
||||||
# If this starts failing analyze memory usage using memray or some equivalent tool to find
|
# If this starts failing analyze memory usage using memray or some equivalent tool to find
|
||||||
# offending imports/function calls.
|
# offending imports/function calls.
|
||||||
# Refer this PR: https://github.com/frappe/frappe/pull/21467
|
# Refer this PR: https://github.com/frappe/frappe/pull/21467
|
||||||
LAST_MEASURED_USAGE = 40
|
LAST_MEASURED_USAGE = 41
|
||||||
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
|
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
|
||||||
|
|
||||||
@timeout(20)
|
@timeout(20)
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,9 @@
|
||||||
"dormant_days",
|
"dormant_days",
|
||||||
"telemetry_section",
|
"telemetry_section",
|
||||||
"allow_error_traceback",
|
"allow_error_traceback",
|
||||||
"enable_telemetry"
|
"enable_telemetry",
|
||||||
|
"search_section",
|
||||||
|
"link_field_results_limit"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
|
|
@ -634,12 +636,24 @@
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_uhqk",
|
"fieldname": "column_break_uhqk",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "search_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Search"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "10",
|
||||||
|
"fieldname": "link_field_results_limit",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Link Field Results Limit",
|
||||||
|
"non_negative": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-12-08 15:52:37.525003",
|
"modified": "2024-01-26 11:29:20.924425",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "System Settings",
|
"name": "System Settings",
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ class SystemSettings(Document):
|
||||||
hide_footer_in_auto_email_reports: DF.Check
|
hide_footer_in_auto_email_reports: DF.Check
|
||||||
language: DF.Link
|
language: DF.Link
|
||||||
lifespan_qrcode_image: DF.Int
|
lifespan_qrcode_image: DF.Int
|
||||||
|
link_field_results_limit: DF.Int
|
||||||
login_with_email_link: DF.Check
|
login_with_email_link: DF.Check
|
||||||
login_with_email_link_expiry: DF.Int
|
login_with_email_link_expiry: DF.Int
|
||||||
logout_on_password_reset: DF.Check
|
logout_on_password_reset: DF.Check
|
||||||
|
|
@ -94,6 +95,7 @@ class SystemSettings(Document):
|
||||||
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
|
two_factor_method: DF.Literal["OTP App", "SMS", "Email"]
|
||||||
welcome_email_template: DF.Link | None
|
welcome_email_template: DF.Link | None
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
from frappe.twofactor import toggle_two_factor_auth
|
from frappe.twofactor import toggle_two_factor_auth
|
||||||
|
|
||||||
|
|
@ -130,6 +132,13 @@ class SystemSettings(Document):
|
||||||
self.validate_backup_limit()
|
self.validate_backup_limit()
|
||||||
self.validate_file_extensions()
|
self.validate_file_extensions()
|
||||||
|
|
||||||
|
if self.link_field_results_limit > 50:
|
||||||
|
self.link_field_results_limit = 50
|
||||||
|
label = _(self.meta.get_label("link_field_results_limit"))
|
||||||
|
frappe.msgprint(
|
||||||
|
_("{0} can not be more than {1}").format(label, 50), alert=True, indicator="yellow"
|
||||||
|
)
|
||||||
|
|
||||||
def validate_user_pass_login(self):
|
def validate_user_pass_login(self):
|
||||||
if not self.disable_user_pass_login:
|
if not self.disable_user_pass_login:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -462,7 +462,7 @@
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "2",
|
||||||
"fieldname": "simultaneous_sessions",
|
"fieldname": "simultaneous_sessions",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Simultaneous Sessions"
|
"label": "Simultaneous Sessions"
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ frappe.listview_settings["User Permission"] = {
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message === 1) {
|
if (r.message === 1) {
|
||||||
frappe.show_alert({
|
frappe.show_alert({
|
||||||
message: __("User Permissions created sucessfully"),
|
message: __("User Permissions created successfully"),
|
||||||
indicator: "blue",
|
indicator: "blue",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import frappe
|
||||||
|
|
||||||
def get_parent_doc(doc):
|
def get_parent_doc(doc):
|
||||||
"""Return document of `reference_doctype`, `reference_doctype`."""
|
"""Return document of `reference_doctype`, `reference_doctype`."""
|
||||||
if not hasattr(doc, "parent_doc"):
|
if not getattr(doc, "parent_doc", None):
|
||||||
if doc.reference_doctype and doc.reference_name:
|
if doc.reference_doctype and doc.reference_name:
|
||||||
doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
|
doc.parent_doc = frappe.get_doc(doc.reference_doctype, doc.reference_name)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -13,21 +13,71 @@
|
||||||
"label": "Build",
|
"label": "Build",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
|
"description": "Customize properties, naming, fields and more for standard doctypes",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Models",
|
"label": "Customization",
|
||||||
"link_count": 0,
|
"link_count": 4,
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
|
||||||
"type": "Card Break"
|
"type": "Card Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "DocType",
|
"label": "Customize Form",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "DocType",
|
"link_to": "Customize Form",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Custom Field",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Custom Field",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Custom Translation",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Translation",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Navbar Settings",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Navbar Settings",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Group your custom doctypes under modules",
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Modules",
|
||||||
|
"link_count": 2,
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Card Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Module Def",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Module Def",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
"only_for": "",
|
||||||
|
|
@ -36,22 +86,112 @@
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Workflow",
|
"label": "Module Onboarding",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "Workflow",
|
"link_to": "Module Onboarding",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
"only_for": "",
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Monitor logs for errors, background jobs, communications, and user activity",
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "System Logs",
|
||||||
|
"link_count": 5,
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Card Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Background Jobs",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "RQ Job",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Scheduled Jobs Logs",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Scheduled Job Log",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Error Logs",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Error Log",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Communication Logs",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Communication",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Activity Log",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Activity Log",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Packages are lightweight apps (collection of Module Defs) that can be created, imported, or released right from the UI",
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Packages",
|
||||||
|
"link_count": 2,
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Card Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Package",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Package",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": 0,
|
||||||
|
"is_query_report": 0,
|
||||||
|
"label": "Package Import",
|
||||||
|
"link_count": 0,
|
||||||
|
"link_to": "Package Import",
|
||||||
|
"link_type": "DocType",
|
||||||
|
"onboard": 0,
|
||||||
|
"type": "Link"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Automate processes and extend standard functionality using scripts and background jobs",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Scripting",
|
"label": "Scripting",
|
||||||
"link_count": 0,
|
"link_count": 3,
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
|
||||||
"type": "Card Break"
|
"type": "Card Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -88,38 +228,12 @@
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"description": "Build your own reports, print formats, and dashboards. Create personalized workspaces for easier navigation",
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Packages",
|
|
||||||
"link_count": 2,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Package",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Package",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Package Import",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Package Import",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Views",
|
"label": "Views",
|
||||||
"link_count": 5,
|
"link_count": 5,
|
||||||
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Card Break"
|
"type": "Card Break"
|
||||||
},
|
},
|
||||||
|
|
@ -177,115 +291,10 @@
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"description": "Create new forms and views with doctypes. Set up multi-level workflows for approval",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Customization",
|
"label": "Models",
|
||||||
"link_count": 4,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Customize Form",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Customize Form",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Custom Field",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Custom Field",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Custom Translation",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Translation",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Navbar Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Navbar Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "System Logs",
|
|
||||||
"link_count": 5,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Background Jobs",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "RQ Job",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Scheduled Jobs Logs",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Scheduled Job Log",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Error Logs",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Error Log",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Communication Logs",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Communication",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Activity Log",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Activity Log",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Modules",
|
|
||||||
"link_count": 2,
|
"link_count": 2,
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
|
|
@ -294,9 +303,9 @@
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Module Def",
|
"label": "DocType",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "Module Def",
|
"link_to": "DocType",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
"only_for": "",
|
||||||
|
|
@ -305,16 +314,16 @@
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Module Onboarding",
|
"label": "Workflow",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "Module Onboarding",
|
"link_to": "Workflow",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"only_for": "",
|
"only_for": "",
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-01-02 15:38:42.806824",
|
"modified": "2024-01-23 17:27:44.769958",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Core",
|
"module": "Core",
|
||||||
"name": "Build",
|
"name": "Build",
|
||||||
|
|
@ -325,7 +334,7 @@
|
||||||
"quick_lists": [],
|
"quick_lists": [],
|
||||||
"restrict_to_domain": "",
|
"restrict_to_domain": "",
|
||||||
"roles": [],
|
"roles": [],
|
||||||
"sequence_id": 16.0,
|
"sequence_id": 27.0,
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
{
|
{
|
||||||
"color": "Grey",
|
"color": "Grey",
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ frappe.ui.form.on("Custom Field", {
|
||||||
label: __("Fieldname"),
|
label: __("Fieldname"),
|
||||||
fieldname: "fieldname",
|
fieldname: "fieldname",
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
|
default: frm.doc.fieldname,
|
||||||
},
|
},
|
||||||
function (data) {
|
function (data) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
|
|
|
||||||
|
|
@ -36,34 +36,32 @@ def bootstrap_database(db_name, verbose=None, source_sql=None):
|
||||||
return frappe.database.mariadb.setup_db.bootstrap_database(db_name, verbose, source_sql)
|
return frappe.database.mariadb.setup_db.bootstrap_database(db_name, verbose, source_sql)
|
||||||
|
|
||||||
|
|
||||||
def drop_user_and_database(db_name, root_login=None, root_password=None):
|
def drop_user_and_database(db_name, db_user):
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
if frappe.conf.db_type == "postgres":
|
if frappe.conf.db_type == "postgres":
|
||||||
import frappe.database.postgres.setup_db
|
import frappe.database.postgres.setup_db
|
||||||
|
|
||||||
return frappe.database.postgres.setup_db.drop_user_and_database(
|
return frappe.database.postgres.setup_db.drop_user_and_database(db_name, db_user)
|
||||||
db_name, root_login, root_password
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
import frappe.database.mariadb.setup_db
|
import frappe.database.mariadb.setup_db
|
||||||
|
|
||||||
return frappe.database.mariadb.setup_db.drop_user_and_database(
|
return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, db_user)
|
||||||
db_name, root_login, root_password
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_db(host=None, user=None, password=None, port=None):
|
def get_db(host=None, user=None, password=None, port=None, cur_db_name=None):
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
if frappe.conf.db_type == "postgres":
|
if frappe.conf.db_type == "postgres":
|
||||||
import frappe.database.postgres.database
|
import frappe.database.postgres.database
|
||||||
|
|
||||||
return frappe.database.postgres.database.PostgresDatabase(host, user, password, port=port)
|
return frappe.database.postgres.database.PostgresDatabase(
|
||||||
|
host, user, password, port, cur_db_name
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
import frappe.database.mariadb.database
|
import frappe.database.mariadb.database
|
||||||
|
|
||||||
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port=port)
|
return frappe.database.mariadb.database.MariaDBDatabase(host, user, password, port, cur_db_name)
|
||||||
|
|
||||||
|
|
||||||
def get_command(
|
def get_command(
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import traceback
|
||||||
from collections.abc import Iterable, Sequence
|
from collections.abc import Iterable, Sequence
|
||||||
from contextlib import contextmanager, suppress
|
from contextlib import contextmanager, suppress
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any
|
from typing import TYPE_CHECKING, Any, Union
|
||||||
|
|
||||||
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
|
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
|
||||||
|
|
||||||
|
|
@ -34,11 +34,19 @@ from frappe.utils import cast as cast_fieldtype
|
||||||
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
|
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
|
||||||
from frappe.utils.deprecations import deprecation_warning
|
from frappe.utils.deprecations import deprecation_warning
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from psycopg2 import connection as PostgresConnection
|
||||||
|
from psycopg2 import cursor as PostgresCursor
|
||||||
|
from pymysql.connections import Connection as MariadbConnection
|
||||||
|
from pymysql.cursors import Cursor as MariadbCursor
|
||||||
|
|
||||||
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
|
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
|
||||||
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
|
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
|
||||||
SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1')
|
SINGLE_WORD_PATTERN = re.compile(r'([`"]?)(tab([A-Z]\w+))\1')
|
||||||
MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1')
|
MULTI_WORD_PATTERN = re.compile(r'([`"])(tab([A-Z]\w+)( [A-Z]\w+)+)\1')
|
||||||
|
|
||||||
|
SQL_ITERATOR_BATCH_SIZE = 100
|
||||||
|
|
||||||
|
|
||||||
class Database:
|
class Database:
|
||||||
"""
|
"""
|
||||||
|
|
@ -65,27 +73,20 @@ class Database:
|
||||||
host=None,
|
host=None,
|
||||||
user=None,
|
user=None,
|
||||||
password=None,
|
password=None,
|
||||||
ac_name=None,
|
|
||||||
use_default=0,
|
|
||||||
port=None,
|
port=None,
|
||||||
|
cur_db_name=None,
|
||||||
):
|
):
|
||||||
self.setup_type_map()
|
self.setup_type_map()
|
||||||
self.host = host or frappe.conf.db_host
|
self.host = host
|
||||||
self.port = port or frappe.conf.db_port
|
self.port = port
|
||||||
self.user = user or frappe.conf.db_name
|
self.user = user
|
||||||
self.db_name = frappe.conf.db_name
|
self.password = password
|
||||||
|
self.cur_db_name = cur_db_name
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
if ac_name:
|
|
||||||
self.user = ac_name or frappe.conf.db_name
|
|
||||||
|
|
||||||
if use_default:
|
|
||||||
self.user = frappe.conf.db_name
|
|
||||||
|
|
||||||
self.transaction_writes = 0
|
self.transaction_writes = 0
|
||||||
self.auto_commit_on_many_writes = 0
|
self.auto_commit_on_many_writes = 0
|
||||||
|
|
||||||
self.password = password or frappe.conf.db_password
|
|
||||||
self.value_cache = {}
|
self.value_cache = {}
|
||||||
self.logger = frappe.logger("database")
|
self.logger = frappe.logger("database")
|
||||||
self.logger.setLevel("WARNING")
|
self.logger.setLevel("WARNING")
|
||||||
|
|
@ -95,17 +96,16 @@ class Database:
|
||||||
self.before_rollback = CallbackManager()
|
self.before_rollback = CallbackManager()
|
||||||
self.after_rollback = CallbackManager()
|
self.after_rollback = CallbackManager()
|
||||||
|
|
||||||
# self.db_type: str
|
# self.db_type: str
|
||||||
# self.last_query (lazy) attribute of last sql query executed
|
# self.last_query (lazy) attribute of last sql query executed
|
||||||
|
|
||||||
def setup_type_map(self):
|
def setup_type_map(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connects to a database as set in `site_config.json`."""
|
"""Connects to a database as set in `site_config.json`."""
|
||||||
self.cur_db_name = self.user
|
self._conn: Union["MariadbConnection", "PostgresConnection"] = self.get_connection()
|
||||||
self._conn = self.get_connection()
|
self._cursor: Union["MariadbCursor", "PostgresCursor"] = self._conn.cursor()
|
||||||
self._cursor = self._conn.cursor()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if execution_timeout := get_query_execution_timeout():
|
if execution_timeout := get_query_execution_timeout():
|
||||||
|
|
@ -121,6 +121,7 @@ class Database:
|
||||||
def use(self, db_name):
|
def use(self, db_name):
|
||||||
"""`USE` db_name."""
|
"""`USE` db_name."""
|
||||||
self._conn.select_db(db_name)
|
self._conn.select_db(db_name)
|
||||||
|
self.cur_db_name = db_name
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
"""Return a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects."""
|
"""Return a Database connection object that conforms with https://peps.python.org/pep-0249/#connection-objects."""
|
||||||
|
|
@ -135,6 +136,9 @@ class Database:
|
||||||
def _transform_result(self, result: list[tuple]) -> list[tuple]:
|
def _transform_result(self, result: list[tuple]) -> list[tuple]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _clean_up(self):
|
||||||
|
pass
|
||||||
|
|
||||||
def sql(
|
def sql(
|
||||||
self,
|
self,
|
||||||
query: Query,
|
query: Query,
|
||||||
|
|
@ -149,6 +153,7 @@ class Database:
|
||||||
explain=False,
|
explain=False,
|
||||||
run=True,
|
run=True,
|
||||||
pluck=False,
|
pluck=False,
|
||||||
|
as_iterator=False,
|
||||||
):
|
):
|
||||||
"""Execute a SQL query and fetch all rows.
|
"""Execute a SQL query and fetch all rows.
|
||||||
|
|
||||||
|
|
@ -163,6 +168,9 @@ class Database:
|
||||||
:param run: Return query without executing it if False.
|
:param run: Return query without executing it if False.
|
||||||
:param pluck: Get the plucked field only.
|
:param pluck: Get the plucked field only.
|
||||||
:param explain: Print `EXPLAIN` in error log.
|
:param explain: Print `EXPLAIN` in error log.
|
||||||
|
:param as_iterator: Returns iterator over results instead of fetching all results at once.
|
||||||
|
This should be used with unbuffered cursor as default cursors used by pymysql and postgres
|
||||||
|
buffer the results internally. See `Database.unbuffered_cursor`.
|
||||||
Examples:
|
Examples:
|
||||||
|
|
||||||
# return customer names as dicts
|
# return customer names as dicts
|
||||||
|
|
@ -264,21 +272,49 @@ class Database:
|
||||||
if not self._cursor.description:
|
if not self._cursor.description:
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
self.last_result = self._transform_result(self._cursor.fetchall())
|
if as_iterator:
|
||||||
|
return self._return_as_iterator(pluck=pluck, as_dict=as_dict, as_list=as_list, update=update)
|
||||||
|
|
||||||
|
last_result = self._transform_result(self._cursor.fetchall())
|
||||||
if pluck:
|
if pluck:
|
||||||
return [r[0] for r in self.last_result]
|
last_result = [r[0] for r in last_result]
|
||||||
|
self._clean_up()
|
||||||
|
return last_result
|
||||||
|
|
||||||
# scrub output if required
|
# scrub output if required
|
||||||
if as_dict:
|
if as_dict:
|
||||||
ret = self.fetch_as_dict()
|
last_result = self.fetch_as_dict(last_result)
|
||||||
if update:
|
if update:
|
||||||
for r in ret:
|
for r in last_result:
|
||||||
r.update(update)
|
r.update(update)
|
||||||
return ret
|
|
||||||
elif as_list:
|
elif as_list:
|
||||||
return self.convert_to_lists(self.last_result)
|
last_result = self.convert_to_lists(last_result)
|
||||||
return self.last_result
|
|
||||||
|
self._clean_up()
|
||||||
|
return last_result
|
||||||
|
|
||||||
|
def _return_as_iterator(self, *, pluck, as_dict, as_list, update):
|
||||||
|
while result := self._transform_result(self._cursor.fetchmany(SQL_ITERATOR_BATCH_SIZE)):
|
||||||
|
if pluck:
|
||||||
|
for row in result:
|
||||||
|
yield row[0]
|
||||||
|
|
||||||
|
elif as_dict:
|
||||||
|
keys = [column[0] for column in self._cursor.description]
|
||||||
|
for row in result:
|
||||||
|
row = frappe._dict(zip(keys, row))
|
||||||
|
if update:
|
||||||
|
row.update(update)
|
||||||
|
yield row
|
||||||
|
|
||||||
|
elif as_list:
|
||||||
|
for row in result:
|
||||||
|
yield list(row)
|
||||||
|
else:
|
||||||
|
frappe.throw(_("`as_iterator` only works with `as_list=True` or `as_dict=True`"))
|
||||||
|
|
||||||
|
self._clean_up()
|
||||||
|
|
||||||
def _log_query(
|
def _log_query(
|
||||||
self,
|
self,
|
||||||
|
|
@ -396,9 +432,8 @@ class Database:
|
||||||
):
|
):
|
||||||
raise ImplicitCommitError("This statement can cause implicit commit", query)
|
raise ImplicitCommitError("This statement can cause implicit commit", query)
|
||||||
|
|
||||||
def fetch_as_dict(self) -> list[frappe._dict]:
|
def fetch_as_dict(self, result) -> list[frappe._dict]:
|
||||||
"""Internal. Convert results to dict."""
|
"""Internal. Convert results to dict."""
|
||||||
result = self.last_result
|
|
||||||
if result:
|
if result:
|
||||||
keys = [column[0] for column in self._cursor.description]
|
keys = [column[0] for column in self._cursor.description]
|
||||||
|
|
||||||
|
|
@ -741,7 +776,7 @@ class Database:
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
# Update the `deny_multiple_sessions` field in System Settings DocType.
|
# Update the `deny_multiple_sessions` field in System Settings DocType.
|
||||||
company = frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
|
frappe.db.set_single_value("System Settings", "deny_multiple_sessions", True)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
to_update = self._get_update_dict(
|
to_update = self._get_update_dict(
|
||||||
|
|
@ -1304,6 +1339,22 @@ class Database:
|
||||||
def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
|
def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def unbuffered_cursor(self):
|
||||||
|
"""Context manager to temporarily use unbuffered cursor.
|
||||||
|
|
||||||
|
Using this with `as_iterator=True` provides O(1) memory usage while reading large result sets.
|
||||||
|
|
||||||
|
NOTE: You MUST do entire result set processing in the context, otherwise underlying cursor
|
||||||
|
will be switched and you'll not get complete results.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
with frappe.db.unbuffered_cursor():
|
||||||
|
for row in frappe.db.sql("query with huge result", as_iterator=True):
|
||||||
|
continue # Do some processing.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def savepoint(catch: type | tuple[type, ...] = Exception):
|
def savepoint(catch: type | tuple[type, ...] = Exception):
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class DbManager:
|
||||||
def create_user(self, user, password, host=None):
|
def create_user(self, user, password, host=None):
|
||||||
host = host or self.get_current_host()
|
host = host or self.get_current_host()
|
||||||
password_predicate = f" IDENTIFIED BY '{password}'" if password else ""
|
password_predicate = f" IDENTIFIED BY '{password}'" if password else ""
|
||||||
self.db.sql(f"CREATE USER '{user}'@'{host}'{password_predicate}")
|
self.db.sql(f"CREATE USER IF NOT EXISTS '{user}'@'{host}'{password_predicate}")
|
||||||
|
|
||||||
def delete_user(self, target, host=None):
|
def delete_user(self, target, host=None):
|
||||||
host = host or self.get_current_host()
|
host = host or self.get_current_host()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import re
|
import re
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
import pymysql
|
import pymysql
|
||||||
from pymysql.constants import ER, FIELD_TYPE
|
from pymysql.constants import ER, FIELD_TYPE
|
||||||
|
|
@ -123,8 +124,8 @@ class MariaDBConnectionUtil:
|
||||||
"use_unicode": True,
|
"use_unicode": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.user not in (frappe.flags.root_login, "root"):
|
if self.cur_db_name:
|
||||||
conn_settings["database"] = self.user
|
conn_settings["database"] = self.cur_db_name
|
||||||
|
|
||||||
if self.port:
|
if self.port:
|
||||||
conn_settings["port"] = int(self.port)
|
conn_settings["port"] = int(self.port)
|
||||||
|
|
@ -198,7 +199,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
||||||
SUM(`data_length` + `index_length`) / 1024 / 1024 AS `database_size`
|
SUM(`data_length` + `index_length`) / 1024 / 1024 AS `database_size`
|
||||||
FROM information_schema.tables WHERE `table_schema` = %s GROUP BY `table_schema`
|
FROM information_schema.tables WHERE `table_schema` = %s GROUP BY `table_schema`
|
||||||
""",
|
""",
|
||||||
self.db_name,
|
self.cur_db_name,
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -209,6 +210,13 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
||||||
self._log_query(self.last_query, debug, explain, query)
|
self._log_query(self.last_query, debug, explain, query)
|
||||||
return self.last_query
|
return self.last_query
|
||||||
|
|
||||||
|
def _clean_up(self):
|
||||||
|
# PERF: Erase internal references of pymysql to trigger GC as soon as
|
||||||
|
# results are consumed.
|
||||||
|
self._cursor._result = None
|
||||||
|
self._cursor._rows = None
|
||||||
|
self._cursor.connection._result = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def escape(s, percent=True):
|
def escape(s, percent=True):
|
||||||
"""Excape quotes and percent in given string."""
|
"""Excape quotes and percent in given string."""
|
||||||
|
|
@ -518,3 +526,15 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
|
||||||
|
|
||||||
if est_row_size:
|
if est_row_size:
|
||||||
return int(est_row_size[0][0])
|
return int(est_row_size[0][0])
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def unbuffered_cursor(self):
|
||||||
|
from pymysql.cursors import SSCursor
|
||||||
|
|
||||||
|
try:
|
||||||
|
original_cursor = self._cursor
|
||||||
|
new_cursor = self._cursor = self._conn.cursor(SSCursor)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
self._cursor = original_cursor
|
||||||
|
new_cursor.close()
|
||||||
|
|
|
||||||
|
|
@ -26,42 +26,45 @@ def get_mariadb_version(version_string: str = ""):
|
||||||
def setup_database(force, verbose, no_mariadb_socket=False):
|
def setup_database(force, verbose, no_mariadb_socket=False):
|
||||||
frappe.local.session = frappe._dict({"user": "Administrator"})
|
frappe.local.session = frappe._dict({"user": "Administrator"})
|
||||||
|
|
||||||
|
db_user = frappe.conf.db_user
|
||||||
db_name = frappe.local.conf.db_name
|
db_name = frappe.local.conf.db_name
|
||||||
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
|
root_conn = get_root_connection()
|
||||||
dbman = DbManager(root_conn)
|
dbman = DbManager(root_conn)
|
||||||
dbman_kwargs = {}
|
dbman_kwargs = {}
|
||||||
if no_mariadb_socket:
|
if no_mariadb_socket:
|
||||||
dbman_kwargs["host"] = "%"
|
dbman_kwargs["host"] = "%"
|
||||||
|
|
||||||
|
dbman.create_user(db_user, frappe.conf.db_password, **dbman_kwargs)
|
||||||
|
if verbose:
|
||||||
|
print(f"Created or updated user {db_user}")
|
||||||
|
|
||||||
if force or (db_name not in dbman.get_database_list()):
|
if force or (db_name not in dbman.get_database_list()):
|
||||||
dbman.delete_user(db_name, **dbman_kwargs)
|
|
||||||
dbman.drop_database(db_name)
|
dbman.drop_database(db_name)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Database {db_name} already exists")
|
raise Exception(f"Database {db_name} already exists")
|
||||||
|
|
||||||
dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs)
|
|
||||||
if verbose:
|
|
||||||
print("Created user %s" % db_name)
|
|
||||||
|
|
||||||
dbman.create_database(db_name)
|
dbman.create_database(db_name)
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Created database %s" % db_name)
|
print("Created database %s" % db_name)
|
||||||
|
|
||||||
dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs)
|
dbman.grant_all_privileges(db_name, db_user, **dbman_kwargs)
|
||||||
dbman.flush_privileges()
|
dbman.flush_privileges()
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"Granted privileges to user {db_name} and database {db_name}")
|
print(f"Granted privileges to user {db_user} and database {db_name}")
|
||||||
|
|
||||||
# close root connection
|
# close root connection
|
||||||
root_conn.close()
|
root_conn.close()
|
||||||
|
|
||||||
|
|
||||||
def drop_user_and_database(db_name, root_login, root_password):
|
def drop_user_and_database(
|
||||||
frappe.local.db = get_root_connection(root_login, root_password)
|
db_name,
|
||||||
|
db_user,
|
||||||
|
):
|
||||||
|
frappe.local.db = get_root_connection()
|
||||||
dbman = DbManager(frappe.local.db)
|
dbman = DbManager(frappe.local.db)
|
||||||
dbman.drop_database(db_name)
|
dbman.drop_database(db_name)
|
||||||
dbman.delete_user(db_name, host="%")
|
dbman.delete_user(db_user, host="%")
|
||||||
dbman.delete_user(db_name)
|
dbman.delete_user(db_user)
|
||||||
|
|
||||||
|
|
||||||
def bootstrap_database(db_name, verbose, source_sql=None):
|
def bootstrap_database(db_name, verbose, source_sql=None):
|
||||||
|
|
@ -96,14 +99,13 @@ def import_db_from_sql(source_sql=None, verbose=False):
|
||||||
if not source_sql:
|
if not source_sql:
|
||||||
source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql")
|
source_sql = os.path.join(os.path.dirname(__file__), "framework_mariadb.sql")
|
||||||
DbManager(frappe.local.db).restore_database(
|
DbManager(frappe.local.db).restore_database(
|
||||||
verbose, db_name, source_sql, db_name, frappe.conf.db_password
|
verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password
|
||||||
)
|
)
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Imported from database %s" % source_sql)
|
print("Imported from database %s" % source_sql)
|
||||||
|
|
||||||
|
|
||||||
def check_database_settings():
|
def check_database_settings():
|
||||||
|
|
||||||
check_compatible_versions()
|
check_compatible_versions()
|
||||||
|
|
||||||
# Check each expected value vs. actuals:
|
# Check each expected value vs. actuals:
|
||||||
|
|
@ -152,24 +154,26 @@ def check_compatible_versions():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_root_connection(root_login, root_password):
|
def get_root_connection():
|
||||||
import getpass
|
|
||||||
|
|
||||||
if not frappe.local.flags.root_connection:
|
if not frappe.local.flags.root_connection:
|
||||||
if not root_login:
|
from getpass import getpass
|
||||||
root_login = "root"
|
|
||||||
|
|
||||||
if not root_password:
|
if not frappe.flags.root_login:
|
||||||
root_password = frappe.conf.get("root_password") or None
|
frappe.flags.root_login = (
|
||||||
|
frappe.conf.get("root_login") or input("Enter mysql super user [root]: ") or "root"
|
||||||
|
)
|
||||||
|
|
||||||
if not root_password:
|
if not frappe.flags.root_password:
|
||||||
root_password = getpass.getpass("MySQL root password: ")
|
frappe.flags.root_password = frappe.conf.get("root_password") or getpass(
|
||||||
|
"MySQL root password: "
|
||||||
|
)
|
||||||
|
|
||||||
frappe.local.flags.root_connection = frappe.database.get_db(
|
frappe.local.flags.root_connection = frappe.database.get_db(
|
||||||
host=frappe.conf.db_host,
|
host=frappe.conf.db_host,
|
||||||
port=frappe.conf.db_port,
|
port=frappe.conf.db_port,
|
||||||
user=root_login,
|
user=frappe.flags.root_login,
|
||||||
password=root_password,
|
password=frappe.flags.root_password,
|
||||||
|
cur_db_name=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return frappe.local.flags.root_connection
|
return frappe.local.flags.root_connection
|
||||||
|
|
|
||||||
|
|
@ -161,8 +161,8 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
||||||
|
|
||||||
def get_connection(self):
|
def get_connection(self):
|
||||||
conn_settings = {
|
conn_settings = {
|
||||||
|
"dbname": self.cur_db_name,
|
||||||
"user": self.user,
|
"user": self.user,
|
||||||
"dbname": self.user,
|
|
||||||
"host": self.host,
|
"host": self.host,
|
||||||
"password": self.password,
|
"password": self.password,
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +199,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
||||||
def get_database_size(self):
|
def get_database_size(self):
|
||||||
"""Return database size in MB"""
|
"""Return database size in MB"""
|
||||||
db_size = self.sql(
|
db_size = self.sql(
|
||||||
"SELECT (pg_database_size(%s) / 1024 / 1024) as database_size", self.db_name, as_dict=True
|
"SELECT (pg_database_size(%s) / 1024 / 1024) as database_size", self.cur_db_name, as_dict=True
|
||||||
)
|
)
|
||||||
return db_size[0].get("database_size")
|
return db_size[0].get("database_size")
|
||||||
|
|
||||||
|
|
@ -219,7 +219,7 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
|
||||||
where table_catalog='{}'
|
where table_catalog='{}'
|
||||||
and table_type = 'BASE TABLE'
|
and table_type = 'BASE TABLE'
|
||||||
and table_schema='{}'""".format(
|
and table_schema='{}'""".format(
|
||||||
frappe.conf.db_name, frappe.conf.get("db_schema", "public")
|
self.cur_db_name, frappe.conf.get("db_schema", "public")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,25 @@ from frappe.utils import cint
|
||||||
|
|
||||||
|
|
||||||
def setup_database():
|
def setup_database():
|
||||||
root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password)
|
root_conn = get_root_connection()
|
||||||
root_conn.commit()
|
root_conn.commit()
|
||||||
root_conn.sql("end")
|
root_conn.sql("end")
|
||||||
root_conn.sql(f"DROP DATABASE IF EXISTS `{frappe.conf.db_name}`")
|
root_conn.sql(f'DROP DATABASE IF EXISTS "{frappe.conf.db_name}"')
|
||||||
root_conn.sql(f"DROP USER IF EXISTS {frappe.conf.db_name}")
|
|
||||||
root_conn.sql(f"CREATE DATABASE `{frappe.conf.db_name}`")
|
# If user exists, just update password
|
||||||
root_conn.sql(f"CREATE user {frappe.conf.db_name} password '{frappe.conf.db_password}'")
|
if root_conn.sql(f"SELECT 1 FROM pg_roles WHERE rolname='{frappe.conf.db_user}'"):
|
||||||
root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name))
|
root_conn.sql(f"ALTER USER \"{frappe.conf.db_user}\" WITH PASSWORD '{frappe.conf.db_password}'")
|
||||||
|
else:
|
||||||
|
root_conn.sql(f"CREATE USER \"{frappe.conf.db_user}\" WITH PASSWORD '{frappe.conf.db_password}'")
|
||||||
|
root_conn.sql(f'CREATE DATABASE "{frappe.conf.db_name}"')
|
||||||
|
root_conn.sql(
|
||||||
|
f'GRANT ALL PRIVILEGES ON DATABASE "{frappe.conf.db_name}" TO "{frappe.conf.db_user}"'
|
||||||
|
)
|
||||||
if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True):
|
if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True):
|
||||||
version_string = psql_version[0].get("version") or "PostgreSQL 14"
|
version_string = psql_version[0].get("version") or "PostgreSQL 14"
|
||||||
major_version = cint(re.split(r"[\w\.]", version_string)[1])
|
major_version = cint(re.split(r"[\w\.]", version_string)[1])
|
||||||
if major_version > 15:
|
if major_version > 15:
|
||||||
root_conn.sql("ALTER DATABASE `{0}` OWNER TO {0}".format(frappe.conf.db_name))
|
root_conn.sql(f'ALTER DATABASE "{frappe.conf.db_name}" OWNER TO "{frappe.conf.db_user}"')
|
||||||
root_conn.close()
|
root_conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,42 +55,39 @@ def import_db_from_sql(source_sql=None, verbose=False):
|
||||||
if not source_sql:
|
if not source_sql:
|
||||||
source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql")
|
source_sql = os.path.join(os.path.dirname(__file__), "framework_postgres.sql")
|
||||||
DbManager(frappe.local.db).restore_database(
|
DbManager(frappe.local.db).restore_database(
|
||||||
verbose, db_name, source_sql, db_name, frappe.conf.db_password
|
verbose, db_name, source_sql, frappe.conf.db_user, frappe.conf.db_password
|
||||||
)
|
)
|
||||||
if verbose:
|
if verbose:
|
||||||
print("Imported from database %s" % source_sql)
|
print("Imported from database %s" % source_sql)
|
||||||
|
|
||||||
|
|
||||||
def get_root_connection(root_login=None, root_password=None):
|
def get_root_connection():
|
||||||
if not frappe.local.flags.root_connection:
|
if not frappe.local.flags.root_connection:
|
||||||
if not root_login:
|
from getpass import getpass
|
||||||
root_login = frappe.conf.get("root_login") or None
|
|
||||||
|
|
||||||
if not root_login:
|
if not frappe.flags.root_login:
|
||||||
root_login = input("Enter postgres super user: ")
|
frappe.flags.root_login = (
|
||||||
|
frappe.conf.get("root_login") or input("Enter postgres super user [postgres]: ") or "postgres"
|
||||||
|
)
|
||||||
|
|
||||||
if not root_password:
|
if not frappe.flags.root_password:
|
||||||
root_password = frappe.conf.get("root_password") or None
|
frappe.flags.root_password = frappe.conf.get("root_password") or getpass(
|
||||||
|
"Postgres super user password: "
|
||||||
if not root_password:
|
)
|
||||||
from getpass import getpass
|
|
||||||
|
|
||||||
root_password = getpass("Postgres super user password: ")
|
|
||||||
|
|
||||||
frappe.local.flags.root_connection = frappe.database.get_db(
|
frappe.local.flags.root_connection = frappe.database.get_db(
|
||||||
host=frappe.conf.db_host,
|
host=frappe.conf.db_host,
|
||||||
port=frappe.conf.db_port,
|
port=frappe.conf.db_port,
|
||||||
user=root_login,
|
user=frappe.flags.root_login,
|
||||||
password=root_password,
|
password=frappe.flags.root_password,
|
||||||
|
cur_db_name=frappe.flags.root_login,
|
||||||
)
|
)
|
||||||
|
|
||||||
return frappe.local.flags.root_connection
|
return frappe.local.flags.root_connection
|
||||||
|
|
||||||
|
|
||||||
def drop_user_and_database(db_name, root_login, root_password):
|
def drop_user_and_database(db_name, db_user):
|
||||||
root_conn = get_root_connection(
|
root_conn = get_root_connection()
|
||||||
frappe.flags.root_login or root_login, frappe.flags.root_password or root_password
|
|
||||||
)
|
|
||||||
root_conn.commit()
|
root_conn.commit()
|
||||||
root_conn.sql(
|
root_conn.sql(
|
||||||
"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s",
|
"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s",
|
||||||
|
|
@ -92,4 +95,4 @@ def drop_user_and_database(db_name, root_login, root_password):
|
||||||
)
|
)
|
||||||
root_conn.sql("end")
|
root_conn.sql("end")
|
||||||
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")
|
root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}")
|
||||||
root_conn.sql(f"DROP USER IF EXISTS {db_name}")
|
root_conn.sql(f"DROP USER IF EXISTS {db_user}")
|
||||||
|
|
|
||||||
|
|
@ -271,7 +271,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True):
|
||||||
frappe.db.sql("update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1")
|
frappe.db.sql("update `tabDesktop Icon` set blocked=0, hidden=1 where standard=1")
|
||||||
|
|
||||||
# set as visible if present, or add icon
|
# set as visible if present, or add icon
|
||||||
for module_name in visible_list:
|
for module_name in list(visible_list):
|
||||||
name = frappe.db.get_value("Desktop Icon", {"module_name": module_name})
|
name = frappe.db.get_value("Desktop Icon", {"module_name": module_name})
|
||||||
if name:
|
if name:
|
||||||
frappe.db.set_value("Desktop Icon", name, "hidden", 0)
|
frappe.db.set_value("Desktop Icon", name, "hidden", 0)
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,13 @@ frappe.ui.form.on("Event", {
|
||||||
|
|
||||||
const [ends_on_date] = frm.doc.ends_on
|
const [ends_on_date] = frm.doc.ends_on
|
||||||
? frm.doc.ends_on.split(" ")
|
? frm.doc.ends_on.split(" ")
|
||||||
: frm.doc.starts_on.split(" ");
|
: frm.doc.starts_on?.split(" ") || [];
|
||||||
|
|
||||||
if (frm.doc.google_meet_link && frappe.datetime.now_date() <= ends_on_date) {
|
if (
|
||||||
|
ends_on_date &&
|
||||||
|
frm.doc.google_meet_link &&
|
||||||
|
frappe.datetime.now_date() <= ends_on_date
|
||||||
|
) {
|
||||||
frm.dashboard.set_headline(
|
frm.dashboard.set_headline(
|
||||||
__("Join video conference with {0}", [
|
__("Join video conference with {0}", [
|
||||||
`<a target='_blank' href='${frm.doc.google_meet_link}'>Google Meet</a>`,
|
`<a target='_blank' href='${frm.doc.google_meet_link}'>Google Meet</a>`,
|
||||||
|
|
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_global_search": 1,
|
"in_global_search": 1,
|
||||||
"label": "Repeat On",
|
"label": "Repeat On",
|
||||||
"options": "\nDaily\nWeekly\nMonthly\nYearly"
|
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf Yearly\nYearly"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "repeat_this_event",
|
"depends_on": "repeat_this_event",
|
||||||
|
|
@ -295,7 +295,7 @@
|
||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-23 10:33:15.685368",
|
"modified": "2024-01-11 07:11:17.467503",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Desk",
|
"module": "Desk",
|
||||||
"name": "Event",
|
"name": "Event",
|
||||||
|
|
@ -336,4 +336,4 @@
|
||||||
"track_changes": 1,
|
"track_changes": 1,
|
||||||
"track_seen": 1,
|
"track_seen": 1,
|
||||||
"track_views": 1
|
"track_views": 1
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ from frappe.utils import (
|
||||||
format_datetime,
|
format_datetime,
|
||||||
get_datetime_str,
|
get_datetime_str,
|
||||||
getdate,
|
getdate,
|
||||||
|
month_diff,
|
||||||
now_datetime,
|
now_datetime,
|
||||||
nowdate,
|
nowdate,
|
||||||
)
|
)
|
||||||
|
|
@ -62,7 +63,7 @@ class Event(Document):
|
||||||
google_meet_link: DF.Data | None
|
google_meet_link: DF.Data | None
|
||||||
monday: DF.Check
|
monday: DF.Check
|
||||||
pulled_from_google_calendar: DF.Check
|
pulled_from_google_calendar: DF.Check
|
||||||
repeat_on: DF.Literal["", "Daily", "Weekly", "Monthly", "Yearly"]
|
repeat_on: DF.Literal["", "Daily", "Weekly", "Monthly", "Quarterly", "Half Yearly", "Yearly"]
|
||||||
repeat_this_event: DF.Check
|
repeat_this_event: DF.Check
|
||||||
repeat_till: DF.Date | None
|
repeat_till: DF.Date | None
|
||||||
saturday: DF.Check
|
saturday: DF.Check
|
||||||
|
|
@ -392,6 +393,62 @@ def get_events(start, end, user=None, for_reminder=False, filters=None) -> list[
|
||||||
|
|
||||||
remove_events.append(e)
|
remove_events.append(e)
|
||||||
|
|
||||||
|
if e.repeat_on == "Half Yearly":
|
||||||
|
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||||
|
year, month = start.split("-", maxsplit=2)[:2]
|
||||||
|
date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
|
||||||
|
|
||||||
|
# last day of month issue, start from prev month!
|
||||||
|
try:
|
||||||
|
getdate(date)
|
||||||
|
except Exception:
|
||||||
|
date = date.split("-")
|
||||||
|
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
|
||||||
|
|
||||||
|
start_from = date
|
||||||
|
for i in range(int(date_diff(end, start) / 30) + 3):
|
||||||
|
diff = month_diff(date, event_start) - 1
|
||||||
|
if diff % 6 != 0:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
getdate(date) >= getdate(start)
|
||||||
|
and getdate(date) <= getdate(end)
|
||||||
|
and getdate(date) <= getdate(repeat)
|
||||||
|
and getdate(date) >= getdate(event_start)
|
||||||
|
):
|
||||||
|
add_event(e, date)
|
||||||
|
|
||||||
|
date = add_months(start_from, i + 1)
|
||||||
|
remove_events.append(e)
|
||||||
|
|
||||||
|
if e.repeat_on == "Quarterly":
|
||||||
|
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||||
|
year, month = start.split("-", maxsplit=2)[:2]
|
||||||
|
date = f"{year}-{month}-" + event_start.split("-", maxsplit=3)[2]
|
||||||
|
|
||||||
|
# last day of month issue, start from prev month!
|
||||||
|
try:
|
||||||
|
getdate(date)
|
||||||
|
except Exception:
|
||||||
|
date = date.split("-")
|
||||||
|
date = date[0] + "-" + str(cint(date[1]) - 1) + "-" + date[2]
|
||||||
|
|
||||||
|
start_from = date
|
||||||
|
for i in range(int(date_diff(end, start) / 30) + 3):
|
||||||
|
diff = month_diff(date, event_start) - 1
|
||||||
|
if diff % 3 != 0:
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
getdate(date) >= getdate(start)
|
||||||
|
and getdate(date) <= getdate(end)
|
||||||
|
and getdate(date) <= getdate(repeat)
|
||||||
|
and getdate(date) >= getdate(event_start)
|
||||||
|
):
|
||||||
|
add_event(e, date)
|
||||||
|
|
||||||
|
date = add_months(start_from, i + 1)
|
||||||
|
remove_events.append(e)
|
||||||
|
|
||||||
if e.repeat_on == "Monthly":
|
if e.repeat_on == "Monthly":
|
||||||
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
# creates a string with date (27) and month (07) and year (2019) eg: 2019-07-27
|
||||||
year, month = start.split("-", maxsplit=2)[:2]
|
year, month = start.split("-", maxsplit=2)[:2]
|
||||||
|
|
|
||||||
|
|
@ -136,3 +136,77 @@ class TestEvent(FrappeTestCase):
|
||||||
|
|
||||||
ev_list3 = get_events("2015-02-01", "2015-02-01", "Administrator", for_reminder=True)
|
ev_list3 = get_events("2015-02-01", "2015-02-01", "Administrator", for_reminder=True)
|
||||||
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3))))
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3))))
|
||||||
|
|
||||||
|
def test_quaterly_repeat(self):
|
||||||
|
ev = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Event",
|
||||||
|
"subject": "_Test Event",
|
||||||
|
"starts_on": "2023-02-17",
|
||||||
|
"repeat_till": "2024-02-17",
|
||||||
|
"event_type": "Public",
|
||||||
|
"repeat_this_event": 1,
|
||||||
|
"repeat_on": "Quarterly",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ev.insert()
|
||||||
|
# Test Quaterly months
|
||||||
|
ev_list = get_events("2023-02-17", "2023-02-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list))))
|
||||||
|
|
||||||
|
ev_list1 = get_events("2023-05-17", "2023-05-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list1))))
|
||||||
|
|
||||||
|
ev_list2 = get_events("2023-08-17", "2023-08-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list2))))
|
||||||
|
|
||||||
|
ev_list3 = get_events("2023-11-17", "2023-11-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list3))))
|
||||||
|
|
||||||
|
# Test before event start date and after event end date
|
||||||
|
ev_list4 = get_events("2022-11-17", "2022-11-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
ev_list4 = get_events("2024-02-17", "2024-02-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
# Test months that aren't part of the quarterly cycle
|
||||||
|
ev_list4 = get_events("2023-12-17", "2023-12-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
ev_list4 = get_events("2023-03-17", "2023-03-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
def test_half_yearly_repeat(self):
|
||||||
|
ev = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Event",
|
||||||
|
"subject": "_Test Event",
|
||||||
|
"starts_on": "2023-02-17",
|
||||||
|
"repeat_till": "2024-02-17",
|
||||||
|
"event_type": "Public",
|
||||||
|
"repeat_this_event": 1,
|
||||||
|
"repeat_on": "Half Yearly",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ev.insert()
|
||||||
|
# Test Half Yearly months
|
||||||
|
ev_list = get_events("2023-02-17", "2023-02-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list))))
|
||||||
|
|
||||||
|
ev_list1 = get_events("2023-08-17", "2023-08-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertTrue(bool(list(filter(lambda e: e.name == ev.name, ev_list1))))
|
||||||
|
|
||||||
|
# Test before event start date and after event end date
|
||||||
|
ev_list4 = get_events("2022-08-17", "2022-08-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
ev_list4 = get_events("2024-02-17", "2024-02-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
# Test months that aren't part of the half yearly cycle
|
||||||
|
ev_list4 = get_events("2023-12-17", "2023-12-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
||||||
|
ev_list4 = get_events("2023-05-17", "2023-05-17", "Administrator", for_reminder=True)
|
||||||
|
self.assertFalse(bool(list(filter(lambda e: e.name == ev.name, ev_list4))))
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,7 @@ def update_column_order(board_name, order):
|
||||||
new_columns = []
|
new_columns = []
|
||||||
|
|
||||||
for col in order:
|
for col in order:
|
||||||
for column in old_columns:
|
for column in list(old_columns):
|
||||||
if col == column.column_name:
|
if col == column.column_name:
|
||||||
new_columns.append(column)
|
new_columns.append(column)
|
||||||
old_columns.remove(column)
|
old_columns.remove(column)
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,7 @@ class Workspace(Document):
|
||||||
"label": card.get("label"),
|
"label": card.get("label"),
|
||||||
"type": "Card Break",
|
"type": "Card Break",
|
||||||
"icon": card.get("icon"),
|
"icon": card.get("icon"),
|
||||||
|
"description": card.get("description"),
|
||||||
"hidden": card.get("hidden") or False,
|
"hidden": card.get("hidden") or False,
|
||||||
"link_count": card.get("link_count"),
|
"link_count": card.get("link_count"),
|
||||||
"idx": 1 if not self.links else self.links[-1].idx + 1,
|
"idx": 1 if not self.links else self.links[-1].idx + 1,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
"type",
|
"type",
|
||||||
"label",
|
"label",
|
||||||
"icon",
|
"icon",
|
||||||
|
"description",
|
||||||
"hidden",
|
"hidden",
|
||||||
"link_details_section",
|
"link_details_section",
|
||||||
"link_type",
|
"link_type",
|
||||||
|
|
@ -107,12 +108,20 @@
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Link Count"
|
"label": "Link Count"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "eval:doc.type == \"Card Break\"",
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "HTML Editor",
|
||||||
|
"ignore_xss_filter": 1,
|
||||||
|
"label": "Description",
|
||||||
|
"max_height": "7rem"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-01 11:23:28.990593",
|
"modified": "2024-01-23 17:39:16.833318",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Desk",
|
"module": "Desk",
|
||||||
"name": "Workspace Link",
|
"name": "Workspace Link",
|
||||||
|
|
@ -121,5 +130,6 @@
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ class WorkspaceLink(Document):
|
||||||
from frappe.types import DF
|
from frappe.types import DF
|
||||||
|
|
||||||
dependencies: DF.Data | None
|
dependencies: DF.Data | None
|
||||||
|
description: DF.HTMLEditor | None
|
||||||
hidden: DF.Check
|
hidden: DF.Check
|
||||||
icon: DF.Data | None
|
icon: DF.Data | None
|
||||||
is_query_report: DF.Check
|
is_query_report: DF.Check
|
||||||
|
|
@ -29,4 +30,5 @@ class WorkspaceLink(Document):
|
||||||
parenttype: DF.Data
|
parenttype: DF.Data
|
||||||
type: DF.Literal["Link", "Card Break"]
|
type: DF.Literal["Link", "Card Break"]
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -476,18 +476,15 @@ frappe.setup.slides_settings = [
|
||||||
|
|
||||||
onload: function (slide) {
|
onload: function (slide) {
|
||||||
if (frappe.session.user !== "Administrator") {
|
if (frappe.session.user !== "Administrator") {
|
||||||
slide.form.fields_dict.email.$wrapper.toggle(false);
|
const { first_name, last_name, email } = frappe.boot.user;
|
||||||
slide.form.fields_dict.password.$wrapper.toggle(false);
|
if (first_name || last_name) {
|
||||||
|
|
||||||
// remove password field
|
|
||||||
delete slide.form.fields_dict.password;
|
|
||||||
|
|
||||||
if (frappe.boot.user.first_name || frappe.boot.user.last_name) {
|
|
||||||
slide.form.fields_dict.full_name.set_input(
|
slide.form.fields_dict.full_name.set_input(
|
||||||
[frappe.boot.user.first_name, frappe.boot.user.last_name].join(" ").trim()
|
[first_name, last_name].join(" ").trim()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
delete slide.form.fields_dict.email;
|
slide.form.fields_dict.email.set_input(email);
|
||||||
|
slide.form.fields_dict.email.df.read_only = 1;
|
||||||
|
slide.form.fields_dict.email.refresh();
|
||||||
} else {
|
} else {
|
||||||
slide.form.fields_dict.email.df.reqd = 1;
|
slide.form.fields_dict.email.df.reqd = 1;
|
||||||
slide.form.fields_dict.email.refresh();
|
slide.form.fields_dict.email.refresh();
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ from frappe.utils.password import update_password
|
||||||
from . import install_fixtures
|
from . import install_fixtures
|
||||||
|
|
||||||
|
|
||||||
def get_setup_stages(args):
|
def get_setup_stages(args): # nosemgrep
|
||||||
|
|
||||||
# App setup stage functions should not include frappe.db.commit
|
# App setup stage functions should not include frappe.db.commit
|
||||||
# That is done by frappe after successful completion of all stages
|
# That is done by frappe after successful completion of all stages
|
||||||
|
|
@ -104,18 +104,18 @@ def process_setup_stages(stages, user_input, is_background_task=False):
|
||||||
frappe.flags.in_setup_wizard = False
|
frappe.flags.in_setup_wizard = False
|
||||||
|
|
||||||
|
|
||||||
def update_global_settings(args):
|
def update_global_settings(args): # nosemgrep
|
||||||
if args.language and args.language != "English":
|
if args.language and args.language != "English":
|
||||||
set_default_language(get_language_code(args.lang))
|
set_default_language(get_language_code(args.lang))
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
||||||
update_system_settings(args)
|
update_system_settings(args)
|
||||||
update_user_name(args)
|
create_or_update_user(args)
|
||||||
set_timezone(args)
|
set_timezone(args)
|
||||||
|
|
||||||
|
|
||||||
def run_post_setup_complete(args):
|
def run_post_setup_complete(args): # nosemgrep
|
||||||
disable_future_access()
|
disable_future_access()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.clear_cache()
|
frappe.clear_cache()
|
||||||
|
|
@ -124,20 +124,20 @@ def run_post_setup_complete(args):
|
||||||
frappe.get_cached_doc("System Settings") and frappe.get_doc("System Settings")
|
frappe.get_cached_doc("System Settings") and frappe.get_doc("System Settings")
|
||||||
|
|
||||||
|
|
||||||
def run_setup_success(args):
|
def run_setup_success(args): # nosemgrep
|
||||||
for hook in frappe.get_hooks("setup_wizard_success"):
|
for hook in frappe.get_hooks("setup_wizard_success"):
|
||||||
frappe.get_attr(hook)(args)
|
frappe.get_attr(hook)(args)
|
||||||
install_fixtures.install()
|
install_fixtures.install()
|
||||||
|
|
||||||
|
|
||||||
def get_stages_hooks(args):
|
def get_stages_hooks(args): # nosemgrep
|
||||||
stages = []
|
stages = []
|
||||||
for method in frappe.get_hooks("setup_wizard_stages"):
|
for method in frappe.get_hooks("setup_wizard_stages"):
|
||||||
stages += frappe.get_attr(method)(args)
|
stages += frappe.get_attr(method)(args)
|
||||||
return stages
|
return stages
|
||||||
|
|
||||||
|
|
||||||
def get_setup_complete_hooks(args):
|
def get_setup_complete_hooks(args): # nosemgrep
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"status": "Executing method",
|
"status": "Executing method",
|
||||||
|
|
@ -154,7 +154,7 @@ def get_setup_complete_hooks(args):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def handle_setup_exception(args):
|
def handle_setup_exception(args): # nosemgrep
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
if args:
|
if args:
|
||||||
traceback = frappe.get_traceback(with_context=True)
|
traceback = frappe.get_traceback(with_context=True)
|
||||||
|
|
@ -163,7 +163,7 @@ def handle_setup_exception(args):
|
||||||
frappe.get_attr(hook)(traceback, args)
|
frappe.get_attr(hook)(traceback, args)
|
||||||
|
|
||||||
|
|
||||||
def update_system_settings(args):
|
def update_system_settings(args): # nosemgrep
|
||||||
number_format = get_country_info(args.get("country")).get("number_format", "#,###.##")
|
number_format = get_country_info(args.get("country")).get("number_format", "#,###.##")
|
||||||
|
|
||||||
# replace these as float number formats, as they have 0 precision
|
# replace these as float number formats, as they have 0 precision
|
||||||
|
|
@ -194,72 +194,51 @@ def update_system_settings(args):
|
||||||
frappe.db.set_default("session_recording_start", now())
|
frappe.db.set_default("session_recording_start", now())
|
||||||
|
|
||||||
|
|
||||||
def update_user_name(args):
|
def create_or_update_user(args): # nosemgrep
|
||||||
|
email = args.get("email")
|
||||||
|
if not email:
|
||||||
|
return
|
||||||
|
|
||||||
first_name, last_name = args.get("full_name", ""), ""
|
first_name, last_name = args.get("full_name", ""), ""
|
||||||
if " " in first_name:
|
if " " in first_name:
|
||||||
first_name, last_name = first_name.split(" ", 1)
|
first_name, last_name = first_name.split(" ", 1)
|
||||||
|
|
||||||
if args.get("email"):
|
if user := frappe.db.get_value("User", email, ["first_name", "last_name"], as_dict=True):
|
||||||
if frappe.db.exists("User", args.get("email")):
|
if user.first_name != first_name or user.last_name != last_name:
|
||||||
# running again
|
(
|
||||||
return
|
frappe.qb.update("User")
|
||||||
|
.set("first_name", first_name)
|
||||||
args["name"] = args.get("email")
|
.set("last_name", last_name)
|
||||||
|
.set("full_name", args.get("full_name"))
|
||||||
|
).run()
|
||||||
|
else:
|
||||||
_mute_emails, frappe.flags.mute_emails = frappe.flags.mute_emails, True
|
_mute_emails, frappe.flags.mute_emails = frappe.flags.mute_emails, True
|
||||||
doc = frappe.get_doc(
|
|
||||||
|
user = frappe.new_doc("User")
|
||||||
|
user.update(
|
||||||
{
|
{
|
||||||
"doctype": "User",
|
"email": email,
|
||||||
"email": args.get("email"),
|
|
||||||
"first_name": first_name,
|
"first_name": first_name,
|
||||||
"last_name": last_name,
|
"last_name": last_name,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
user.append_roles(*_get_default_roles())
|
||||||
|
user.flags.no_welcome_mail = True
|
||||||
|
user.insert()
|
||||||
|
|
||||||
doc.append_roles(*_get_default_roles())
|
|
||||||
doc.flags.no_welcome_mail = True
|
|
||||||
doc.insert()
|
|
||||||
frappe.flags.mute_emails = _mute_emails
|
frappe.flags.mute_emails = _mute_emails
|
||||||
update_password(args.get("email"), args.get("password"))
|
|
||||||
|
|
||||||
elif first_name:
|
if args.get("password"):
|
||||||
args.update({"name": frappe.session.user, "first_name": first_name, "last_name": last_name})
|
update_password(email, args.get("password"))
|
||||||
|
|
||||||
frappe.db.sql(
|
|
||||||
"""update `tabUser` SET first_name=%(first_name)s,
|
|
||||||
last_name=%(last_name)s WHERE name=%(name)s""",
|
|
||||||
args,
|
|
||||||
)
|
|
||||||
|
|
||||||
if args.get("attach_user"):
|
|
||||||
attach_user = args.get("attach_user").split(",")
|
|
||||||
if len(attach_user) == 3:
|
|
||||||
filename, filetype, content = attach_user
|
|
||||||
_file = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "File",
|
|
||||||
"file_name": filename,
|
|
||||||
"attached_to_doctype": "User",
|
|
||||||
"attached_to_name": args.get("name"),
|
|
||||||
"content": content,
|
|
||||||
"decode": True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
_file.save()
|
|
||||||
fileurl = _file.file_url
|
|
||||||
frappe.db.set_value("User", args.get("name"), "user_image", fileurl)
|
|
||||||
|
|
||||||
if args.get("name"):
|
|
||||||
add_all_roles_to(args.get("name"))
|
|
||||||
|
|
||||||
|
|
||||||
def set_timezone(args):
|
def set_timezone(args): # nosemgrep
|
||||||
if args.get("timezone"):
|
if args.get("timezone"):
|
||||||
for name in frappe.STANDARD_USERS:
|
for name in frappe.STANDARD_USERS:
|
||||||
frappe.db.set_value("User", name, "time_zone", args.get("timezone"))
|
frappe.db.set_value("User", name, "time_zone", args.get("timezone"))
|
||||||
|
|
||||||
|
|
||||||
def parse_args(args):
|
def parse_args(args): # nosemgrep
|
||||||
if not args:
|
if not args:
|
||||||
args = frappe.local.form_dict
|
args = frappe.local.form_dict
|
||||||
if isinstance(args, str):
|
if isinstance(args, str):
|
||||||
|
|
@ -344,7 +323,7 @@ def load_user_details():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def prettify_args(args):
|
def prettify_args(args): # nosemgrep
|
||||||
# remove attachments
|
# remove attachments
|
||||||
for key, val in args.items():
|
for key, val in args.items():
|
||||||
if isinstance(val, str) and "data:image" in val:
|
if isinstance(val, str) and "data:image" in val:
|
||||||
|
|
@ -357,7 +336,7 @@ def prettify_args(args):
|
||||||
return pretty_args
|
return pretty_args
|
||||||
|
|
||||||
|
|
||||||
def email_setup_wizard_exception(traceback, args):
|
def email_setup_wizard_exception(traceback, args): # nosemgrep
|
||||||
if not frappe.conf.setup_wizard_exception_email:
|
if not frappe.conf.setup_wizard_exception_email:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -402,7 +381,7 @@ def email_setup_wizard_exception(traceback, args):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def log_setup_wizard_exception(traceback, args):
|
def log_setup_wizard_exception(traceback, args): # nosemgrep
|
||||||
with open("../logs/setup-wizard.log", "w+") as setup_log:
|
with open("../logs/setup-wizard.log", "w+") as setup_log:
|
||||||
setup_log.write(traceback)
|
setup_log.write(traceback)
|
||||||
setup_log.write(json.dumps(args))
|
setup_log.write(json.dumps(args))
|
||||||
|
|
|
||||||
|
|
@ -498,6 +498,8 @@ class EmailAccount(Document):
|
||||||
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
|
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
|
||||||
|
|
||||||
def _disable_broken_incoming_account(self, description):
|
def _disable_broken_incoming_account(self, description):
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
return
|
||||||
self.db_set("enable_incoming", 0)
|
self.db_set("enable_incoming", 0)
|
||||||
|
|
||||||
for user in get_system_managers(only_name=True):
|
for user in get_system_managers(only_name=True):
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,8 @@ from frappe.desk.form.load import get_attachments
|
||||||
from frappe.email.doctype.email_account.email_account import notify_unreplied
|
from frappe.email.doctype.email_account.email_account import notify_unreplied
|
||||||
from frappe.email.email_body import get_message_id
|
from frappe.email.email_body import get_message_id
|
||||||
from frappe.email.receive import Email, InboundMail, SentEmailInInboxError
|
from frappe.email.receive import Email, InboundMail, SentEmailInInboxError
|
||||||
from frappe.test_runner import make_test_records
|
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
make_test_records("User")
|
|
||||||
make_test_records("Email Account")
|
|
||||||
|
|
||||||
|
|
||||||
class TestEmailAccount(FrappeTestCase):
|
class TestEmailAccount(FrappeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -65,9 +61,18 @@ class TestEmailAccount(FrappeTestCase):
|
||||||
self.assertTrue(frappe.db.get_value(comm.reference_doctype, comm.reference_name, "name"))
|
self.assertTrue(frappe.db.get_value(comm.reference_doctype, comm.reference_name, "name"))
|
||||||
|
|
||||||
def test_unread_notification(self):
|
def test_unread_notification(self):
|
||||||
self.test_incoming()
|
todo = frappe.get_last_doc("ToDo")
|
||||||
|
|
||||||
comm = frappe.get_doc("Communication", {"sender": "test_sender@example.com"})
|
comm = frappe.new_doc(
|
||||||
|
"Communication",
|
||||||
|
sender="test_sender@example.com",
|
||||||
|
subject="test unread reminder",
|
||||||
|
sent_or_received="Received",
|
||||||
|
reference_doctype=todo.doctype,
|
||||||
|
reference_name=todo.name,
|
||||||
|
email_account="_Test Email Account 1",
|
||||||
|
)
|
||||||
|
comm.insert()
|
||||||
comm.db_set("creation", datetime.now() - timedelta(seconds=30 * 60))
|
comm.db_set("creation", datetime.now() - timedelta(seconds=30 * 60))
|
||||||
|
|
||||||
frappe.db.delete("Email Queue")
|
frappe.db.delete("Email Queue")
|
||||||
|
|
@ -78,7 +83,6 @@ class TestEmailAccount(FrappeTestCase):
|
||||||
{
|
{
|
||||||
"reference_doctype": comm.reference_doctype,
|
"reference_doctype": comm.reference_doctype,
|
||||||
"reference_name": comm.reference_name,
|
"reference_name": comm.reference_name,
|
||||||
"status": "Not Sent",
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-07-04 09:42:52.425440",
|
"modified": "2024-01-17 15:37:31.605278",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Geo",
|
"module": "Geo",
|
||||||
"name": "Currency",
|
"name": "Currency",
|
||||||
|
|
@ -102,6 +102,10 @@
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts Manager"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "Accounts User"
|
"role": "Accounts User"
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ def _new_site(
|
||||||
db_type=None,
|
db_type=None,
|
||||||
db_host=None,
|
db_host=None,
|
||||||
db_port=None,
|
db_port=None,
|
||||||
|
db_user=None,
|
||||||
setup_db=True,
|
setup_db=True,
|
||||||
):
|
):
|
||||||
"""Install a new Frappe site"""
|
"""Install a new Frappe site"""
|
||||||
|
|
@ -63,7 +64,7 @@ def _new_site(
|
||||||
print("--no-mariadb-socket requires db_type to be set to mariadb.")
|
print("--no-mariadb-socket requires db_type to be set to mariadb.")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
frappe.init(site=site)
|
frappe.init(site=site, site_ready=False)
|
||||||
|
|
||||||
if not db_name:
|
if not db_name:
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
@ -97,6 +98,7 @@ def _new_site(
|
||||||
db_type=db_type,
|
db_type=db_type,
|
||||||
db_host=db_host,
|
db_host=db_host,
|
||||||
db_port=db_port,
|
db_port=db_port,
|
||||||
|
db_user=db_user,
|
||||||
no_mariadb_socket=no_mariadb_socket,
|
no_mariadb_socket=no_mariadb_socket,
|
||||||
setup=setup_db,
|
setup=setup_db,
|
||||||
)
|
)
|
||||||
|
|
@ -135,6 +137,7 @@ def install_db(
|
||||||
db_type=None,
|
db_type=None,
|
||||||
db_host=None,
|
db_host=None,
|
||||||
db_port=None,
|
db_port=None,
|
||||||
|
db_user=None,
|
||||||
no_mariadb_socket=False,
|
no_mariadb_socket=False,
|
||||||
setup=True,
|
setup=True,
|
||||||
):
|
):
|
||||||
|
|
@ -156,6 +159,7 @@ def install_db(
|
||||||
db_type=db_type,
|
db_type=db_type,
|
||||||
db_host=db_host,
|
db_host=db_host,
|
||||||
db_port=db_port,
|
db_port=db_port,
|
||||||
|
db_user=db_user,
|
||||||
)
|
)
|
||||||
frappe.flags.in_install_db = True
|
frappe.flags.in_install_db = True
|
||||||
|
|
||||||
|
|
@ -533,19 +537,37 @@ def init_singles():
|
||||||
|
|
||||||
|
|
||||||
def make_conf(
|
def make_conf(
|
||||||
db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None
|
db_name=None,
|
||||||
|
db_password=None,
|
||||||
|
site_config=None,
|
||||||
|
db_type=None,
|
||||||
|
db_host=None,
|
||||||
|
db_port=None,
|
||||||
|
db_user=None,
|
||||||
):
|
):
|
||||||
site = frappe.local.site
|
site = frappe.local.site
|
||||||
make_site_config(
|
make_site_config(
|
||||||
db_name, db_password, site_config, db_type=db_type, db_host=db_host, db_port=db_port
|
db_name,
|
||||||
|
db_password,
|
||||||
|
site_config,
|
||||||
|
db_type=db_type,
|
||||||
|
db_host=db_host,
|
||||||
|
db_port=db_port,
|
||||||
|
db_user=db_user,
|
||||||
)
|
)
|
||||||
sites_path = frappe.local.sites_path
|
sites_path = frappe.local.sites_path
|
||||||
frappe.destroy()
|
frappe.destroy()
|
||||||
frappe.init(site, sites_path=sites_path)
|
frappe.init(site, sites_path=sites_path, site_ready=False)
|
||||||
|
|
||||||
|
|
||||||
def make_site_config(
|
def make_site_config(
|
||||||
db_name=None, db_password=None, site_config=None, db_type=None, db_host=None, db_port=None
|
db_name=None,
|
||||||
|
db_password=None,
|
||||||
|
site_config=None,
|
||||||
|
db_type=None,
|
||||||
|
db_host=None,
|
||||||
|
db_port=None,
|
||||||
|
db_user=None,
|
||||||
):
|
):
|
||||||
frappe.create_folder(os.path.join(frappe.local.site_path))
|
frappe.create_folder(os.path.join(frappe.local.site_path))
|
||||||
site_file = get_site_config_path()
|
site_file = get_site_config_path()
|
||||||
|
|
@ -563,6 +585,8 @@ def make_site_config(
|
||||||
if db_port:
|
if db_port:
|
||||||
site_config["db_port"] = db_port
|
site_config["db_port"] = db_port
|
||||||
|
|
||||||
|
site_config["db_user"] = db_user or db_name
|
||||||
|
|
||||||
with open(site_file, "w") as f:
|
with open(site_file, "w") as f:
|
||||||
f.write(json.dumps(site_config, indent=1, sort_keys=True))
|
f.write(json.dumps(site_config, indent=1, sort_keys=True))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,14 @@ frappe.ui.form.on("Google Drive", {
|
||||||
|
|
||||||
frappe.realtime.on("upload_to_google_drive", (data) => {
|
frappe.realtime.on("upload_to_google_drive", (data) => {
|
||||||
if (data.progress) {
|
if (data.progress) {
|
||||||
|
const progress_title = __("Uploading to Google Drive");
|
||||||
frm.dashboard.show_progress(
|
frm.dashboard.show_progress(
|
||||||
"Uploading to Google Drive",
|
progress_title,
|
||||||
(data.progress / data.total) * 100,
|
(data.progress / data.total) * 100,
|
||||||
__("{0}", [data.message])
|
data.message
|
||||||
);
|
);
|
||||||
if (data.progress === data.total) {
|
if (data.progress === data.total) {
|
||||||
frm.dashboard.hide_progress("Uploading to Google Drive");
|
frm.dashboard.hide_progress(progress_title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -170,7 +170,7 @@ def upload_system_backup_to_google_drive():
|
||||||
validate_file_size()
|
validate_file_size()
|
||||||
|
|
||||||
if frappe.flags.create_new_backup:
|
if frappe.flags.create_new_backup:
|
||||||
set_progress(1, "Backing up Data.")
|
set_progress(1, _("Backing up Data."))
|
||||||
backup = new_backup()
|
backup = new_backup()
|
||||||
file_urls = []
|
file_urls = []
|
||||||
file_urls.append(backup.backup_path_db)
|
file_urls.append(backup.backup_path_db)
|
||||||
|
|
@ -196,12 +196,12 @@ def upload_system_backup_to_google_drive():
|
||||||
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
|
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
set_progress(2, "Uploading backup to Google Drive.")
|
set_progress(2, _("Uploading backup to Google Drive."))
|
||||||
google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute()
|
google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute()
|
||||||
except HttpError as e:
|
except HttpError as e:
|
||||||
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
|
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
|
||||||
|
|
||||||
set_progress(3, "Uploading successful.")
|
set_progress(3, _("Uploading successful."))
|
||||||
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
|
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
|
||||||
send_email(True, "Google Drive", "Google Drive", "email")
|
send_email(True, "Google Drive", "Google Drive", "email")
|
||||||
return _("Google Drive Backup Successful.")
|
return _("Google Drive Backup Successful.")
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,7 @@
|
||||||
"description": "The browser API key obtained from the Google Cloud Console under <a href=\"https://console.cloud.google.com/apis/credentials\">\n\"APIs & Services\" > \"Credentials\"\n</a>",
|
"description": "The browser API key obtained from the Google Cloud Console under <a href=\"https://console.cloud.google.com/apis/credentials\">\n\"APIs & Services\" > \"Credentials\"\n</a>",
|
||||||
"fieldname": "api_key",
|
"fieldname": "api_key",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "API Key",
|
"label": "API Key"
|
||||||
"mandatory_depends_on": "google_drive_picker_enabled"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "enable",
|
"depends_on": "enable",
|
||||||
|
|
@ -76,7 +75,7 @@
|
||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-06-29 18:26:07.094851",
|
"modified": "2024-01-16 13:19:22.365362",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Integrations",
|
"module": "Integrations",
|
||||||
"name": "Google Settings",
|
"name": "Google Settings",
|
||||||
|
|
@ -96,5 +95,6 @@
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ class GoogleSettings(Document):
|
||||||
enable: DF.Check
|
enable: DF.Check
|
||||||
google_drive_picker_enabled: DF.Check
|
google_drive_picker_enabled: DF.Check
|
||||||
# end: auto-generated types
|
# end: auto-generated types
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -34,6 +35,5 @@ def get_file_picker_settings():
|
||||||
return {
|
return {
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"appId": google_settings.app_id,
|
"appId": google_settings.app_id,
|
||||||
"developerKey": google_settings.api_key,
|
|
||||||
"clientId": google_settings.client_id,
|
"clientId": google_settings.client_id,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,3 @@ class TestGoogleSettings(FrappeTestCase):
|
||||||
self.assertEqual(True, settings.get("enabled", False))
|
self.assertEqual(True, settings.get("enabled", False))
|
||||||
self.assertEqual("test_client_id", settings.get("clientId", ""))
|
self.assertEqual("test_client_id", settings.get("clientId", ""))
|
||||||
self.assertEqual("test_app_id", settings.get("appId", ""))
|
self.assertEqual("test_app_id", settings.get("appId", ""))
|
||||||
self.assertEqual("test_api_key", settings.get("developerKey", ""))
|
|
||||||
|
|
|
||||||
|
|
@ -438,7 +438,7 @@ class LDAP_TestCase:
|
||||||
for user_role in updated_user_roles: # match each users role mapped to ldap groups
|
for user_role in updated_user_roles: # match each users role mapped to ldap groups
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
role_to_group_map[user_role] in test_user_data[test_user],
|
role_to_group_map[user_role] in test_user_data[test_user],
|
||||||
f"during sync_roles(), the user was given role {user_role} which should not have occured",
|
f"during sync_roles(), the user was given role {user_role} which should not have occurred",
|
||||||
)
|
)
|
||||||
|
|
||||||
@mock_ldap_connection
|
@mock_ldap_connection
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ def get_latest_backup_file(with_files=False):
|
||||||
|
|
||||||
odb = BackupGenerator(
|
odb = BackupGenerator(
|
||||||
frappe.conf.db_name,
|
frappe.conf.db_name,
|
||||||
frappe.conf.db_name,
|
frappe.conf.db_user,
|
||||||
frappe.conf.db_password,
|
frappe.conf.db_password,
|
||||||
db_host=frappe.conf.db_host,
|
db_host=frappe.conf.db_host,
|
||||||
db_port=frappe.conf.db_port,
|
db_port=frappe.conf.db_port,
|
||||||
|
|
@ -110,7 +110,7 @@ def generate_files_backup():
|
||||||
|
|
||||||
backup = BackupGenerator(
|
backup = BackupGenerator(
|
||||||
frappe.conf.db_name,
|
frappe.conf.db_name,
|
||||||
frappe.conf.db_name,
|
frappe.conf.db_user,
|
||||||
frappe.conf.db_password,
|
frappe.conf.db_password,
|
||||||
db_host=frappe.conf.db_host,
|
db_host=frappe.conf.db_host,
|
||||||
db_port=frappe.conf.db_port,
|
db_port=frappe.conf.db_port,
|
||||||
|
|
|
||||||
3968
frappe/locale/de.po
3968
frappe/locale/de.po
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@
|
||||||
# License: MIT. See LICENSE
|
# License: MIT. See LICENSE
|
||||||
import datetime
|
import datetime
|
||||||
import json
|
import json
|
||||||
|
import weakref
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import TYPE_CHECKING, TypeVar
|
from typing import TYPE_CHECKING, TypeVar
|
||||||
|
|
||||||
|
|
@ -163,6 +164,7 @@ class BaseDocument:
|
||||||
|
|
||||||
state.pop("meta", None)
|
state.pop("meta", None)
|
||||||
state.pop("permitted_fieldnames", None)
|
state.pop("permitted_fieldnames", None)
|
||||||
|
state.pop("_parent_doc", None)
|
||||||
|
|
||||||
def update(self, d):
|
def update(self, d):
|
||||||
"""Update multiple fields of a doctype using a dictionary of key-value pairs.
|
"""Update multiple fields of a doctype using a dictionary of key-value pairs.
|
||||||
|
|
@ -261,11 +263,28 @@ class BaseDocument:
|
||||||
ret_value = self._init_child(value, key)
|
ret_value = self._init_child(value, key)
|
||||||
table.append(ret_value)
|
table.append(ret_value)
|
||||||
|
|
||||||
# reference parent document
|
# reference parent document but with weak reference, parent_doc will be deleted if self is garbage collected.
|
||||||
ret_value.parent_doc = self
|
ret_value.parent_doc = weakref.ref(self)
|
||||||
|
|
||||||
return ret_value
|
return ret_value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent_doc(self):
|
||||||
|
parent_doc_ref = getattr(self, "_parent_doc", None)
|
||||||
|
|
||||||
|
if isinstance(parent_doc_ref, BaseDocument):
|
||||||
|
return parent_doc_ref
|
||||||
|
elif isinstance(parent_doc_ref, weakref.ReferenceType):
|
||||||
|
return parent_doc_ref()
|
||||||
|
|
||||||
|
@parent_doc.setter
|
||||||
|
def parent_doc(self, value):
|
||||||
|
self._parent_doc = value
|
||||||
|
|
||||||
|
@parent_doc.deleter
|
||||||
|
def parent_doc(self):
|
||||||
|
self._parent_doc = None
|
||||||
|
|
||||||
def extend(self, key, value):
|
def extend(self, key, value):
|
||||||
try:
|
try:
|
||||||
value = iter(value)
|
value = iter(value)
|
||||||
|
|
@ -1231,7 +1250,7 @@ class BaseDocument:
|
||||||
ref_doc = frappe.new_doc(self.doctype)
|
ref_doc = frappe.new_doc(self.doctype)
|
||||||
else:
|
else:
|
||||||
# get values from old doc
|
# get values from old doc
|
||||||
if self.get("parent_doc"):
|
if self.parent_doc:
|
||||||
parent_doc = self.parent_doc.get_latest()
|
parent_doc = self.parent_doc.get_latest()
|
||||||
child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name]
|
child_docs = [d for d in parent_doc.get(self.parentfield) if d.name == self.name]
|
||||||
if not child_docs:
|
if not child_docs:
|
||||||
|
|
|
||||||
|
|
@ -754,7 +754,7 @@ class DatabaseQuery:
|
||||||
ref_doctype = field.options if field else f.doctype
|
ref_doctype = field.options if field else f.doctype
|
||||||
lft, rgt = "", ""
|
lft, rgt = "", ""
|
||||||
if f.value:
|
if f.value:
|
||||||
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"]) or (0, 0)
|
||||||
|
|
||||||
# Get descendants elements of a DocType with a tree structure
|
# Get descendants elements of a DocType with a tree structure
|
||||||
if f.operator.lower() in (
|
if f.operator.lower() in (
|
||||||
|
|
|
||||||
|
|
@ -197,11 +197,7 @@ def map_fields(source_doc, target_doc, table_map, source_parent):
|
||||||
for d in source_doc.meta.get("fields")
|
for d in source_doc.meta.get("fields")
|
||||||
if (d.no_copy == 1 or d.fieldtype in table_fields)
|
if (d.no_copy == 1 or d.fieldtype in table_fields)
|
||||||
]
|
]
|
||||||
+ [
|
+ [d.fieldname for d in target_doc.meta.get("fields") if (d.fieldtype in table_fields)]
|
||||||
d.fieldname
|
|
||||||
for d in target_doc.meta.get("fields")
|
|
||||||
if (d.no_copy == 1 or d.fieldtype in table_fields)
|
|
||||||
]
|
|
||||||
+ list(default_fields)
|
+ list(default_fields)
|
||||||
+ list(child_table_fields)
|
+ list(child_table_fields)
|
||||||
+ list(table_map.get("field_no_map", []))
|
+ list(table_map.get("field_no_map", []))
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ def _is_implicit_int_pk(doctype: str) -> bool:
|
||||||
values = ()
|
values = ()
|
||||||
if frappe.db.db_type == "mariadb":
|
if frappe.db.db_type == "mariadb":
|
||||||
query += " and table_schema = %s"
|
query += " and table_schema = %s"
|
||||||
values = (frappe.db.db_name,)
|
values = (frappe.db.cur_db_name,)
|
||||||
|
|
||||||
col_type = frappe.db.sql(query, values)
|
col_type = frappe.db.sql(query, values)
|
||||||
return bool(col_type and col_type[0][0] == "bigint")
|
return bool(col_type and col_type[0][0] == "bigint")
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,10 @@ def has_user_permission(doc, user=None, debug=False):
|
||||||
debug and _debug_log("User permission bypassed because user can modify user permissions.")
|
debug and _debug_log("User permission bypassed because user can modify user permissions.")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
|
# don't apply strict user permissions for single doctypes since they contain empty link fields
|
||||||
|
apply_strict_user_permissions = (
|
||||||
|
False if doc.meta.issingle else frappe.get_system_settings("apply_strict_user_permissions")
|
||||||
|
)
|
||||||
if apply_strict_user_permissions:
|
if apply_strict_user_permissions:
|
||||||
debug and _debug_log("Strict user permissions will be applied")
|
debug and _debug_log("Strict user permissions will be applied")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -384,13 +384,13 @@ frappe.ui.form.PrintView = class {
|
||||||
this.print_wrapper.find(".preview-beta-wrapper").hide();
|
this.print_wrapper.find(".preview-beta-wrapper").hide();
|
||||||
this.print_wrapper.find(".print-preview-wrapper").show();
|
this.print_wrapper.find(".print-preview-wrapper").show();
|
||||||
|
|
||||||
const $print_format = this.print_wrapper.find("iframe");
|
|
||||||
this.$print_format_body = $print_format.contents();
|
|
||||||
this.get_print_html((out) => {
|
this.get_print_html((out) => {
|
||||||
if (!out.html) {
|
if (!out.html) {
|
||||||
out.html = this.get_no_preview_html();
|
out.html = this.get_no_preview_html();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $print_format = this.print_wrapper.find("iframe");
|
||||||
|
this.$print_format_body = $print_format.contents();
|
||||||
this.setup_print_format_dom(out, $print_format);
|
this.setup_print_format_dom(out, $print_format);
|
||||||
|
|
||||||
const print_height = $print_format.get(0).offsetHeight;
|
const print_height = $print_format.get(0).offsetHeight;
|
||||||
|
|
|
||||||
|
|
@ -121,9 +121,7 @@
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
|
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-external-link">
|
||||||
<path d="M9.75003 7.83333V9C9.75003 9.82843 9.07846 10.5 8.25003 10.5H3.25C2.42157 10.5 1.75 9.82843 1.75 9V4C1.75 3.17158 2.42151 2.50001 3.24993 2.50001C3.62327 2.5 4.02808 2.5 4.4167 2.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
<path fill-rule="evenodd" stroke-width="0" clip-rule="evenodd" d="M4.52409 7.47575C4.3889 7.34059 4.3889 7.12141 4.52409 6.98623L9.31795 2.19237H6.69231C6.50113 2.19237 6.34615 2.03739 6.34615 1.84622C6.34615 1.65505 6.50113 1.50007 6.69231 1.50007H10.1469C10.2283 1.49846 10.3104 1.52546 10.3764 1.5811C10.452 1.64459 10.5 1.7398 10.5 1.84622V5.30773C10.5 5.4989 10.345 5.65388 10.1538 5.65388C9.9627 5.65388 9.80769 5.4989 9.80769 5.30773V2.68173L5.01362 7.47575C4.87844 7.61095 4.65927 7.61095 4.52409 7.47575ZM2.19231 3.23082C2.19231 2.6573 2.65724 2.19237 3.23077 2.19237H4.47692C4.6681 2.19237 4.82308 2.03739 4.82308 1.84622C4.82308 1.65505 4.6681 1.50007 4.47692 1.50007H3.23077C2.27489 1.50007 1.5 2.27496 1.5 3.23082V8.76924C1.5 9.72511 2.27489 10.5 3.23077 10.5H8.76923C9.7251 10.5 10.5 9.72511 10.5 8.76924V7.5231C10.5 7.33193 10.345 7.17695 10.1538 7.17695C9.9627 7.17695 9.80769 7.33193 9.80769 7.5231V8.76924C9.80769 9.34275 9.34274 9.8077 8.76923 9.8077H3.23077C2.65724 9.8077 2.19231 9.34275 2.19231 8.76924V3.23082Z" fill="var(--icon-stroke)"/>
|
||||||
<path d="M6.75 1.5H10.25V4.5" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<path d="M6.75 5L9.75 2" stroke="var(--icon-stroke)" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</symbol>
|
</symbol>
|
||||||
|
|
||||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">
|
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" fill="#112B42" id="icon-up">
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 114 KiB |
|
|
@ -185,6 +185,11 @@ function is_filter_applied() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function open_child_doctype() {
|
||||||
|
if (!props.field?.df?.options) return;
|
||||||
|
window.open(`/app/doctype/${props.field.df.options}`, "_blank");
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => selected.value && label_input.value.focus_on_label());
|
onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -199,6 +204,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||||
<component
|
<component
|
||||||
:is="component"
|
:is="component"
|
||||||
:df="field.df"
|
:df="field.df"
|
||||||
|
:is-customize-form="store.is_customize_form"
|
||||||
:data-fieldname="field.df.fieldname"
|
:data-fieldname="field.df.fieldname"
|
||||||
:data-fieldtype="field.df.fieldtype"
|
:data-fieldtype="field.df.fieldtype"
|
||||||
>
|
>
|
||||||
|
|
@ -216,7 +222,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||||
class="help-icon"
|
class="help-icon"
|
||||||
v-if="field.df.documentation_url"
|
v-if="field.df.documentation_url"
|
||||||
v-html="frappe.utils.icon('help', 'sm')"
|
v-html="frappe.utils.icon('help', 'sm')"
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
|
|
@ -227,7 +233,7 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||||
:class="is_filter_applied()"
|
:class="is_filter_applied()"
|
||||||
@click="edit_filters"
|
@click="edit_filters"
|
||||||
>
|
>
|
||||||
<div v-html="frappe.utils.icon('filter', 'sm')"></div>
|
<div v-html="frappe.utils.icon('filter', 'sm')" />
|
||||||
</button>
|
</button>
|
||||||
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
|
<AddFieldButton ref="add_field_ref" :column="column" :field="field">
|
||||||
<div v-html="frappe.utils.icon('add', 'sm')" />
|
<div v-html="frappe.utils.icon('add', 'sm')" />
|
||||||
|
|
@ -240,21 +246,29 @@ onMounted(() => selected.value && label_input.value.focus_on_label());
|
||||||
"
|
"
|
||||||
@click="move_fields_to_column"
|
@click="move_fields_to_column"
|
||||||
>
|
>
|
||||||
<div v-html="frappe.utils.icon('move', 'sm')"></div>
|
<div v-html="frappe.utils.icon('move', 'sm')" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-icon"
|
class="btn btn-xs btn-icon"
|
||||||
:title="__('Duplicate field')"
|
:title="__('Duplicate field')"
|
||||||
@click.stop="duplicate_field"
|
@click.stop="duplicate_field"
|
||||||
>
|
>
|
||||||
<div v-html="frappe.utils.icon('duplicate', 'sm')"></div>
|
<div v-html="frappe.utils.icon('duplicate', 'sm')" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="field.df.fieldtype === 'Table' && field.df.options"
|
||||||
|
class="btn btn-xs btn-icon"
|
||||||
|
@click="open_child_doctype"
|
||||||
|
:title="__(`Edit ${field.df.options} Doctype`)"
|
||||||
|
>
|
||||||
|
<div v-html="frappe.utils.icon('external-link', 'sm')" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
class="btn btn-xs btn-icon"
|
class="btn btn-xs btn-icon"
|
||||||
:title="__('Remove field')"
|
:title="__('Remove field')"
|
||||||
@click.stop="remove_field"
|
@click.stop="remove_field"
|
||||||
>
|
>
|
||||||
<div v-html="frappe.utils.icon('remove', 'sm')"></div>
|
<div v-html="frappe.utils.icon('remove', 'sm')" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
import { get_table_columns, load_doctype_model } from "../../utils";
|
import { get_table_columns, load_doctype_model } from "../../utils";
|
||||||
import { computedAsync } from "@vueuse/core";
|
import { computedAsync } from "@vueuse/core";
|
||||||
|
|
||||||
const props = defineProps(["df"]);
|
const props = defineProps(["df", "is-customize-form"]);
|
||||||
|
|
||||||
let table_columns = computedAsync(async () => {
|
let table_columns = computedAsync(async () => {
|
||||||
let doctype = props.df.options;
|
let doctype = props.df.options;
|
||||||
|
|
@ -13,6 +13,13 @@ let table_columns = computedAsync(async () => {
|
||||||
let child_doctype = frappe.get_meta(doctype);
|
let child_doctype = frappe.get_meta(doctype);
|
||||||
return get_table_columns(props.df, child_doctype);
|
return get_table_columns(props.df, child_doctype);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function open_new_child_doctype_dialog() {
|
||||||
|
let is_custom = props.isCustomizeForm;
|
||||||
|
frappe.model.with_doctype("DocType").then(() => {
|
||||||
|
frappe.listview_settings["DocType"].new_doctype_dialog({ is_child: 1, is_custom });
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -46,7 +53,15 @@ let table_columns = computedAsync(async () => {
|
||||||
:alt="__('Grid Empty State')"
|
:alt="__('Grid Empty State')"
|
||||||
class="grid-empty-illustration"
|
class="grid-empty-illustration"
|
||||||
/>
|
/>
|
||||||
{{ __("No Data") }}
|
<!-- render this button when there are no columns, which means that options is not added for the table -->
|
||||||
|
<button
|
||||||
|
class="btn btn-xs btn-secondary"
|
||||||
|
@click="open_new_child_doctype_dialog"
|
||||||
|
v-if="!table_columns.length"
|
||||||
|
>
|
||||||
|
{{ __("Create Child Doctype") }}
|
||||||
|
</button>
|
||||||
|
<p v-else>{{ __("No Data") }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- description -->
|
<!-- description -->
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,6 @@ frappe.ui.form.Control = class BaseControl {
|
||||||
if (this.df.get_status) {
|
if (this.df.get_status) {
|
||||||
return this.df.get_status(this);
|
return this.df.get_status(this);
|
||||||
}
|
}
|
||||||
if (this.df.is_virtual) {
|
|
||||||
return "Read";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(!this.doctype && !this.docname) ||
|
(!this.doctype && !this.docname) ||
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
|
||||||
let invalid = !!this.df.invalid;
|
let invalid = !!this.df.invalid;
|
||||||
if (this.grid) {
|
if (this.grid) {
|
||||||
this.$wrapper.parents(".grid-static-col").toggleClass("invalid", invalid);
|
this.$wrapper.parents(".grid-static-col").toggleClass("invalid", invalid);
|
||||||
this.$input.toggleClass("invalid", invalid);
|
this.$input?.toggleClass("invalid", invalid);
|
||||||
this.grid_row.columns[this.df.fieldname].is_invalid = invalid;
|
this.grid_row.columns[this.df.fieldname].is_invalid = invalid;
|
||||||
} else {
|
} else {
|
||||||
this.$wrapper.toggleClass("has-error", invalid);
|
this.$wrapper.toggleClass("has-error", invalid);
|
||||||
|
|
|
||||||
|
|
@ -252,6 +252,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
||||||
doctype: doctype,
|
doctype: doctype,
|
||||||
ignore_user_permissions: me.df.ignore_user_permissions,
|
ignore_user_permissions: me.df.ignore_user_permissions,
|
||||||
reference_doctype: me.get_reference_doctype() || "",
|
reference_doctype: me.get_reference_doctype() || "",
|
||||||
|
page_length: cint(frappe.boot.sysdefaults.link_field_results_limit) || 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
me.set_custom_query(args);
|
me.set_custom_query(args);
|
||||||
|
|
@ -476,7 +477,8 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
||||||
filter[3].push("...");
|
filter[3].push("...");
|
||||||
}
|
}
|
||||||
|
|
||||||
let value = filter[3] == null || filter[3] === "" ? __("empty") : String(filter[3]);
|
let value =
|
||||||
|
filter[3] == null || filter[3] === "" ? __("empty") : String(__(filter[3]));
|
||||||
|
|
||||||
return [__(label).bold(), __(filter[2]), value.bold()].join(" ");
|
return [__(label).bold(), __(filter[2]), value.bold()].join(" ");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ frappe.ui.form.ControlMarkdownEditor = class ControlMarkdownEditor extends (
|
||||||
}
|
}
|
||||||
|
|
||||||
update_preview() {
|
update_preview() {
|
||||||
|
if (!this.markdown_preview) return;
|
||||||
const value = this.get_value() || "";
|
const value = this.get_value() || "";
|
||||||
this.markdown_preview.html(frappe.markdown(value));
|
this.markdown_preview.html(frappe.markdown(value));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ frappe.ui.form.add_options = function (input, options_list, sort) {
|
||||||
|
|
||||||
let options = options_list.map((raw_option) => parse_option(raw_option));
|
let options = options_list.map((raw_option) => parse_option(raw_option));
|
||||||
if (sort) {
|
if (sort) {
|
||||||
options = options.sort((a, b) => a.label.localeCompare(b.label));
|
options = options.sort((a, b) => cstr(a.label).localeCompare(cstr(b.label)));
|
||||||
}
|
}
|
||||||
|
|
||||||
options
|
options
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ class FormTimeline extends BaseTimeline {
|
||||||
.append(
|
.append(
|
||||||
`
|
`
|
||||||
<div class="d-flex align-items-center show-all-activity">
|
<div class="d-flex align-items-center show-all-activity">
|
||||||
<span style="color: var(--text-light); margin:0px 6px;">Show all activity</span>
|
<span style="color: var(--text-light); margin:0px 6px;">${__("Show all activity")}</span>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox">
|
<input type="checkbox">
|
||||||
<span class="slider round"></span>
|
<span class="slider round"></span>
|
||||||
|
|
@ -550,11 +550,34 @@ class FormTimeline extends BaseTimeline {
|
||||||
title: communication_doc ? __("Reply") : null,
|
title: communication_doc ? __("Reply") : null,
|
||||||
last_email: communication_doc,
|
last_email: communication_doc,
|
||||||
subject: communication_doc && communication_doc.subject,
|
subject: communication_doc && communication_doc.subject,
|
||||||
|
reply_all: reply_all,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (communication_doc && reply_all) {
|
const email_accounts = frappe.boot.email_accounts
|
||||||
args.cc = communication_doc.cc;
|
.filter((account) => {
|
||||||
args.bcc = communication_doc.bcc;
|
return (
|
||||||
|
!["All Accounts", "Sent", "Spam", "Trash"].includes(account.email_account) &&
|
||||||
|
account.enable_outgoing
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((e) => e.email_id);
|
||||||
|
|
||||||
|
if (communication_doc && args.is_a_reply) {
|
||||||
|
args.cc = "";
|
||||||
|
if (
|
||||||
|
email_accounts.includes(frappe.session.user_email) &&
|
||||||
|
communication_doc.sender != frappe.session.user_email
|
||||||
|
) {
|
||||||
|
// add recipients to cc if replying sender is different from last email
|
||||||
|
const recipients = communication_doc.recipients.split(",").map((r) => r.trim());
|
||||||
|
args.cc =
|
||||||
|
recipients.filter((r) => r != frappe.session.user_email).join(", ") + ", ";
|
||||||
|
}
|
||||||
|
if (reply_all) {
|
||||||
|
// if reply_all then add cc and bcc as well.
|
||||||
|
args.cc += communication_doc.cc;
|
||||||
|
args.bcc = communication_doc.bcc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.frm.doctype === "Communication") {
|
if (this.frm.doctype === "Communication") {
|
||||||
|
|
|
||||||
|
|
@ -1463,20 +1463,20 @@ frappe.ui.form.Form = class FrappeForm {
|
||||||
// Remove actions from menu
|
// Remove actions from menu
|
||||||
delete this.custom_buttons[label];
|
delete this.custom_buttons[label];
|
||||||
let menu_item_label = group ? `${group} > ${label}` : label;
|
let menu_item_label = group ? `${group} > ${label}` : label;
|
||||||
let $linkBody = this.page
|
let $btn = this.page.is_in_group_button_dropdown(
|
||||||
.is_in_group_button_dropdown(
|
this.page.menu,
|
||||||
this.page.menu,
|
"li > a.grey-link > span",
|
||||||
"li > a.grey-link > span",
|
menu_item_label
|
||||||
menu_item_label
|
);
|
||||||
)
|
|
||||||
?.parent()
|
|
||||||
?.parent();
|
|
||||||
|
|
||||||
if ($linkBody) {
|
if ($btn) {
|
||||||
// If last button, remove divider too
|
let $linkBody = $btn.parent().parent();
|
||||||
let $divider = $linkBody.next(".dropdown-divider");
|
if ($linkBody) {
|
||||||
if ($divider) $divider.remove();
|
// If last button, remove divider too
|
||||||
$linkBody.remove();
|
let $divider = $linkBody.next(".dropdown-divider");
|
||||||
|
if ($divider) $divider.remove();
|
||||||
|
$linkBody.remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,8 @@ frappe.ui.form.FormTour = class FormTour {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.tour.steps) return;
|
||||||
|
|
||||||
if (on_finish) this.on_finish = on_finish;
|
if (on_finish) this.on_finish = on_finish;
|
||||||
|
|
||||||
this.init_driver();
|
this.init_driver();
|
||||||
|
|
|
||||||
|
|
@ -1012,15 +1012,17 @@ export default class Grid {
|
||||||
|
|
||||||
let user_settings = frappe.get_user_settings(this.frm.doctype, "GridView");
|
let user_settings = frappe.get_user_settings(this.frm.doctype, "GridView");
|
||||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
|
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
|
||||||
this.user_defined_columns = user_settings[this.doctype].map((row) => {
|
this.user_defined_columns = user_settings[this.doctype]
|
||||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
|
.map((row) => {
|
||||||
|
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
|
||||||
|
|
||||||
if (column) {
|
if (column) {
|
||||||
column.in_list_view = 1;
|
column.in_list_view = 1;
|
||||||
column.columns = row.columns;
|
column.columns = row.columns;
|
||||||
return column;
|
return column;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.filter(Boolean);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -461,6 +461,7 @@ export default class GridRow {
|
||||||
fieldname: "fields",
|
fieldname: "fields",
|
||||||
options: docfields,
|
options: docfields,
|
||||||
columns: 2,
|
columns: 2,
|
||||||
|
sort_options: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
@ -495,12 +496,31 @@ export default class GridRow {
|
||||||
|
|
||||||
const show_field = (f) => always_allow.includes(f) || !blocked_fields.includes(f);
|
const show_field = (f) => always_allow.includes(f) || !blocked_fields.includes(f);
|
||||||
|
|
||||||
|
// First, add selected fields
|
||||||
|
selected_fields.forEach((selectedField) => {
|
||||||
|
const selectedColumn = this.docfields.find(
|
||||||
|
(column) => column.fieldname === selectedField
|
||||||
|
);
|
||||||
|
if (selectedColumn && !selectedColumn.hidden && show_field(selectedColumn.fieldtype)) {
|
||||||
|
fields.push({
|
||||||
|
label: selectedColumn.label,
|
||||||
|
value: selectedColumn.fieldname,
|
||||||
|
checked: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Then, add the rest of the fields
|
||||||
this.docfields.forEach((column) => {
|
this.docfields.forEach((column) => {
|
||||||
if (!column.hidden && show_field(column.fieldtype)) {
|
if (
|
||||||
|
!selected_fields.includes(column.fieldname) &&
|
||||||
|
!column.hidden &&
|
||||||
|
show_field(column.fieldtype)
|
||||||
|
) {
|
||||||
fields.push({
|
fields.push({
|
||||||
label: column.label,
|
label: column.label,
|
||||||
value: column.fieldname,
|
value: column.fieldname,
|
||||||
checked: selected_fields ? selected_fields.includes(column.fieldname) : false,
|
checked: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@
|
||||||
<li>
|
<li>
|
||||||
<span class="form-sidebar-items">
|
<span class="form-sidebar-items">
|
||||||
<span>
|
<span>
|
||||||
<svg class="es-icon ml-0 icon-sm"><use href="#es-line-star"></use></svg>
|
<svg class="es-icon ml-0 icon-sm"><use href="#es-line-tag"></use></svg>
|
||||||
<span class="tags-label ellipsis">{%= __("Tags") %}</span>
|
<span class="tags-label ellipsis">{%= __("Tags") %}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -739,15 +739,17 @@ class FilterArea {
|
||||||
this.standard_filters_wrapper = this.list_view.page.page_form.find(
|
this.standard_filters_wrapper = this.list_view.page.page_form.find(
|
||||||
".standard-filter-section"
|
".standard-filter-section"
|
||||||
);
|
);
|
||||||
let fields = [
|
let fields = [];
|
||||||
{
|
|
||||||
|
if (!this.list_view.settings.hide_name_filter) {
|
||||||
|
fields.push({
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
label: "ID",
|
label: "ID",
|
||||||
condition: "like",
|
condition: "like",
|
||||||
fieldname: "name",
|
fieldname: "name",
|
||||||
onchange: () => this.refresh_list_view(),
|
onchange: () => this.refresh_list_view(),
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
|
||||||
if (this.list_view.custom_filter_configs) {
|
if (this.list_view.custom_filter_configs) {
|
||||||
this.list_view.custom_filter_configs.forEach((config) => {
|
this.list_view.custom_filter_configs.forEach((config) => {
|
||||||
|
|
|
||||||
|
|
@ -1819,7 +1819,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
||||||
label: __("Clear Assignment", null, "Button in list view actions menu"),
|
label: __("Clear Assignment", null, "Button in list view actions menu"),
|
||||||
action: () => {
|
action: () => {
|
||||||
frappe.confirm(
|
frappe.confirm(
|
||||||
"Are you sure you want to clear the assignments?",
|
__("Are you sure you want to clear the assignments?"),
|
||||||
() => {
|
() => {
|
||||||
this.disable_list_update = true;
|
this.disable_list_update = true;
|
||||||
bulk_operations.clear_assignment(this.get_checked_items(true), () => {
|
bulk_operations.clear_assignment(this.get_checked_items(true), () => {
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ $.extend(frappe.perm, {
|
||||||
|
|
||||||
if (!perm) {
|
if (!perm) {
|
||||||
let is_hidden = df && (cint(df.hidden) || cint(df.hidden_due_to_dependency));
|
let is_hidden = df && (cint(df.hidden) || cint(df.hidden_due_to_dependency));
|
||||||
let is_read_only = df && cint(df.read_only);
|
let is_read_only = df && (cint(df.read_only) || cint(df.is_virtual));
|
||||||
return is_hidden ? "None" : is_read_only ? "Read" : "Write";
|
return is_hidden ? "None" : is_read_only ? "Read" : "Write";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,7 +254,7 @@ frappe.router = {
|
||||||
standard_route = ["Tree", doctype_route.doctype];
|
standard_route = ["Tree", doctype_route.doctype];
|
||||||
} else {
|
} else {
|
||||||
let new_route = this.list_views_route[_route.toLowerCase()];
|
let new_route = this.list_views_route[_route.toLowerCase()];
|
||||||
let re_route = route[2].toLowerCase() !== new_route.toLowerCase();
|
let re_route = route[2].toLowerCase() !== new_route?.toLowerCase();
|
||||||
|
|
||||||
if (re_route) {
|
if (re_route) {
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ frappe.ui.Page = class Page {
|
||||||
let sidebar_toggle = $(".page-head").find(".sidebar-toggle-btn");
|
let sidebar_toggle = $(".page-head").find(".sidebar-toggle-btn");
|
||||||
let sidebar_wrapper = this.wrapper.find(".layout-side-section");
|
let sidebar_wrapper = this.wrapper.find(".layout-side-section");
|
||||||
if (this.disable_sidebar_toggle || !sidebar_wrapper.length) {
|
if (this.disable_sidebar_toggle || !sidebar_wrapper.length) {
|
||||||
sidebar_toggle.remove();
|
sidebar_toggle.last().remove();
|
||||||
} else {
|
} else {
|
||||||
if (!frappe.is_mobile()) {
|
if (!frappe.is_mobile()) {
|
||||||
sidebar_toggle.attr("title", __("Toggle Sidebar"));
|
sidebar_toggle.attr("title", __("Toggle Sidebar"));
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,9 @@ frappe.search.AwesomeBar = class AwesomeBar {
|
||||||
var route = frappe.get_route();
|
var route = frappe.get_route();
|
||||||
if (route[0] === "List" && txt.indexOf(" in") === -1) {
|
if (route[0] === "List" && txt.indexOf(" in") === -1) {
|
||||||
// search in title field
|
// search in title field
|
||||||
var meta = frappe.get_meta(frappe.container.page.list_view.doctype);
|
const doctype = frappe.container.page?.list_view?.doctype;
|
||||||
|
if (!doctype) return;
|
||||||
|
var meta = frappe.get_meta(doctype);
|
||||||
var search_field = meta.title_field || "name";
|
var search_field = meta.title_field || "name";
|
||||||
var options = {};
|
var options = {};
|
||||||
options[search_field] = ["like", "%" + txt + "%"];
|
options[search_field] = ["like", "%" + txt + "%"];
|
||||||
|
|
|
||||||
|
|
@ -317,7 +317,7 @@ frappe.search.SearchDialog = class {
|
||||||
get_link(result) {
|
get_link(result) {
|
||||||
let link = "";
|
let link = "";
|
||||||
if (result.route) {
|
if (result.route) {
|
||||||
link = `href="/app/${result.route.join("/")}"`;
|
link = `href="${frappe.router.make_url(result.route)}"`;
|
||||||
} else if (result.data_path) {
|
} else if (result.data_path) {
|
||||||
link = `data-path=${result.data_path}"`;
|
link = `data-path=${result.data_path}"`;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,27 +59,43 @@ frappe.search.utils = {
|
||||||
});
|
});
|
||||||
|
|
||||||
find(values, keywords, function (match) {
|
find(values, keywords, function (match) {
|
||||||
var out = {
|
const route = match[1];
|
||||||
route: match[1],
|
const out = { route: route };
|
||||||
};
|
|
||||||
if (match[1][0] === "Form") {
|
if (route[0] === "Form") {
|
||||||
if (match[1].length > 2 && match[1][1] !== match[1][2]) {
|
const doctype = route[1];
|
||||||
out.label = __(match[1][1]) + " " + match[1][2].bold();
|
if (route.length > 2 && doctype !== route[2]) {
|
||||||
out.value = __(match[1][1]) + " " + match[1][2];
|
const docname = route[2];
|
||||||
|
out.label = __(doctype) + " " + docname.bold();
|
||||||
|
out.value = __(doctype) + " " + docname;
|
||||||
} else {
|
} else {
|
||||||
out.label = __(match[1][1]).bold();
|
out.label = __(doctype).bold();
|
||||||
out.value = __(match[1][1]);
|
out.value = __(doctype);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
["List", "Tree", "Workspaces", "query-report"].includes(match[1][0]) &&
|
["List", "Tree", "Workspaces", "query-report"].includes(route[0]) &&
|
||||||
match[1].length > 1
|
route.length > 1
|
||||||
) {
|
) {
|
||||||
var type = match[1][0],
|
const view_type = route[0];
|
||||||
label = type;
|
const view_name = route[1];
|
||||||
if (type === "Workspaces") label = "Workspace";
|
switch (view_type) {
|
||||||
else if (type === "query-report" || match[1][2] === "Report") label = "Report";
|
case "List":
|
||||||
out.label = __(`{0} ${label}`, [__(match[1][1]).bold()]);
|
out.label = __("{0} List", [__(view_name).bold()]);
|
||||||
out.value = __(`{0} ${label}`, [__(match[1][1])]);
|
out.value = __("{0} List", [__(view_name)]);
|
||||||
|
break;
|
||||||
|
case "Tree":
|
||||||
|
out.label = __("{0} Tree", [__(view_name).bold()]);
|
||||||
|
out.value = __("{0} Tree", [__(view_name)]);
|
||||||
|
break;
|
||||||
|
case "Workspaces":
|
||||||
|
out.label = __("{0} Workspace", [__(view_name).bold()]);
|
||||||
|
out.value = __("{0} Workspace", [__(view_name)]);
|
||||||
|
break;
|
||||||
|
case "query-report":
|
||||||
|
out.label = __("{0} Report", [__(view_name).bold()]);
|
||||||
|
out.value = __("{0} Report", [__(view_name)]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else if (match[0]) {
|
} else if (match[0]) {
|
||||||
out.label = match[0].bold();
|
out.label = match[0].bold();
|
||||||
out.value = match[0];
|
out.value = match[0];
|
||||||
|
|
|
||||||
|
|
@ -172,10 +172,15 @@ frappe.views.CommunicationComposer = class {
|
||||||
reqd: 1,
|
reqd: 1,
|
||||||
fieldname: "sender",
|
fieldname: "sender",
|
||||||
options: this.user_email_accounts,
|
options: this.user_email_accounts,
|
||||||
|
onchange: () => {
|
||||||
|
this.setup_recipients_if_reply();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
//Preselect email senders if there is only one
|
//Preselect email senders if there is only one
|
||||||
if (this.user_email_accounts.length == 1) {
|
if (this.user_email_accounts.length == 1) {
|
||||||
this["sender"] = this.user_email_accounts;
|
this["sender"] = this.user_email_accounts;
|
||||||
|
} else if (this.user_email_accounts.includes(frappe.session.user_email)) {
|
||||||
|
this["sender"] = frappe.session.user_email;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,6 +230,43 @@ frappe.views.CommunicationComposer = class {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setup_recipients_if_reply() {
|
||||||
|
if (!this.is_a_reply || !this.last_email) return;
|
||||||
|
let sender = this.dialog.get_value("sender");
|
||||||
|
if (!sender) return;
|
||||||
|
const fields = {
|
||||||
|
recipients: this.dialog.fields_dict.recipients,
|
||||||
|
cc: this.dialog.fields_dict.cc,
|
||||||
|
bcc: this.dialog.fields_dict.bcc,
|
||||||
|
};
|
||||||
|
// If same user replies to their own email, set recipients to last email recipients
|
||||||
|
if (this.last_email.sender == sender) {
|
||||||
|
fields.recipients.set_value(this.last_email.recipients);
|
||||||
|
if (this.reply_all) {
|
||||||
|
fields.cc.set_value(this.last_email.cc);
|
||||||
|
fields.bcc.set_value(this.last_email.bcc);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fields.recipients.set_value(this.last_email.sender);
|
||||||
|
if (this.reply_all) {
|
||||||
|
// if sending reply add ( last email's recipients - sender's email_id ) to cc.
|
||||||
|
const recipients = this.last_email.recipients.split(",").map((r) => r.trim());
|
||||||
|
if (!this.cc) {
|
||||||
|
this.cc = "";
|
||||||
|
}
|
||||||
|
const cc_array = this.cc.split(",").map((r) => r.trim());
|
||||||
|
if (this.cc && !this.cc.endsWith(", ")) {
|
||||||
|
this.cc += ", ";
|
||||||
|
}
|
||||||
|
this.cc += recipients
|
||||||
|
.filter((r) => !cc_array.includes(r) && r != sender)
|
||||||
|
.join(", ");
|
||||||
|
this.cc = this.cc.replace(sender + ", ", "");
|
||||||
|
fields.cc.set_value(this.cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setup_subject_and_recipients() {
|
setup_subject_and_recipients() {
|
||||||
this.subject = this.subject || "";
|
this.subject = this.subject || "";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ frappe.views.Workspace = class Workspace {
|
||||||
$(".item-anchor").on("click", () => {
|
$(".item-anchor").on("click", () => {
|
||||||
$(".list-sidebar.hidden-xs.hidden-sm").removeClass("opened");
|
$(".list-sidebar.hidden-xs.hidden-sm").removeClass("opened");
|
||||||
$(".close-sidebar").css("display", "none");
|
$(".close-sidebar").css("display", "none");
|
||||||
|
$("body").css("overflow", "auto");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export default class LinksWidget extends Widget {
|
||||||
link_count: this.links.length,
|
link_count: this.links.length,
|
||||||
label: this.label,
|
label: this.label,
|
||||||
hidden: this.hidden,
|
hidden: this.hidden,
|
||||||
|
description: this.description,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,6 +25,22 @@ export default class LinksWidget extends Widget {
|
||||||
this.options.links = this.links;
|
this.options.links = this.links;
|
||||||
}
|
}
|
||||||
this.widget.addClass("links-widget-box");
|
this.widget.addClass("links-widget-box");
|
||||||
|
|
||||||
|
if (this.description) {
|
||||||
|
const description = $(`
|
||||||
|
<button class="btn-reset card-description-btn ml-2">
|
||||||
|
${frappe.utils.icon("help", "sm")}
|
||||||
|
</button>
|
||||||
|
`).appendTo(this.widget.find(".widget-title"));
|
||||||
|
|
||||||
|
description.popover({
|
||||||
|
trigger: "hover",
|
||||||
|
placement: "top",
|
||||||
|
content: () => `<div class="card-description small">${__(this.description)}</div>`,
|
||||||
|
html: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const is_link_disabled = (item) => {
|
const is_link_disabled = (item) => {
|
||||||
return item.dependencies && item.incomplete_dependencies;
|
return item.dependencies && item.incomplete_dependencies;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,8 @@ export default class QuickListWidget extends Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_quick_list_item(doc) {
|
setup_quick_list_item(doc) {
|
||||||
|
const indicator = frappe.get_indicator(doc, this.document_type);
|
||||||
|
|
||||||
let $quick_list_item = $(`
|
let $quick_list_item = $(`
|
||||||
<div class="quick-list-item">
|
<div class="quick-list-item">
|
||||||
<div class="ellipsis left">
|
<div class="ellipsis left">
|
||||||
|
|
@ -147,6 +149,14 @@ export default class QuickListWidget extends Widget {
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
if (indicator) {
|
||||||
|
$(`
|
||||||
|
<div class="status indicator-pill ${indicator[1]} ellipsis">
|
||||||
|
${__(indicator[0])}
|
||||||
|
</div>
|
||||||
|
`).appendTo($quick_list_item);
|
||||||
|
}
|
||||||
|
|
||||||
$(`<div class="right-arrow">${frappe.utils.icon("right", "xs")}</div>`).appendTo(
|
$(`<div class="right-arrow">${frappe.utils.icon("right", "xs")}</div>`).appendTo(
|
||||||
$quick_list_item
|
$quick_list_item
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -219,7 +219,13 @@ class CardDialog extends WidgetDialog {
|
||||||
{
|
{
|
||||||
fieldtype: "Data",
|
fieldtype: "Data",
|
||||||
fieldname: "label",
|
fieldname: "label",
|
||||||
label: "Label",
|
label: __("Label"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "HTML Editor",
|
||||||
|
fieldname: "description",
|
||||||
|
label: __("Description"),
|
||||||
|
max_height: "7rem",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldname: "links",
|
fieldname: "links",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
/* global gapi:false, google:false */
|
/* global gapi:false, google:false */
|
||||||
export default class GoogleDrivePicker {
|
export default class GoogleDrivePicker {
|
||||||
constructor({ pickerCallback, enabled, appId, developerKey, clientId } = {}) {
|
constructor({ pickerCallback, enabled, appId, clientId } = {}) {
|
||||||
this.scope = "https://www.googleapis.com/auth/drive.file";
|
this.scope = "https://www.googleapis.com/auth/drive.file";
|
||||||
this.pickerApiLoaded = false;
|
this.pickerApiLoaded = false;
|
||||||
this.enabled = enabled;
|
this.enabled = enabled;
|
||||||
this.appId = appId;
|
this.appId = appId;
|
||||||
this.pickerCallback = pickerCallback;
|
this.pickerCallback = pickerCallback;
|
||||||
this.developerKey = developerKey;
|
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +44,6 @@ export default class GoogleDrivePicker {
|
||||||
createPicker(access_token) {
|
createPicker(access_token) {
|
||||||
this.view = new google.picker.View(google.picker.ViewId.DOCS);
|
this.view = new google.picker.View(google.picker.ViewId.DOCS);
|
||||||
this.picker = new google.picker.PickerBuilder()
|
this.picker = new google.picker.PickerBuilder()
|
||||||
.setDeveloperKey(this.developerKey)
|
|
||||||
.setAppId(this.appId)
|
.setAppId(this.appId)
|
||||||
.setOAuthToken(access_token)
|
.setOAuthToken(access_token)
|
||||||
.addView(this.view)
|
.addView(this.view)
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
:group="df.fieldname"
|
:group="df.fieldname"
|
||||||
handle=".icon-drag"
|
handle=".icon-drag"
|
||||||
>
|
>
|
||||||
<template #item="{ element }">
|
<template #item="{ element: column }">
|
||||||
<div class="mt-2 row align-center column-row" v-for="column in df.table_columns">
|
<div class="mt-2 row align-center column-row">
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<div class="column-label d-flex align-center">
|
<div class="column-label d-flex align-center">
|
||||||
<div class="px-2 icon-drag ml-n2">
|
<div class="px-2 icon-drag ml-n2">
|
||||||
|
|
|
||||||
|
|
@ -126,9 +126,9 @@ function configure_columns() {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
on_page_show: () => {
|
on_page_show: () => {
|
||||||
createApp(ConfigureColumnsVue, { df: props.df }).mount(
|
const app = createApp(ConfigureColumnsVue, { df: props.df });
|
||||||
dialog.get_field("columns_area").$wrapper.get(0)
|
SetVueGlobals(app);
|
||||||
);
|
app.mount(dialog.get_field("columns_area").$wrapper.get(0));
|
||||||
},
|
},
|
||||||
on_hide: () => {
|
on_hide: () => {
|
||||||
props.df["table_columns"] = props.df.table_columns.filter((col) => !col.invalid_width);
|
props.df["table_columns"] = props.df.table_columns.filter((col) => !col.invalid_width);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue