diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index faab3344a6..d9603e89aa 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -98,8 +98,6 @@ rules:
languages: [python]
severity: WARNING
paths:
- exclude:
- - test_*.py
include:
- "*/**/doctype/*"
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
index b2cc4b16fc..5a5098bf50 100644
--- a/.github/helper/semgrep_rules/security.yml
+++ b/.github/helper/semgrep_rules/security.yml
@@ -8,10 +8,6 @@ rules:
dynamic content. Avoid it or use safe_eval().
languages: [python]
severity: ERROR
- paths:
- exclude:
- - frappe/__init__.py
- - frappe/commands/utils.py
- id: frappe-sqli-format-strings
patterns:
diff --git a/.github/semantic.yml b/.github/semantic.yml
index e1e53bc1a4..fa15046b4a 100644
--- a/.github/semantic.yml
+++ b/.github/semantic.yml
@@ -11,3 +11,20 @@ allowRevertCommits: true
# For allowed PR types: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
# Tool Reference: https://github.com/zeke/semantic-pull-requests
+
+# By default types specified in commitizen/conventional-commit-types is used.
+# See: https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json
+# You can override the valid types
+types:
+ - BREAKING CHANGE
+ - feat
+ - fix
+ - docs
+ - style
+ - refactor
+ - perf
+ - test
+ - build
+ - ci
+ - chore
+ - revert
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index 389524e968..e27b406df0 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -1,34 +1,18 @@
name: Semgrep
on:
- pull_request:
- branches:
- - develop
- - version-13-hotfix
- - version-13-pre-release
+ pull_request: { }
+
jobs:
semgrep:
name: Frappe Linter
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
- - name: Setup python3
- uses: actions/setup-python@v2
- with:
- python-version: 3.8
-
- - name: Setup semgrep
- run: |
- python -m pip install -q semgrep
- git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
-
- - name: Semgrep errors
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
- semgrep --config="r/python.lang.correctness" --quiet --error $files
-
- - name: Semgrep warnings
- run: |
- files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
- [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
+ - uses: actions/checkout@v2
+ - uses: returntocorp/semgrep-action@v1
+ env:
+ SEMGREP_TIMEOUT: 120
+ with:
+ config: >-
+ r/python.lang.correctness
+ .github/helper/semgrep_rules
diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js
new file mode 100644
index 0000000000..7e1426aa46
--- /dev/null
+++ b/cypress/integration/navigation.js
@@ -0,0 +1,14 @@
+context('Navigation', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ });
+ it('Navigate to route with hash in document name', () => {
+ cy.insert_doc('ToDo', {'__newname': 'ABC#123', 'description': 'Test this', 'ignore_duplicate': true});
+ cy.visit('/app/todo/ABC#123');
+ cy.title().should('eq', 'Test this - ABC#123');
+ cy.get_field('description', 'Text Editor').contains('Test this');
+ cy.go('back');
+ cy.title().should('eq', 'Website');
+ });
+});
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index efa1959969..9074beae06 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -8,6 +8,7 @@ let yargs = require("yargs");
let cliui = require("cliui")();
let chalk = require("chalk");
let html_plugin = require("./frappe-html");
+let rtlcss = require('rtlcss');
let postCssPlugin = require("esbuild-plugin-postcss2").default;
let ignore_assets = require("./ignore-assets");
let sass_options = require("./sass_options");
@@ -96,9 +97,9 @@ async function execute() {
await clean_dist_folders(APPS);
}
- let result;
+ let results;
try {
- result = await build_assets_for_apps(APPS, FILES_TO_BUILD);
+ results = await build_assets_for_apps(APPS, FILES_TO_BUILD);
} catch (e) {
log_error("There were some problems during build");
log();
@@ -107,13 +108,15 @@ async function execute() {
}
if (!WATCH_MODE) {
- log_built_assets(result.metafile);
+ log_built_assets(results);
console.timeEnd(TOTAL_BUILD_TIME);
log();
} else {
log("Watching for changes...");
}
- return await write_assets_json(result.metafile);
+ for (const result of results) {
+ await write_assets_json(result.metafile);
+ }
}
function build_assets_for_apps(apps, files) {
@@ -125,6 +128,8 @@ function build_assets_for_apps(apps, files) {
let output_path = assets_path;
let file_map = {};
+ let style_file_map = {};
+ let rtl_style_file_map = {};
for (let file of files) {
let relative_app_path = path.relative(apps_path, file);
let app = relative_app_path.split(path.sep)[0];
@@ -140,19 +145,32 @@ function build_assets_for_apps(apps, files) {
}
output_name = path.join(app, "dist", output_name);
- if (Object.keys(file_map).includes(output_name)) {
+ if (Object.keys(file_map).includes(output_name) || Object.keys(style_file_map).includes(output_name)) {
log_warn(
`Duplicate output file ${output_name} generated from ${file}`
);
}
-
- file_map[output_name] = file;
+ if ([".css", ".scss", ".less", ".sass", ".styl"].includes(extension)) {
+ style_file_map[output_name] = file;
+ rtl_style_file_map[output_name.replace('/css/', '/css-rtl/')] = file;
+ } else {
+ file_map[output_name] = file;
+ }
}
-
- return build_files({
+ let build = build_files({
files: file_map,
outdir: output_path
});
+ let style_build = build_style_files({
+ files: style_file_map,
+ outdir: output_path
+ });
+ let rtl_style_build = build_style_files({
+ files: rtl_style_file_map,
+ outdir: output_path,
+ rtl_style: true
+ });
+ return Promise.all([build, style_build, rtl_style_build]);
});
}
@@ -203,7 +221,33 @@ function get_files_to_build(files) {
}
function build_files({ files, outdir }) {
- return esbuild.build({
+ let build_plugins = [
+ html_plugin,
+ vue(),
+ ];
+ return esbuild.build(get_build_options(files, outdir, build_plugins));
+}
+
+function build_style_files({ files, outdir, rtl_style=false }) {
+ let plugins = [];
+ if (rtl_style) {
+ plugins.push(rtlcss);
+ }
+
+ let build_plugins = [
+ ignore_assets,
+ postCssPlugin({
+ plugins: plugins,
+ sassOptions: sass_options
+ })
+ ];
+
+ plugins.push(require("autoprefixer"));
+ return esbuild.build(get_build_options(files, outdir, build_plugins));
+}
+
+function get_build_options(files, outdir, plugins) {
+ return {
entryPoints: files,
entryNames: "[dir]/[name].[hash]",
outdir,
@@ -217,17 +261,9 @@ function build_files({ files, outdir }) {
PRODUCTION ? "production" : "development"
)
},
- plugins: [
- html_plugin,
- ignore_assets,
- vue(),
- postCssPlugin({
- plugins: [require("autoprefixer")],
- sassOptions: sass_options
- })
- ],
+ plugins: plugins,
watch: get_watch_config()
- });
+ };
}
function get_watch_config() {
@@ -260,7 +296,8 @@ async function clean_dist_folders(apps) {
let public_path = get_public_path(app);
let paths = [
path.resolve(public_path, "dist", "js"),
- path.resolve(public_path, "dist", "css")
+ path.resolve(public_path, "dist", "css"),
+ path.resolve(public_path, "dist", "css-rtl")
];
for (let target of paths) {
if (fs.existsSync(target)) {
@@ -272,7 +309,11 @@ async function clean_dist_folders(apps) {
}
}
-function log_built_assets(metafile) {
+function log_built_assets(results) {
+ let outputs = {};
+ for (const result of results) {
+ outputs = Object.assign(outputs, result.metafile.outputs);
+ }
let column_widths = [60, 20];
cliui.div(
{
@@ -287,9 +328,9 @@ function log_built_assets(metafile) {
cliui.div("");
let output_by_dist_path = {};
- for (let outfile in metafile.outputs) {
+ for (let outfile in outputs) {
if (outfile.endsWith(".map")) continue;
- let data = metafile.outputs[outfile];
+ let data = outputs[outfile];
outfile = path.resolve(outfile);
outfile = path.relative(assets_path, outfile);
let filename = path.basename(outfile);
@@ -344,7 +385,11 @@ async function write_assets_json(metafile) {
let info = metafile.outputs[output];
let asset_path = "/" + path.relative(sites_path, output);
if (info.entryPoint) {
- out[path.basename(info.entryPoint)] = asset_path;
+ let key = path.basename(info.entryPoint);
+ if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) {
+ key = `rtl_${key}`;
+ }
+ out[key] = asset_path;
}
}
@@ -483,4 +528,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
log(" " + filename);
}
log();
-}
+}
\ No newline at end of file
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 1c978945c7..b4728f9ac3 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -28,6 +28,8 @@ from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
from .utils.lazy_loader import lazy_import
+from frappe.query_builder import get_query_builder
+
# Lazy imports
faker = lazy_import('faker')
@@ -118,6 +120,7 @@ def set_user_lang(user, user_language=None):
# local-globals
db = local("db")
+qb = local("qb")
conf = local("conf")
form = form_dict = local("form_dict")
request = local("request")
@@ -202,6 +205,7 @@ def init(site, sites_path=None, new_site=False):
local.form_dict = _dict()
local.session = _dict()
local.dev_server = _dev_server
+ local.qb = get_query_builder(local.conf.db_type or "mariadb")
setup_module_map()
diff --git a/frappe/auth.py b/frappe/auth.py
index ef79d96ddb..fc1cb09e1a 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -1,31 +1,58 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-import datetime
-from frappe import _
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+from urllib.parse import quote
+
import frappe
import frappe.database
import frappe.utils
-from frappe.utils import cint, flt, get_datetime, datetime, date_diff, today
import frappe.utils.user
-from frappe import conf
-from frappe.sessions import Session, clear_sessions, delete_session
-from frappe.modules.patch_handler import check_session_stopped
-from frappe.translate import get_lang_code
-from frappe.utils.password import check_password, delete_login_failed_cache
+from frappe import _, conf
from frappe.core.doctype.activity_log.activity_log import add_authentication_log
-from frappe.twofactor import (should_run_2fa, authenticate_for_2factor,
- confirm_otp_token, get_cached_user_pass)
+from frappe.modules.patch_handler import check_session_stopped
+from frappe.sessions import Session, clear_sessions, delete_session
+from frappe.translate import get_language
+from frappe.twofactor import authenticate_for_2factor, confirm_otp_token, get_cached_user_pass, should_run_2fa
+from frappe.utils import cint, date_diff, datetime, get_datetime, today
+from frappe.utils.password import check_password
from frappe.website.utils import get_home_page
-from urllib.parse import quote
class HTTPRequest:
def __init__(self):
- # Get Environment variables
- self.domain = frappe.request.host
- if self.domain and self.domain.startswith('www.'):
- self.domain = self.domain[4:]
+ # set frappe.local.request_ip
+ self.set_request_ip()
+ # load cookies
+ self.set_cookies()
+
+ # set frappe.local.db
+ self.connect()
+
+ # login and start/resume user session
+ self.set_session()
+
+ # set request language
+ self.set_lang()
+
+ # match csrf token from current session
+ self.validate_csrf_token()
+
+ # write out latest cookies
+ frappe.local.cookie_manager.init_cookies()
+
+ # check session status
+ check_session_stopped()
+
+ @property
+ def domain(self):
+ if not getattr(self, "_domain", None):
+ self._domain = frappe.request.host
+ if self._domain and self._domain.startswith('www.'):
+ self._domain = self._domain[4:]
+
+ return self._domain
+
+ def set_request_ip(self):
if frappe.get_request_header('X-Forwarded-For'):
frappe.local.request_ip = (frappe.get_request_header('X-Forwarded-For').split(",")[0]).strip()
@@ -35,37 +62,21 @@ class HTTPRequest:
else:
frappe.local.request_ip = '127.0.0.1'
- # language
- self.set_lang()
-
- # load cookies
+ def set_cookies(self):
frappe.local.cookie_manager = CookieManager()
- # set db
- self.connect()
-
- # login
+ def set_session(self):
frappe.local.login_manager = LoginManager()
- if frappe.form_dict._lang:
- lang = get_lang_code(frappe.form_dict._lang)
- if lang:
- frappe.local.lang = lang
-
- self.validate_csrf_token()
-
- # write out latest cookies
- frappe.local.cookie_manager.init_cookies()
-
- # check status
- check_session_stopped()
-
def validate_csrf_token(self):
if frappe.local.request and frappe.local.request.method in ("POST", "PUT", "DELETE"):
- if not frappe.local.session: return
- if not frappe.local.session.data.csrf_token \
- or frappe.local.session.data.device=="mobile" \
- or frappe.conf.get('ignore_csrf', None):
+ if not frappe.local.session:
+ return
+ if (
+ not frappe.local.session.data.csrf_token
+ or frappe.local.session.data.device == "mobile"
+ or frappe.conf.get('ignore_csrf', None)
+ ):
# not via boot
return
@@ -79,17 +90,18 @@ class HTTPRequest:
frappe.throw(_("Invalid Request"), frappe.CSRFTokenError)
def set_lang(self):
- from frappe.translate import guess_language
- frappe.local.lang = guess_language()
+ frappe.local.lang = get_language()
def get_db_name(self):
"""get database name from conf"""
return conf.db_name
- def connect(self, ac_name = None):
+ def connect(self):
"""connect to db, from ac_name or db_name"""
- frappe.local.db = frappe.database.get_db(user = self.get_db_name(), \
- password = getattr(conf, 'db_password', ''))
+ frappe.local.db = frappe.database.get_db(
+ user=self.get_db_name(),
+ password=getattr(conf, 'db_password', '')
+ )
class LoginManager:
def __init__(self):
@@ -142,8 +154,9 @@ class LoginManager:
self.make_session()
self.setup_boot_cache()
self.set_user_info()
+ self.clear_preferred_language()
- def get_user_info(self, resume=False):
+ def get_user_info(self):
self.info = frappe.db.get_value("User", self.user,
["user_type", "first_name", "last_name", "user_image"], as_dict=1)
@@ -181,11 +194,13 @@ class LoginManager:
frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel('redirect_after_login', self.user)
-
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user)
frappe.local.cookie_manager.set_cookie("user_image", self.info.user_image or "")
+ def clear_preferred_language(self):
+ frappe.local.cookie_manager.delete_cookie("preferred_language")
+
def make_session(self, resume=False):
# start session
frappe.local.session_obj = Session(user=self.user, resume=resume,
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 9f09f26be8..c17ae583ed 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -141,17 +141,13 @@ def build_table_count_cache():
return
_cache = frappe.cache()
- data = frappe.db.multisql({
- "mariadb": """
- SELECT table_name AS name,
- table_rows AS count
- FROM information_schema.tables""",
- "postgres": """
- SELECT "relname" AS name,
- "n_tup_ins" AS count
- FROM "pg_stat_all_tables"
- """
- }, as_dict=1)
+ table_name = frappe.qb.Field("table_name").as_("name")
+ table_rows = frappe.qb.Field("table_rows").as_("count")
+ information_schema = frappe.qb.Schema("information_schema")
+
+ query = frappe.qb.from_(information_schema.tables).select(table_name, table_rows)
+
+ data = frappe.db.sql(query, as_dict=1)
counts = {d.get('name').lstrip('tab'): d.get('count', None) for d in data}
_cache.set_value("information_schema:counts", counts)
diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py
index be9d107025..9ed333d034 100644
--- a/frappe/commands/__init__.py
+++ b/frappe/commands/__init__.py
@@ -102,7 +102,9 @@ def get_commands():
from .site import commands as site_commands
from .translate import commands as translate_commands
from .utils import commands as utils_commands
+ from .redis import commands as redis_commands
- return list(set(scheduler_commands + site_commands + translate_commands + utils_commands))
+ all_commands = scheduler_commands + site_commands + translate_commands + utils_commands + redis_commands
+ return list(set(all_commands))
commands = get_commands()
diff --git a/frappe/commands/redis.py b/frappe/commands/redis.py
new file mode 100644
index 0000000000..38a46c2142
--- /dev/null
+++ b/frappe/commands/redis.py
@@ -0,0 +1,53 @@
+import os
+
+import click
+
+import frappe
+from frappe.utils.rq import RedisQueue
+from frappe.installer import update_site_config
+
+@click.command('create-rq-users')
+@click.option('--set-admin-password', is_flag=True, default=False, help='Set new Redis admin(default user) password')
+@click.option('--use-rq-auth', is_flag=True, default=False, help='Enable Redis authentication for sites')
+def create_rq_users(set_admin_password=False, use_rq_auth=False):
+ """Create Redis Queue users and add to acl and app configs.
+
+ acl config file will be used by redis server while starting the server
+ and app config is used by app while connecting to redis server.
+ """
+ acl_file_path = os.path.abspath('../config/redis_queue.acl')
+
+ with frappe.init_site():
+ acl_list, user_credentials = RedisQueue.gen_acl_list(
+ set_admin_password=set_admin_password)
+
+ with open(acl_file_path, 'w') as f:
+ f.writelines([acl+'\n' for acl in acl_list])
+
+ sites_path = os.getcwd()
+ common_site_config_path = os.path.join(sites_path, 'common_site_config.json')
+ update_site_config("rq_username", user_credentials['bench'][0], validate=False,
+ site_config_path=common_site_config_path)
+ update_site_config("rq_password", user_credentials['bench'][1], validate=False,
+ site_config_path=common_site_config_path)
+ update_site_config("use_rq_auth", use_rq_auth, validate=False,
+ site_config_path=common_site_config_path)
+
+ click.secho('* ACL and site configs are updated with new user credentials. '
+ 'Please restart Redis Queue server to enable namespaces.',
+ fg='green')
+
+ if set_admin_password:
+ env_key = 'RQ_ADMIN_PASWORD'
+ click.secho('* Redis admin password is successfully set up. '
+ 'Include below line in .bashrc file for system to use',
+ fg='green')
+ click.secho(f"`export {env_key}={user_credentials['default'][1]}`")
+ click.secho('NOTE: Please save the admin password as you '
+ 'can not access redis server without the password',
+ fg='yellow')
+
+
+commands = [
+ create_rq_users
+]
diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py
index d69ebb3024..f82473fd55 100755
--- a/frappe/commands/scheduler.py
+++ b/frappe/commands/scheduler.py
@@ -172,9 +172,13 @@ def start_scheduler():
@click.command('worker')
@click.option('--queue', type=str)
@click.option('--quiet', is_flag = True, default = False, help = 'Hide Log Outputs')
-def start_worker(queue, quiet = False):
+@click.option('-u', '--rq-username', default=None, help='Redis ACL user')
+@click.option('-p', '--rq-password', default=None, help='Redis ACL user password')
+def start_worker(queue, quiet = False, rq_username=None, rq_password=None):
+ """Site is used to find redis credentals.
+ """
from frappe.utils.background_jobs import start_worker
- start_worker(queue, quiet = quiet)
+ start_worker(queue, quiet = quiet, rq_username=rq_username, rq_password=rq_password)
@click.command('ready-for-migration')
@click.option('--site', help='site name')
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index ca58e78870..8fc6877d4f 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -1,5 +1,3 @@
-# -*- coding: utf-8 -*-
-
import json
import os
import subprocess
@@ -14,6 +12,13 @@ from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import get_bench_path, update_progress_bar, cint
+DATA_IMPORT_DEPRECATION = click.style(
+ "[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
+ "Use `data-import` command instead to import data via 'Data Import'.",
+ fg="yellow"
+)
+
+
@click.command('build')
@click.option('--app', help='Build assets for app')
@click.option('--apps', help='Build assets for specific apps')
@@ -350,7 +355,8 @@ def import_doc(context, path, force=False):
if not context.sites:
raise SiteNotSpecifiedError
-@click.command('import-csv')
+
+@click.command('import-csv', help=DATA_IMPORT_DEPRECATION)
@click.argument('path')
@click.option('--only-insert', default=False, is_flag=True, help='Do not overwrite existing records')
@click.option('--submit-after-import', default=False, is_flag=True, help='Submit document after importing it')
@@ -358,32 +364,8 @@ def import_doc(context, path, force=False):
@click.option('--no-email', default=True, is_flag=True, help='Send email if applicable')
@pass_context
def import_csv(context, path, only_insert=False, submit_after_import=False, ignore_encoding_errors=False, no_email=True):
- "Import CSV using data import"
- from frappe.core.doctype.data_import_legacy import importer
- from frappe.utils.csvutils import read_csv_content
- site = get_site(context)
-
- if not os.path.exists(path):
- path = os.path.join('..', path)
- if not os.path.exists(path):
- print('Invalid path {0}'.format(path))
- sys.exit(1)
-
- with open(path, 'r') as csvfile:
- content = read_csv_content(csvfile.read())
-
- frappe.init(site=site)
- frappe.connect()
-
- try:
- importer.upload(content, submit_after_import=submit_after_import, no_email=no_email,
- ignore_encoding_errors=ignore_encoding_errors, overwrite=not only_insert,
- via_console=True)
- frappe.db.commit()
- except Exception:
- print(frappe.get_traceback())
-
- frappe.destroy()
+ click.secho(DATA_IMPORT_DEPRECATION)
+ sys.exit(1)
@click.command('data-import')
@@ -569,32 +551,16 @@ def run_tests(context, app=None, module=None, doctype=None, test=(), profile=Fal
if coverage:
from coverage import Coverage
+ from frappe.coverage import STANDARD_INCLUSIONS, STANDARD_EXCLUSIONS, FRAPPE_EXCLUSIONS
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', app or 'frappe')
- incl = [
- '*.py',
- ]
- omit = [
- '*.js',
- '*.xml',
- '*.pyc',
- '*.css',
- '*.less',
- '*.scss',
- '*.vue',
- '*.html',
- '*/test_*',
- '*/node_modules/*',
- '*/doctype/*/*_dashboard.py',
- '*/patches/*',
- ]
+ omit = STANDARD_EXCLUSIONS[:]
if not app or app == 'frappe':
- omit.append('*/tests/*')
- omit.append('*/commands/*')
+ omit.extend(FRAPPE_EXCLUSIONS)
- cov = Coverage(source=[source_path], omit=omit, include=incl)
+ cov = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
cov.start()
ret = frappe.test_runner.main(app, module, doctype, context.verbose, tests=tests,
@@ -767,26 +733,49 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False):
frappe.destroy()
-@click.command('version')
-def get_version():
- "Show the versions of all the installed apps"
+@click.command("version")
+@click.option("-f", "--format", "output",
+ type=click.Choice(["plain", "table", "json", "legacy"]), help="Output format", default="legacy")
+def get_version(output):
+ """Show the versions of all the installed apps."""
from git import Repo
+ from frappe.utils.commands import render_table
from frappe.utils.change_log import get_app_branch
- frappe.init('')
+
+ frappe.init("")
+ data = []
for app in sorted(frappe.get_all_apps()):
- branch_name = get_app_branch(app)
module = frappe.get_module(app)
app_hooks = frappe.get_module(app + ".hooks")
repo = Repo(frappe.get_app_path(app, ".."))
- branch = repo.head.ref.name
- commit = repo.head.ref.commit.hexsha[:7]
- if hasattr(app_hooks, '{0}_version'.format(branch_name)):
- click.echo("{0} {1} {2} ({3})".format(app, getattr(app_hooks, '{0}_version'.format(branch_name)), branch, commit))
+ app_info = frappe._dict()
+ app_info.app = app
+ app_info.branch = get_app_branch(app)
+ app_info.commit = repo.head.object.hexsha[:7]
+ app_info.version = getattr(app_hooks, f"{app_info.branch}_version", None) or module.__version__
- elif hasattr(module, "__version__"):
- click.echo("{0} {1} {2} ({3})".format(app, module.__version__, branch, commit))
+ data.append(app_info)
+
+ {
+ "legacy": lambda: [
+ click.echo(f"{app_info.app} {app_info.version}")
+ for app_info in data
+ ],
+ "plain": lambda: [
+ click.echo(f"{app_info.app} {app_info.version} {app_info.branch} ({app_info.commit})")
+ for app_info in data
+ ],
+ "table": lambda: render_table(
+ [["App", "Version", "Branch", "Commit"]] +
+ [
+ [app_info.app, app_info.version, app_info.branch, app_info.commit]
+ for app_info in data
+ ]
+ ),
+ "json": lambda: click.echo(json.dumps(data, indent=4)),
+ }[output]()
@click.command('rebuild-global-search')
diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py
index 62a877be24..e7f0f1a763 100644
--- a/frappe/config/__init__.py
+++ b/frappe/config/__init__.py
@@ -39,18 +39,13 @@ def get_modules_from_app(app):
)
def get_all_empty_tables_by_module():
- empty_tables = set(r[0] for r in frappe.db.multisql({
- "mariadb": """
- SELECT table_name
- FROM information_schema.tables
- WHERE table_rows = 0 and table_schema = "{}"
- """.format(frappe.conf.db_name),
- "postgres": """
- SELECT "relname" as "table_name"
- FROM "pg_stat_all_tables"
- WHERE n_tup_ins = 0
- """
- }))
+ table_rows = frappe.qb.Field("table_rows")
+ table_name = frappe.qb.Field("table_name")
+ information_schema = frappe.qb.Schema("information_schema")
+
+ query = frappe.qb.from_(information_schema.tables).select(table_name).where(table_rows == 0)
+
+ empty_tables = {r[0] for r in frappe.db.sql(query)}
results = frappe.get_all("DocType", fields=["name", "module"])
empty_tables_by_module = {}
diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py
index d2fbee108b..2ea014f981 100644
--- a/frappe/core/doctype/access_log/access_log.py
+++ b/frappe/core/doctype/access_log/access_log.py
@@ -1,9 +1,5 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
-
-# imports - standard imports
-# imports - module imports
import frappe
from frappe.model.document import Document
diff --git a/frappe/core/doctype/activity_log/feed.py b/frappe/core/doctype/activity_log/feed.py
index caa3cae613..19d7b77184 100644
--- a/frappe/core/doctype/activity_log/feed.py
+++ b/frappe/core/doctype/activity_log/feed.py
@@ -29,10 +29,12 @@ def update_feed(doc, method=None):
name = feed.name or doc.name
# delete earlier feed
- frappe.db.sql("""delete from `tabActivity Log`
- where
- reference_doctype=%s and reference_name=%s
- and link_doctype=%s""", (doctype, name,feed.link_doctype))
+ frappe.db.delete("Activity Log", {
+ "reference_doctype": doctype,
+ "reference_name": name,
+ "link_doctype": feed.link_doctype
+ })
+
frappe.get_doc({
"doctype": "Activity Log",
"reference_doctype": doctype,
diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py
index 160018c4a2..52cd370890 100644
--- a/frappe/core/doctype/communication/mixins.py
+++ b/frappe/core/doctype/communication/mixins.py
@@ -3,6 +3,7 @@ from frappe import _
from frappe.core.utils import get_parent_doc
from frappe.utils import parse_addr, get_formatted_email, get_url
from frappe.email.doctype.email_account.email_account import EmailAccount
+from frappe.desk.doctype.todo.todo import ToDo
class CommunicationEmailMixin:
"""Mixin class to handle communication mails.
@@ -76,6 +77,7 @@ class CommunicationEmailMixin:
if is_inbound_mail_communcation:
cc.append(self.get_owner())
cc = set(cc) - {self.sender_mailid}
+ cc.update(self.get_assignees())
cc = set(cc) - set(self.filter_thread_notification_disbled_users(cc))
cc = cc - set(self.mail_recipients(is_inbound_mail_communcation=is_inbound_mail_communcation))
@@ -176,8 +178,8 @@ class CommunicationEmailMixin:
def mail_attachments(self, print_format=None, print_html=None):
final_attachments = []
- if print_format and print_html:
- d = {'print_format': print_format, 'print_html': print_html, 'print_format_attachment': 1,
+ if print_format or print_html:
+ d = {'print_format': print_format, 'html': print_html, 'print_format_attachment': 1,
'doctype': self.reference_doctype, 'name': self.reference_name}
final_attachments.append(d)
@@ -201,6 +203,13 @@ class CommunicationEmailMixin:
self.mail_cc(is_inbound_mail_communcation = is_inbound_mail_communcation, include_sender=include_sender)
return set(all_ids) - set(final_ids)
+ def get_assignees(self):
+ """Get owners of the reference document.
+ """
+ filters = {'status': 'Open', 'reference_name': self.reference_name,
+ 'reference_type': self.reference_doctype}
+ return ToDo.get_owners(filters)
+
@staticmethod
def filter_thread_notification_disbled_users(emails):
"""Filter users based on notifications for email threads setting is disabled.
diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py
index 389948449e..ffd828bfdb 100644
--- a/frappe/core/doctype/data_export/exporter.py
+++ b/frappe/core/doctype/data_export/exporter.py
@@ -7,7 +7,6 @@ import frappe.permissions
import re, csv, os
from frappe.utils.csvutils import UnicodeWriter
from frappe.utils import cstr, formatdate, format_datetime, parse_json, cint, format_duration
-from frappe.core.doctype.data_import_legacy.importer import get_data_keys
from frappe.core.doctype.access_log.access_log import make_access_log
reflags = {
@@ -20,6 +19,15 @@ reflags = {
"D": re.DEBUG
}
+def get_data_keys():
+ return frappe._dict({
+ "data_separator": _('Start entering data below this line'),
+ "main_table": _("Table") + ":",
+ "parent_table": _("Parent Table") + ":",
+ "columns": _("Column Name") + ":",
+ "doctype": _("DocType") + ":"
+ })
+
@frappe.whitelist()
def export_data(doctype=None, parent_doctype=None, all_doctypes=True, with_data=False,
select_columns=None, file_type='CSV', template=False, filters=None):
diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py
index 7e8374a0a2..50469eeb4d 100644
--- a/frappe/core/doctype/data_import/data_import.py
+++ b/frappe/core/doctype/data_import/data_import.py
@@ -171,9 +171,6 @@ def import_file(
i.import_data()
-##############
-
-
def import_doc(path, pre_process=None):
if os.path.isdir(path):
files = [os.path.join(path, f) for f in os.listdir(path)]
@@ -192,19 +189,8 @@ def import_doc(path, pre_process=None):
)
frappe.flags.mute_emails = False
frappe.db.commit()
- elif f.endswith(".csv"):
- validate_csv_import_file(f)
- frappe.db.commit()
-
-
-def validate_csv_import_file(path):
- if path.endswith(".csv"):
- print()
- print("This method is deprecated.")
- print('Import CSV files using the command "bench --site sitename data-import"')
- print("Or use the method frappe.core.doctype.data_import.data_import.import_file")
- print()
- raise Exception("Method deprecated")
+ else:
+ raise NotImplementedError("Only .json files can be imported")
def export_json(
diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.js b/frappe/core/doctype/data_import_legacy/data_import_legacy.js
deleted file mode 100644
index 8e4f397171..0000000000
--- a/frappe/core/doctype/data_import_legacy/data_import_legacy.js
+++ /dev/null
@@ -1,324 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Data Import Legacy', {
- onload: function(frm) {
- if (frm.doc.__islocal) {
- frm.set_value("action", "");
- }
-
- frappe.call({
- method: "frappe.core.doctype.data_import_legacy.data_import_legacy.get_importable_doctypes",
- callback: function (r) {
- let importable_doctypes = r.message;
- frm.set_query("reference_doctype", function () {
- return {
- "filters": {
- "issingle": 0,
- "istable": 0,
- "name": ['in', importable_doctypes]
- }
- };
- });
- }
- }),
-
- // should never check public
- frm.fields_dict["import_file"].df.is_private = 1;
-
- frappe.realtime.on("data_import_progress", function(data) {
- if (data.data_import === frm.doc.name) {
- if (data.reload && data.reload === true) {
- frm.reload_doc();
- }
- if (data.progress) {
- let progress_bar = $(frm.dashboard.progress_area.body).find(".progress-bar");
- if (progress_bar) {
- $(progress_bar).removeClass("progress-bar-danger").addClass("progress-bar-success progress-bar-striped");
- $(progress_bar).css("width", data.progress + "%");
- }
- }
- }
- });
- },
-
- reference_doctype: function(frm){
- if (frm.doc.reference_doctype) {
- frappe.model.with_doctype(frm.doc.reference_doctype);
- }
- },
-
- refresh: function(frm) {
- frm.disable_save();
- frm.dashboard.clear_headline();
- if (frm.doc.reference_doctype && !frm.doc.import_file) {
- frm.page.set_indicator(__('Attach file'), 'orange');
- } else {
- if (frm.doc.import_status) {
- const listview_settings = frappe.listview_settings['Data Import Legacy'];
- const indicator = listview_settings.get_indicator(frm.doc);
-
- frm.page.set_indicator(indicator[0], indicator[1]);
-
- if (frm.doc.import_status === "In Progress") {
- frm.dashboard.add_progress("Data Import Progress", "0");
- frm.set_read_only();
- frm.refresh_fields();
- }
- }
- }
-
- if (frm.doc.reference_doctype) {
- frappe.model.with_doctype(frm.doc.reference_doctype);
- }
-
- if(frm.doc.action == "Insert new records" || frm.doc.action == "Update records") {
- frm.set_df_property("action", "read_only", 1);
- }
-
- frm.add_custom_button(__("Help"), function() {
- frappe.help.show_video("6wiriRKPhmg");
- });
-
- if (frm.doc.reference_doctype && frm.doc.docstatus === 0) {
- frm.add_custom_button(__("Download template"), function() {
- frappe.data_import.download_dialog(frm).show();
- });
- }
-
- if (frm.doc.reference_doctype && frm.doc.import_file && frm.doc.total_rows &&
- frm.doc.docstatus === 0 && (!frm.doc.import_status || frm.doc.import_status == "Failed")) {
- frm.page.set_primary_action(__("Start Import"), function() {
- frappe.call({
- btn: frm.page.btn_primary,
- method: "frappe.core.doctype.data_import_legacy.data_import_legacy.import_data",
- args: {
- data_import: frm.doc.name
- }
- });
- }).addClass('btn btn-primary');
- }
-
- if (frm.doc.log_details) {
- frm.events.create_log_table(frm);
- } else {
- $(frm.fields_dict.import_log.wrapper).empty();
- }
- },
-
- action: function(frm) {
- if(!frm.doc.action) return;
- if(!frm.doc.reference_doctype) {
- frappe.msgprint(__("Please select document type first."));
- frm.set_value("action", "");
- return;
- }
-
- if(frm.doc.action == "Insert new records") {
- frm.doc.insert_new = 1;
- } else if (frm.doc.action == "Update records"){
- frm.doc.overwrite = 1;
- }
- frm.save();
- },
-
- only_update: function(frm) {
- frm.save();
- },
-
- submit_after_import: function(frm) {
- frm.save();
- },
-
- skip_errors: function(frm) {
- frm.save();
- },
-
- ignore_encoding_errors: function(frm) {
- frm.save();
- },
-
- no_email: function(frm) {
- frm.save();
- },
-
- show_only_errors: function(frm) {
- frm.events.create_log_table(frm);
- },
-
- create_log_table: function(frm) {
- let msg = JSON.parse(frm.doc.log_details);
- var $log_wrapper = $(frm.fields_dict.import_log.wrapper).empty();
- $(frappe.render_template("log_details", {
- data: msg.messages,
- import_status: frm.doc.import_status,
- show_only_errors: frm.doc.show_only_errors,
- })).appendTo($log_wrapper);
- }
-});
-
-frappe.provide('frappe.data_import');
-frappe.data_import.download_dialog = function(frm) {
- var dialog;
- const filter_fields = df => frappe.model.is_value_type(df) && !df.hidden;
- const get_fields = dt => frappe.meta.get_docfields(dt).filter(filter_fields);
-
- const get_doctype_checkbox_fields = () => {
- return dialog.fields.filter(df => df.fieldname.endsWith('_fields'))
- .map(df => dialog.fields_dict[df.fieldname]);
- };
-
- const doctype_fields = get_fields(frm.doc.reference_doctype)
- .map(df => {
- let reqd = (df.reqd || df.fieldname == 'naming_series') ? 1 : 0;
- return {
- label: df.label,
- reqd: reqd,
- danger: reqd,
- value: df.fieldname,
- checked: 1
- };
- });
-
- let fields = [
- {
- "label": __("Select Columns"),
- "fieldname": "select_columns",
- "fieldtype": "Select",
- "options": "All\nMandatory\nManually",
- "reqd": 1,
- "onchange": function() {
- const fields = get_doctype_checkbox_fields();
- fields.map(f => f.toggle(true));
- if(this.value == 'Mandatory' || this.value == 'Manually') {
- checkbox_toggle(true);
- fields.map(multicheck_field => {
- multicheck_field.options.map(option => {
- if(!option.reqd) return;
- $(multicheck_field.$wrapper).find(`:checkbox[data-unit="${option.value}"]`)
- .prop('checked', false)
- .trigger('click');
- });
- });
- } else if(this.value == 'All'){
- $(dialog.body).find(`[data-fieldtype="MultiCheck"] :checkbox`)
- .prop('disabled', true);
- }
- }
- },
- {
- "label": __("File Type"),
- "fieldname": "file_type",
- "fieldtype": "Select",
- "options": "Excel\nCSV",
- "default": "Excel"
- },
- {
- "label": __("Download with Data"),
- "fieldname": "with_data",
- "fieldtype": "Check",
- "hidden": !frm.doc.overwrite,
- "default": 1
- },
- {
- "label": __("Select All"),
- "fieldname": "select_all",
- "fieldtype": "Button",
- "depends_on": "eval:doc.select_columns=='Manually'",
- click: function() {
- checkbox_toggle();
- }
- },
- {
- "label": __("Unselect All"),
- "fieldname": "unselect_all",
- "fieldtype": "Button",
- "depends_on": "eval:doc.select_columns=='Manually'",
- click: function() {
- checkbox_toggle(true);
- }
- },
- {
- "label": frm.doc.reference_doctype,
- "fieldname": "doctype_fields",
- "fieldtype": "MultiCheck",
- "options": doctype_fields,
- "columns": 2,
- "hidden": 1
- }
- ];
-
- const child_table_fields = frappe.meta.get_table_fields(frm.doc.reference_doctype)
- .map(df => {
- return {
- "label": df.options,
- "fieldname": df.fieldname + '_fields',
- "fieldtype": "MultiCheck",
- "options": frappe.meta.get_docfields(df.options)
- .filter(filter_fields)
- .map(df => ({
- label: df.label,
- reqd: df.reqd ? 1 : 0,
- value: df.fieldname,
- checked: 1,
- danger: df.reqd
- })),
- "columns": 2,
- "hidden": 1
- };
- });
-
- fields = fields.concat(child_table_fields);
-
- dialog = new frappe.ui.Dialog({
- title: __('Download Template'),
- fields: fields,
- primary_action: function(values) {
- var data = values;
- if (frm.doc.reference_doctype) {
- var export_params = () => {
- let columns = {};
- if(values.select_columns) {
- columns = get_doctype_checkbox_fields().reduce((columns, field) => {
- const options = field.get_checked_options();
- columns[field.df.label] = options;
- return columns;
- }, {});
- }
-
- return {
- doctype: frm.doc.reference_doctype,
- parent_doctype: frm.doc.reference_doctype,
- select_columns: JSON.stringify(columns),
- with_data: frm.doc.overwrite && data.with_data,
- all_doctypes: true,
- file_type: data.file_type,
- template: true
- };
- };
- let get_template_url = '/api/method/frappe.core.doctype.data_export.exporter.export_data';
- open_url_post(get_template_url, export_params());
- } else {
- frappe.msgprint(__("Please select the Document Type."));
- }
- dialog.hide();
- },
- primary_action_label: __('Download')
- });
-
- $(dialog.body).find('div[data-fieldname="select_all"], div[data-fieldname="unselect_all"]')
- .wrapAll('
');
- const button_container = $(dialog.body).find('.inline-buttons');
- button_container.addClass('flex');
- $(button_container).find('.frappe-control').map((index, button) => {
- $(button).css({"margin-right": "1em"});
- });
-
- function checkbox_toggle(checked=false) {
- $(dialog.body).find('[data-fieldtype="MultiCheck"]').map((index, element) => {
- $(element).find(`:checkbox`).prop("checked", checked).trigger('click');
- });
- }
-
- return dialog;
-};
diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.json b/frappe/core/doctype/data_import_legacy/data_import_legacy.json
deleted file mode 100644
index 852ccba156..0000000000
--- a/frappe/core/doctype/data_import_legacy/data_import_legacy.json
+++ /dev/null
@@ -1,218 +0,0 @@
-{
- "actions": [],
- "allow_copy": 1,
- "creation": "2020-06-11 16:13:23.813709",
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "reference_doctype",
- "action",
- "insert_new",
- "overwrite",
- "only_update",
- "section_break_4",
- "import_file",
- "column_break_4",
- "error_file",
- "section_break_6",
- "skip_errors",
- "submit_after_import",
- "ignore_encoding_errors",
- "no_email",
- "import_detail",
- "import_status",
- "show_only_errors",
- "import_log",
- "log_details",
- "amended_from",
- "total_rows",
- "amended_from"
- ],
- "fields": [
- {
- "fieldname": "reference_doctype",
- "fieldtype": "Link",
- "ignore_user_permissions": 1,
- "in_list_view": 1,
- "label": "Document Type",
- "options": "DocType",
- "reqd": 1
- },
- {
- "fieldname": "action",
- "fieldtype": "Select",
- "label": "Action",
- "options": "Insert new records\nUpdate records",
- "reqd": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.overwrite",
- "description": "New data will be inserted.",
- "fieldname": "insert_new",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Insert new records",
- "set_only_once": 1
- },
- {
- "default": "0",
- "depends_on": "eval:!doc.insert_new",
- "description": "If you are updating/overwriting already created records.",
- "fieldname": "overwrite",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Update records",
- "set_only_once": 1
- },
- {
- "default": "0",
- "depends_on": "overwrite",
- "description": "If you don't want to create any new records while updating the older records.",
- "fieldname": "only_update",
- "fieldtype": "Check",
- "label": "Don't create new records"
- },
- {
- "depends_on": "eval:(!doc.__islocal)",
- "fieldname": "section_break_4",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "import_file",
- "fieldtype": "Attach",
- "label": "Attach file for Import"
- },
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval: doc.import_status == \"Partially Successful\"",
- "description": "This is the template file generated with only the rows having some error. You should use this file for correction and import.",
- "fieldname": "error_file",
- "fieldtype": "Attach",
- "label": "Generated File"
- },
- {
- "depends_on": "eval:(!doc.__islocal)",
- "fieldname": "section_break_6",
- "fieldtype": "Section Break"
- },
- {
- "default": "0",
- "description": "If this is checked, rows with valid data will be imported and invalid rows will be dumped into a new file for you to import later.",
- "fieldname": "skip_errors",
- "fieldtype": "Check",
- "label": "Skip rows with errors"
- },
- {
- "default": "0",
- "fieldname": "submit_after_import",
- "fieldtype": "Check",
- "label": "Submit after importing"
- },
- {
- "default": "0",
- "fieldname": "ignore_encoding_errors",
- "fieldtype": "Check",
- "label": "Ignore encoding errors"
- },
- {
- "default": "1",
- "fieldname": "no_email",
- "fieldtype": "Check",
- "label": "Do not send Emails"
- },
- {
- "collapsible": 1,
- "collapsible_depends_on": "eval: doc.import_status == \"Failed\"",
- "depends_on": "import_status",
- "fieldname": "import_detail",
- "fieldtype": "Section Break",
- "label": "Import Log"
- },
- {
- "fieldname": "import_status",
- "fieldtype": "Select",
- "label": "Import Status",
- "options": "\nSuccessful\nFailed\nIn Progress\nPartially Successful",
- "read_only": 1
- },
- {
- "allow_on_submit": 1,
- "default": "1",
- "fieldname": "show_only_errors",
- "fieldtype": "Check",
- "label": "Show only errors",
- "no_copy": 1,
- "print_hide": 1
- },
- {
- "allow_on_submit": 1,
- "depends_on": "import_status",
- "fieldname": "import_log",
- "fieldtype": "HTML",
- "label": "Import Log"
- },
- {
- "allow_on_submit": 1,
- "fieldname": "log_details",
- "fieldtype": "Code",
- "hidden": 1,
- "label": "Log Details",
- "read_only": 1
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "label": "Amended From",
- "no_copy": 1,
- "options": "Data Import",
- "print_hide": 1,
- "read_only": 1
- },
- {
- "fieldname": "total_rows",
- "fieldtype": "Int",
- "hidden": 1,
- "label": "Total Rows",
- "read_only": 1
- },
- {
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "label": "Amended From",
- "no_copy": 1,
- "options": "Data Import Legacy",
- "print_hide": 1,
- "read_only": 1
- }
- ],
- "is_submittable": 1,
- "links": [],
- "max_attachments": 1,
- "modified": "2020-06-11 16:13:23.813709",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Data Import Legacy",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 1
-}
\ No newline at end of file
diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy.py b/frappe/core/doctype/data_import_legacy/data_import_legacy.py
deleted file mode 100644
index 63f806d75b..0000000000
--- a/frappe/core/doctype/data_import_legacy/data_import_legacy.py
+++ /dev/null
@@ -1,126 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-import os
-
-import frappe
-import frappe.modules.import_file
-from frappe import _
-from frappe.core.doctype.data_import_legacy.importer import upload
-from frappe.model.document import Document
-from frappe.modules.import_file import import_file_by_path as _import_file_by_path
-from frappe.utils.background_jobs import enqueue
-from frappe.utils.data import format_datetime
-
-
-class DataImportLegacy(Document):
- def autoname(self):
- if not self.name:
- self.name = "Import on " + format_datetime(self.creation)
-
- def validate(self):
- if not self.import_file:
- self.db_set("total_rows", 0)
- if self.import_status == "In Progress":
- frappe.throw(_("Can't save the form as data import is in progress."))
-
- # validate the template just after the upload
- # if there is total_rows in the doc, it means that the template is already validated and error free
- if self.import_file and not self.total_rows:
- upload(data_import_doc=self, from_data_import="Yes", validate_template=True)
-
-
-@frappe.whitelist()
-def get_importable_doctypes():
- return frappe.cache().hget("can_import", frappe.session.user)
-
-
-@frappe.whitelist()
-def import_data(data_import):
- frappe.db.set_value("Data Import Legacy", data_import, "import_status", "In Progress", update_modified=False)
- frappe.publish_realtime("data_import_progress", {"progress": "0",
- "data_import": data_import, "reload": True}, user=frappe.session.user)
-
- from frappe.core.page.background_jobs.background_jobs import get_info
- enqueued_jobs = [d.get("job_name") for d in get_info()]
-
- if data_import not in enqueued_jobs:
- enqueue(upload, queue='default', timeout=6000, event='data_import', job_name=data_import,
- data_import_doc=data_import, from_data_import="Yes", user=frappe.session.user)
-
-
-def import_doc(path, overwrite=False, ignore_links=False, ignore_insert=False,
- insert=False, submit=False, pre_process=None):
- if os.path.isdir(path):
- files = [os.path.join(path, f) for f in os.listdir(path)]
- else:
- files = [path]
-
- for f in files:
- if f.endswith(".json"):
- frappe.flags.mute_emails = True
- _import_file_by_path(f, data_import=True, force=True, pre_process=pre_process, reset_permissions=True)
- frappe.flags.mute_emails = False
- frappe.db.commit()
- elif f.endswith(".csv"):
- import_file_by_path(f, ignore_links=ignore_links, overwrite=overwrite, submit=submit, pre_process=pre_process)
- frappe.db.commit()
-
-
-def import_file_by_path(path, ignore_links=False, overwrite=False, submit=False, pre_process=None, no_email=True):
- from frappe.utils.csvutils import read_csv_content
- print("Importing " + path)
- with open(path, "r") as infile:
- upload(rows=read_csv_content(infile.read()), ignore_links=ignore_links, no_email=no_email, overwrite=overwrite,
- submit_after_import=submit, pre_process=pre_process)
-
-
-def export_json(doctype, path, filters=None, or_filters=None, name=None, order_by="creation asc"):
- def post_process(out):
- del_keys = ('modified_by', 'creation', 'owner', 'idx')
- for doc in out:
- for key in del_keys:
- if key in doc:
- del doc[key]
- for k, v in doc.items():
- if isinstance(v, list):
- for child in v:
- for key in del_keys + ('docstatus', 'doctype', 'modified', 'name'):
- if key in child:
- del child[key]
-
- out = []
- if name:
- out.append(frappe.get_doc(doctype, name).as_dict())
- elif frappe.db.get_value("DocType", doctype, "issingle"):
- out.append(frappe.get_doc(doctype).as_dict())
- else:
- for doc in frappe.get_all(doctype, fields=["name"], filters=filters, or_filters=or_filters, limit_page_length=0, order_by=order_by):
- out.append(frappe.get_doc(doctype, doc.name).as_dict())
- post_process(out)
-
- dirname = os.path.dirname(path)
- if not os.path.exists(dirname):
- path = os.path.join('..', path)
-
- with open(path, "w") as outfile:
- outfile.write(frappe.as_json(out))
-
-
-def export_csv(doctype, path):
- from frappe.core.doctype.data_export.exporter import export_data
- with open(path, "wb") as csvfile:
- export_data(doctype=doctype, all_doctypes=True, template=True, with_data=True)
- csvfile.write(frappe.response.result.encode("utf-8"))
-
-
-@frappe.whitelist()
-def export_fixture(doctype, app):
- if frappe.session.user != "Administrator":
- raise frappe.PermissionError
-
- if not os.path.exists(frappe.get_app_path(app, "fixtures")):
- os.mkdir(frappe.get_app_path(app, "fixtures"))
-
- export_json(doctype, frappe.get_app_path(app, "fixtures", frappe.scrub(doctype) + ".json"), order_by="name asc")
diff --git a/frappe/core/doctype/data_import_legacy/data_import_legacy_list.js b/frappe/core/doctype/data_import_legacy/data_import_legacy_list.js
deleted file mode 100644
index fcf2391313..0000000000
--- a/frappe/core/doctype/data_import_legacy/data_import_legacy_list.js
+++ /dev/null
@@ -1,24 +0,0 @@
-frappe.listview_settings['Data Import Legacy'] = {
- add_fields: ["import_status"],
- has_indicator_for_draft: 1,
- get_indicator: function(doc) {
-
- let status = {
- 'Successful': [__("Success"), "green", "import_status,=,Successful"],
- 'Partially Successful': [__("Partial Success"), "blue", "import_status,=,Partially Successful"],
- 'In Progress': [__("In Progress"), "orange", "import_status,=,In Progress"],
- 'Failed': [__("Failed"), "red", "import_status,=,Failed"],
- 'Pending': [__("Pending"), "orange", "import_status,=,"]
- }
-
- if (doc.import_status) {
- return status[doc.import_status];
- }
-
- if (doc.docstatus == 0) {
- return status['Pending'];
- }
-
- return status['Pending'];
- }
-};
diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py
deleted file mode 100644
index ceefff4410..0000000000
--- a/frappe/core/doctype/data_import_legacy/importer.py
+++ /dev/null
@@ -1,538 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-import requests
-import frappe, json
-import frappe.permissions
-
-from frappe import _
-
-from frappe.utils.csvutils import getlink
-from frappe.utils.dateutils import parse_date
-
-from frappe.utils import cint, cstr, flt, getdate, get_datetime, get_url, get_absolute_url, duration_to_seconds
-
-
-@frappe.whitelist()
-def get_data_keys():
- return frappe._dict({
- "data_separator": _('Start entering data below this line'),
- "main_table": _("Table") + ":",
- "parent_table": _("Parent Table") + ":",
- "columns": _("Column Name") + ":",
- "doctype": _("DocType") + ":"
- })
-
-
-
-@frappe.whitelist()
-def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False, no_email=True, overwrite=None,
- update_only = None, ignore_links=False, pre_process=None, via_console=False, from_data_import="No",
- skip_errors = True, data_import_doc=None, validate_template=False, user=None):
- """upload data"""
-
- # for translations
- if user:
- frappe.cache().hdel("lang", user)
- frappe.set_user_lang(user)
-
- if data_import_doc and isinstance(data_import_doc, str):
- data_import_doc = frappe.get_doc("Data Import Legacy", data_import_doc)
- if data_import_doc and from_data_import == "Yes":
- no_email = data_import_doc.no_email
- ignore_encoding_errors = data_import_doc.ignore_encoding_errors
- update_only = data_import_doc.only_update
- submit_after_import = data_import_doc.submit_after_import
- overwrite = data_import_doc.overwrite
- skip_errors = data_import_doc.skip_errors
- else:
- # extra input params
- params = json.loads(frappe.form_dict.get("params") or '{}')
- if params.get("submit_after_import"):
- submit_after_import = True
- if params.get("ignore_encoding_errors"):
- ignore_encoding_errors = True
- if not params.get("no_email"):
- no_email = False
- if params.get('update_only'):
- update_only = True
- if params.get('from_data_import'):
- from_data_import = params.get('from_data_import')
- if not params.get('skip_errors'):
- skip_errors = params.get('skip_errors')
-
- frappe.flags.in_import = True
- frappe.flags.mute_emails = no_email
-
- def get_data_keys_definition():
- return get_data_keys()
-
- def bad_template():
- frappe.throw(_("Please do not change the rows above {0}").format(get_data_keys_definition().data_separator))
-
- def check_data_length():
- if not data:
- frappe.throw(_("No data found in the file. Please reattach the new file with data."))
-
- def get_start_row():
- for i, row in enumerate(rows):
- if row and row[0]==get_data_keys_definition().data_separator:
- return i+1
- bad_template()
-
- def get_header_row(key):
- return get_header_row_and_idx(key)[0]
-
- def get_header_row_and_idx(key):
- for i, row in enumerate(header):
- if row and row[0]==key:
- return row, i
- return [], -1
-
- def filter_empty_columns(columns):
- empty_cols = list(filter(lambda x: x in ("", None), columns))
-
- if empty_cols:
- if columns[-1*len(empty_cols):] == empty_cols:
- # filter empty columns if they exist at the end
- columns = columns[:-1*len(empty_cols)]
- else:
- frappe.msgprint(_("Please make sure that there are no empty columns in the file."),
- raise_exception=1)
-
- return columns
-
- def make_column_map():
- doctype_row, row_idx = get_header_row_and_idx(get_data_keys_definition().doctype)
- if row_idx == -1: # old style
- return
-
- dt = None
- for i, d in enumerate(doctype_row[1:]):
- if d not in ("~", "-"):
- if d and doctype_row[i] in (None, '' ,'~', '-', _("DocType") + ":"):
- dt, parentfield = d, None
- # xls format truncates the row, so it may not have more columns
- if len(doctype_row) > i+2:
- parentfield = doctype_row[i+2]
- doctypes.append((dt, parentfield))
- column_idx_to_fieldname[(dt, parentfield)] = {}
- column_idx_to_fieldtype[(dt, parentfield)] = {}
- if dt:
- column_idx_to_fieldname[(dt, parentfield)][i+1] = rows[row_idx + 2][i+1]
- column_idx_to_fieldtype[(dt, parentfield)][i+1] = rows[row_idx + 4][i+1]
-
- def get_doc(start_idx):
- if doctypes:
- doc = {}
- attachments = []
- last_error_row_idx = None
- for idx in range(start_idx, len(rows)):
- last_error_row_idx = idx # pylint: disable=W0612
- if (not doc) or main_doc_empty(rows[idx]):
- for dt, parentfield in doctypes:
- d = {}
- for column_idx in column_idx_to_fieldname[(dt, parentfield)]:
- try:
- fieldname = column_idx_to_fieldname[(dt, parentfield)][column_idx]
- fieldtype = column_idx_to_fieldtype[(dt, parentfield)][column_idx]
-
- if not fieldname or not rows[idx][column_idx]:
- continue
-
- d[fieldname] = rows[idx][column_idx]
- if fieldtype in ("Int", "Check"):
- d[fieldname] = cint(d[fieldname])
- elif fieldtype in ("Float", "Currency", "Percent"):
- d[fieldname] = flt(d[fieldname])
- elif fieldtype == "Date":
- if d[fieldname] and isinstance(d[fieldname], str):
- d[fieldname] = getdate(parse_date(d[fieldname]))
- elif fieldtype == "Datetime":
- if d[fieldname]:
- if " " in d[fieldname]:
- _date, _time = d[fieldname].split()
- else:
- _date, _time = d[fieldname], '00:00:00'
- _date = parse_date(d[fieldname])
- d[fieldname] = get_datetime(_date + " " + _time)
- else:
- d[fieldname] = None
- elif fieldtype == "Duration":
- d[fieldname] = duration_to_seconds(cstr(d[fieldname]))
- elif fieldtype in ("Image", "Attach Image", "Attach"):
- # added file to attachments list
- attachments.append(d[fieldname])
-
- elif fieldtype in ("Link", "Dynamic Link", "Data") and d[fieldname]:
- # as fields can be saved in the number format(long type) in data import template
- d[fieldname] = cstr(d[fieldname])
-
- except IndexError:
- pass
-
- # scrub quotes from name and modified
- if d.get("name") and d["name"].startswith('"'):
- d["name"] = d["name"][1:-1]
-
- if sum(0 if not val else 1 for val in d.values()):
- d['doctype'] = dt
- if dt == doctype:
- doc.update(d)
- else:
- if not overwrite and doc.get("name"):
- d['parent'] = doc["name"]
- d['parenttype'] = doctype
- d['parentfield'] = parentfield
- doc.setdefault(d['parentfield'], []).append(d)
- else:
- break
-
- return doc, attachments, last_error_row_idx
- else:
- doc = frappe._dict(zip(columns, rows[start_idx][1:]))
- doc['doctype'] = doctype
- return doc, [], None
-
- # used in testing whether a row is empty or parent row or child row
- # checked only 3 first columns since first two columns can be blank for example the case of
- # importing the item variant where item code and item name will be blank.
- def main_doc_empty(row):
- if row:
- for i in range(3,0,-1):
- if len(row) > i and row[i]:
- return False
- return True
-
- def validate_naming(doc):
- autoname = frappe.get_meta(doctype).autoname
- if autoname:
- if autoname[0:5] == 'field':
- autoname = autoname[6:]
- elif autoname == 'naming_series:':
- autoname = 'naming_series'
- else:
- return True
-
- if (autoname not in doc) or (not doc[autoname]):
- from frappe.model.base_document import get_controller
- if not hasattr(get_controller(doctype), "autoname"):
- frappe.throw(_("{0} is a mandatory field").format(autoname))
- return True
-
- users = frappe.db.sql_list("select name from tabUser")
- def prepare_for_insert(doc):
- # don't block data import if user is not set
- # migrating from another system
- if not doc.owner in users:
- doc.owner = frappe.session.user
- if not doc.modified_by in users:
- doc.modified_by = frappe.session.user
-
- def is_valid_url(url):
- is_valid = False
- if url.startswith("/files") or url.startswith("/private/files"):
- url = get_url(url)
-
- try:
- r = requests.get(url)
- is_valid = True if r.status_code == 200 else False
- except Exception:
- pass
-
- return is_valid
-
- def attach_file_to_doc(doctype, docname, file_url):
- # check if attachment is already available
- # check if the attachement link is relative or not
- if not file_url:
- return
- if not is_valid_url(file_url):
- return
-
- files = frappe.db.sql("""Select name from `tabFile` where attached_to_doctype='{doctype}' and
- attached_to_name='{docname}' and (file_url='{file_url}' or thumbnail_url='{file_url}')""".format(
- doctype=doctype,
- docname=docname,
- file_url=file_url
- ))
-
- if files:
- # file is already attached
- return
-
- _file = frappe.get_doc({
- "doctype": "File",
- "file_url": file_url,
- "attached_to_name": docname,
- "attached_to_doctype": doctype,
- "attached_to_field": 0,
- "folder": "Home/Attachments"})
- _file.save()
-
-
- # header
- filename, file_extension = ['','']
- if not rows:
- _file = frappe.get_doc("File", {"file_url": data_import_doc.import_file})
- fcontent = _file.get_content()
- filename, file_extension = _file.get_extension()
-
- if file_extension == '.xlsx' and from_data_import == 'Yes':
- from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
- rows = read_xlsx_file_from_attached_file(file_url=data_import_doc.import_file)
-
- elif file_extension == '.csv':
- from frappe.utils.csvutils import read_csv_content
- rows = read_csv_content(fcontent, ignore_encoding_errors)
-
- else:
- frappe.throw(_("Unsupported File Format"))
-
- start_row = get_start_row()
- header = rows[:start_row]
- data = rows[start_row:]
- try:
- doctype = get_header_row(get_data_keys_definition().main_table)[1]
- columns = filter_empty_columns(get_header_row(get_data_keys_definition().columns)[1:])
- except:
- frappe.throw(_("Cannot change header content"))
- doctypes = []
- column_idx_to_fieldname = {}
- column_idx_to_fieldtype = {}
-
- if skip_errors:
- data_rows_with_error = header
-
- if submit_after_import and not cint(frappe.db.get_value("DocType",
- doctype, "is_submittable")):
- submit_after_import = False
-
- parenttype = get_header_row(get_data_keys_definition().parent_table)
-
- if len(parenttype) > 1:
- parenttype = parenttype[1]
-
- # check permissions
- if not frappe.permissions.can_import(parenttype or doctype):
- frappe.flags.mute_emails = False
- return {"messages": [_("Not allowed to Import") + ": " + _(doctype)], "error": True}
-
- # Throw expception in case of the empty data file
- check_data_length()
- make_column_map()
- total = len(data)
-
- if validate_template:
- if total:
- data_import_doc.total_rows = total
- return True
-
- if overwrite==None:
- overwrite = params.get('overwrite')
-
- # delete child rows (if parenttype)
- parentfield = None
- if parenttype:
- parentfield = get_parent_field(doctype, parenttype)
-
- if overwrite:
- delete_child_rows(data, doctype)
-
- import_log = []
- def log(**kwargs):
- if via_console:
- print((kwargs.get("title") + kwargs.get("message")).encode('utf-8'))
- else:
- import_log.append(kwargs)
-
- def as_link(doctype, name):
- if via_console:
- return "{0}: {1}".format(doctype, name)
- else:
- return getlink(doctype, name)
-
- # publish realtime task update
- def publish_progress(achieved, reload=False):
- if data_import_doc:
- frappe.publish_realtime("data_import_progress", {"progress": str(int(100.0*achieved/total)),
- "data_import": data_import_doc.name, "reload": reload}, user=frappe.session.user)
-
-
- error_flag = rollback_flag = False
-
- batch_size = frappe.conf.data_import_batch_size or 1000
-
- for batch_start in range(0, total, batch_size):
- batch = data[batch_start:batch_start + batch_size]
-
- for i, row in enumerate(batch):
- # bypass empty rows
- if main_doc_empty(row):
- continue
-
- row_idx = i + start_row
- doc = None
-
- publish_progress(i)
-
- try:
- doc, attachments, last_error_row_idx = get_doc(row_idx)
- validate_naming(doc)
- if pre_process:
- pre_process(doc)
-
- original = None
- if parentfield:
- parent = frappe.get_doc(parenttype, doc["parent"])
- doc = parent.append(parentfield, doc)
- parent.save()
- else:
- if overwrite and doc.get("name") and frappe.db.exists(doctype, doc["name"]):
- original = frappe.get_doc(doctype, doc["name"])
- original_name = original.name
- original.update(doc)
- # preserve original name for case sensitivity
- original.name = original_name
- original.flags.ignore_links = ignore_links
- original.save()
- doc = original
- else:
- if not update_only:
- doc = frappe.get_doc(doc)
- prepare_for_insert(doc)
- doc.flags.ignore_links = ignore_links
- doc.insert()
- if attachments:
- # check file url and create a File document
- for file_url in attachments:
- attach_file_to_doc(doc.doctype, doc.name, file_url)
- if submit_after_import:
- doc.submit()
-
- # log errors
- if parentfield:
- log(**{"row": doc.idx, "title": 'Inserted row for "%s"' % (as_link(parenttype, doc.parent)),
- "link": get_absolute_url(parenttype, doc.parent), "message": 'Document successfully saved', "indicator": "green"})
- elif submit_after_import:
- log(**{"row": row_idx + 1, "title":'Submitted row for "%s"' % (as_link(doc.doctype, doc.name)),
- "message": "Document successfully submitted", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "blue"})
- elif original:
- log(**{"row": row_idx + 1,"title":'Updated row for "%s"' % (as_link(doc.doctype, doc.name)),
- "message": "Document successfully updated", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "green"})
- elif not update_only:
- log(**{"row": row_idx + 1, "title":'Inserted row for "%s"' % (as_link(doc.doctype, doc.name)),
- "message": "Document successfully saved", "link": get_absolute_url(doc.doctype, doc.name), "indicator": "green"})
- else:
- log(**{"row": row_idx + 1, "title":'Ignored row for %s' % (row[1]), "link": None,
- "message": "Document updation ignored", "indicator": "orange"})
-
- except Exception as e:
- error_flag = True
-
- # build error message
- if frappe.local.message_log:
- err_msg = "\n".join(['{}
'.format(json.loads(msg).get('message')) for msg in frappe.local.message_log])
- else:
- err_msg = '{}
'.format(cstr(e))
-
- error_trace = frappe.get_traceback()
- if error_trace:
- error_log_doc = frappe.log_error(error_trace)
- error_link = get_absolute_url("Error Log", error_log_doc.name)
- else:
- error_link = None
-
- log(**{
- "row": row_idx + 1,
- "title": 'Error for row %s' % (len(row)>1 and frappe.safe_decode(row[1]) or ""),
- "message": err_msg,
- "indicator": "red",
- "link":error_link
- })
-
- # data with error to create a new file
- # include the errored data in the last row as last_error_row_idx will not be updated for the last row
- if skip_errors:
- if last_error_row_idx == len(rows)-1:
- last_error_row_idx = len(rows)
- data_rows_with_error += rows[row_idx:last_error_row_idx]
- else:
- rollback_flag = True
- finally:
- frappe.local.message_log = []
-
- start_row += batch_size
- if rollback_flag:
- frappe.db.rollback()
- else:
- frappe.db.commit()
-
- frappe.flags.mute_emails = False
- frappe.flags.in_import = False
-
- log_message = {"messages": import_log, "error": error_flag}
- if data_import_doc:
- data_import_doc.log_details = json.dumps(log_message)
-
- import_status = None
- if error_flag and data_import_doc.skip_errors and len(data) != len(data_rows_with_error):
- import_status = "Partially Successful"
- # write the file with the faulty row
- file_name = 'error_' + filename + file_extension
- if file_extension == '.xlsx':
- from frappe.utils.xlsxutils import make_xlsx
- xlsx_file = make_xlsx(data_rows_with_error, "Data Import Template")
- file_data = xlsx_file.getvalue()
- else:
- from frappe.utils.csvutils import to_csv
- file_data = to_csv(data_rows_with_error)
- _file = frappe.get_doc({
- "doctype": "File",
- "file_name": file_name,
- "attached_to_doctype": "Data Import Legacy",
- "attached_to_name": data_import_doc.name,
- "folder": "Home/Attachments",
- "content": file_data})
- _file.save()
- data_import_doc.error_file = _file.file_url
-
- elif error_flag:
- import_status = "Failed"
- else:
- import_status = "Successful"
-
- data_import_doc.import_status = import_status
- data_import_doc.save()
- if data_import_doc.import_status in ["Successful", "Partially Successful"]:
- data_import_doc.submit()
- publish_progress(100, True)
- else:
- publish_progress(0, True)
- frappe.db.commit()
- else:
- return log_message
-
-def get_parent_field(doctype, parenttype):
- parentfield = None
-
- # get parentfield
- if parenttype:
- for d in frappe.get_meta(parenttype).get_table_fields():
- if d.options==doctype:
- parentfield = d.fieldname
- break
-
- if not parentfield:
- frappe.msgprint(_("Did not find {0} for {0} ({1})").format("parentfield", parenttype, doctype))
- raise Exception
-
- return parentfield
-
-def delete_child_rows(rows, doctype):
- """delete child rows for all parents"""
- for p in list(set(r[1] for r in rows)):
- if p:
- frappe.db.sql("""delete from `tab{0}` where parent=%s""".format(doctype), p)
diff --git a/frappe/core/doctype/data_import_legacy/log_details.html b/frappe/core/doctype/data_import_legacy/log_details.html
deleted file mode 100644
index aa160a742b..0000000000
--- a/frappe/core/doctype/data_import_legacy/log_details.html
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
- | {{ __("Row No") }} |
- {{ __("Row Status") }} |
- {{ __("Message") }} |
-
-
- {% for row in data %}
- {% if (!show_only_errors) || (show_only_errors && row.indicator == "red") %}
-
- |
- {{ row.row }}
- |
-
- {{ row.title }}
- |
-
- {% if (import_status != "Failed" || (row.indicator == "red")) { %}
- {{ row.message }}
- {% if row.link %}
-
-
-
-
-
- {% endif %}
- {% } else { %}
- {{ __("Document can't saved.") }}
- {% } %}
- |
-
- {% endif %}
- {% endfor %}
-
-
-
\ No newline at end of file
diff --git a/frappe/core/doctype/data_import_legacy/test_data_import_legacy.py b/frappe/core/doctype/data_import_legacy/test_data_import_legacy.py
deleted file mode 100644
index 6f9964e8f5..0000000000
--- a/frappe/core/doctype/data_import_legacy/test_data_import_legacy.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
-import unittest
-
-class TestDataImportLegacy(unittest.TestCase):
- pass
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index 7f93d3130a..6a427f71e1 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -76,6 +76,7 @@
"index_web_pages_for_search",
"route",
"is_published_field",
+ "website_search_field",
"advanced",
"engine"
],
@@ -547,6 +548,12 @@
{
"fieldname": "column_break_51",
"fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "has_web_view",
+ "fieldname": "website_search_field",
+ "fieldtype": "Data",
+ "label": "Website Search Field"
}
],
"icon": "fa fa-bolt",
@@ -628,7 +635,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2021-04-16 12:26:41.031135",
+ "modified": "2021-06-17 23:31:44.974199",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@@ -662,4 +669,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 3cdc45ea08..d2f62d0a15 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -396,10 +396,7 @@ class DocType(Document):
frappe.db.sql("""update tabSingles set value=%s
where doctype=%s and field='name' and value = %s""", (new, new, old))
else:
- frappe.db.multisql({
- "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`",
- "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`"
- })
+ frappe.db.rename_table(old, new)
frappe.db.commit()
# Do not rename and move files and folders for custom doctype
@@ -927,6 +924,13 @@ def validate_fields(meta):
if meta.is_published_field not in fieldname_list:
frappe.throw(_("Is Published Field must be a valid fieldname"), InvalidFieldNameError)
+ def check_website_search_field(meta):
+ if not meta.website_search_field:
+ return
+
+ if meta.website_search_field not in fieldname_list:
+ frappe.throw(_("Website Search Field must be a valid fieldname"), InvalidFieldNameError)
+
def check_timeline_field(meta):
if not meta.timeline_field:
return
@@ -1046,6 +1050,7 @@ def validate_fields(meta):
check_title_field(meta)
check_timeline_field(meta)
check_is_published_field(meta)
+ check_website_search_field(meta)
check_sort_field(meta)
check_image_field(meta)
diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py
index 7ad0aeff21..a8c7c6a747 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.py
+++ b/frappe/core/doctype/domain_settings/domain_settings.py
@@ -34,7 +34,7 @@ class DomainSettings(Document):
all_domains = list((frappe.get_hooks('domains') or {}))
def remove_role(role):
- frappe.db.sql('delete from `tabHas Role` where role=%s', role)
+ frappe.db.delete("Has Role", {"role": role})
frappe.set_value('Role', role, 'disabled', 1)
for domain in all_domains:
diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py
index 8223238c57..3d66253b08 100644
--- a/frappe/core/doctype/error_log/error_log.py
+++ b/frappe/core/doctype/error_log/error_log.py
@@ -20,4 +20,4 @@ def set_old_logs_as_seen():
def clear_error_logs():
'''Flush all Error Logs'''
frappe.only_for('System Manager')
- frappe.db.sql('''DELETE FROM `tabError Log`''')
\ No newline at end of file
+ frappe.db.truncate("Error Log")
diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py
index 9d0c0b9af0..9c953db1f0 100644
--- a/frappe/core/doctype/report/test_report.py
+++ b/frappe/core/doctype/report/test_report.py
@@ -82,9 +82,11 @@ class TestReport(unittest.TestCase):
def test_report_permissions(self):
frappe.set_user('test@example.com')
- frappe.db.sql("""delete from `tabHas Role` where parent = %s
- and role = 'Test Has Role'""", frappe.session.user, auto_commit=1)
-
+ frappe.db.delete("Has Role", {
+ "parent": frappe.session.user,
+ "role": "Test Has Role"
+ })
+ frappe.db.commit()
if not frappe.db.exists('Role', 'Test Has Role'):
role = frappe.get_doc({
'doctype': 'Role',
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 02482c75ca..28b444e1e7 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -38,7 +38,7 @@ class Role(Document):
self.set(key, 0)
def remove_roles(self):
- frappe.db.sql("delete from `tabHas Role` where role = %s", self.name)
+ frappe.db.delete("Has Role", {"role": self.name})
frappe.clear_cache()
def on_update(self):
diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
index 59089d12ad..b6515b1e79 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
import json
@@ -110,7 +109,7 @@ class ScheduledJobType(Document):
return 'long' if ('Long' in self.frequency) else 'default'
def on_trash(self):
- frappe.db.sql('delete from `tabScheduled Job Log` where scheduled_job_type=%s', self.name)
+ frappe.db.delete("Scheduled Job Log", {"scheduled_job_type": self.name})
@frappe.whitelist()
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 5b605504e8..5d799f8ee9 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -53,6 +53,7 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
def validate(self):
self.check_demo()
@@ -129,6 +130,9 @@ class User(Document):
if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'):
frappe.cache().delete_key('users_for_mentions')
+ if self.has_value_changed('enabled'):
+ frappe.cache().delete_key('enabled_users')
+
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
return self.name == frappe.session.user
@@ -364,17 +368,15 @@ class User(Document):
frappe.local.login_manager.logout(user=self.name)
# delete todos
- frappe.db.sql("""DELETE FROM `tabToDo` WHERE `owner`=%s""", (self.name,))
+ frappe.db.delete("ToDo", {"owner": self.name})
frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
(self.name,))
# delete events
- frappe.db.sql("""delete from `tabEvent` where owner=%s
- and event_type='Private'""", (self.name,))
+ frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
# delete shares
- frappe.db.sql("""delete from `tabDocShare` where user=%s""", self.name)
-
+ frappe.db.delete("DocShare", {"user": self.name})
# delete messages
frappe.db.sql("""delete from `tabCommunication`
where communication_type in ('Chat', 'Notification')
@@ -392,6 +394,8 @@ class User(Document):
if self.get('allow_in_mentions'):
frappe.cache().delete_key('users_for_mentions')
+ frappe.cache().delete_key('enabled_users')
+
def before_rename(self, old_name, new_name, merge=False):
self.check_demo()
@@ -1230,3 +1234,10 @@ def generate_keys(user):
def switch_theme(theme):
if theme in ["Dark", "Light"]:
frappe.db.set_value("User", frappe.session.user, "desk_theme", theme)
+
+def get_enabled_users():
+ def _get_enabled_users():
+ enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
+ return enabled_users
+
+ return frappe.cache().get_value("enabled_users", _get_enabled_users)
\ No newline at end of file
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 1a442b53e7..85db846982 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -1,6 +1,5 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and Contributors
-# See license.txt
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See LICENSE
from frappe.core.doctype.user_permission.user_permission import add_user_permissions, remove_applicable
from frappe.permissions import has_user_permission
from frappe.core.doctype.doctype.test_doctype import new_doctype
@@ -10,11 +9,14 @@ import unittest
class TestUserPermission(unittest.TestCase):
def setUp(self):
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user` in (
- 'test_bulk_creation_update@example.com',
- 'test_user_perm1@example.com',
- 'nested_doc_user@example.com')""")
+ test_users = (
+ "test_bulk_creation_update@example.com",
+ "test_user_perm1@example.com",
+ "nested_doc_user@example.com",
+ )
+ frappe.db.delete("User Permission", {
+ "user": ("in", test_users)
+ })
frappe.delete_doc_if_exists("DocType", "Person")
frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`")
frappe.delete_doc_if_exists("DocType", "Doc A")
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 4aa5797c7f..5201ffef8d 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe, json
@@ -179,11 +178,16 @@ def check_applicable_doc_perm(user, doctype, docname):
@frappe.whitelist()
def clear_user_permissions(user, for_doctype):
- frappe.only_for('System Manager')
- total = frappe.db.count('User Permission', filters = dict(user=user, allow=for_doctype))
+ frappe.only_for("System Manager")
+ total = frappe.db.count("User Permission", {"user": user, "allow": for_doctype})
+
if total:
- frappe.db.sql('DELETE FROM `tabUser Permission` WHERE `user`=%s AND `allow`=%s', (user, for_doctype))
+ frappe.db.delete("User Permission", {
+ "allow": for_doctype,
+ "user": user,
+ })
frappe.clear_cache()
+
return total
@frappe.whitelist()
@@ -225,7 +229,7 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
user_perm.is_default = is_default
user_perm.hide_descendants = hide_descendants
if applicable:
- user_perm.applicable_for = applicable
+ user_perm.applicable_for = applicable
user_perm.apply_to_all_doctypes = 0
else:
user_perm.apply_to_all_doctypes = 1
@@ -233,27 +237,27 @@ def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, a
def remove_applicable(perm_applied_docs, user, doctype, docname):
for applicable_for in perm_applied_docs:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """, (user, applicable_for, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applicable_for,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def remove_apply_to_all(user, doctype, docname):
- frappe.db.sql("""DELETE from `tabUser Permission`
- WHERE `user`=%s
- AND `apply_to_all_doctypes`=1
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "apply_to_all_doctypes": 1,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
def update_applicable(already_applied, to_apply, user, doctype, docname):
for applied in already_applied:
if applied not in to_apply:
- frappe.db.sql("""DELETE FROM `tabUser Permission`
- WHERE `user`=%s
- AND `applicable_for`=%s
- AND `allow`=%s
- AND `for_value`=%s
- """,(user, applied, doctype, docname))
+ frappe.db.delete("User Permission", {
+ "applicable_for": applied,
+ "for_value": docname,
+ "allow": doctype,
+ "user": user,
+ })
diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py
index 847b23bd3e..1f3555e351 100644
--- a/frappe/core/page/background_jobs/background_jobs.py
+++ b/frappe/core/page/background_jobs/background_jobs.py
@@ -4,12 +4,12 @@
import json
from typing import TYPE_CHECKING, Dict, List
-from rq import Queue, Worker
+from rq import Worker
import frappe
from frappe import _
from frappe.utils import convert_utc_to_user_timezone, format_datetime
-from frappe.utils.background_jobs import get_redis_conn
+from frappe.utils.background_jobs import get_redis_conn, get_queues
from frappe.utils.scheduler import is_scheduler_inactive
if TYPE_CHECKING:
@@ -29,7 +29,7 @@ def get_info(show_failed=False) -> List[Dict]:
show_failed = json.loads(show_failed)
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
workers = Worker.all(conn)
jobs = []
@@ -75,7 +75,7 @@ def get_info(show_failed=False) -> List[Dict]:
@frappe.whitelist()
def remove_failed_jobs():
conn = get_redis_conn()
- queues = Queue.all(conn)
+ queues = get_queues()
for queue in queues:
fail_registry = queue.failed_job_registry
for job_id in fail_registry.get_job_ids():
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index 15c7cb55ae..2a99283dda 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -92,14 +92,14 @@ def update(doctype, role, permlevel, ptype, value=None):
"""Update role permission params
Args:
- doctype (str): Name of the DocType to update params for
- role (str): Role to be updated for, eg "Website Manager".
- permlevel (int): perm level the provided rule applies to
- ptype (str): permission type, example "read", "delete", etc.
- value (None, optional): value for ptype, None indicates False
+ doctype (str): Name of the DocType to update params for
+ role (str): Role to be updated for, eg "Website Manager".
+ permlevel (int): perm level the provided rule applies to
+ ptype (str): permission type, example "read", "delete", etc.
+ value (None, optional): value for ptype, None indicates False
Returns:
- str: Refresh flag is permission is updated successfully
+ str: Refresh flag is permission is updated successfully
"""
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
@@ -110,10 +110,9 @@ def remove(doctype, role, permlevel):
frappe.only_for("System Manager")
setup_custom_perms(doctype)
- name = frappe.get_value('Custom DocPerm', dict(parent=doctype, role=role, permlevel=permlevel))
+ frappe.db.delete("Custom DocPerm", {"parent": doctype, "role": role, "permlevel": permlevel})
- frappe.db.sql('delete from `tabCustom DocPerm` where name=%s', name)
- if not frappe.get_all('Custom DocPerm', dict(parent=doctype)):
+ if not frappe.get_all('Custom DocPerm', {"parent": doctype}):
frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove'))
validate_permissions_for_doctype(doctype, for_remove=True, alert=True)
diff --git a/frappe/coverage.py b/frappe/coverage.py
new file mode 100644
index 0000000000..a59c24a714
--- /dev/null
+++ b/frappe/coverage.py
@@ -0,0 +1,35 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
+"""
+ frappe.coverage
+ ~~~~~~~~~~~~~~~~
+
+ Coverage settings for frappe
+"""
+
+STANDARD_INCLUSIONS = ["*.py"]
+
+STANDARD_EXCLUSIONS = [
+ '*.js',
+ '*.xml',
+ '*.pyc',
+ '*.css',
+ '*.less',
+ '*.scss',
+ '*.vue',
+ '*.html',
+ '*/test_*',
+ '*/node_modules/*',
+ '*/doctype/*/*_dashboard.py',
+ '*/patches/*',
+]
+
+FRAPPE_EXCLUSIONS = [
+ "*/tests/*",
+ "*/commands/*",
+ "*/frappe/change_log/*",
+ "*/frappe/exceptions*",
+ "*frappe/setup.py",
+ "*/doctype/*/*_dashboard.py",
+ "*/patches/*",
+]
diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json
index 2f0819ab68..19462e79de 100644
--- a/frappe/custom/doctype/custom_field/custom_field.json
+++ b/frappe/custom/doctype/custom_field/custom_field.json
@@ -120,7 +120,7 @@
"label": "Field Type",
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
- "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nGeolocation\nHTML\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
+ "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature",
"reqd": 1
},
{
@@ -417,7 +417,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-29 06:14:43.073329",
+ "modified": "2021-07-12 04:54:12.042319",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 7e6ea1875a..e266455f7a 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -85,12 +85,10 @@ class CustomField(Document):
frappe.bold(self.label)))
# delete property setter entries
- frappe.db.sql("""\
- DELETE FROM `tabProperty Setter`
- WHERE doc_type = %s
- AND field_name = %s""",
- (self.dt, self.fieldname))
-
+ frappe.db.delete("Property Setter", {
+ "doc_type": self.dt,
+ "field_name": self.fieldname
+ })
frappe.clear_cache(doctype=self.dt)
def validate_insert_after(self, meta):
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 1b8977acc4..8de194fb00 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -1,5 +1,5 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See LICENSE
"""
Customize Form is a Single DocType used to mask the Property Setter
@@ -18,10 +18,11 @@ from frappe.custom.doctype.property_setter.property_setter import delete_propert
from frappe.model.docfield import supports_translation
from frappe.core.doctype.doctype.doctype import validate_series
+
class CustomizeForm(Document):
def on_update(self):
- frappe.db.sql("delete from tabSingles where doctype='Customize Form'")
- frappe.db.sql("delete from `tabCustomize Form Field`")
+ frappe.db.delete("Singles", {"doctype": "Customize Form"})
+ frappe.db.delete("Customize Form Field")
@frappe.whitelist()
def fetch_to_customize(self):
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6012e47445..b1dec95139 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -6,6 +6,7 @@
import re
import time
+from typing import Dict, List, Union
import frappe
import datetime
import frappe.defaults
@@ -13,7 +14,7 @@ import frappe.model.meta
from frappe import _
from time import time
-from frappe.utils import now, getdate, cast_fieldtype, get_datetime
+from frappe.utils import now, getdate, cast_fieldtype, get_datetime, get_table_name
from frappe.model.utils.link_count import flush_local_link_count
@@ -103,6 +104,7 @@ class Database(object):
{"name": "a%", "owner":"test@example.com"})
"""
+ query = str(query)
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -951,15 +953,37 @@ class Database(object):
query = sql_dict.get(current_dialect)
return self.sql(query, values, **kwargs)
- def delete(self, doctype, conditions, debug=False):
- if conditions:
- conditions, values = self.build_conditions(conditions)
- return self.sql("DELETE FROM `tab{doctype}` where {conditions}".format(
- doctype=doctype,
- conditions=conditions
- ), values, debug=debug)
- else:
- frappe.throw(_('No conditions provided'))
+ def delete(self, doctype: str, filters: Union[Dict, List] = None, debug=False, **kwargs):
+ """Delete rows from a table in site which match the passed filters. This
+ does trigger DocType hooks. Simply runs a DELETE query in the database.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ values = ()
+ filters = filters or kwargs.get("conditions")
+ table = get_table_name(doctype)
+ query = f"DELETE FROM `{table}`"
+
+ if "debug" not in kwargs:
+ kwargs["debug"] = debug
+
+ if filters:
+ conditions, values = self.build_conditions(filters)
+ query = f"{query} WHERE {conditions}"
+
+ return self.sql(query, values, **kwargs)
+
+ def truncate(self, doctype: str):
+ """Truncate a table in the database. This runs a DDL command `TRUNCATE TABLE`.
+ This cannot be rolled back.
+
+ Doctype name can be passed directly, it will be pre-pended with `tab`.
+ """
+ table = doctype if doctype.startswith("__") else f"tab{doctype}"
+ return self.sql_ddl(f"truncate `{table}`")
+
+ def clear_table(self, doctype):
+ return self.truncate(doctype)
def get_last_created(self, doctype):
last_record = self.get_all(doctype, ('creation'), limit=1, order_by='creation desc')
@@ -968,9 +992,6 @@ class Database(object):
else:
return None
- def clear_table(self, doctype):
- self.sql('truncate `tab{}`'.format(doctype))
-
def log_touched_tables(self, query, values=None):
if values:
query = frappe.safe_decode(self._cursor.mogrify(query, values))
@@ -1021,6 +1042,7 @@ class Database(object):
), tuple(insert_list))
insert_list = []
+
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import execute_job, get_queue
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 879c8394d7..5dd6d9e58a 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -1,3 +1,5 @@
+from typing import List, Tuple, Union
+
import pymysql
from pymysql.constants import ER, FIELD_TYPE
from pymysql.converters import conversions, escape_string
@@ -5,7 +7,7 @@ from pymysql.converters import conversions, escape_string
import frappe
from frappe.database.database import Database
from frappe.database.mariadb.schema import MariaDBTable
-from frappe.utils import UnicodeWithAttrs, cstr, get_datetime
+from frappe.utils import UnicodeWithAttrs, cstr, get_datetime, get_table_name
class MariaDBDatabase(Database):
@@ -123,6 +125,19 @@ class MariaDBDatabase(Database):
def is_type_datetime(code):
return code in (pymysql.DATE, pymysql.DATETIME)
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"RENAME TABLE `{old_name}` TO `{new_name}`")
+
+ def describe(self, doctype: str) -> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"DESC `{table_name}`")
+
+ def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(table)
+ return self.sql(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} NOT NULL")
+
# exception types
@staticmethod
def is_deadlocked(e):
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index a52efd01e3..f8841e9417 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -220,6 +220,7 @@ CREATE TABLE `tabDocType` (
`allow_guest_to_view` int(1) NOT NULL DEFAULT 0,
`route` varchar(255) DEFAULT NULL,
`is_published_field` varchar(255) DEFAULT NULL,
+ `website_search_field` varchar(255) DEFAULT NULL,
`email_append_to` int(1) NOT NULL DEFAULT 0,
`subject_field` varchar(255) DEFAULT NULL,
`sender_field` varchar(255) DEFAULT NULL,
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 8235277e30..0b73c8b44b 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -1,12 +1,14 @@
import re
-import frappe
+from typing import List, Tuple, Union
+
import psycopg2
import psycopg2.extensions
-from frappe.utils import cstr
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
+import frappe
from frappe.database.database import Database
from frappe.database.postgres.schema import PostgresTable
+from frappe.utils import cstr, get_table_name
# cast decimals as floats
DEC2FLOAT = psycopg2.extensions.new_type(
@@ -170,6 +172,19 @@ class PostgresDatabase(Database):
def is_data_too_long(e):
return e.pgcode == '22001'
+ def rename_table(self, old_name: str, new_name: str) -> Union[List, Tuple]:
+ old_name = get_table_name(old_name)
+ new_name = get_table_name(new_name)
+ return self.sql(f"ALTER TABLE `{old_name}` RENAME TO `{new_name}`")
+
+ def describe(self, doctype: str)-> Union[List, Tuple]:
+ table_name = get_table_name(doctype)
+ return self.sql(f"SELECT COLUMN_NAME FROM information_schema.COLUMNS WHERE TABLE_NAME = '{table_name}'")
+
+ def change_column_type(self, table: str, column: str, type: str) -> Union[List, Tuple]:
+ table_name = get_table_name(table)
+ return self.sql(f'ALTER TABLE "{table_name}" ALTER COLUMN "{column}" TYPE {type}')
+
def create_auth_table(self):
self.sql_ddl("""create table if not exists "__Auth" (
"doctype" VARCHAR(140) NOT NULL,
@@ -297,6 +312,7 @@ class PostgresDatabase(Database):
def modify_query(query):
""""Modifies query according to the requirements of postgres"""
# replace ` with " for definitions
+ query = str(query)
query = query.replace('`', '"')
query = replace_locate_with_strpos(query)
# select from requires ""
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index eeb0eecd3f..a4e94aa326 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -225,6 +225,7 @@ CREATE TABLE "tabDocType" (
"allow_guest_to_view" smallint NOT NULL DEFAULT 0,
"route" varchar(255) DEFAULT NULL,
"is_published_field" varchar(255) DEFAULT NULL,
+ "website_search_field" varchar(255) DEFAULT NULL,
"email_append_to" smallint NOT NULL DEFAULT 0,
"subject_field" varchar(255) DEFAULT NULL,
"sender_field" varchar(255) DEFAULT NULL,
diff --git a/frappe/defaults.py b/frappe/defaults.py
index fde48d71ff..d4c338388d 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -124,11 +124,10 @@ def set_default(key, value, parent, parenttype="__default"):
where
defkey=%s and parent=%s
for update''', (key, parent)):
- frappe.db.sql("""
- delete from
- `tabDefaultValue`
- where
- defkey=%s and parent=%s""", (key, parent))
+ frappe.db.delete("DefaultValue", {
+ "defkey": key,
+ "parent": parent
+ })
if value != None:
add_default(key, value, parent)
else:
@@ -155,29 +154,23 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
:param name: Default ID.
:param parenttype: Clear defaults table for a particular type e.g. **User**.
"""
- conditions = []
- values = []
+ filters = {}
if name:
- conditions.append("name=%s")
- values.append(name)
+ filters.update({"name": name})
else:
if key:
- conditions.append("defkey=%s")
- values.append(key)
+ filters.update({"defkey": key})
if value:
- conditions.append("defvalue=%s")
- values.append(value)
+ filters.update({"defvalue": value})
if parent:
- conditions.append("parent=%s")
- values.append(parent)
+ filters.update({"parent": parent})
if parenttype:
- conditions.append("parenttype=%s")
- values.append(parenttype)
+ filters.update({"parenttype": parenttype})
if parent:
clear_defaults_cache(parent)
@@ -185,11 +178,10 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
clear_defaults_cache("__default")
clear_defaults_cache("__global")
- if not conditions:
+ if not filters:
raise Exception("[clear_default] No key specified.")
- frappe.db.sql("""delete from tabDefaultValue where {0}""".format(" and ".join(conditions)),
- tuple(values))
+ frappe.db.delete("DefaultValue", filters)
_clear_cache(parent)
diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
index 78d133b2d5..9f10522b12 100644
--- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py
@@ -64,7 +64,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
frappe.get_doc(dict(
doctype = 'Dashboard Chart',
@@ -94,7 +94,7 @@ class TestDashboardChart(unittest.TestCase):
if frappe.db.exists('Dashboard Chart', 'Test Empty Dashboard Chart 2'):
frappe.delete_doc('Dashboard Chart', 'Test Empty Dashboard Chart 2')
- frappe.db.sql('delete from `tabError Log`')
+ frappe.db.delete("Error Log")
# create one data point
frappe.get_doc(dict(doctype = 'Error Log', creation = '2018-06-01 00:00:00')).insert()
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 81a79cdb09..28c5a670cb 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -197,7 +197,7 @@ def set_desktop_icons(visible_list, ignore_duplicate=True):
# clear all custom only if setup is not complete
if not int(frappe.defaults.get_defaults().setup_complete or 0):
- frappe.db.sql('delete from `tabDesktop Icon` where standard=0')
+ frappe.db.delete("Desktop Icon", {"standard": 0})
# set standard as blocked and hidden if setting first active domain
if not frappe.flags.keep_desktop_icons:
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 57c89eaf2e..e7e7be530b 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -338,9 +338,8 @@ def delete_events(ref_type, ref_name, delete_event=False):
total_participants = frappe.get_all("Event Participants", filters={"parenttype": "Event", "parent": participation.parent})
if len(total_participants) <= 1:
- frappe.db.sql("DELETE FROM `tabEvent` WHERE `name` = %(name)s", {'name': participation.parent})
-
- frappe.db.sql("DELETE FROM `tabEvent Participants ` WHERE `name` = %(name)s", {'name': participation.name})
+ frappe.db.delete("Event", {"name": participation.parent})
+ frappe.db.delete("Event Participants", {"name": participation.name})
# Close events if ends_on or repeat_till is less than now_datetime
def set_status_of_events():
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index efb853cfa5..8d70dcd3dc 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -15,14 +15,14 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
+ let route_changed = null;
if (issingle) {
- frappe.set_route('Form', frm.doc.reference_doctype);
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
} else {
- const new_name = 'new-' + frappe.scrub(frm.doc.reference_doctype) + '-1';
- frappe.set_route('Form', frm.doc.reference_doctype, new_name);
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
}
- frappe.utils.sleep(500).then(() => {
+ route_changed.then(() => {
const tour_name = frm.doc.name;
cur_frm.tour
.init({ tour_name })
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 414f272f59..d7d7f68b74 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -12,7 +12,10 @@ class NotificationLog(Document):
frappe.publish_realtime('notification', after_commit=True, user=self.for_user)
set_notifications_as_unseen(self.for_user)
if is_email_notifications_enabled_for_type(self.for_user, self.type):
- send_notification_email(self)
+ try:
+ send_notification_email(self)
+ except frappe.OutgoingEmailError:
+ frappe.log_error(message=frappe.get_traceback(), title=_("Failed to send notification email"))
def get_permission_query_conditions(for_user):
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index b82077f485..95872440c7 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# For license information, please see license.txt
import frappe
@@ -8,6 +7,7 @@ from frappe.model.document import Document
class RouteHistory(Document):
pass
+
def flush_old_route_records():
"""Deletes all route records except last 500 records per user"""
@@ -24,19 +24,14 @@ def flush_old_route_records():
for user in users:
user = user[0]
last_record_to_keep = frappe.db.get_all('Route History',
- filters={
- 'user': user,
- },
+ filters={'user': user},
limit=1,
limit_start=500,
fields=['modified'],
- order_by='modified desc')
+ order_by='modified desc'
+ )
- frappe.db.sql('''
- DELETE
- FROM `tabRoute History`
- WHERE `modified` <= %(modified)s and `user`=%(modified)s
- ''', {
- "modified": last_record_to_keep[0].modified,
+ frappe.db.delete("Route History", {
+ "modified": ("<=", last_record_to_keep[0].modified),
"user": user
- })
\ No newline at end of file
+ })
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 4ea5c9cd7e..2341d721e2 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -1,4 +1,3 @@
-# -*- coding: utf-8 -*-
# Copyright (c) 2019, Frappe Technologies and contributors
# For license information, please see license.txt
@@ -123,7 +122,10 @@ def delete_tags_for_document(doc):
if not frappe.db.table_exists("Tag Link"):
return
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s""", (doc.doctype, doc.name))
+ frappe.db.delete("Tag Link", {
+ "document_type": doc.doctype,
+ "document_name": doc.name
+ })
def update_tags(doc, tags):
"""
@@ -161,7 +163,11 @@ def get_deleted_tags(new_tags, existing_tags):
return list(set(existing_tags) - set(new_tags))
def delete_tag_for_document(dt, dn, tag):
- frappe.db.sql("""DELETE FROM `tabTag Link` WHERE `document_type`=%s AND `document_name`=%s AND tag=%s""", (dt, dn, tag))
+ frappe.db.delete("Tag Link", {
+ "document_type": dt,
+ "document_name": dn,
+ "tag": tag
+ })
@frappe.whitelist()
def get_documents_for_tag(tag):
diff --git a/frappe/desk/doctype/tag/test_tag.py b/frappe/desk/doctype/tag/test_tag.py
index 442a891fd8..6eb7219c26 100644
--- a/frappe/desk/doctype/tag/test_tag.py
+++ b/frappe/desk/doctype/tag/test_tag.py
@@ -1,8 +1,26 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2019, Frappe Technologies and Contributors
-# See license.txt
-# import frappe
import unittest
+import frappe
+
+from frappe.desk.reportview import get_stats
+from frappe.desk.doctype.tag.tag import add_tag
class TestTag(unittest.TestCase):
- pass
+ def setUp(self) -> None:
+ frappe.db.sql("DELETE from `tabTag`")
+ frappe.db.sql("UPDATE `tabDocType` set _user_tags=''")
+
+ def test_tag_count_query(self):
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['No Tags', frappe.db.count('DocType')]]})
+ add_tag('Standard', 'DocType', 'User')
+ add_tag('Standard', 'DocType', 'ToDo')
+
+ # count with no filter
+ self.assertDictEqual(get_stats('["_user_tags"]', 'DocType'),
+ {'_user_tags': [['Standard', 2], ['No Tags', frappe.db.count('DocType') - 2]]})
+
+ # count with child table field filter
+ self.assertDictEqual(get_stats('["_user_tags"]',
+ 'DocType',
+ filters='[["DocField", "fieldname", "like", "%last_name%"], ["DocType", "name", "like", "%use%"]]'),
+ {'_user_tags': [['Standard', 1], ['No Tags', 0]]})
\ No newline at end of file
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 4696563445..09297b4e5e 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -5,11 +5,13 @@ import frappe
import json
from frappe.model.document import Document
-from frappe.utils import get_fullname
+from frappe.utils import get_fullname, parse_addr
exclude_from_linked_with = True
class ToDo(Document):
+ DocType = 'ToDo'
+
def validate(self):
self._assignment = None
if self.is_new():
@@ -39,13 +41,7 @@ class ToDo(Document):
self.update_in_reference()
def on_trash(self):
- # unlink todo from linked comments
- frappe.db.sql("""
- delete from `tabCommunication Link`
- where link_doctype=%(doctype)s and link_name=%(name)s""", {
- "doctype": self.doctype, "name": self.name
- })
-
+ self.delete_communication_links()
self.update_in_reference()
def add_assign_comment(self, text, comment_type):
@@ -54,6 +50,13 @@ class ToDo(Document):
frappe.get_doc(self.reference_type, self.reference_name).add_comment(comment_type, text)
+ def delete_communication_links(self):
+ # unlink todo from linked comments
+ return frappe.db.delete("Communication Link", {
+ "link_doctype": self.doctype,
+ "link_name": self.name
+ })
+
def update_in_reference(self):
if not (self.reference_type and self.reference_name):
return
@@ -84,6 +87,13 @@ class ToDo(Document):
else:
raise
+ @classmethod
+ def get_owners(cls, filters=None):
+ """Returns list of owners after applying filters on todo's.
+ """
+ rows = frappe.get_all(cls.DocType, filters=filters or {}, fields=['owner'])
+ return [parse_addr(row.owner)[1] for row in rows if row.owner]
+
# NOTE: todo is viewable if a user is an owner, or set as assigned_to value, or has any role that is allowed to access ToDo doctype.
def on_doctype_update():
frappe.db.add_index("ToDo", ["reference_type", "reference_name"])
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index 55515856f1..1dbc52eb5b 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -445,24 +445,36 @@ def get_stats(stats, doctype, filters=[]):
for tag in tags:
if not tag in columns: continue
try:
- tagcount = frappe.get_list(doctype, fields=[tag, "count(*)"],
- #filters=["ifnull(`%s`,'')!=''" % tag], group_by=tag, as_list=True)
- filters = filters + ["ifnull(`%s`,'')!=''" % tag], group_by = tag, as_list = True)
+ tag_count = frappe.get_list(doctype,
+ fields=[tag, "count(*)"],
+ filters=filters + [[tag, '!=', '']],
+ group_by=tag,
+ as_list=True,
+ distinct=1,
+ )
- if tag=='_user_tags':
- stats[tag] = scrub_user_tags(tagcount)
- stats[tag].append([_("No Tags"), frappe.get_list(doctype,
+ if tag == '_user_tags':
+ stats[tag] = scrub_user_tags(tag_count)
+ no_tag_count = frappe.get_list(doctype,
fields=[tag, "count(*)"],
- filters=filters +["({0} = ',' or {0} = '' or {0} is null)".format(tag)], as_list=True)[0][1]])
+ filters=filters + [[tag, "in", ('', ',')]],
+ as_list=True,
+ group_by=tag,
+ order_by=tag,
+ )
+
+ no_tag_count = no_tag_count[0][1] if no_tag_count else 0
+
+ stats[tag].append([_("No Tags"), no_tag_count])
else:
- stats[tag] = tagcount
+ stats[tag] = tag_count
except frappe.db.SQLError:
- # does not work for child tables
pass
- except frappe.db.InternalError:
+ except frappe.db.InternalError as e:
# raised when _user_tags column is added on the fly
pass
+
return stats
@frappe.whitelist()
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 040a8c2118..f9b65fc98e 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -168,7 +168,18 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0,
strict=False)
if doctype in UNTRANSLATED_DOCTYPES:
- values = tuple([v for v in list(values) if re.search(re.escape(txt)+".*", (_(v.name) if as_dict else _(v[0])), re.IGNORECASE)])
+ # Filtering the values array so that query is included in very element
+ values = (
+ v for v in values
+ if re.search(
+ f"{re.escape(txt)}.*", _(v.name if as_dict else v[0]), re.IGNORECASE
+ )
+ )
+
+ # Sorting the values array so that relevant results always come first
+ # This will first bring elements on top in which query is a prefix of element
+ # Then it will bring the rest of the elements and sort them in lexicographical order
+ values = sorted(values, key=lambda x: relevance_sorter(x, txt, as_dict))
# remove _relevance from results
if as_dict:
@@ -208,6 +219,13 @@ def scrub_custom_query(query, key, txt):
query = query.replace('%s', ((txt or '') + '%'))
return query
+def relevance_sorter(key, query, as_dict):
+ value = _(key.name if as_dict else key[0])
+ return (
+ value.lower().startswith(query.lower()) is not True,
+ value
+ )
+
@wrapt.decorator
def validate_and_sanitize_search_inputs(fn, instance, args, kwargs):
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
diff --git a/frappe/email/doctype/unhandled_email/unhandled_email.py b/frappe/email/doctype/unhandled_email/unhandled_email.py
index 6414dbece3..b445c98aa6 100644
--- a/frappe/email/doctype/unhandled_email/unhandled_email.py
+++ b/frappe/email/doctype/unhandled_email/unhandled_email.py
@@ -10,5 +10,6 @@ class UnhandledEmail(Document):
def remove_old_unhandled_emails():
- frappe.db.sql("""DELETE FROM `tabUnhandled Email`
- WHERE creation < %s""", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ frappe.db.delete("Unhandled Email", {
+ "creation": ("<", frappe.utils.add_days(frappe.utils.nowdate(), -30))
+ })
\ No newline at end of file
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 885a306cfb..ef59302bab 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -173,13 +173,8 @@ def clear_outbox(days=None):
WHERE `priority`=0 AND `modified` < (NOW() - INTERVAL '{0}' DAY)""".format(days))
if email_queues:
- frappe.db.sql("""DELETE FROM `tabEmail Queue` WHERE `name` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
-
- frappe.db.sql("""DELETE FROM `tabEmail Queue Recipient` WHERE `parent` IN ({0})""".format(
- ','.join(['%s']*len(email_queues)
- )), tuple(email_queues))
+ frappe.db.delete("Email Queue", {"name": ("in", email_queues)})
+ frappe.db.delete("Email Queue Recipient", {"parent": ("in", email_queues)})
def set_expiry_for_email_queue():
''' Mark emails as expire that has not sent for 7 days.
diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py
index 8e637273ed..2c7d119fce 100644
--- a/frappe/email/test_email_body.py
+++ b/frappe/email/test_email_body.py
@@ -127,7 +127,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
'''
transformed_html = '''
Hi John
-This is a test email
+This is a test email
'''
self.assertTrue(transformed_html in inline_style_in_html(html))
diff --git a/frappe/hooks.py b/frappe/hooks.py
index ac42a03461..f3d25d6bf4 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -171,6 +171,9 @@ doc_events = {
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
"frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers"
],
+ "on_update_after_submit": [
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ ],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
"frappe.automation.doctype.milestone_tracker.milestone_tracker.evaluate_milestone"
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index 75122f5aba..4aa8fb9000 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -152,32 +152,22 @@ def delete_fields(args_dict, delete=0):
if not fields:
continue
- frappe.db.sql("""
- DELETE FROM `tabDocField`
- WHERE parent='%s' AND fieldname IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ frappe.db.delete("DocField", {
+ "parent": dt,
+ "fieldname": ("in", fields),
+ })
# Delete the data/column only if delete is specified
if not delete:
continue
if frappe.db.get_value("DocType", dt, "issingle"):
- frappe.db.sql("""
- DELETE FROM `tabSingles`
- WHERE doctype='%s' AND field IN (%s)
- """ % (dt, ", ".join("'{}'".format(f) for f in fields)))
+ frappe.db.delete("Singles", {
+ "doctype": dt,
+ "field": ("in", fields),
+ })
else:
- existing_fields = frappe.db.multisql({
- "mariadb": "DESC `tab%s`" % dt,
- "postgres": """
- SELECT
- COLUMN_NAME
- FROM
- information_schema.COLUMNS
- WHERE
- TABLE_NAME = 'tab%s';
- """ % dt,
- })
+ existing_fields = frappe.db.describe(dt)
existing_fields = existing_fields and [e[0] for e in existing_fields] or []
fields_need_to_delete = set(fields) & set(existing_fields)
if not fields_need_to_delete:
diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py
index cc88cfa106..fbbf1a4852 100644
--- a/frappe/model/delete_doc.py
+++ b/frappe/model/delete_doc.py
@@ -65,12 +65,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa
update_flags(doc, flags, ignore_permissions)
check_permission_and_not_submitted(doc)
- frappe.db.sql("delete from `tabCustom Field` where dt = %s", name)
- frappe.db.sql("delete from `tabClient Script` where dt = %s", name)
- frappe.db.sql("delete from `tabProperty Setter` where doc_type = %s", name)
- frappe.db.sql("delete from `tabReport` where ref_doctype=%s", name)
- frappe.db.sql("delete from `tabCustom DocPerm` where parent=%s", name)
- frappe.db.sql("delete from `__global_search` where doctype=%s", name)
+ frappe.db.delete("Custom Field", {"dt": name})
+ frappe.db.delete("Client Script", {"dt": name})
+ frappe.db.delete("Property Setter", {"doc_type": name})
+ frappe.db.delete("Report", {"ref_doctype": name})
+ frappe.db.delete("Custom DocPerm", {"parent": name})
+ frappe.db.delete("__global_search", {"doctype": name})
delete_from_table(doctype, name, ignore_doctypes, None)
@@ -162,10 +162,9 @@ def update_naming_series(doc):
def delete_from_table(doctype, name, ignore_doctypes, doc):
if doctype!="DocType" and doctype==name:
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", name)
+ frappe.db.delete("Singles", {"doctype": name})
else:
- frappe.db.sql("delete from `tab{0}` where `name`=%s".format(doctype), name)
-
+ frappe.db.delete(doctype, {"name": name})
# get child tables
if doc:
tables = [d.options for d in doc.meta.get_table_fields()]
@@ -339,8 +338,10 @@ def clear_references(doctype, reference_doctype, reference_name,
(reference_doctype, reference_name))
def clear_timeline_references(link_doctype, link_name):
- frappe.db.sql("""DELETE FROM `tabCommunication Link`
- WHERE `tabCommunication Link`.link_doctype=%s AND `tabCommunication Link`.link_name=%s""", (link_doctype, link_name))
+ frappe.db.delete("Communication Link", {
+ "link_doctype": link_doctype,
+ "link_name": link_name
+ })
def insert_feed(doc):
if (
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 61160e1f01..b44d95716e 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -390,10 +390,11 @@ class Document(BaseDocument):
else:
# no rows found, delete all rows
- frappe.db.sql("""delete from `tab{0}` where parent=%s
- and parenttype=%s and parentfield=%s""".format(df.options),
- (self.name, self.doctype, fieldname))
-
+ frappe.db.delete(df.options, {
+ "parent": self.name,
+ "parenttype": self.doctype,
+ "parentfield": fieldname
+ })
def get_doc_before_save(self):
return getattr(self, '_doc_before_save', None)
@@ -451,7 +452,9 @@ class Document(BaseDocument):
def update_single(self, d):
"""Updates values for Single type Document in `tabSingles`."""
- frappe.db.sql("""delete from `tabSingles` where doctype=%s""", self.doctype)
+ frappe.db.delete("Singles", {
+ "doctype": self.doctype
+ })
for field, value in d.items():
if field != "doctype":
frappe.db.sql("""insert into `tabSingles` (doctype, field, value)
diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py
index 2f83b88572..6265498c96 100644
--- a/frappe/parallel_test_runner.py
+++ b/frappe/parallel_test_runner.py
@@ -111,33 +111,16 @@ class ParallelTestRunner():
if self.with_coverage:
from coverage import Coverage
from frappe.utils import get_bench_path
+ from frappe.coverage import STANDARD_INCLUSIONS, STANDARD_EXCLUSIONS, FRAPPE_EXCLUSIONS
# Generate coverage report only for app that is being tested
source_path = os.path.join(get_bench_path(), 'apps', self.app)
- incl = [
- '*.py',
- ]
- omit = [
- '*.js',
- '*.xml',
- '*.pyc',
- '*.css',
- '*.less',
- '*.scss',
- '*.vue',
- '*.pyc',
- '*.html',
- '*/test_*',
- '*/node_modules/*',
- '*/doctype/*/*_dashboard.py',
- '*/patches/*',
- ]
+ omit = STANDARD_EXCLUSIONS[:]
if self.app == 'frappe':
- omit.append('*/tests/*')
- omit.append('*/commands/*')
+ omit.extend(FRAPPE_EXCLUSIONS)
- self.coverage = Coverage(source=[source_path], omit=omit, include=incl)
+ self.coverage = Coverage(source=[source_path], omit=omit, include=STANDARD_INCLUSIONS)
self.coverage.start()
def save_coverage(self):
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 7605d8ea2b..493c4dc9f6 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -180,3 +180,4 @@ frappe.patches.v12_0.rename_uploaded_files_with_proper_name
frappe.patches.v13_0.queryreport_columns
frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
+frappe.patches.v14_0.drop_data_import_legacy
diff --git a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
index 49b68ed240..7e84c5ae24 100644
--- a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
+++ b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
@@ -28,7 +28,7 @@ def execute():
for prop in property_setters:
property_setter_map[prop.field_name] = prop
- frappe.db.sql('DELETE FROM `tabProperty Setter` WHERE `name`=%s', prop.name)
+ frappe.db.delete("Property Setter", {"name": prop.name})
meta = frappe.get_meta(doctype.name)
@@ -50,6 +50,6 @@ def execute():
df = frappe.new_doc('DocField', meta, 'fields')
df.update(cf)
meta.fields.append(df)
- frappe.db.sql('DELETE FROM `tabCustom Field` WHERE name=%s', cf.name)
+ frappe.db.delete("Custom Field", {"name": cf.name})
meta.save()
diff --git a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
index a8e9bd4de1..901ab66bfd 100644
--- a/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
+++ b/frappe/patches/v11_0/sync_stripe_settings_before_migrate.py
@@ -17,4 +17,4 @@ def execute():
settings.secret_key = secret_key
settings.save(ignore_permissions=True)
- frappe.db.sql("""DELETE FROM tabSingles WHERE doctype='Stripe Settings'""")
\ No newline at end of file
+ frappe.db.delete("Singles", {"doctype": "Stripe Settings"})
diff --git a/frappe/patches/v12_0/delete_feedback_request_if_exists.py b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
index fdbcecfc5a..c1bf46b14a 100644
--- a/frappe/patches/v12_0/delete_feedback_request_if_exists.py
+++ b/frappe/patches/v12_0/delete_feedback_request_if_exists.py
@@ -2,7 +2,4 @@
import frappe
def execute():
- frappe.db.sql('''
- DELETE from `tabDocType`
- WHERE name = 'Feedback Request'
- ''')
\ No newline at end of file
+ frappe.db.delete("DocType", {"name": "Feedback Request"})
diff --git a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
index 60599066e6..9c9a79ccbf 100644
--- a/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
+++ b/frappe/patches/v12_0/remove_deprecated_fields_from_doctype.py
@@ -8,7 +8,6 @@ def execute():
'DocType': ['hide_heading', 'image_view', 'read_only_onload']
}, delete=1)
- frappe.db.sql('''
- DELETE from `tabProperty Setter`
- WHERE property = 'read_only_onload'
- ''')
+ frappe.db.delete("Property Setter", {
+ "property": "read_only_onload"
+ })
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
index 65a635c170..90766b5f64 100644
--- a/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
+++ b/frappe/patches/v12_0/set_correct_assign_value_in_docs.py
@@ -1,32 +1,29 @@
import frappe
+from frappe.query_builder.functions import GroupConcat, Coalesce
def execute():
- frappe.reload_doc('desk', 'doctype', 'todo')
+ frappe.reload_doc("desk", "doctype", "todo")
- query = '''
- SELECT
- name, reference_type, reference_name, {} as assignees
- FROM
- `tabToDo`
- WHERE
- COALESCE(reference_type, '') != '' AND
- COALESCE(reference_name, '') != '' AND
- status != 'Cancelled'
- GROUP BY
- reference_type, reference_name
- '''
+ ToDo = frappe.qb.Table("ToDo")
+ assignees = GroupConcat("owner").distinct().as_("assignees")
- assignments = frappe.db.multisql({
- 'mariadb': query.format('GROUP_CONCAT(DISTINCT `owner`)'),
- 'postgres': query.format('STRING_AGG(DISTINCT "owner", ",")')
- }, as_dict=True)
+ query = (
+ frappe.qb.from_(ToDo)
+ .select(ToDo.name, ToDo.reference_type, assignees)
+ .where(Coalesce(ToDo.reference_type, "") != "")
+ .where(Coalesce(ToDo.reference_name, "") != "")
+ .where(ToDo.status != "Cancelled")
+ .groupby(ToDo.reference_type, ToDo.reference_name)
+ )
+
+ assignments = frappe.db.sql(query, as_dict=True)
for doc in assignments:
- assignments = doc.assignees.split(',')
+ assignments = doc.assignees.split(",")
frappe.db.set_value(
doc.reference_type,
doc.reference_name,
- '_assign',
+ "_assign",
frappe.as_json(assignments),
update_modified=False
- )
+ )
\ No newline at end of file
diff --git a/frappe/patches/v12_0/set_primary_key_in_series.py b/frappe/patches/v12_0/set_primary_key_in_series.py
index e5ed2204ba..83a903fc2d 100644
--- a/frappe/patches/v12_0/set_primary_key_in_series.py
+++ b/frappe/patches/v12_0/set_primary_key_in_series.py
@@ -1,21 +1,24 @@
import frappe
def execute():
- #if current = 0, simply delete the key as it'll be recreated on first entry
- frappe.db.sql('delete from `tabSeries` where current = 0')
- duplicate_keys = frappe.db.sql('''
- SELECT name, max(current) as current
- from
- `tabSeries`
- group by
- name
- having count(name) > 1
- ''', as_dict=True)
- for row in duplicate_keys:
- frappe.db.sql('delete from `tabSeries` where name = %(key)s', {
- 'key': row.name
- })
- if row.current:
- frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
- frappe.db.commit()
- frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
+ #if current = 0, simply delete the key as it'll be recreated on first entry
+ frappe.db.delete("Series", {"current": 0})
+
+ duplicate_keys = frappe.db.sql('''
+ SELECT name, max(current) as current
+ from
+ `tabSeries`
+ group by
+ name
+ having count(name) > 1
+ ''', as_dict=True)
+
+ for row in duplicate_keys:
+ frappe.db.delete("Series", {
+ "name": row.name
+ })
+ if row.current:
+ frappe.db.sql('insert into `tabSeries`(`name`, `current`) values (%(name)s, %(current)s)', row)
+ frappe.db.commit()
+
+ frappe.db.sql('ALTER table `tabSeries` ADD PRIMARY KEY IF NOT EXISTS (name)')
diff --git a/frappe/patches/v12_0/setup_comments_from_communications.py b/frappe/patches/v12_0/setup_comments_from_communications.py
index 039ceeff35..11e02965f1 100644
--- a/frappe/patches/v12_0/setup_comments_from_communications.py
+++ b/frappe/patches/v12_0/setup_comments_from_communications.py
@@ -29,4 +29,6 @@ def execute():
frappe.db.auto_commit_on_many_writes = False
# clean up
- frappe.db.sql("delete from `tabCommunication` where communication_type = 'Comment'")
+ frappe.db.delete("Communication", {
+ "communication_type": "Comment"
+ })
diff --git a/frappe/patches/v13_0/increase_password_length.py b/frappe/patches/v13_0/increase_password_length.py
index 1bb1979051..62ca2ed779 100644
--- a/frappe/patches/v13_0/increase_password_length.py
+++ b/frappe/patches/v13_0/increase_password_length.py
@@ -1,7 +1,4 @@
import frappe
def execute():
- frappe.db.multisql({
- "mariadb": "ALTER TABLE `__Auth` MODIFY `password` TEXT NOT NULL",
- "postgres": 'ALTER TABLE "__Auth" ALTER COLUMN "password" TYPE TEXT'
- })
+ frappe.db.change_column_type(table="__Auth", column="password", type="TEXT")
diff --git a/frappe/patches/v13_0/remove_twilio_settings.py b/frappe/patches/v13_0/remove_twilio_settings.py
index 363cbdd4b6..7efaf876e2 100644
--- a/frappe/patches/v13_0/remove_twilio_settings.py
+++ b/frappe/patches/v13_0/remove_twilio_settings.py
@@ -12,7 +12,9 @@ def execute():
frappe.delete_doc_if_exists('DocType', 'Twilio Number Group')
if twilio_settings_doctype_in_integrations():
frappe.delete_doc_if_exists('DocType', 'Twilio Settings')
- frappe.db.sql("delete from `tabSingles` where `doctype`=%s", 'Twilio Settings')
+ frappe.db.delete("Singles", {
+ "doctype": "Twilio Settings"
+ })
def twilio_settings_doctype_in_integrations() -> bool:
"""Check Twilio Settings doctype exists in integrations module or not.
diff --git a/frappe/core/doctype/data_import_legacy/__init__.py b/frappe/patches/v14_0/__init__.py
similarity index 100%
rename from frappe/core/doctype/data_import_legacy/__init__.py
rename to frappe/patches/v14_0/__init__.py
diff --git a/frappe/patches/v14_0/drop_data_import_legacy.py b/frappe/patches/v14_0/drop_data_import_legacy.py
new file mode 100644
index 0000000000..2037930c9f
--- /dev/null
+++ b/frappe/patches/v14_0/drop_data_import_legacy.py
@@ -0,0 +1,22 @@
+import frappe
+import click
+
+
+def execute():
+ doctype = "Data Import Legacy"
+ table = frappe.utils.get_table_name(doctype)
+
+ # delete the doctype record to avoid broken links
+ frappe.db.delete("DocType", {"name": doctype})
+
+ # leaving table in database for manual cleanup
+ click.secho(
+ f"`{doctype}` has been deprecated. The DocType is deleted, but the data still"
+ " exists on the database. If this data is worth recovering, you may export it"
+ f" using\n\n\tbench --site {frappe.local.site} backup -i '{doctype}'\n\nAfter"
+ " this, the table will continue to persist in the database, until you choose"
+ " to remove it yourself. If you want to drop the table, you may run\n\n\tbench"
+ f" --site {frappe.local.site} execute frappe.db.sql --args \"('DROP TABLE IF"
+ f" EXISTS `{table}`', )\"\n",
+ fg="yellow",
+ )
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 07b4a2e68f..33aef4ab41 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -7,9 +7,11 @@ import frappe.share
from frappe import _, msgprint
from frappe.utils import cint
+
rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend",
"print", "email", "report", "import", "export", "set_user_permissions", "share")
+
def check_admin_or_system_manager(user=None):
if not user: user = frappe.session.user
@@ -516,8 +518,7 @@ def reset_perms(doctype):
"""Reset permissions for given doctype."""
from frappe.desk.notifications import delete_notification_count_for
delete_notification_count_for(doctype)
-
- frappe.db.sql("""delete from `tabCustom DocPerm` where parent=%s""", doctype)
+ frappe.db.delete("Custom DocPerm", {"parent": doctype})
def get_linked_doctypes(dt):
return list(set([dt] + [d.options for d in
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 908479fd02..ca2a340661 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -403,19 +403,14 @@ frappe.ui.form.PrintView = class {
setup_print_format_dom(out, $print_format) {
this.print_wrapper.find('.print-format-skeleton').remove();
let base_url = frappe.urllib.get_base_url();
- let print_css = frappe.assets.bundled_asset('print.bundle.css');
+ let print_css = frappe.assets.bundled_asset('print.bundle.css', frappe.utils.is_rtl(this.lang_code));
+ this.$print_format_body.find('html').attr('dir', frappe.utils.is_rtl(this.lang_code) ? 'rtl': 'ltr');
+ this.$print_format_body.find('html').attr('lang', this.lang_code);
this.$print_format_body.find('head').html(
`
`
);
- if (frappe.utils.is_rtl(this.lang_code)) {
- let rtl_css = frappe.assets.bundled_asset('frappe-rtl.bundle.css');
- this.$print_format_body.find('head').append(
- ``
- );
- }
-
this.$print_format_body.find('body').html(
`${out.html}
`
);
diff --git a/frappe/public/build.json b/frappe/public/build.json
deleted file mode 100755
index 942871ee9b..0000000000
--- a/frappe/public/build.json
+++ /dev/null
@@ -1,299 +0,0 @@
-{
- "css/frappe-web-b4.css": "public/scss/website.scss",
- "css/frappe-chat-web.css": [
- "public/css/font-awesome.css",
- "public/css/octicons/octicons.css",
- "public/less/chat.less"
- ],
- "concat:js/moment-bundle.min.js": [
- "node_modules/moment/min/moment-with-locales.min.js",
- "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"
- ],
- "js/chat.js": "public/js/frappe/chat.js",
- "js/frappe-recorder.min.js": "public/js/frappe/recorder/recorder.js",
- "js/checkout.min.js": "public/js/integrations/razorpay.js",
- "js/frappe-web.min.js": [
- "public/js/frappe/class.js",
- "public/js/frappe/polyfill.js",
- "public/js/lib/md5.min.js",
- "public/js/frappe/provide.js",
- "public/js/frappe/format.js",
- "public/js/frappe/utils/number_format.js",
- "public/js/frappe/utils/utils.js",
- "public/js/frappe/utils/common.js",
- "public/js/frappe/ui/messages.js",
- "public/js/frappe/translate.js",
- "public/js/frappe/utils/pretty_date.js",
- "public/js/frappe/microtemplate.js",
- "public/js/frappe/query_string.js",
-
- "public/js/frappe/upload.js",
-
- "public/js/frappe/model/meta.js",
- "public/js/frappe/model/model.js",
- "public/js/frappe/model/perm.js",
-
- "website/js/website.js",
- "public/js/frappe/socketio_client.js"
- ],
- "js/bootstrap-4-web.min.js": "website/js/bootstrap-4.js",
- "js/control.min.js": [
- "node_modules/air-datepicker/dist/js/datepicker.min.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.cs.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.da.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.de.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.en.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.es.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.fi.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.fr.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.hu.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.nl.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pl.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pt-BR.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.pt.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.ro.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.sk.js",
- "node_modules/air-datepicker/dist/js/i18n/datepicker.zh.js",
- "public/js/frappe/ui/capture.js",
- "public/js/frappe/form/controls/control.js"
- ],
- "js/dialog.min.js": [
- "public/js/frappe/dom.js",
- "public/js/frappe/form/formatters.js",
- "public/js/frappe/form/layout.js",
- "public/js/frappe/ui/field_group.js",
- "public/js/frappe/form/link_selector.js",
- "public/js/frappe/form/multi_select_dialog.js",
- "public/js/frappe/ui/dialog.js"
- ],
- "css/desk.min.css": [
- "public/js/lib/leaflet/leaflet.css",
- "public/js/lib/leaflet/leaflet.draw.css",
- "public/js/lib/leaflet/L.Control.Locate.css",
- "public/js/lib/leaflet/easy-button.css",
- "public/css/font-awesome.css",
- "public/css/octicons/octicons.css",
- "public/less/desk.less",
- "public/less/module.less",
- "public/less/mobile.less",
- "public/less/controls.less",
- "public/less/chat.less",
- "public/css/fonts/inter/inter.css",
- "node_modules/frappe-charts/dist/frappe-charts.min.css",
- "node_modules/plyr/dist/plyr.css",
- "public/scss/desk.scss"
- ],
- "css/frappe-rtl.css": [
- "public/css/bootstrap-rtl.css",
- "public/css/desk-rtl.css",
- "public/css/report-rtl.css"
- ],
- "css/printview.css": [
- "public/css/bootstrap.css",
- "public/scss/print.scss"
- ],
- "concat:js/libs.min.js": [
- "public/js/lib/Sortable.min.js",
- "public/js/lib/jquery/jquery.hotkeys.js",
- "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js",
- "node_modules/vue/dist/vue.min.js",
- "node_modules/moment/min/moment-with-locales.min.js",
- "node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
- "node_modules/socket.io-client/dist/socket.io.slim.js",
- "node_modules/localforage/dist/localforage.min.js",
- "public/js/lib/jSignature.min.js",
- "public/js/lib/leaflet/leaflet.js",
- "public/js/lib/leaflet/leaflet.draw.js",
- "public/js/lib/leaflet/L.Control.Locate.js",
- "public/js/lib/leaflet/easy-button.js"
- ],
- "js/desk.min.js": [
- "public/js/frappe/translate.js",
- "public/js/frappe/class.js",
- "public/js/frappe/polyfill.js",
- "public/js/frappe/provide.js",
- "public/js/frappe/assets.js",
- "public/js/frappe/format.js",
- "public/js/frappe/form/formatters.js",
- "public/js/frappe/dom.js",
- "public/js/frappe/ui/messages.js",
- "public/js/frappe/ui/keyboard.js",
- "public/js/frappe/ui/colors.js",
- "public/js/frappe/ui/sidebar.js",
- "public/js/frappe/ui/link_preview.js",
-
- "public/js/frappe/request.js",
- "public/js/frappe/socketio_client.js",
- "public/js/frappe/utils/utils.js",
- "public/js/frappe/event_emitter.js",
- "public/js/frappe/router.js",
- "public/js/frappe/router_history.js",
- "public/js/frappe/defaults.js",
- "public/js/frappe/roles_editor.js",
- "public/js/frappe/module_editor.js",
- "public/js/frappe/microtemplate.js",
-
- "public/js/frappe/ui/page.html",
- "public/js/frappe/ui/page.js",
- "public/js/frappe/ui/slides.js",
- "public/js/frappe/ui/onboarding_dialog.js",
- "public/js/frappe/ui/find.js",
- "public/js/frappe/ui/iconbar.js",
- "public/js/frappe/form/layout.js",
- "public/js/frappe/ui/field_group.js",
- "public/js/frappe/form/link_selector.js",
- "public/js/frappe/form/multi_select_dialog.js",
- "public/js/frappe/ui/dialog.js",
- "public/js/frappe/ui/capture.js",
- "public/js/frappe/ui/app_icon.js",
- "public/js/frappe/ui/theme_switcher.js",
-
- "public/js/frappe/model/model.js",
- "public/js/frappe/db.js",
- "public/js/frappe/model/meta.js",
- "public/js/frappe/model/sync.js",
- "public/js/frappe/model/create_new.js",
- "public/js/frappe/model/perm.js",
- "public/js/frappe/model/workflow.js",
- "public/js/frappe/model/user_settings.js",
-
- "public/js/lib/md5.min.js",
- "public/js/frappe/utils/user.js",
- "public/js/frappe/utils/common.js",
- "public/js/frappe/utils/urllib.js",
- "public/js/frappe/utils/pretty_date.js",
- "public/js/frappe/utils/tools.js",
- "public/js/frappe/utils/datetime.js",
- "public/js/frappe/utils/number_format.js",
- "public/js/frappe/utils/help.js",
- "public/js/frappe/utils/help_links.js",
- "public/js/frappe/utils/address_and_contact.js",
- "public/js/frappe/utils/preview_email.js",
- "public/js/frappe/utils/file_manager.js",
-
- "public/js/frappe/upload.js",
- "public/js/frappe/ui/tree.js",
-
- "public/js/frappe/views/container.js",
- "public/js/frappe/views/breadcrumbs.js",
- "public/js/frappe/views/factory.js",
- "public/js/frappe/views/pageview.js",
-
- "public/js/frappe/ui/toolbar/awesome_bar.js",
- "public/js/frappe/ui/toolbar/energy_points_notifications.js",
- "public/js/frappe/ui/notifications/notifications.js",
- "public/js/frappe/ui/toolbar/search.js",
- "public/js/frappe/ui/toolbar/tag_utils.js",
- "public/js/frappe/ui/toolbar/search.html",
- "public/js/frappe/ui/toolbar/search_utils.js",
- "public/js/frappe/ui/toolbar/about.js",
- "public/js/frappe/ui/toolbar/navbar.html",
- "public/js/frappe/ui/toolbar/toolbar.js",
- "public/js/frappe/ui/toolbar/notifications.js",
- "public/js/frappe/views/communication.js",
- "public/js/frappe/views/translation_manager.js",
- "public/js/frappe/views/workspace/workspace.js",
-
- "public/js/frappe/widgets/widget_group.js",
-
- "public/js/frappe/ui/sort_selector.html",
- "public/js/frappe/ui/sort_selector.js",
-
- "public/js/frappe/change_log.html",
- "public/js/frappe/ui/workspace_loading_skeleton.html",
- "public/js/frappe/desk.js",
- "public/js/frappe/query_string.js",
-
- "public/js/frappe/ui/comment.js",
-
- "public/js/frappe/chat.js",
- "public/js/frappe/utils/energy_point_utils.js",
- "public/js/frappe/utils/dashboard_utils.js",
- "public/js/frappe/ui/chart.js",
- "public/js/frappe/ui/datatable.js",
- "public/js/frappe/ui/driver.js",
- "public/js/frappe/ui/plyr.js",
- "public/js/frappe/barcode_scanner/index.js"
- ],
- "js/form.min.js": [
- "public/js/frappe/form/templates/**.html",
- "public/js/frappe/form/controls/control.js",
- "public/js/frappe/views/formview.js",
- "public/js/frappe/form/form.js",
- "public/js/frappe/meta_tag.js"
- ],
- "js/list.min.js": [
- "public/js/frappe/ui/listing.html",
-
- "public/js/frappe/model/indicator.js",
- "public/js/frappe/ui/filters/filter.js",
- "public/js/frappe/ui/filters/filter_list.js",
- "public/js/frappe/ui/filters/field_select.js",
- "public/js/frappe/ui/filters/edit_filter.html",
- "public/js/frappe/ui/tags.js",
- "public/js/frappe/ui/tag_editor.js",
- "public/js/frappe/ui/like.js",
- "public/js/frappe/ui/liked_by.html",
- "public/html/print_template.html",
-
- "public/js/frappe/list/base_list.js",
- "public/js/frappe/list/list_view.js",
- "public/js/frappe/list/list_factory.js",
-
- "public/js/frappe/list/list_view_select.js",
- "public/js/frappe/list/list_sidebar.js",
- "public/js/frappe/list/list_sidebar.html",
- "public/js/frappe/list/list_sidebar_stat.html",
- "public/js/frappe/list/list_sidebar_group_by.js",
- "public/js/frappe/list/list_view_permission_restrictions.html",
-
- "public/js/frappe/views/gantt/gantt_view.js",
- "public/js/frappe/views/calendar/calendar.js",
- "public/js/frappe/views/dashboard/dashboard_view.js",
- "public/js/frappe/views/image/image_view.js",
- "public/js/frappe/views/map/map_view.js",
- "public/js/frappe/views/kanban/kanban_view.js",
- "public/js/frappe/views/inbox/inbox_view.js",
- "public/js/frappe/views/file/file_view.js",
-
- "public/js/frappe/views/treeview.js",
- "public/js/frappe/views/interaction.js",
-
- "public/js/frappe/views/image/image_view_item_row.html",
- "public/js/frappe/views/image/photoswipe_dom.html",
-
- "public/js/frappe/views/kanban/kanban_board.html",
- "public/js/frappe/views/kanban/kanban_column.html",
- "public/js/frappe/views/kanban/kanban_card.html"
- ],
- "css/report.min.css": [
- "node_modules/frappe-datatable/dist/frappe-datatable.css",
- "public/css/tree_grid.css"
- ],
- "js/report.min.js": [
- "public/js/lib/clusterize.min.js",
- "public/js/frappe/views/reports/report_factory.js",
- "public/js/frappe/views/reports/report_view.js",
- "public/js/frappe/views/reports/query_report.js",
- "public/js/frappe/views/reports/print_grid.html",
- "public/js/frappe/views/reports/print_tree.html",
- "public/js/frappe/ui/group_by/group_by.html",
- "public/js/frappe/ui/group_by/group_by.js",
- "public/js/frappe/views/reports/report_utils.js"
- ],
- "js/web_form.min.js": [
- "public/js/frappe/utils/datetime.js",
- "public/js/frappe/web_form/webform_script.js"
- ],
- "css/web_form.css": [
- "website/css/web_form.css",
- "public/css/octicons/octicons.css",
- "public/scss/controls.scss",
- "node_modules/frappe-datatable/dist/frappe-datatable.css"
- ],
- "css/email.css": "public/scss/email.scss",
- "js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js",
- "js/user_profile_controller.min.js": "desk/page/user_profile/user_profile_controller.js",
- "css/login.css": "public/scss/login.scss",
- "js/data_import_tools.min.js": "public/js/frappe/data_import/index.js"
-}
diff --git a/frappe/public/css/bootstrap-rtl.css b/frappe/public/css/bootstrap-rtl.css
deleted file mode 100644
index 5dfa46c055..0000000000
--- a/frappe/public/css/bootstrap-rtl.css
+++ /dev/null
@@ -1,1476 +0,0 @@
-/*******************************************************************************
- * bootstrap-rtl (version 3.3.4)
- * Author: Morteza Ansarinia (http://github.com/morteza)
- * Created on: August 13,2015
- * Project: bootstrap-rtl
- * Copyright: Unlicensed Public Domain
- *******************************************************************************/
-
-html {
- direction: rtl;
-}
-body {
- direction: rtl;
-}
-.flip.text-left {
- text-align: right;
-}
-.flip.text-right {
- text-align: left;
-}
-.list-unstyled {
- padding-right: 0;
- padding-left: initial;
-}
-.list-inline {
- padding-right: 0;
- padding-left: initial;
- margin-right: -5px;
- margin-left: 0;
-}
-dd {
- margin-right: 0;
- margin-left: initial;
-}
-@media (min-width: 768px) {
- .dl-horizontal dt {
- float: right;
- clear: right;
- text-align: left;
- }
- .dl-horizontal dd {
- margin-right: 180px;
- margin-left: 0;
- }
-}
-blockquote {
- border-right: 5px solid #eeeeee;
- border-left: 0;
-}
-.blockquote-reverse,
-blockquote.pull-left {
- padding-left: 15px;
- padding-right: 0;
- border-left: 5px solid #eeeeee;
- border-right: 0;
- text-align: left;
-}
-.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
- position: relative;
- min-height: 1px;
- padding-left: 15px;
- padding-right: 15px;
-}
-.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
- float: right;
-}
-.col-xs-12 {
- width: 100%;
-}
-.col-xs-11 {
- width: 91.66666667%;
-}
-.col-xs-10 {
- width: 83.33333333%;
-}
-.col-xs-9 {
- width: 75%;
-}
-.col-xs-8 {
- width: 66.66666667%;
-}
-.col-xs-7 {
- width: 58.33333333%;
-}
-.col-xs-6 {
- width: 50%;
-}
-.col-xs-5 {
- width: 41.66666667%;
-}
-.col-xs-4 {
- width: 33.33333333%;
-}
-.col-xs-3 {
- width: 25%;
-}
-.col-xs-2 {
- width: 16.66666667%;
-}
-.col-xs-1 {
- width: 8.33333333%;
-}
-.col-xs-pull-12 {
- left: 100%;
- right: auto;
-}
-.col-xs-pull-11 {
- left: 91.66666667%;
- right: auto;
-}
-.col-xs-pull-10 {
- left: 83.33333333%;
- right: auto;
-}
-.col-xs-pull-9 {
- left: 75%;
- right: auto;
-}
-.col-xs-pull-8 {
- left: 66.66666667%;
- right: auto;
-}
-.col-xs-pull-7 {
- left: 58.33333333%;
- right: auto;
-}
-.col-xs-pull-6 {
- left: 50%;
- right: auto;
-}
-.col-xs-pull-5 {
- left: 41.66666667%;
- right: auto;
-}
-.col-xs-pull-4 {
- left: 33.33333333%;
- right: auto;
-}
-.col-xs-pull-3 {
- left: 25%;
- right: auto;
-}
-.col-xs-pull-2 {
- left: 16.66666667%;
- right: auto;
-}
-.col-xs-pull-1 {
- left: 8.33333333%;
- right: auto;
-}
-.col-xs-pull-0 {
- left: auto;
- right: auto;
-}
-.col-xs-push-12 {
- right: 100%;
- left: 0;
-}
-.col-xs-push-11 {
- right: 91.66666667%;
- left: 0;
-}
-.col-xs-push-10 {
- right: 83.33333333%;
- left: 0;
-}
-.col-xs-push-9 {
- right: 75%;
- left: 0;
-}
-.col-xs-push-8 {
- right: 66.66666667%;
- left: 0;
-}
-.col-xs-push-7 {
- right: 58.33333333%;
- left: 0;
-}
-.col-xs-push-6 {
- right: 50%;
- left: 0;
-}
-.col-xs-push-5 {
- right: 41.66666667%;
- left: 0;
-}
-.col-xs-push-4 {
- right: 33.33333333%;
- left: 0;
-}
-.col-xs-push-3 {
- right: 25%;
- left: 0;
-}
-.col-xs-push-2 {
- right: 16.66666667%;
- left: 0;
-}
-.col-xs-push-1 {
- right: 8.33333333%;
- left: 0;
-}
-.col-xs-push-0 {
- right: auto;
- left: 0;
-}
-.col-xs-offset-12 {
- margin-right: 100%;
- margin-left: 0;
-}
-.col-xs-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-9 {
- margin-right: 75%;
- margin-left: 0;
-}
-.col-xs-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-6 {
- margin-right: 50%;
- margin-left: 0;
-}
-.col-xs-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-3 {
- margin-right: 25%;
- margin-left: 0;
-}
-.col-xs-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
-}
-.col-xs-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
-}
-.col-xs-offset-0 {
- margin-right: 0%;
- margin-left: 0;
-}
-@media (min-width: 768px) {
- .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
- float: right;
- }
- .col-sm-12 {
- width: 100%;
- }
- .col-sm-11 {
- width: 91.66666667%;
- }
- .col-sm-10 {
- width: 83.33333333%;
- }
- .col-sm-9 {
- width: 75%;
- }
- .col-sm-8 {
- width: 66.66666667%;
- }
- .col-sm-7 {
- width: 58.33333333%;
- }
- .col-sm-6 {
- width: 50%;
- }
- .col-sm-5 {
- width: 41.66666667%;
- }
- .col-sm-4 {
- width: 33.33333333%;
- }
- .col-sm-3 {
- width: 25%;
- }
- .col-sm-2 {
- width: 16.66666667%;
- }
- .col-sm-1 {
- width: 8.33333333%;
- }
- .col-sm-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-sm-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-sm-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-sm-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-sm-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-sm-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-sm-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-sm-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-sm-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-sm-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-sm-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-sm-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-sm-pull-0 {
- left: auto;
- right: auto;
- }
- .col-sm-push-12 {
- right: 100%;
- left: 0;
- }
- .col-sm-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-sm-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-sm-push-9 {
- right: 75%;
- left: 0;
- }
- .col-sm-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-sm-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-sm-push-6 {
- right: 50%;
- left: 0;
- }
- .col-sm-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-sm-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-sm-push-3 {
- right: 25%;
- left: 0;
- }
- .col-sm-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-sm-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-sm-push-0 {
- right: auto;
- left: 0;
- }
- .col-sm-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-sm-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-sm-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-sm-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-sm-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-sm-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-sm-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-@media (min-width: 992px) {
- .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
- float: right;
- }
- .col-md-12 {
- width: 100%;
- }
- .col-md-11 {
- width: 91.66666667%;
- }
- .col-md-10 {
- width: 83.33333333%;
- }
- .col-md-9 {
- width: 75%;
- }
- .col-md-8 {
- width: 66.66666667%;
- }
- .col-md-7 {
- width: 58.33333333%;
- }
- .col-md-6 {
- width: 50%;
- }
- .col-md-5 {
- width: 41.66666667%;
- }
- .col-md-4 {
- width: 33.33333333%;
- }
- .col-md-3 {
- width: 25%;
- }
- .col-md-2 {
- width: 16.66666667%;
- }
- .col-md-1 {
- width: 8.33333333%;
- }
- .col-md-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-md-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-md-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-md-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-md-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-md-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-md-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-md-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-md-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-md-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-md-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-md-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-md-pull-0 {
- left: auto;
- right: auto;
- }
- .col-md-push-12 {
- right: 100%;
- left: 0;
- }
- .col-md-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-md-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-md-push-9 {
- right: 75%;
- left: 0;
- }
- .col-md-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-md-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-md-push-6 {
- right: 50%;
- left: 0;
- }
- .col-md-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-md-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-md-push-3 {
- right: 25%;
- left: 0;
- }
- .col-md-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-md-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-md-push-0 {
- right: auto;
- left: 0;
- }
- .col-md-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-md-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-md-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-md-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-md-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-md-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-md-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-md-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-md-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-md-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-md-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-md-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-md-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-@media (min-width: 1200px) {
- .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
- float: right;
- }
- .col-lg-12 {
- width: 100%;
- }
- .col-lg-11 {
- width: 91.66666667%;
- }
- .col-lg-10 {
- width: 83.33333333%;
- }
- .col-lg-9 {
- width: 75%;
- }
- .col-lg-8 {
- width: 66.66666667%;
- }
- .col-lg-7 {
- width: 58.33333333%;
- }
- .col-lg-6 {
- width: 50%;
- }
- .col-lg-5 {
- width: 41.66666667%;
- }
- .col-lg-4 {
- width: 33.33333333%;
- }
- .col-lg-3 {
- width: 25%;
- }
- .col-lg-2 {
- width: 16.66666667%;
- }
- .col-lg-1 {
- width: 8.33333333%;
- }
- .col-lg-pull-12 {
- left: 100%;
- right: auto;
- }
- .col-lg-pull-11 {
- left: 91.66666667%;
- right: auto;
- }
- .col-lg-pull-10 {
- left: 83.33333333%;
- right: auto;
- }
- .col-lg-pull-9 {
- left: 75%;
- right: auto;
- }
- .col-lg-pull-8 {
- left: 66.66666667%;
- right: auto;
- }
- .col-lg-pull-7 {
- left: 58.33333333%;
- right: auto;
- }
- .col-lg-pull-6 {
- left: 50%;
- right: auto;
- }
- .col-lg-pull-5 {
- left: 41.66666667%;
- right: auto;
- }
- .col-lg-pull-4 {
- left: 33.33333333%;
- right: auto;
- }
- .col-lg-pull-3 {
- left: 25%;
- right: auto;
- }
- .col-lg-pull-2 {
- left: 16.66666667%;
- right: auto;
- }
- .col-lg-pull-1 {
- left: 8.33333333%;
- right: auto;
- }
- .col-lg-pull-0 {
- left: auto;
- right: auto;
- }
- .col-lg-push-12 {
- right: 100%;
- left: 0;
- }
- .col-lg-push-11 {
- right: 91.66666667%;
- left: 0;
- }
- .col-lg-push-10 {
- right: 83.33333333%;
- left: 0;
- }
- .col-lg-push-9 {
- right: 75%;
- left: 0;
- }
- .col-lg-push-8 {
- right: 66.66666667%;
- left: 0;
- }
- .col-lg-push-7 {
- right: 58.33333333%;
- left: 0;
- }
- .col-lg-push-6 {
- right: 50%;
- left: 0;
- }
- .col-lg-push-5 {
- right: 41.66666667%;
- left: 0;
- }
- .col-lg-push-4 {
- right: 33.33333333%;
- left: 0;
- }
- .col-lg-push-3 {
- right: 25%;
- left: 0;
- }
- .col-lg-push-2 {
- right: 16.66666667%;
- left: 0;
- }
- .col-lg-push-1 {
- right: 8.33333333%;
- left: 0;
- }
- .col-lg-push-0 {
- right: auto;
- left: 0;
- }
- .col-lg-offset-12 {
- margin-right: 100%;
- margin-left: 0;
- }
- .col-lg-offset-11 {
- margin-right: 91.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-10 {
- margin-right: 83.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-9 {
- margin-right: 75%;
- margin-left: 0;
- }
- .col-lg-offset-8 {
- margin-right: 66.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-7 {
- margin-right: 58.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-6 {
- margin-right: 50%;
- margin-left: 0;
- }
- .col-lg-offset-5 {
- margin-right: 41.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-4 {
- margin-right: 33.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-3 {
- margin-right: 25%;
- margin-left: 0;
- }
- .col-lg-offset-2 {
- margin-right: 16.66666667%;
- margin-left: 0;
- }
- .col-lg-offset-1 {
- margin-right: 8.33333333%;
- margin-left: 0;
- }
- .col-lg-offset-0 {
- margin-right: 0%;
- margin-left: 0;
- }
-}
-caption {
- text-align: right;
-}
-th {
- text-align: right;
-}
-@media screen and (max-width: 767px) {
- .table-responsive > .table-bordered {
- border: 0;
- }
- .table-responsive > .table-bordered > thead > tr > th:first-child,
- .table-responsive > .table-bordered > tbody > tr > th:first-child,
- .table-responsive > .table-bordered > tfoot > tr > th:first-child,
- .table-responsive > .table-bordered > thead > tr > td:first-child,
- .table-responsive > .table-bordered > tbody > tr > td:first-child,
- .table-responsive > .table-bordered > tfoot > tr > td:first-child {
- border-right: 0;
- border-left: initial;
- }
- .table-responsive > .table-bordered > thead > tr > th:last-child,
- .table-responsive > .table-bordered > tbody > tr > th:last-child,
- .table-responsive > .table-bordered > tfoot > tr > th:last-child,
- .table-responsive > .table-bordered > thead > tr > td:last-child,
- .table-responsive > .table-bordered > tbody > tr > td:last-child,
- .table-responsive > .table-bordered > tfoot > tr > td:last-child {
- border-left: 0;
- border-right: initial;
- }
-}
-.radio label,
-.checkbox label {
- padding-right: 20px;
- padding-left: initial;
-}
-.radio input[type="radio"],
-.radio-inline input[type="radio"],
-.checkbox input[type="checkbox"],
-.checkbox-inline input[type="checkbox"] {
- margin-right: -20px;
- margin-left: auto;
-}
-.radio-inline,
-.checkbox-inline {
- padding-right: 20px;
- padding-left: 0;
-}
-.radio-inline + .radio-inline,
-.checkbox-inline + .checkbox-inline {
- margin-right: 10px;
- margin-left: 0;
-}
-.has-feedback .form-control {
- padding-left: 42.5px;
- padding-right: 12px;
-}
-.form-control-feedback {
- left: 0;
- right: auto;
-}
-@media (min-width: 768px) {
- .form-inline label {
- padding-right: 0;
- padding-left: initial;
- }
- .form-inline .radio input[type="radio"],
- .form-inline .checkbox input[type="checkbox"] {
- margin-right: 0;
- margin-left: auto;
- }
-}
-@media (min-width: 768px) {
- .form-horizontal .control-label {
- text-align: left;
- }
-}
-.form-horizontal .has-feedback .form-control-feedback {
- left: 15px;
- right: auto;
-}
-.caret {
- margin-right: 2px;
- margin-left: 0;
-}
-.dropdown-menu {
- right: 0;
- left: auto;
- float: left;
- text-align: right;
-}
-.dropdown-menu.pull-right {
- left: 0;
- right: auto;
- float: right;
-}
-.dropdown-menu-right {
- left: auto;
- right: 0;
-}
-.dropdown-menu-left {
- left: 0;
- right: auto;
-}
-@media (min-width: 768px) {
- .navbar-right .dropdown-menu {
- left: auto;
- right: 0;
- }
- .navbar-right .dropdown-menu-left {
- left: 0;
- right: auto;
- }
-}
-.btn-group > .btn,
-.btn-group-vertical > .btn {
- float: right;
-}
-.btn-group .btn + .btn,
-.btn-group .btn + .btn-group,
-.btn-group .btn-group + .btn,
-.btn-group .btn-group + .btn-group {
- margin-right: -1px;
- margin-left: 0px;
-}
-.btn-toolbar {
- margin-right: -5px;
- margin-left: 0px;
-}
-.btn-toolbar .btn-group,
-.btn-toolbar .input-group {
- float: right;
-}
-.btn-toolbar > .btn,
-.btn-toolbar > .btn-group,
-.btn-toolbar > .input-group {
- margin-right: 5px;
- margin-left: 0px;
-}
-.btn-group > .btn:first-child {
- margin-right: 0;
-}
-.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.btn-group > .btn:last-child:not(:first-child),
-.btn-group > .dropdown-toggle:not(:first-child) {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.btn-group > .btn-group {
- float: right;
-}
-.btn-group.btn-group-justified > .btn,
-.btn-group.btn-group-justified > .btn-group {
- float: none;
-}
-.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
- border-radius: 0;
-}
-.btn-group > .btn-group:first-child > .btn:last-child,
-.btn-group > .btn-group:first-child > .dropdown-toggle {
- border-top-right-radius: 4px;
- border-bottom-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.btn-group > .btn-group:last-child > .btn:first-child {
- border-top-left-radius: 4px;
- border-bottom-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.btn .caret {
- margin-right: 0;
-}
-.btn-group-vertical > .btn + .btn,
-.btn-group-vertical > .btn + .btn-group,
-.btn-group-vertical > .btn-group + .btn,
-.btn-group-vertical > .btn-group + .btn-group {
- margin-top: -1px;
- margin-right: 0;
-}
-.input-group .form-control {
- float: right;
-}
-.input-group .form-control:first-child,
-.input-group-addon:first-child,
-.input-group-btn:first-child > .btn,
-.input-group-btn:first-child > .btn-group > .btn,
-.input-group-btn:first-child > .dropdown-toggle,
-.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),
-.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
- border-bottom-right-radius: 4px;
- border-top-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.input-group-addon:first-child {
- border-left: 0px;
- border-right: 1px solid;
-}
-.input-group .form-control:last-child,
-.input-group-addon:last-child,
-.input-group-btn:last-child > .btn,
-.input-group-btn:last-child > .btn-group > .btn,
-.input-group-btn:last-child > .dropdown-toggle,
-.input-group-btn:first-child > .btn:not(:first-child),
-.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
- border-bottom-left-radius: 4px;
- border-top-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.input-group-addon:last-child {
- border-left-width: 1px;
- border-left-style: solid;
- border-right: 0px;
-}
-.input-group-btn > .btn + .btn {
- margin-right: -1px;
- margin-left: auto;
-}
-.input-group-btn:first-child > .btn,
-.input-group-btn:first-child > .btn-group {
- margin-left: -1px;
- margin-right: auto;
-}
-.input-group-btn:last-child > .btn,
-.input-group-btn:last-child > .btn-group {
- margin-right: -1px;
- margin-left: auto;
-}
-.nav {
- padding-right: 0;
- padding-left: initial;
-}
-.nav-tabs > li {
- float: right;
-}
-.nav-tabs > li > a {
- margin-left: auto;
- margin-right: -2px;
- border-radius: 4px 4px 0 0;
-}
-.nav-pills > li {
- float: right;
-}
-.nav-pills > li > a {
- border-radius: 4px;
-}
-.nav-pills > li + li {
- margin-right: 2px;
- margin-left: auto;
-}
-.nav-stacked > li {
- float: none;
-}
-.nav-stacked > li + li {
- margin-right: 0;
- margin-left: auto;
-}
-.nav-justified > .dropdown .dropdown-menu {
- right: auto;
-}
-.nav-tabs-justified > li > a {
- margin-left: 0;
- margin-right: auto;
-}
-@media (min-width: 768px) {
- .nav-tabs-justified > li > a {
- border-radius: 4px 4px 0 0;
- }
-}
-@media (min-width: 768px) {
- .navbar-header {
- float: right;
- }
-}
-.navbar-collapse {
- padding-right: 15px;
- padding-left: 15px;
-}
-.navbar-brand {
- float: right;
-}
-@media (min-width: 768px) {
- .navbar > .container .navbar-brand,
- .navbar > .container-fluid .navbar-brand {
- margin-right: -15px;
- margin-left: auto;
- }
-}
-.navbar-toggle {
- float: left;
- margin-left: 15px;
- margin-right: auto;
-}
-@media (max-width: 767px) {
- .navbar-nav .open .dropdown-menu > li > a,
- .navbar-nav .open .dropdown-menu .dropdown-header {
- padding: 5px 25px 5px 15px;
- }
-}
-@media (min-width: 768px) {
- .navbar-nav {
- float: right;
- }
- .navbar-nav > li {
- float: right;
- }
-}
-@media (min-width: 768px) {
- .navbar-left.flip {
- float: right !important;
- }
- .navbar-right:last-child {
- margin-left: -15px;
- margin-right: auto;
- }
- .navbar-right.flip {
- float: left !important;
- margin-left: -15px;
- margin-right: auto;
- }
- .navbar-right .dropdown-menu {
- left: 0;
- right: auto;
- }
-}
-@media (min-width: 768px) {
- .navbar-text {
- float: right;
- }
- .navbar-text.navbar-right:last-child {
- margin-left: 0;
- margin-right: auto;
- }
-}
-.pagination {
- padding-right: 0;
-}
-.pagination > li > a,
-.pagination > li > span {
- float: right;
- margin-right: -1px;
- margin-left: 0px;
-}
-.pagination > li:first-child > a,
-.pagination > li:first-child > span {
- margin-left: 0;
- border-bottom-right-radius: 4px;
- border-top-right-radius: 4px;
- border-bottom-left-radius: 0;
- border-top-left-radius: 0;
-}
-.pagination > li:last-child > a,
-.pagination > li:last-child > span {
- margin-right: -1px;
- border-bottom-left-radius: 4px;
- border-top-left-radius: 4px;
- border-bottom-right-radius: 0;
- border-top-right-radius: 0;
-}
-.pager {
- padding-right: 0;
- padding-left: initial;
-}
-.pager .next > a,
-.pager .next > span {
- float: left;
-}
-.pager .previous > a,
-.pager .previous > span {
- float: right;
-}
-.nav-pills > li > a > .badge {
- margin-left: 0px;
- margin-right: 3px;
-}
-.list-group-item > .badge {
- float: left;
-}
-.list-group-item > .badge + .badge {
- margin-left: 5px;
- margin-right: auto;
-}
-.alert-dismissable,
-.alert-dismissible {
- padding-left: 35px;
- padding-right: 15px;
-}
-.alert-dismissable .close,
-.alert-dismissible .close {
- right: auto;
- left: -21px;
-}
-.progress-bar {
- float: right;
-}
-.media > .pull-left {
- margin-right: 10px;
-}
-.media > .pull-left.flip {
- margin-right: 0;
- margin-left: 10px;
-}
-.media > .pull-right {
- margin-left: 10px;
-}
-.media > .pull-right.flip {
- margin-left: 0;
- margin-right: 10px;
-}
-.media-right,
-.media > .pull-right {
- padding-right: 10px;
- padding-left: initial;
-}
-.media-left,
-.media > .pull-left {
- padding-left: 10px;
- padding-right: initial;
-}
-.media-list {
- padding-right: 0;
- padding-left: initial;
- list-style: none;
-}
-.list-group {
- padding-right: 0;
- padding-left: initial;
-}
-.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,
-.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
- border-top-right-radius: 3px;
- border-top-left-radius: 0;
-}
-.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,
-.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,
-.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,
-.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,
-.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
- border-top-left-radius: 3px;
- border-top-right-radius: 0;
-}
-.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,
-.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
- border-bottom-left-radius: 3px;
- border-top-right-radius: 0;
-}
-.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,
-.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,
-.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,
-.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
- border-bottom-right-radius: 3px;
- border-top-left-radius: 0;
-}
-.panel > .table-bordered > thead > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,
-.panel > .table-bordered > tbody > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,
-.panel > .table-bordered > tfoot > tr > th:first-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,
-.panel > .table-bordered > thead > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,
-.panel > .table-bordered > tbody > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,
-.panel > .table-bordered > tfoot > tr > td:first-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
- border-right: 0;
- border-left: none;
-}
-.panel > .table-bordered > thead > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,
-.panel > .table-bordered > tbody > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,
-.panel > .table-bordered > tfoot > tr > th:last-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,
-.panel > .table-bordered > thead > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,
-.panel > .table-bordered > tbody > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,
-.panel > .table-bordered > tfoot > tr > td:last-child,
-.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
- border-right: none;
- border-left: 0;
-}
-.embed-responsive .embed-responsive-item,
-.embed-responsive iframe,
-.embed-responsive embed,
-.embed-responsive object {
- right: 0;
- left: auto;
-}
-.close {
- float: left;
-}
-.modal-footer {
- text-align: left;
-}
-.modal-footer.flip {
- text-align: right;
-}
-.modal-footer .btn + .btn {
- margin-left: auto;
- margin-right: 5px;
-}
-.modal-footer .btn-group .btn + .btn {
- margin-right: -1px;
- margin-left: auto;
-}
-.modal-footer .btn-block + .btn-block {
- margin-right: 0;
- margin-left: auto;
-}
-.popover {
- left: auto;
- text-align: right;
-}
-.popover.top > .arrow {
- right: 50%;
- left: auto;
- margin-right: -11px;
- margin-left: auto;
-}
-.popover.top > .arrow:after {
- margin-right: -10px;
- margin-left: auto;
-}
-.popover.bottom > .arrow {
- right: 50%;
- left: auto;
- margin-right: -11px;
- margin-left: auto;
-}
-.popover.bottom > .arrow:after {
- margin-right: -10px;
- margin-left: auto;
-}
-.carousel-control {
- right: 0;
- bottom: 0;
-}
-.carousel-control.left {
- right: auto;
- left: 0;
- background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0%), color-stop(rgba(0, 0, 0, 0.0001) 100%));
- background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
- background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);
-}
-.carousel-control.right {
- left: auto;
- right: 0;
- background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0%), color-stop(rgba(0, 0, 0, 0.5) 100%));
- background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
- background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
- background-repeat: repeat-x;
- filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);
-}
-.carousel-control .icon-prev,
-.carousel-control .glyphicon-chevron-left {
- left: 50%;
- right: auto;
- margin-right: -10px;
-}
-.carousel-control .icon-next,
-.carousel-control .glyphicon-chevron-right {
- right: 50%;
- left: auto;
- margin-left: -10px;
-}
-.carousel-indicators {
- right: 50%;
- left: 0;
- margin-right: -30%;
- margin-left: 0;
- padding-left: 0;
-}
-@media screen and (min-width: 768px) {
- .carousel-control .glyphicon-chevron-left,
- .carousel-control .icon-prev {
- margin-left: 0;
- margin-right: -15px;
- }
- .carousel-control .glyphicon-chevron-right,
- .carousel-control .icon-next {
- margin-left: 0;
- margin-right: -15px;
- }
- .carousel-caption {
- left: 20%;
- right: 20%;
- padding-bottom: 30px;
- }
-}
-.pull-right.flip {
- float: left !important;
-}
-.pull-left.flip {
- float: right !important;
-}
-/*# sourceMappingURL=bootstrap-rtl.css.map */
\ No newline at end of file
diff --git a/frappe/public/css/desk-rtl.css b/frappe/public/css/desk-rtl.css
deleted file mode 100644
index a38f6864ff..0000000000
--- a/frappe/public/css/desk-rtl.css
+++ /dev/null
@@ -1,118 +0,0 @@
-.navbar .navbar-search-icon{
- right: auto;
- left: 24px;
-}
-.navbar > .container > .navbar-header{
- float: right !important;
-}
-body[data-sidebar="0"] .navbar-home {
- margin-left: auto !important;
- margin-right: 15px !important;
-}
-.navbar-desk ~ ul > li {
- float: right !important;
-}
-body.no-breadcrumbs .navbar .navbar-home:before {
- margin-right: auto;
- margin-left: 10px !important;
- -ms-transform:rotate(180deg); /* Internet Explorer 9 */
- -webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
- transform:rotate(180deg); /* Standard syntax */
-}
-.layout-side-section .overlay-sidebar {
- left: auto !important;
- right: 0 !important;
-}
-.layout-side-section .overlay-sidebar.opened {
- transform:translateX(0) !important;
-}
-.navbar-right {
- float: left !important;
-}
-#navbar-breadcrumbs > li > a:before {
- margin-right: auto;
- margin-left: 10px;
- top: 6px;
- -ms-transform:rotate(180deg); /* Internet Explorer 9 */
- -webkit-transform:rotate(180deg); /* Chrome, Safari, Opera */
- transform:rotate(180deg); /* Standard syntax */
-}
-.case-wrapper {
- float: right;
-}
-.link-btn {
- right: auto;
- left: 4px;
- transform:rotate(180deg); /* Rotate icon*/
-}
-.sidebar-menu .badge {
- right: auto;
- left: 0px;
-}
-.indicator::before {
- margin: 0 0 0 4px;
-}
-.pull-left {
- float: right !important;
-}
-.grid-row > .row .col:last-child {
- margin-right: auto;
- margin-left: -10px;
-}
-.text-right {
- text-align: left;
-}
-.list-row-head .octicon-heart {
- margin-right: auto;
- margin-left: 13px;
-}
-.list-id {
- margin-left: 7px !important;
-}
-.avatar-small .avatar-sm {
- margin-left: 5px;
- margin-right: auto;
-}
-.list-row-right .list-row-modified {
- margin-right: auto;
- margin-left: 9px;
-}
-.list-comment-count {
- text-align: right;
-}
-ul.tree-children {
- padding-right: 20px;
- padding-left: inherit !important;
-}
-.balance-area {
- float: left !important;
-}
-.tree.opened::before, .tree-node.opened::before, .tree:last-child::after, .tree-node:last-child::after {
- left: inherit !important;
- right: 8px;
-}
-.tree.opened > .tree-children > .tree-node > .tree-link::before, .tree-node.opened > .tree-children > .tree-node > .tree-link::before {
- left: inherit !important;
- right: -11px;
-}
-.tree:last-child::after, .tree-node:last-child::after {
- right: -13px !important;
-}
-.tree.opened::before {
- left: auto !important;
- right: 23px;
-}
-.results {
- direction: ltr;
-}
-.data-table {
- direction: ltr;
-}
-.section-header {
- direction: ltr;
-}
-
-.ql-editor {
- direction: rtl;
- text-align: right;
-}
\ No newline at end of file
diff --git a/frappe/public/css/report-rtl.css b/frappe/public/css/report-rtl.css
deleted file mode 100644
index 03e986c56b..0000000000
--- a/frappe/public/css/report-rtl.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.grid-report {
- direction: ltr;
-}
-
-.page-form .awesomplete > ul {
- left: auto;
-}
-
-.chart_area{
- direction: ltr;
-}
-
-.grid-report .show-zero{
- direction: rtl;
-}
diff --git a/frappe/public/html/print_template.html b/frappe/public/html/print_template.html
index 721bec7fa7..e2ff9c9c76 100644
--- a/frappe/public/html/print_template.html
+++ b/frappe/public/html/print_template.html
@@ -1,5 +1,5 @@
-
+
@@ -7,7 +7,7 @@
{{ title }}
-
+
diff --git a/frappe/public/js/frappe/assets.js b/frappe/public/js/frappe/assets.js
index 3fca8640f3..0a3fb33cb8 100644
--- a/frappe/public/js/frappe/assets.js
+++ b/frappe/public/js/frappe/assets.js
@@ -168,9 +168,13 @@ frappe.assets = {
}
},
- bundled_asset(path) {
+ bundled_asset(path, is_rtl=null) {
if (!path.startsWith('/assets') && path.includes('.bundle.')) {
- return frappe.boot.assets_json[path] || path;
+ if (path.endsWith('.css') && is_rtl) {
+ path = `rtl_${path}`;
+ }
+ path = frappe.boot.assets_json[path] || path;
+ return path;
}
return path;
}
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 65c0139b65..9d106f46f4 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -64,8 +64,6 @@ frappe.Application = class Application {
}
});
- this.set_rtl();
-
// page container
this.make_page_container();
this.set_route();
@@ -489,16 +487,6 @@ frappe.Application = class Application {
}, 100);
}
- set_rtl() {
- if (frappe.utils.is_rtl()) {
- var ls = document.createElement('link');
- ls.rel="stylesheet";
- ls.type = "text/css";
- ls.href= frappe.assets.bundled_asset("frappe-rtl.bundle.css");
- document.getElementsByTagName('head')[0].appendChild(ls);
- $('body').addClass('frappe-rtl');
- }
- }
show_change_log() {
var me = this;
diff --git a/frappe/public/js/frappe/form/controls/signature.js b/frappe/public/js/frappe/form/controls/signature.js
index bcbdfa0d27..445f5b7b3b 100644
--- a/frappe/public/js/frappe/form/controls/signature.js
+++ b/frappe/public/js/frappe/form/controls/signature.js
@@ -53,13 +53,15 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
this.img = $("
")
.appendTo(this.img_wrapper).toggle(false);
}
- refresh_input(e) {
+ refresh_input() {
+ // signature dom is not ready
+ if (!this.body) return;
// prevent to load the second time
this.make_pad();
this.$wrapper.find(".control-input").toggle(false);
this.set_editable(this.get_status()=="Write");
this.load_pad();
- if(this.get_status()=="Read") {
+ if (this.get_status() == "Read") {
$(this.disp_area).toggle(false);
}
}
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index b2b0c11d54..420704149b 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -204,7 +204,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
$(this).removeClass('hidden');
}
});
- this.set_open_count();
+ !this.frm.is_new() && this.set_open_count();
}
init_data() {
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 8064f90a98..faaa3dfbd9 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1265,7 +1265,9 @@ frappe.ui.form.Form = class FrappeForm {
if (df && df[property] != value) {
df[property] = value;
if (table_field && table_row_name) {
- this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
+ if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) {
+ this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name].refresh_field(fieldname);
+ }
} else {
this.refresh_field(fieldname);
}
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 7f7ec9ce4f..6bef2a0cb8 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -2,8 +2,6 @@ frappe.ui.form.FormTour = class FormTour {
constructor({ frm }) {
this.frm = frm;
this.driver_steps = [];
-
- this.init_driver();
}
init_driver() {
@@ -37,11 +35,17 @@ frappe.ui.form.FormTour = class FormTour {
if (tour_name) {
this.tour = await frappe.db.get_doc('Form Tour', tour_name);
} else {
- this.tour = { steps: frappe.tour[this.frm.doctype] };
+ const doctype_tour_exists = await frappe.db.exists('Form Tour', this.frm.doctype);
+ if (doctype_tour_exists) {
+ this.tour = await frappe.db.get_doc('Form Tour', this.frm.doctype);
+ } else {
+ this.tour = { steps: frappe.tour[this.frm.doctype] };
+ }
}
if (on_finish) this.on_finish = on_finish;
+ this.init_driver();
this.build_steps();
this.update_driver_steps();
}
@@ -232,7 +236,7 @@ frappe.ui.form.FormTour = class FormTour {
}
add_step_to_save() {
- const page_id = `#page-${this.frm.doctype}`;
+ const page_id = `[id="page-${this.frm.doctype}"]`;
const $save_btn = `${page_id} .standard-actions .primary-action`;
const save_step = {
element: $save_btn,
@@ -244,6 +248,9 @@ frappe.ui.form.FormTour = class FormTour {
description: "",
position: "left",
doneBtnText: __("Save")
+ },
+ onNext: () => {
+ this.frm.save();
}
};
this.driver_steps.push(save_step);
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 045e5dc1b3..528f485354 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -263,9 +263,6 @@ frappe.ui.form.Layout = class Layout {
section.addClass("empty-section");
}
});
-
- this.frm && this.frm.dashboard.refresh();
-
}
refresh_fields (fields) {
diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js
index 7b45db952e..151d008d3e 100644
--- a/frappe/public/js/frappe/microtemplate.js
+++ b/frappe/public/js/frappe/microtemplate.js
@@ -119,6 +119,10 @@ frappe.render_grid = function(opts) {
// render HTML wrapper page
opts.base_url = frappe.urllib.get_base_url();
opts.print_css = frappe.boot.print_css;
+
+ opts.lang = opts.lang || frappe.boot.lang,
+ opts.layout_direction = opts.layout_direction || frappe.utils.is_rtl() ? "rtl" : "ltr";
+
var html = frappe.render_template("print_template", opts);
var w = window.open();
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index 2bfa7c7be6..8a29dc1040 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -14,11 +14,10 @@ frappe.view_factories = [];
frappe.route_options = null;
frappe.route_hooks = {};
-$(window).on('hashchange', function() {
+$(window).on('hashchange', function(e) {
// v1 style routing, route is in hash
- if (window.location.hash) {
+ if (window.location.hash && !frappe.router.is_app_route(e.currentTarget.pathname)) {
let sub_path = frappe.router.get_sub_path(window.location.hash);
- window.location.hash = '';
frappe.router.push_state(sub_path);
return false;
}
@@ -48,18 +47,18 @@ $('body').on('click', 'a', function(e) {
return;
}
- if (href==='') {
+ if (href === '') {
return override('/app');
}
- // target has "#" ,this is a v1 style route, so remake it.
- if (e.currentTarget.hash) {
+ if (href.startsWith('#')) {
+ // target startswith "#", this is a v1 style route, so remake it.
return override(e.currentTarget.hash);
}
- // target has "/app, this is a v2 style route.
- if (e.currentTarget.pathname && frappe.router.is_app_route(e.currentTarget.pathname)) {
- return override(e.currentTarget.pathname);
+ if (frappe.router.is_app_route(e.currentTarget.pathname)) {
+ // target has "/app, this is a v2 style route.
+ return override(e.currentTarget.pathname + e.currentTarget.hash);
}
});
@@ -172,7 +171,7 @@ frappe.router = {
standard_route = ['List', doctype_route.doctype, frappe.utils.to_title_case(route[2])];
if (route[3]) {
// calendar / kanban / dashboard / folder name
- standard_route.push(...route.splice(3, route.length));
+ standard_route.push([...route].splice(3, route.length));
}
}
return standard_route;
@@ -298,7 +297,7 @@ frappe.router = {
new_route = [this.slug(route[1]), 'view', route[2].toLowerCase()];
// calendar / inbox / file folder
- if (route[3]) new_route.push(...route.slice(3, route.length));
+ if (route[3]) new_route.push([...route].slice(3, route.length));
} else {
if ($.isPlainObject(route[2])) {
frappe.route_options = route[2];
@@ -349,8 +348,6 @@ frappe.router = {
push_state(url) {
// change the URL and call the router
if (window.location.pathname !== url) {
- // cleanup any remenants of v1 routing
- window.location.hash = '';
// push state so the browser looks fine
history.pushState(null, null, url);
@@ -364,7 +361,11 @@ frappe.router = {
// return clean sub_path from hash or url
// supports both v1 and v2 routing
if (!route) {
- route = window.location.hash || (window.location.pathname + window.location.search);
+ route = window.location.pathname + window.location.hash + window.location.search;
+ if (route.includes('app#')) {
+ // to support v1
+ route = window.location.hash;
+ }
}
return this.strip_prefix(route);
diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js
index b8b908eb95..db06d99615 100644
--- a/frappe/public/js/frappe/ui/field_group.js
+++ b/frappe/public/js/frappe/ui/field_group.js
@@ -40,14 +40,16 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
this.catch_enter_as_submit();
}
- $(this.wrapper).find('input, select').on('change', () => {
- this.dirty = true;
-
- frappe.run_serially([
- () => frappe.timeout(0.1),
- () => me.refresh_dependency()
- ]);
- });
+ $(this.wrapper).find('input, select').on(
+ 'change awesomplete-selectcomplete',
+ () => {
+ this.dirty = true;
+ frappe.run_serially([
+ () => frappe.timeout(0.1),
+ () => me.refresh_dependency()
+ ]);
+ }
+ );
}
}
diff --git a/frappe/public/js/frappe/views/reports/print_grid.html b/frappe/public/js/frappe/views/reports/print_grid.html
index e607f12b52..6f693bc932 100644
--- a/frappe/public/js/frappe/views/reports/print_grid.html
+++ b/frappe/public/js/frappe/views/reports/print_grid.html
@@ -11,6 +11,7 @@
+ | # |
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
{% for row in data %}
|
+ |
+ {{ row._index + 1 }}
+ |
{% for col in columns %}
{% if col.name && col._id !== "_check" %}
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index 208d5b2f67..a4b3564e37 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -1264,7 +1264,9 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
print_css: print_css,
print_settings: print_settings,
landscape: landscape,
- columns: columns
+ columns: columns,
+ lang: frappe.boot.lang,
+ layout_direction: frappe.utils.is_rtl() ? "rtl" : "ltr"
});
frappe.render_pdf(html, print_settings);
diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss
index c939c6de39..a7a8d49510 100644
--- a/frappe/public/scss/common/controls.scss
+++ b/frappe/public/scss/common/controls.scss
@@ -166,6 +166,9 @@ select.form-control {
.ace_print-margin {
background-color: var(--dark-border-color);
}
+ .ace_scrollbar {
+ z-index: 3;
+ }
}
.frappe-control[data-fieldtype="Attach"],
diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss
index 48a8a48f5f..112238bfe5 100644
--- a/frappe/public/scss/common/css_variables.scss
+++ b/frappe/public/scss/common/css_variables.scss
@@ -179,9 +179,20 @@
--text-on-pink: var(--pink-500);
--text-on-cyan: var(--cyan-600);
+ // Layout Colors
+ --bg-color: var(--gray-50);
+ --fg-color: white;
+ --navbar-bg: white;
+ --fg-hover-color: var(--gray-100);
+ --card-bg: var(--fg-color);
+ --disabled-text-color: var(--gray-700);
--disabled-control-bg: var(--gray-50);
--control-bg: var(--gray-100);
--control-bg-on-gray: var(--gray-200);
+ --awesomebar-focus-bg: var(--fg-color);
+ --modal-bg: white;
+ --toast-bg: var(--modal-bg);
+ --popover-bg: white;
--awesomplete-hover-bg: var(--control-bg);
diff --git a/frappe/public/scss/desk/breadcrumb.scss b/frappe/public/scss/desk/breadcrumb.scss
index 6324e6f012..b466bab7ae 100644
--- a/frappe/public/scss/desk/breadcrumb.scss
+++ b/frappe/public/scss/desk/breadcrumb.scss
@@ -4,7 +4,7 @@
background-color: white;
font-size: var(--text-sm);
}
-
+/*! This comment will be included even in compressed mode. */
#navbar-breadcrumbs {
margin-left: var(--margin-md);
font-size: var(--text-sm);
@@ -12,7 +12,7 @@
font-size: var(--text-md);
margin-right: 10px;
&:before {
- content: var(--right-arrow-svg);
+ content: #{"/*!rtl:var(--left-arrow-svg);*/"}var(--right-arrow-svg);
display: inline-block;
margin-right: 10px;
}
diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss
index 5bb2614dcc..255d0a91e4 100644
--- a/frappe/public/scss/desk/css_variables.scss
+++ b/frappe/public/scss/desk/css_variables.scss
@@ -25,20 +25,7 @@ $input-height: 28px !default;
--navbar-height: 60px;
- // Layout Colors
- --bg-color: var(--gray-50);
- --fg-color: white;
- --navbar-bg: white;
- --fg-hover-color: var(--gray-100);
- --card-bg: var(--fg-color);
- --disabled-text-color: var(--gray-700);
- --disabled-control-bg: var(--gray-50);
- --control-bg: var(--gray-100);
- --control-bg-on-gray: var(--gray-200);
- --awesomebar-focus-bg: var(--fg-color);
- --modal-bg: white;
- --toast-bg: var(--modal-bg);
- --popover-bg: white;
+
--appreciation-color: var(--dark-green-600);
--appreciation-bg: var(--dark-green-100);
@@ -77,4 +64,6 @@ $input-height: 28px !default;
--skeleton-bg: var(--gray-100);
--right-arrow-svg: url("data: image/svg+xml;utf8, ");
+
+ --left-arrow-svg: url("data: image/svg+xml;utf8, ");
}
diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss
index 76dcf90bc3..7f0dfe73b8 100644
--- a/frappe/public/scss/desk/dark.scss
+++ b/frappe/public/scss/desk/dark.scss
@@ -157,4 +157,6 @@
--skeleton-bg: var(--gray-800);
--right-arrow-svg: url("data: image/svg+xml;utf8, ");
+
+ --left-arrow-svg: url("data: image/svg+xml;utf8, ");
}
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index afcf2957cc..d1205e0e38 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -574,3 +574,15 @@ details > summary:focus {
// font-family: 'Octicons';
// content: "\f00b";
// }
+
+/*rtl:raw:
+.dropdown-menu {
+ right: auto;
+}
+.popover {
+ right: auto;
+}
+.chart-container {
+ direction: ltr;
+}
+*/
\ No newline at end of file
diff --git a/frappe/public/scss/email.bundle.scss b/frappe/public/scss/email.bundle.scss
index b1a8d6c485..9b58c130a2 100644
--- a/frappe/public/scss/email.bundle.scss
+++ b/frappe/public/scss/email.bundle.scss
@@ -36,7 +36,13 @@ a {
}
p {
- margin: 5px 0 !important;
+ margin: 1em 0 !important;
+}
+
+.with-container {
+ p {
+ margin: 5px 0 !important;
+ }
}
.ql-editor {
diff --git a/frappe/public/scss/frappe-rtl.bundle.scss b/frappe/public/scss/frappe-rtl.bundle.scss
deleted file mode 100644
index bc618270a8..0000000000
--- a/frappe/public/scss/frappe-rtl.bundle.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-@import "frappe/public/css/bootstrap-rtl.css";
-@import "frappe/public/css/desk-rtl.css";
-@import "frappe/public/css/report-rtl.css";
diff --git a/frappe/pythonrc.py b/frappe/pythonrc.py
deleted file mode 100755
index 6761ead05b..0000000000
--- a/frappe/pythonrc.py
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python2.7
-
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-# MIT License. See license.txt
-
-import os
-import frappe
-frappe.connect(site=os.environ.get("site"))
\ No newline at end of file
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
new file mode 100644
index 0000000000..798c34b6cc
--- /dev/null
+++ b/frappe/query_builder/__init__.py
@@ -0,0 +1 @@
+from frappe.query_builder.utils import get_query_builder
diff --git a/frappe/query_builder/builder.py b/frappe/query_builder/builder.py
new file mode 100644
index 0000000000..da1533fb1a
--- /dev/null
+++ b/frappe/query_builder/builder.py
@@ -0,0 +1,55 @@
+from pypika import MySQLQuery, Order, PostgreSQLQuery, terms
+from pypika.queries import Schema, Table
+from frappe.utils import get_table_name
+
+
+class Base:
+ terms = terms
+ desc = Order.desc
+ Schema = Schema
+
+ @staticmethod
+ def Table(table_name: str, *args, **kwargs) -> Table:
+ table_name = get_table_name(table_name)
+ return Table(table_name, *args, **kwargs)
+
+
+class MariaDB(Base, MySQLQuery):
+ Field = terms.Field
+
+ @classmethod
+ def from_(cls, table, *args, **kwargs):
+ if isinstance(table, str):
+ table = cls.Table(table)
+ return super().from_(table, *args, **kwargs)
+
+
+class Postgres(Base, PostgreSQLQuery):
+ field_translation = {"table_name": "relname", "table_rows": "n_tup_ins"}
+ schema_translation = {"tables": "pg_stat_all_tables"}
+ # TODO: Find a better way to do this
+ # These are interdependent query changes that need fixing. These
+ # translations happen in the same query. But there is no check to see if
+ # the Fields are changed only when a particular `information_schema` schema
+ # is used. Replacing them is not straightforward because the "from_"
+ # function can not see the arguments passed to the "select" function as
+ # they are two different objects. The quick fix used here is to replace the
+ # Field names in the "Field" function.
+
+ @classmethod
+ def Field(cls, field_name, *args, **kwargs):
+ if field_name in cls.field_translation:
+ field_name = cls.field_translation[field_name]
+ return terms.Field(field_name, *args, **kwargs)
+
+ @classmethod
+ def from_(cls, table, *args, **kwargs):
+ if isinstance(table, Table):
+ if table._schema:
+ if table._schema._name == "information_schema":
+ table = cls.schema_translation[table._table_name]
+
+ elif isinstance(table, str):
+ table = cls.Table(table)
+
+ return super().from_(table, *args, **kwargs)
diff --git a/frappe/query_builder/custom.py b/frappe/query_builder/custom.py
new file mode 100644
index 0000000000..5aaed463d9
--- /dev/null
+++ b/frappe/query_builder/custom.py
@@ -0,0 +1,83 @@
+from typing import Optional
+
+from pypika.functions import DistinctOptionFunction
+from pypika.utils import builder
+
+import frappe
+
+
+class GROUP_CONCAT(DistinctOptionFunction):
+ def __init__(self, column: str, alias: Optional[str] = None):
+ """[ Implements the group concat function read more about it at https://www.geeksforgeeks.org/mysql-group_concat-function ]
+ Args:
+ column (str): [ name of the column you want to concat]
+ alias (Optional[str], optional): [ is this an alias? ]. Defaults to None.
+ """
+ super(GROUP_CONCAT, self).__init__("GROUP_CONCAT", column, alias=alias)
+
+
+class STRING_AGG(DistinctOptionFunction):
+ def __init__(self, column: str, separator: str = ",", alias: Optional[str] = None):
+ """[ Implements the group concat function read more about it at https://docs.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-ver15 ]
+
+ Args:
+ column (str): [ name of the column you want to concat ]
+ separator (str, optional): [separator to be used]. Defaults to ",".
+ alias (Optional[str], optional): [description]. Defaults to None.
+ """
+ super(STRING_AGG, self).__init__("STRING_AGG", column, separator, alias=alias)
+
+
+class MATCH(DistinctOptionFunction):
+ def __init__(self, column: str, *args, **kwargs):
+ """[ Implementation of Match Against read more about it https://dev.mysql.com/doc/refman/8.0/en/fulltext-search.html#function_match ]
+
+ Args:
+ column (str):[ column to search in ]
+ """
+ alias = kwargs.get("alias")
+ super(MATCH, self).__init__(" MATCH", column, *args, alias=alias)
+ self._Against = False
+
+ def get_function_sql(self, **kwargs):
+ s = super(DistinctOptionFunction, self).get_function_sql(**kwargs)
+
+ if self._Against:
+ return f"{s} AGAINST ({frappe.db.escape(f'+{self._Against}*')} IN BOOLEAN MODE)"
+ return s
+
+ @builder
+ def Against(self, text: str):
+ """[ Text that has to be searched against ]
+
+ Args:
+ text (str): [ the text string that we match it against ]
+ """
+ self._Against = text
+
+
+class TO_TSVECTOR(DistinctOptionFunction):
+ def __init__(self, column: str, *args, **kwargs):
+ """[ Implementation of TO_TSVECTOR read more about it https://www.postgresql.org/docs/9.1/textsearch-controls.html]
+
+ Args:
+ column (str): [ column to search in ]
+ """
+ alias = kwargs.get("alias")
+ super(TO_TSVECTOR, self).__init__("TO_TSVECTOR", column, *args, alias=alias)
+ self._PLAINTO_TSQUERY = False
+
+ def get_function_sql(self, **kwargs):
+ s = super(DistinctOptionFunction, self).get_function_sql(**kwargs)
+ if self._PLAINTO_TSQUERY:
+ return f"{s} @@ PLAINTO_TSQUERY({frappe.db.escape(self._PLAINTO_TSQUERY)})"
+ return s
+
+ @builder
+ def Against(self, text: str):
+ """[ Text that has to be searched against ]
+
+ Args:
+ text (str): [ the text string that we match it against ]
+ """
+ self._PLAINTO_TSQUERY = text
diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py
new file mode 100644
index 0000000000..5ccb266945
--- /dev/null
+++ b/frappe/query_builder/functions.py
@@ -0,0 +1,17 @@
+from pypika.functions import *
+from frappe.query_builder.utils import ImportMapper, db_type_is
+from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR
+
+GroupConcat = ImportMapper(
+ {
+ db_type_is.MARIADB: GROUP_CONCAT,
+ db_type_is.POSTGRES: STRING_AGG
+ }
+)
+
+Match = ImportMapper(
+ {
+ db_type_is.MARIADB: MATCH,
+ db_type_is.POSTGRES: TO_TSVECTOR
+ }
+)
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
new file mode 100644
index 0000000000..b52a3606e8
--- /dev/null
+++ b/frappe/query_builder/utils.py
@@ -0,0 +1,34 @@
+from enum import Enum
+from typing import Any, Callable, Dict
+
+from pypika import Query
+
+import frappe
+from .builder import MariaDB, Postgres
+
+
+class db_type_is(Enum):
+ MARIADB = "mariadb"
+ POSTGRES = "postgres"
+
+class ImportMapper:
+ def __init__(self, func_map: Dict[db_type_is, Callable]) -> None:
+ self.func_map = func_map
+
+ def __call__(self, *args: Any, **kwds: Any) -> Callable:
+ db = db_type_is(frappe.conf.db_type or "mariadb")
+ return self.func_map[db](*args, **kwds)
+
+
+def get_query_builder(type_of_db: str) -> Query:
+ """[return the query builder object]
+
+ Args:
+ type_of_db (str): [string value of the db used]
+
+ Returns:
+ Query: [Query object]
+ """
+ db = db_type_is(type_of_db)
+ picks = {db_type_is.MARIADB: MariaDB, db_type_is.POSTGRES: Postgres}
+ return picks[db]
diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py
index 49bdade936..70962e8172 100644
--- a/frappe/search/website_search.py
+++ b/frappe/search/website_search.py
@@ -35,10 +35,12 @@ class WebsiteSearch(FullTextSearch):
if getattr(self, "_items_to_index", False):
return self._items_to_index
- routes = get_static_pages_from_all_apps() + slugs_with_web_view()
-
self._items_to_index = []
+
+ routes = get_static_pages_from_all_apps() + slugs_with_web_view(self._items_to_index)
+
+
for i, route in enumerate(routes):
update_progress_bar("Retrieving Routes", i, len(routes))
self._items_to_index += [self.get_document_to_index(route)]
@@ -85,16 +87,23 @@ class WebsiteSearch(FullTextSearch):
)
-def slugs_with_web_view():
+def slugs_with_web_view(_items_to_index):
all_routes = []
filters = { "has_web_view": 1, "allow_guest_to_view": 1, "index_web_pages_for_search": 1}
- fields = ["name", "is_published_field"]
+ fields = ["name", "is_published_field", 'website_search_field']
doctype_with_web_views = frappe.get_all("DocType", filters=filters, fields=fields)
for doctype in doctype_with_web_views:
if doctype.is_published_field:
- routes = frappe.get_all(doctype.name, filters={doctype.is_published_field: 1}, fields="route")
- all_routes += [route.route for route in routes]
+ docs = frappe.get_all(doctype.name, filters={doctype.is_published_field: 1}, fields=["route", doctype.website_search_field, 'title'])
+ if doctype.website_search_field:
+ for doc in docs:
+ content = frappe.utils.md_to_html(getattr(doc, doctype.website_search_field))
+ soup = BeautifulSoup(content, "html.parser")
+ text_content = soup.text if soup else ""
+ _items_to_index += [frappe._dict(title=doc.title, content=text_content, path=doc.route)]
+ else:
+ all_routes += [route.route for route in docs]
return all_routes
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 4d922d6769..4f769ea88f 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
"""
Boot session from cache or build
@@ -84,7 +84,7 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
if user_details: user = user_details[0].get("user")
logout_feed(user, reason)
- frappe.db.sql("""delete from tabSessions where sid=%s""", sid)
+ frappe.db.delete("Sessions", {"sid": sid})
frappe.db.commit()
def clear_all_sessions(reason=None):
@@ -249,7 +249,6 @@ class Session:
data = self.get_session_record()
if data:
- # set language
self.data.update({'data': data, 'user':data.user, 'sid': self.sid})
self.user = data.user
validate_ip_address(self.user)
diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
index 8c4dba5d6b..4a6e86463e 100644
--- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
@@ -273,6 +273,31 @@ class TestEnergyPointLog(unittest.TestCase):
self.assertEqual(points_after_reverting_todo, points_after_closing_todo - rule_points)
self.assertEqual(points_after_saving_todo_again, points_after_reverting_todo + rule_points)
+ def test_energy_points_disabled_user(self):
+ frappe.set_user('test@example.com')
+ user = frappe.get_doc('User', 'test@example.com')
+ user.enabled = 0
+ user.save()
+ todo_point_rule = create_energy_point_rule_for_todo()
+ energy_point_of_user = get_points('test@example.com')
+
+ created_todo = create_a_todo()
+
+ created_todo.status = 'Closed'
+ created_todo.save()
+ points_after_closing_todo = get_points('test@example.com')
+
+ # do not update energy points for disabled user
+ self.assertEqual(points_after_closing_todo, energy_point_of_user)
+
+ user.enabled = 1
+ user.save()
+
+ created_todo.save()
+ points_after_re_saving_todo = get_points('test@example.com')
+ self.assertEqual(points_after_re_saving_todo, energy_point_of_user + todo_point_rule.points)
+
+
def create_energy_point_rule_for_todo(multiplier_field=None, for_doc_event='Custom', max_points=None,
for_assigned_users=0, field_to_check=None, apply_once=False, user_field='owner'):
name = 'ToDo Closed'
diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.py b/frappe/social/doctype/energy_point_rule/energy_point_rule.py
index 1c736528de..b673e8e199 100644
--- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py
+++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py
@@ -5,6 +5,7 @@
import frappe
from frappe import _
import frappe.cache_manager
+from frappe.core.doctype.user.user import get_enabled_users
from frappe.model import log_types
from frappe.model.document import Document
from frappe.social.doctype.energy_point_settings.energy_point_settings import is_energy_point_enabled
@@ -44,7 +45,7 @@ class EnergyPointRule(Document):
try:
for user in users:
- if not user or user == 'Administrator': continue
+ if not is_eligible_user(user): continue
create_energy_points_log(reference_doctype, reference_name, {
'points': points,
'user': user,
@@ -119,3 +120,8 @@ def get_energy_point_doctypes():
d.reference_doctype for d in frappe.get_all('Energy Point Rule',
['reference_doctype'], {'enabled': 1})
]
+
+def is_eligible_user(user):
+ '''Checks if user is eligible to get energy points'''
+ enabled_users = get_enabled_users()
+ return user and user in enabled_users and user != 'Administrator'
diff --git a/frappe/templates/print_formats/pdf_header_footer.html b/frappe/templates/print_formats/pdf_header_footer.html
index a9c621f505..189cf43cd9 100644
--- a/frappe/templates/print_formats/pdf_header_footer.html
+++ b/frappe/templates/print_formats/pdf_header_footer.html
@@ -1,5 +1,5 @@
-
+
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index 0c30fbbd00..8112362f34 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -56,6 +56,7 @@ def main(app=None, module=None, doctype=None, verbose=False, tests=(),
frappe.clear_cache()
frappe.utils.scheduler.disable_scheduler()
set_test_email_config()
+ frappe.conf.update({'bench_id': 'test_bench', 'use_rq_auth': False})
if not frappe.flags.skip_before_tests:
if verbose:
diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py
index bc23cb591c..b02b53338d 100644
--- a/frappe/tests/test_auth.py
+++ b/frappe/tests/test_auth.py
@@ -4,8 +4,9 @@ import time
import unittest
import frappe
-from frappe.auth import LoginAttemptTracker
+from frappe.auth import HTTPRequest, LoginAttemptTracker
from frappe.frappeclient import FrappeClient, AuthError
+from frappe.utils import set_request
class TestAuth(unittest.TestCase):
def __init__(self, *args, **kwargs):
diff --git a/frappe/tests/test_background_jobs.py b/frappe/tests/test_background_jobs.py
index 88783f14f1..188f3e166f 100644
--- a/frappe/tests/test_background_jobs.py
+++ b/frappe/tests/test_background_jobs.py
@@ -4,7 +4,7 @@ from rq import Queue
import frappe
from frappe.core.page.background_jobs.background_jobs import remove_failed_jobs
-from frappe.utils.background_jobs import get_redis_conn
+from frappe.utils.background_jobs import get_redis_conn, generate_qname
import time
@@ -17,14 +17,14 @@ class TestBackgroundJobs(unittest.TestCase):
queues = Queue.all(conn)
for queue in queues:
- if queue.name == "short":
+ if queue.name == generate_qname("short"):
fail_registry = queue.failed_job_registry
self.assertGreater(fail_registry.count, 0)
remove_failed_jobs()
for queue in queues:
- if queue.name == "short":
+ if queue.name == generate_qname("short"):
fail_registry = queue.failed_job_registry
self.assertEqual(fail_registry.count, 0)
diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py
index 4ae78c94de..259d5a9194 100644
--- a/frappe/tests/test_boilerplate.py
+++ b/frappe/tests/test_boilerplate.py
@@ -20,7 +20,7 @@ class TestBoilerPlate(unittest.TestCase):
def test_create_app(self):
title = "Test App"
- description = "Test app for unit testing"
+ description = "This app's description contains 'single quotes' and \"double quotes\"."
publisher = "Test Publisher"
email = "example@example.org"
icon = "" # empty -> default
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index 07bdf8791e..f687f70228 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -426,3 +426,13 @@ class TestCommands(BaseTestCommands):
self.assertEqual(self.returncode, 0)
self.assertIn("pong", self.stdout)
+ def test_version(self):
+ self.execute("bench version")
+ self.assertEqual(self.returncode, 0)
+
+ for output in ["legacy", "plain", "table", "json"]:
+ self.execute(f"bench version -f {output}")
+ self.assertEqual(self.returncode, 0)
+
+ self.execute("bench version -f invalid")
+ self.assertEqual(self.returncode, 2)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 04c9a525b1..044ce455d9 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -12,6 +12,8 @@ from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.utils import random_string
from frappe.utils.testutils import clear_custom_fields
+from .test_query_builder import run_only_if, db_type_is
+
class TestDB(unittest.TestCase):
def test_get_value(self):
@@ -146,7 +148,7 @@ class TestDB(unittest.TestCase):
# Create documents under that doctype and query them via ORM
for _ in range(10):
- docfields = { key.lower(): random_string(10) for key in fields }
+ docfields = {key.lower(): random_string(10) for key in fields}
doc = frappe.get_doc({"doctype": test_doctype, "description": random_string(20), **docfields})
doc.insert()
created_docs.append(doc.name)
@@ -189,3 +191,98 @@ class TestDB(unittest.TestCase):
for doc in created_docs:
frappe.delete_doc(test_doctype, doc)
clear_custom_fields(test_doctype)
+
+@run_only_if(db_type_is.MARIADB)
+class TestDDLCommandsMaria(unittest.TestCase):
+ test_table_name = "TestNotes"
+
+ def setUp(self) -> None:
+ frappe.db.commit()
+ frappe.db.sql(
+ f"""
+ CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL,PRIMARY KEY (`id`));
+ """
+ )
+
+ def tearDown(self) -> None:
+ frappe.db.sql(f"DROP TABLE tab{self.test_table_name};")
+ self.test_table_name = "TestNotes"
+
+ def test_rename(self) -> None:
+ new_table_name = f"{self.test_table_name}_new"
+ frappe.db.rename_table(self.test_table_name, new_table_name)
+ check_exists = frappe.db.sql(
+ f"""
+ SELECT * FROM INFORMATION_SCHEMA.TABLES
+ WHERE TABLE_NAME = N'tab{new_table_name}';
+ """
+ )
+ self.assertGreater(len(check_exists), 0)
+ self.assertIn(f"tab{new_table_name}", check_exists[0])
+
+ # * so this table is deleted after the rename
+ self.test_table_name = new_table_name
+
+ def test_describe(self) -> None:
+ self.assertEqual(
+ (("id", "int(11)", "NO", "PRI", None, ""),),
+ frappe.db.describe(self.test_table_name),
+ )
+
+ def test_change_type(self) -> None:
+ frappe.db.change_column_type("TestNotes", "id", "varchar(255)")
+ test_table_description = frappe.db.sql(f"DESC tab{self.test_table_name};")
+ self.assertGreater(len(test_table_description), 0)
+ self.assertIn("varchar(255)", test_table_description[0])
+
+
+@run_only_if(db_type_is.POSTGRES)
+class TestDDLCommandsPost(unittest.TestCase):
+ test_table_name = "TestNotes"
+
+ def setUp(self) -> None:
+ frappe.db.sql(
+ f"""
+ CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL,PRIMARY KEY ("id"))
+ """
+ )
+
+ def tearDown(self) -> None:
+ frappe.db.sql(f'DROP TABLE "tab{self.test_table_name}"')
+ self.test_table_name = "TestNotes"
+
+ def test_rename(self) -> None:
+ new_table_name = f"{self.test_table_name}_new"
+ frappe.db.rename_table(self.test_table_name, new_table_name)
+ check_exists = frappe.db.sql(
+ f"""
+ SELECT EXISTS (
+ SELECT FROM information_schema.tables
+ WHERE table_name = 'tab{new_table_name}'
+ );
+ """
+ )
+ self.assertTrue(check_exists[0][0])
+
+ # * so this table is deleted after the rename
+ self.test_table_name = new_table_name
+
+ def test_describe(self) -> None:
+ self.assertEqual([("id",)], frappe.db.describe(self.test_table_name))
+
+ def test_change_type(self) -> None:
+ frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)")
+ check_change = frappe.db.sql(
+ f"""
+ SELECT
+ table_name,
+ column_name,
+ data_type
+ FROM
+ information_schema.columns
+ WHERE
+ table_name = 'tab{self.test_table_name}'
+ """
+ )
+ self.assertGreater(len(check_change), 0)
+ self.assertIn("character varying", check_change[0])
diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py
new file mode 100644
index 0000000000..d155dd95db
--- /dev/null
+++ b/frappe/tests/test_query_builder.py
@@ -0,0 +1,74 @@
+import unittest
+from typing import Callable
+
+import frappe
+from frappe.query_builder.functions import GroupConcat, Match
+from frappe.query_builder.utils import db_type_is
+
+
+def run_only_if(dbtype: db_type_is) -> Callable:
+ return unittest.skipIf(
+ db_type_is(frappe.conf.db_type) != dbtype, f"Only runs for {dbtype.value}"
+ )
+
+
+@run_only_if(db_type_is.MARIADB)
+class TestCustomFunctionsMariaDB(unittest.TestCase):
+ def test_concat(self):
+ self.assertEqual("GROUP_CONCAT('Notes')", GroupConcat("Notes").get_sql())
+
+ def test_match(self):
+ query = Match("Notes").Against("text")
+ self.assertEqual(
+ " MATCH('Notes') AGAINST ('+text*' IN BOOLEAN MODE)", query.get_sql()
+ )
+
+
+@run_only_if(db_type_is.POSTGRES)
+class TestCustomFunctionsPostgres(unittest.TestCase):
+ def test_concat(self):
+ self.assertEqual("STRING_AGG('Notes',',')", GroupConcat("Notes").get_sql())
+
+ def test_match(self):
+ query = Match("Notes").Against("text")
+ self.assertEqual(
+ "TO_TSVECTOR('Notes') @@ PLAINTO_TSQUERY('text')", query.get_sql()
+ )
+
+
+class TestBuilderBase(object):
+ def test_adding_tabs(self):
+ self.assertEqual("tabNotes", frappe.qb.Table("Notes").get_sql())
+ self.assertEqual("__Auth", frappe.qb.Table("__Auth").get_sql())
+
+
+@run_only_if(db_type_is.MARIADB)
+class TestBuilderMaria(unittest.TestCase, TestBuilderBase):
+ def test_adding_tabs_in_from(self):
+ self.assertEqual(
+ "SELECT * FROM `tabNotes`", frappe.qb.from_("Notes").select("*").get_sql()
+ )
+ self.assertEqual(
+ "SELECT * FROM `__Auth`", frappe.qb.from_("__Auth").select("*").get_sql()
+ )
+
+
+@run_only_if(db_type_is.POSTGRES)
+class TestBuilderPostgres(unittest.TestCase, TestBuilderBase):
+ def test_adding_tabs_in_from(self):
+ self.assertEqual(
+ 'SELECT * FROM "tabNotes"', frappe.qb.from_("Notes").select("*").get_sql()
+ )
+ self.assertEqual(
+ 'SELECT * FROM "__Auth"', frappe.qb.from_("__Auth").select("*").get_sql()
+ )
+
+ def test_replace_tables(self):
+ info_schema = frappe.qb.Schema("information_schema")
+ self.assertEqual(
+ 'SELECT * FROM "pg_stat_all_tables"',
+ frappe.qb.from_(info_schema.tables).select("*").get_sql(),
+ )
+
+ def test_replace_fields_post(self):
+ self.assertEqual("relname", frappe.qb.Field("table_name").get_sql())
diff --git a/frappe/tests/test_redis.py b/frappe/tests/test_redis.py
new file mode 100644
index 0000000000..72af1ac699
--- /dev/null
+++ b/frappe/tests/test_redis.py
@@ -0,0 +1,70 @@
+import unittest
+import functools
+
+import redis
+
+import frappe
+from frappe.utils import get_bench_id
+from frappe.utils.rq import RedisQueue
+from frappe.utils.background_jobs import get_redis_conn
+
+def version_tuple(version):
+ return tuple(map(int, (version.split("."))))
+
+def skip_if_redis_version_lt(version):
+ def decorator(func):
+ @functools.wraps(func)
+ def wrapper(*args, **kwargs):
+ conn = get_redis_conn()
+ redis_version = conn.execute_command('info')['redis_version']
+ if version_tuple(redis_version) < version_tuple(version):
+ return
+ return func(*args, **kwargs)
+ return wrapper
+ return decorator
+
+class TestRedisAuth(unittest.TestCase):
+ @skip_if_redis_version_lt('6.0')
+ def test_rq_gen_acllist(self):
+ """Make sure that ACL list is genrated
+ """
+ acl_list = RedisQueue.gen_acl_list()
+ self.assertEqual(acl_list[1]['bench'][0], get_bench_id())
+
+ @skip_if_redis_version_lt('6.0')
+ def test_adding_redis_user(self):
+ acl_list = RedisQueue.gen_acl_list()
+ username, password = acl_list[1]['bench']
+ conn = get_redis_conn()
+
+ conn.acl_deluser(username)
+ _ = RedisQueue(conn).add_user(username, password)
+ self.assertTrue(conn.acl_getuser(username))
+ conn.acl_deluser(username)
+
+ @skip_if_redis_version_lt('6.0')
+ def test_rq_namespace(self):
+ """Make sure that user can access only their respective namespace.
+ """
+ # Current bench ID
+ bench_id = frappe.conf.get('bench_id')
+ conn = get_redis_conn()
+ conn.set('rq:queue:test_bench1:abc', 'value')
+ conn.set(f'rq:queue:{bench_id}:abc', 'value')
+
+ # Create new Redis Queue user
+ tmp_bench_id = 'test_bench1'
+ username, password = tmp_bench_id, 'password1'
+ conn.acl_deluser(username)
+ frappe.conf.update({'bench_id': tmp_bench_id})
+ _ = RedisQueue(conn).add_user(username, password)
+ test_bench1_conn = RedisQueue.get_connection(username, password)
+
+ self.assertEqual(test_bench1_conn.get('rq:queue:test_bench1:abc'), b'value')
+
+ # User should not be able to access queues apart from their bench queues
+ with self.assertRaises(redis.exceptions.NoPermissionError):
+ test_bench1_conn.get(f'rq:queue:{bench_id}:abc')
+
+ frappe.conf.update({'bench_id': bench_id})
+ conn.acl_deluser(username)
diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py
index 9ad02f49a6..afb584ea15 100644
--- a/frappe/tests/test_search.py
+++ b/frappe/tests/test_search.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
import unittest
@@ -6,7 +6,16 @@ import frappe
from frappe.desk.search import search_link
from frappe.desk.search import search_widget
+
class TestSearch(unittest.TestCase):
+ def setUp(self):
+ if self._testMethodName == "test_link_field_order":
+ setup_test_link_field_order(self)
+
+ def tearDown(self):
+ if self._testMethodName == "test_link_field_order":
+ teardown_test_link_field_order(self)
+
def test_search_field_sanitizer(self):
# pass
search_link('DocType', 'User', query=None, filters=None, page_length=20, searchfield='name')
@@ -38,6 +47,18 @@ class TestSearch(unittest.TestCase):
search_link, 'DocType', 'Customer', query=None, filters=None,
page_length=20, searchfield=';')
+ def test_link_field_order(self):
+ # Making a request to the search_link with the tree doctype
+ search_link(doctype=self.tree_doctype_name, txt='all', query=None,
+ filters=None, page_length=20, searchfield=None)
+ result = frappe.response['results']
+
+ # Check whether the result is sorted or not
+ self.assertEquals(self.parent_doctype_name, result[0]['value'])
+
+ # Check whether searching for parent also list out children
+ self.assertEquals(len(result), len(self.child_doctypes_names) + 1)
+
#Search for the word "pay", part of the word "pays" (country) in french.
def test_link_search_in_foreign_language(self):
try:
@@ -80,4 +101,58 @@ class TestSearch(unittest.TestCase):
@frappe.validate_and_sanitize_search_inputs
def get_data(doctype, txt, searchfield, start, page_len, filters):
- return [doctype, txt, searchfield, start, page_len, filters]
\ No newline at end of file
+ return [doctype, txt, searchfield, start, page_len, filters]
+
+def setup_test_link_field_order(TestCase):
+ TestCase.tree_doctype_name = 'Test Tree Order'
+ TestCase.child_doctype_list = []
+ TestCase.child_doctypes_names = ['USA', 'India', 'Russia', 'China']
+ TestCase.parent_doctype_name = 'All Territories'
+
+ # Create Tree doctype
+ TestCase.tree_doc = frappe.get_doc({
+ 'doctype': 'DocType',
+ 'name': TestCase.tree_doctype_name,
+ 'module': 'Custom',
+ 'custom': 1,
+ 'is_tree': 1,
+ 'autoname': 'field:random',
+ 'fields': [{
+ 'fieldname': 'random',
+ 'label': 'Random',
+ 'fieldtype': 'Data'
+ }]
+ }).insert()
+ TestCase.tree_doc.search_fields = 'parent_test_tree_order'
+ TestCase.tree_doc.save()
+
+ # Create root for the tree doctype
+ frappe.get_doc({
+ "doctype": TestCase.tree_doctype_name,
+ "random": TestCase.parent_doctype_name,
+ "is_group": 1
+ }).insert()
+
+ # Create children for the root
+ for child_name in TestCase.child_doctypes_names:
+ temp = frappe.get_doc({
+ "doctype": TestCase.tree_doctype_name,
+ "random": child_name,
+ "parent_test_tree_order": TestCase.parent_doctype_name
+ }).insert()
+ TestCase.child_doctype_list.append(temp)
+
+def teardown_test_link_field_order(TestCase):
+ # Deleting all the created doctype
+ for child_doctype in TestCase.child_doctype_list:
+ child_doctype.delete()
+
+ frappe.delete_doc(
+ TestCase.tree_doctype_name,
+ TestCase.parent_doctype_name,
+ ignore_permissions=True,
+ force=True,
+ for_reload=True,
+ )
+
+ TestCase.tree_doc.delete()
diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py
index f51f31d509..edab3a82c3 100644
--- a/frappe/tests/test_translate.py
+++ b/frappe/tests/test_translate.py
@@ -1,13 +1,26 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
-import frappe, unittest, os
+import os
+import unittest
+from random import choices
+from unittest.mock import patch
+
+import frappe
import frappe.translate
from frappe import _
+from frappe.translate import get_language, get_parent_language
+from frappe.utils import set_request
dirname = os.path.dirname(__file__)
translation_string_file = os.path.join(dirname, 'translation_test_file.txt')
+first_lang, second_lang, third_lang, fourth_lang, fifth_lang = choices(
+ frappe.get_all("Language", pluck="name"), k=5
+)
class TestTranslate(unittest.TestCase):
+ def tearDown(self):
+ frappe.form_dict.pop("_lang", None)
+
def test_extract_message_from_file(self):
data = frappe.translate.get_messages_from_file(translation_string_file)
self.assertListEqual(data, expected_output)
@@ -20,6 +33,41 @@ class TestTranslate(unittest.TestCase):
finally:
frappe.local.lang = 'en'
+ def test_request_language_resolution_with_form_dict(self):
+ """Test for frappe.translate.get_language
+
+ Case 1: frappe.form_dict._lang is set
+ """
+
+ frappe.form_dict._lang = first_lang
+
+ with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
+ return_val = get_language()
+
+ self.assertIn(return_val, [first_lang, get_parent_language(first_lang)])
+
+ def test_request_language_resolution_with_cookie(self):
+ """Test for frappe.translate.get_language
+
+ Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is
+ """
+
+ with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang):
+ set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
+ return_val = get_language()
+
+ self.assertIn(return_val, [second_lang, get_parent_language(second_lang)])
+
+ def test_request_language_resolution_with_request_header(self):
+ """Test for frappe.translate.get_language
+
+ Case 3: frappe.form_dict._lang & preferred_language cookie is not set, but Accept-Language header is
+ """
+ set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)])
+ return_val = get_language()
+ self.assertIn(return_val, [third_lang, get_parent_language(third_lang)])
+
+
expected_output = [
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', 'This is some context', 2),
('apps/frappe/frappe/tests/translation_test_file.txt', 'Warning: Unable to find {0} in any table related to {1}', None, 4),
diff --git a/frappe/translate.py b/frappe/translate.py
index 4ff50d3fd0..d5916f1761 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# MIT License. See license.txt
"""
frappe.translate
@@ -11,71 +11,100 @@ import io
import itertools
import json
import operator
+import functools
import os
import re
from csv import reader
+from typing import List, Union, Tuple
import frappe
from frappe.model.utils import InvalidIncludePath, render_include
-from frappe.utils import is_html, strip, strip_html_tags
+from frappe.utils import get_bench_path, is_html, strip, strip_html_tags
-def guess_language(lang_list=None):
- """Set `frappe.local.lang` from HTTP headers at beginning of request"""
- user_preferred_language = frappe.request.cookies.get('preferred_language')
- is_guest_user = not frappe.session.user or frappe.session.user == 'Guest'
- if is_guest_user and user_preferred_language:
- return user_preferred_language
+def get_language(lang_list: List = None) -> str:
+ """Set `frappe.local.lang` from HTTP headers at beginning of request
- lang_codes = frappe.request.accept_languages.values()
- if not lang_codes:
- return frappe.local.lang
+ Order of priority for setting language:
+ 1. Form Dict => _lang
+ 2. Cookie => preferred_language
+ 3. Request Header => Accept-Language
+ 4. User document => language
+ 5. System Settings => language
+ """
- guess = None
- if not lang_list:
- lang_list = get_all_languages() or []
+ # fetch language from form_dict
+ if frappe.form_dict._lang:
+ language = get_lang_code(
+ frappe.form_dict._lang or get_parent_language(frappe.form_dict._lang)
+ )
+ if language:
+ return language
- for l in lang_codes:
- code = l.strip()
- if not isinstance(code, str):
- code = str(code, 'utf-8')
- if code in lang_list or code == "en":
- guess = code
- break
+ lang_set = set(lang_list or get_all_languages() or [])
- # check if parent language (pt) is setup, if variant (pt-BR)
- if "-" in code:
- code = code.split("-")[0]
- if code in lang_list:
- guess = code
- break
+ # fetch language from cookie
+ preferred_language_cookie = get_preferred_language_cookie()
- return guess or frappe.local.lang
+ if preferred_language_cookie:
+ if preferred_language_cookie in lang_set:
+ return preferred_language_cookie
-def get_user_lang(user=None):
+ parent_language = get_parent_language(language)
+ if parent_language in lang_set:
+ return parent_language
+
+ # fetch language from request headers
+ accept_language = list(frappe.request.accept_languages.values())
+
+ for language in accept_language:
+ if language in lang_set:
+ return language
+
+ parent_language = get_parent_language(language)
+ if parent_language in lang_set:
+ return parent_language
+
+ # fallback to language set in User or System Settings
+ return frappe.local.lang
+
+
+@functools.lru_cache()
+def get_parent_language(lang: str) -> str:
+ """If the passed language is a variant, return its parent
+
+ Eg:
+ 1. zh-TW -> zh
+ 2. sr-BA -> sr
+ """
+ is_language_variant = "-" in lang
+ if is_language_variant:
+ return lang[:lang.index("-")]
+
+
+def get_user_lang(user: str = None) -> str:
"""Set frappe.local.lang from user preferences on session beginning or resumption"""
- if not user:
- user = frappe.session.user
-
- # via cache
+ user = user or frappe.session.user
lang = frappe.cache().hget("lang", user)
if not lang:
-
- # if defined in user profile
- lang = frappe.db.get_value("User", user, "language")
- if not lang:
- lang = frappe.db.get_default("lang")
-
- if not lang:
- lang = frappe.local.lang or 'en'
+ # User.language => Session Defaults => frappe.local.lang => 'en'
+ lang = (
+ frappe.db.get_value("User", user, "language")
+ or frappe.db.get_default("lang")
+ or frappe.local.lang
+ or "en"
+ )
frappe.cache().hset("lang", user, lang)
return lang
-def get_lang_code(lang):
- return frappe.db.get_value('Language', {'language_name': lang}) or lang
+def get_lang_code(lang: str) -> Union[str, None]:
+ return (
+ frappe.db.get_value("Language", {"name": lang})
+ or frappe.db.get_value("Language", {"language_name": lang})
+ )
def set_default_language(lang):
"""Set Global default language"""
@@ -529,7 +558,7 @@ def get_all_messages_from_js_files(app_name=None):
return messages
-def get_messages_from_file(path):
+def get_messages_from_file(path: str) -> List[Tuple[str, str, str, str]]:
"""Returns a list of transatable strings from a code file
:param path: path of the code file
@@ -542,7 +571,7 @@ def get_messages_from_file(path):
frappe.flags.scanned_files.append(path)
- apps_path = get_bench_dir()
+ bench_path = get_bench_path()
if os.path.exists(path):
with open(path, 'r') as sourcefile:
try:
@@ -550,11 +579,12 @@ def get_messages_from_file(path):
except Exception:
print("Could not scan file for translation: {0}".format(path))
return []
- data = [(os.path.relpath(path, apps_path), message, context, line) \
- for line, message, context in extract_messages_from_code(file_contents)]
- return data
+
+ return [
+ (os.path.relpath(path, bench_path), message, context, line)
+ for (line, message, context) in extract_messages_from_code(file_contents)
+ ]
else:
- # print "Translate: {0} missing".format(os.path.abspath(path))
return []
def extract_messages_from_code(code):
@@ -768,9 +798,6 @@ def deduplicate_messages(messages):
ret.append(next(g))
return ret
-def get_bench_dir():
- return os.path.join(frappe.__file__, '..', '..', '..', '..')
-
def rename_language(old_name, new_name):
if not frappe.db.exists('Language', new_name):
return
@@ -879,3 +906,6 @@ def get_all_languages(with_language_name=False):
@frappe.whitelist(allow_guest=True)
def set_preferred_language_cookie(preferred_language):
frappe.local.cookie_manager.set_cookie("preferred_language", preferred_language)
+
+def get_preferred_language_cookie():
+ return frappe.request.cookies.get("preferred_language")
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index af9d5de1ee..b97585aa04 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -383,6 +383,12 @@ def get_files_path(*path, **kwargs):
def get_bench_path():
return os.path.realpath(os.path.join(os.path.dirname(frappe.__file__), '..', '..', '..'))
+def get_bench_id():
+ return frappe.get_conf().get('bench_id', get_bench_path().strip('/').replace('/', '-'))
+
+def get_site_id(site=None):
+ return f"{site or frappe.local.site}@{get_bench_id()}"
+
def get_backups_path():
return get_site_path("private", "backups")
@@ -843,3 +849,6 @@ def groupby_metric(iterable: typing.Dict[str, list], key: str):
for item in items:
records.setdefault(item[key], {}).setdefault(category, []).append(item)
return records
+
+def get_table_name(table_name: str) -> str:
+ return f"tab{table_name}" if not table_name.startswith("__") else table_name
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index 8456835ca7..f0bd06aff4 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -1,13 +1,21 @@
+import os
+import socket
+import time
+from uuid import uuid4
+from collections import defaultdict
+
+
import redis
+from typing import List
from rq import Connection, Queue, Worker
from rq.logutils import setup_loghandlers
-from frappe.utils import cstr
-from collections import defaultdict
+
import frappe
-import os, socket, time
from frappe import _
-from uuid import uuid4
import frappe.monitor
+from frappe.utils import cstr, get_bench_id
+from frappe.utils.rq import RedisQueue
+from frappe.utils.commands import log
default_timeout = 300
@@ -131,21 +139,22 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
if is_async:
frappe.destroy()
-def start_worker(queue=None, quiet = False):
+def start_worker(queue=None, quiet = False, rq_username=None, rq_password=None):
'''Wrapper to start rq worker. Connects to redis and monitors these queues.'''
with frappe.init_site():
# empty init is required to get redis_queue from common_site_config.json
- redis_connection = get_redis_conn()
+ redis_connection = get_redis_conn(username=rq_username, password=rq_password)
+ queues = get_queue_list(queue, build_queue_name=True)
+ queue_name = queue and generate_qname(queue)
if os.environ.get('CI'):
setup_loghandlers('ERROR')
with Connection(redis_connection):
- queues = get_queue_list(queue)
logging_level = "INFO"
if quiet:
logging_level = "WARNING"
- Worker(queues, name=get_worker_name(queue)).work(logging_level = logging_level)
+ Worker(queues, name=get_worker_name(queue_name)).work(logging_level = logging_level)
def get_worker_name(queue):
'''When limiting worker to a specific queue, also append queue name to default worker name'''
@@ -186,7 +195,7 @@ def get_jobs(site=None, queue=None, key='method'):
return jobs_per_site
-def get_queue_list(queue_list=None):
+def get_queue_list(queue_list=None, build_queue_name=False):
'''Defines possible queues. Also wraps a given queue in a list after validating.'''
default_queue_list = list(queue_timeout)
if queue_list:
@@ -195,11 +204,9 @@ def get_queue_list(queue_list=None):
for queue in queue_list:
validate_queue(queue, default_queue_list)
-
- return queue_list
-
else:
- return default_queue_list
+ queue_list = default_queue_list
+ return [generate_qname(qtype) for qtype in queue_list] if build_queue_name else queue_list
def get_workers(queue):
'''Returns a list of Worker objects tied to a queue object'''
@@ -215,10 +222,10 @@ def get_running_jobs_in_queue(queue):
jobs.append(current_job)
return jobs
-def get_queue(queue, is_async=True):
+def get_queue(qtype, is_async=True):
'''Returns a Queue object tied to a redis connection'''
- validate_queue(queue)
- return Queue(queue, connection=get_redis_conn(), is_async=is_async)
+ validate_queue(qtype)
+ return Queue(generate_qname(qtype), connection=get_redis_conn(), is_async=is_async)
def validate_queue(queue, default_queue_list=None):
if not default_queue_list:
@@ -227,7 +234,7 @@ def validate_queue(queue, default_queue_list=None):
if queue not in default_queue_list:
frappe.throw(_("Queue should be one of {0}").format(', '.join(default_queue_list)))
-def get_redis_conn():
+def get_redis_conn(username=None, password=None):
if not hasattr(frappe.local, 'conf'):
raise Exception('You need to call frappe.init')
@@ -236,11 +243,50 @@ def get_redis_conn():
global redis_connection
- if not redis_connection:
- redis_connection = redis.from_url(frappe.local.conf.redis_queue)
+ cred = frappe._dict()
+ if frappe.conf.get('use_rq_auth'):
+ if username:
+ cred['username'] = username
+ cred['password'] = password
+ else:
+ cred['username'] = frappe.get_site_config().rq_username or get_bench_id()
+ cred['password'] = frappe.get_site_config().rq_password
+
+ elif os.environ.get('RQ_ADMIN_PASWORD'):
+ cred['username'] = 'default'
+ cred['password'] = os.environ.get('RQ_ADMIN_PASWORD')
+ try:
+ redis_connection = RedisQueue.get_connection(**cred)
+ except (redis.exceptions.AuthenticationError, redis.exceptions.ResponseError):
+ log(f'Wrong credentials used for {cred.username or "default user"}. '
+ 'You can reset credentials using `bench create-rq-users` CLI and restart the server',
+ colour='red')
+ raise
+ except Exception:
+ log(f'Please make sure that Redis Queue runs @ {frappe.get_conf().redis_queue}', colour='red')
+ raise
return redis_connection
+def get_queues() -> List[Queue]:
+ """Get all the queues linked to the current bench.
+ """
+ queues = Queue.all(connection=get_redis_conn())
+ return [q for q in queues if is_queue_accessible(q)]
+
+def generate_qname(qtype: str) -> str:
+ """Generate qname by combining bench ID and queue type.
+
+ qnames are useful to define namespaces of customers.
+ """
+ return f"{get_bench_id()}:{qtype}"
+
+def is_queue_accessible(qobj: Queue) -> bool:
+ """Checks whether queue is relate to current bench or not.
+ """
+ accessible_queues = [generate_qname(q) for q in list(queue_timeout)]
+ return qobj.name in accessible_queues
+
def enqueue_test_job():
enqueue('frappe.utils.background_jobs.test_job', s=100)
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index 908be52452..f13710dcfe 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -116,16 +116,16 @@ class BackupGenerator:
def setup_backup_tables(self):
"""Sets self.backup_includes, self.backup_excludes based on passed args"""
- existing_doctypes = set([x.name for x in frappe.get_all("DocType")])
+ existing_tables = frappe.db.get_tables()
def get_tables(doctypes):
tables = []
for doctype in doctypes:
- if doctype and doctype in existing_doctypes:
- if doctype.startswith("tab"):
- tables.append(doctype)
- else:
- tables.append("tab" + doctype)
+ if not doctype:
+ continue
+ table = frappe.utils.get_table_name(doctype)
+ if table in existing_tables:
+ tables.append(table)
return tables
passed_tables = {
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index 8dab9b748f..d88eaa5745 100755
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -66,9 +66,6 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name = hooks.app_name)))
- with open(os.path.join(dest, hooks.app_name, "setup.py"), "w") as f:
- f.write(frappe.as_unicode(setup_template.format(**hooks)))
-
with open(os.path.join(dest, hooks.app_name, "requirements.txt"), "w") as f:
f.write("# frappe -- https://github.com/frappe/frappe is installed via 'bench init'")
@@ -82,6 +79,14 @@ def make_boilerplate(dest, app_name):
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "modules.txt"), "w") as f:
f.write(frappe.as_unicode(hooks.app_title))
+ # These values could contain quotes and can break string declarations
+ # So escaping them before setting variables in setup.py and hooks.py
+ for key in ("app_publisher", "app_description", "app_license"):
+ hooks[key] = hooks[key].replace("\\", "\\\\").replace("'", "\\'").replace("\"", "\\\"")
+
+ with open(os.path.join(dest, hooks.app_name, "setup.py"), "w") as f:
+ f.write(frappe.as_unicode(setup_template.format(**hooks)))
+
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.py"), "w") as f:
f.write(frappe.as_unicode(hooks_template.format(**hooks)))
@@ -328,18 +333,18 @@ def get_data():
setup_template = """from setuptools import setup, find_packages
-with open('requirements.txt') as f:
- install_requires = f.read().strip().split('\\n')
+with open("requirements.txt") as f:
+ install_requires = f.read().strip().split("\\n")
# get version from __version__ variable in {app_name}/__init__.py
from {app_name} import __version__ as version
setup(
- name='{app_name}',
+ name="{app_name}",
version=version,
- description='{app_description}',
- author='{app_publisher}',
- author_email='{app_email}',
+ description="{app_description}",
+ author="{app_publisher}",
+ author_email="{app_email}",
packages=find_packages(),
zip_safe=False,
include_package_data=True,
diff --git a/frappe/utils/error.py b/frappe/utils/error.py
index 07e34674fe..05b578d7e8 100644
--- a/frappe/utils/error.py
+++ b/frappe/utils/error.py
@@ -176,6 +176,7 @@ def collect_error_snapshots():
def clear_old_snapshots():
"""Clear snapshots that are older than a month"""
+
frappe.db.sql("""delete from `tabError Snapshot`
where creation < (NOW() - INTERVAL '1' MONTH)""")
diff --git a/frappe/utils/fixtures.py b/frappe/utils/fixtures.py
index 1f33c36b13..895a3c8373 100644
--- a/frappe/utils/fixtures.py
+++ b/frappe/utils/fixtures.py
@@ -20,7 +20,7 @@ def sync_fixtures(app=None):
if os.path.exists(frappe.get_app_path(app, "fixtures")):
fixture_files = sorted(os.listdir(frappe.get_app_path(app, "fixtures")))
for fname in fixture_files:
- if fname.endswith(".json") or fname.endswith(".csv"):
+ if fname.endswith(".json"):
import_doc(frappe.get_app_path(app, "fixtures", fname))
import_custom_scripts(app)
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index efe92232d9..072e3a7c62 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -23,8 +23,7 @@ def reset():
Deletes all data in __global_search
:return:
"""
- frappe.db.sql('DELETE FROM `__global_search`')
-
+ frappe.db.delete("__global_search")
def get_doctypes_with_global_search(with_child_tables=True):
"""
@@ -146,10 +145,9 @@ def rebuild_for_doctype(doctype):
def delete_global_search_records_for_doctype(doctype):
- frappe.db.sql('''DELETE
- FROM `__global_search`
- WHERE doctype = %s''', doctype, as_dict=True)
-
+ frappe.db.delete("__global_search", {
+ "doctype": doctype
+ })
def get_selected_fields(meta, global_search_fields):
fieldnames = [df.fieldname for df in global_search_fields]
@@ -231,9 +229,6 @@ def update_global_search(doc):
if frappe.local.conf.get('disable_global_search'):
return
- if frappe.local.conf.get('disable_global_search'):
- return
-
if doc.docstatus > 1 or (doc.meta.has_field("enabled") and not doc.get("enabled")) \
or doc.get("disabled"):
return
@@ -402,12 +397,10 @@ def delete_for_document(doc):
been deleted
:param doc: Deleted document
"""
-
- frappe.db.sql('''DELETE
- FROM `__global_search`
- WHERE doctype = %s
- AND name = %s''', (doc.doctype, doc.name), as_dict=True)
-
+ frappe.db.delete("__global_search", {
+ "doctype": doc.doctype,
+ "name": doc.name
+ })
@frappe.whitelist()
def search(text, start=0, limit=20, doctype=""):
@@ -418,51 +411,41 @@ def search(text, start=0, limit=20, doctype=""):
:param limit: number of results to return, default 20
:return: Array of result objects
"""
- from frappe.desk.doctype.global_search_settings.global_search_settings import get_doctypes_for_global_search
+ from frappe.desk.doctype.global_search_settings.global_search_settings import (
+ get_doctypes_for_global_search,
+ )
+ from frappe.query_builder.functions import Match
results = []
sorted_results = []
allowed_doctypes = get_doctypes_for_global_search()
- for text in set(text.split('&')):
+ for text in set(text.split("&")):
text = text.strip()
if not text:
continue
- conditions = '1=1'
- offset = ''
-
- mariadb_text = frappe.db.escape('+' + text + '*')
-
- mariadb_fields = '`doctype`, `name`, `content`, MATCH (`content`) AGAINST ({} IN BOOLEAN MODE) AS rank'.format(mariadb_text)
- postgres_fields = '`doctype`, `name`, `content`, TO_TSVECTOR("content") @@ PLAINTO_TSQUERY({}) AS rank'.format(frappe.db.escape(text))
-
- values = {}
+ global_search = frappe.qb.Table("__global_search")
+ rank = Match(global_search.content).Against(text).as_("rank")
+ query = (
+ frappe.qb.from_(global_search)
+ .select(
+ global_search.doctype, global_search.name, global_search.content, rank
+ )
+ .orderby("rank", order=frappe.qb.desc)
+ .limit(limit)
+ )
if doctype:
- conditions = '`doctype` = %(doctype)s'
- values['doctype'] = doctype
+ query = query.where(global_search.doctype == doctype)
elif allowed_doctypes:
- conditions = '`doctype` IN %(allowed_doctypes)s'
- values['allowed_doctypes'] = tuple(allowed_doctypes)
+ query = query.where(global_search.doctype.isin(allowed_doctypes))
- if int(start) > 0:
- offset = 'OFFSET {}'.format(start)
+ if start > 0:
+ query = query.offset(start)
- common_query = """
- SELECT {fields}
- FROM `__global_search`
- WHERE {conditions}
- ORDER BY rank DESC
- LIMIT {limit}
- {offset}
- """
-
- result = frappe.db.multisql({
- 'mariadb': common_query.format(fields=mariadb_fields, conditions=conditions, limit=limit, offset=offset),
- 'postgres': common_query.format(fields=postgres_fields, conditions=conditions, limit=limit, offset=offset)
- }, values=values, as_dict=True)
+ result = frappe.db.sql(query, as_dict=True)
results.extend(result)
@@ -473,7 +456,9 @@ def search(text, start=0, limit=20, doctype=""):
try:
meta = frappe.get_meta(r.doctype)
if meta.image_field:
- r.image = frappe.db.get_value(r.doctype, r.name, meta.image_field)
+ r.image = frappe.db.get_value(
+ r.doctype, r.name, meta.image_field
+ )
except Exception:
frappe.clear_messages()
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index 91d8f04eb4..3d6a2fed97 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -111,9 +111,9 @@ def before_tests():
# don't run before tests if any other app is installed
return
- frappe.db.sql("delete from `tabCustom Field`")
- frappe.db.sql("delete from `tabEvent`")
- frappe.db.commit()
+ frappe.db.truncate("Custom Field")
+ frappe.db.truncate("Event")
+
frappe.clear_cache()
# complete setup if missing
diff --git a/frappe/utils/jinja_globals.py b/frappe/utils/jinja_globals.py
index 2c14249672..67ca9d108a 100644
--- a/frappe/utils/jinja_globals.py
+++ b/frappe/utils/jinja_globals.py
@@ -73,17 +73,25 @@ def include_script(path):
return f''
-def include_style(path):
+def include_style(path, rtl=None):
path = bundled_asset(path)
return f''
-def bundled_asset(path):
+def bundled_asset(path, rtl=None):
from frappe.utils import get_assets_json
from frappe.website.utils import abs_url
if ".bundle." in path and not path.startswith("/assets"):
bundled_assets = get_assets_json()
+ if path.endswith('.css') and is_rtl(rtl):
+ path = f"rtl_{path}"
path = bundled_assets.get(path) or path
return abs_url(path)
+
+def is_rtl(rtl=None):
+ from frappe import local
+ if rtl is None:
+ return local.lang in ["ar", "he", "fa", "ps"]
+ return rtl
\ No newline at end of file
diff --git a/frappe/utils/password.py b/frappe/utils/password.py
index 428f2e9577..a097c58b31 100644
--- a/frappe/utils/password.py
+++ b/frappe/utils/password.py
@@ -65,11 +65,11 @@ def set_encrypted_password(doctype, name, pwd, fieldname='password'):
def remove_encrypted_password(doctype, name, fieldname='password'):
- frappe.db.sql(
- 'DELETE FROM `__Auth` WHERE doctype = %s and name = %s and fieldname = %s',
- values=[doctype, name, fieldname]
- )
-
+ frappe.db.delete("__Auth", {
+ "doctype": doctype,
+ "name": name,
+ "fieldname": fieldname
+ })
def check_password(user, pwd, doctype='User', fieldname='password', delete_tracker_cache=True):
'''Checks if user and password are correct, else raises frappe.AuthenticationError'''
@@ -131,8 +131,10 @@ def update_password(user, pwd, doctype='User', fieldname='password', logout_all_
def delete_all_passwords_for(doctype, name):
try:
- frappe.db.sql("""delete from `__Auth` where `doctype`=%(doctype)s and `name`=%(name)s""",
- { 'doctype': doctype, 'name': name })
+ frappe.db.delete("__Auth", {
+ "doctype": doctype,
+ "name": name
+ })
except Exception as e:
if not frappe.db.is_missing_column(e):
raise
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index fcf483bea6..9cbece5dbe 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -13,7 +13,7 @@ from PyPDF2 import PdfFileReader, PdfFileWriter
import frappe
from frappe import _
from frappe.utils import scrub_urls
-from frappe.utils.jinja_globals import bundled_asset
+from frappe.utils.jinja_globals import bundled_asset, is_rtl
PDF_CONTENT_ERRORS = ["ContentNotFoundError", "ContentOperationNotPermittedError",
"UnknownContentError", "RemoteHostClosedError"]
@@ -177,7 +177,9 @@ def prepare_header_footer(soup):
"content": content,
"styles": styles,
"html_id": html_id,
- "css": css
+ "css": css,
+ "lang": frappe.local.lang,
+ "layout_direction": "rtl" if is_rtl else "ltr"
})
# create temp file
diff --git a/frappe/utils/rq.py b/frappe/utils/rq.py
new file mode 100644
index 0000000000..b344b0caa5
--- /dev/null
+++ b/frappe/utils/rq.py
@@ -0,0 +1,83 @@
+import redis
+
+import frappe
+from frappe.utils import get_bench_id, random_string
+
+class RedisQueue:
+ def __init__(self, conn):
+ self.conn = conn
+
+ def add_user(self, username, password=None):
+ """Create or update the user.
+ """
+ password = password or self.conn.acl_genpass()
+ user_settings = self.get_new_user_settings(username, password)
+ is_created = self.conn.acl_setuser(**user_settings)
+ return frappe._dict(user_settings) if is_created else {}
+
+ @classmethod
+ def get_connection(cls, username=None, password=None):
+ rq_url = frappe.local.conf.redis_queue
+ domain = rq_url.split("redis://", 1)[-1]
+ url = (username and f"redis://{username}:{password or ''}@{domain}") or rq_url
+ conn = redis.from_url(url)
+ conn.ping()
+ return conn
+
+ @classmethod
+ def new(cls, username='default', password=None):
+ return cls(cls.get_connection(username, password))
+
+ @classmethod
+ def set_admin_password(cls, cur_password=None, new_password=None, reset_passwords=False):
+ username = 'default'
+ conn = cls.get_connection(username, cur_password)
+ password = '+'+(new_password or conn.acl_genpass())
+ conn.acl_setuser(
+ username=username, enabled=True, reset_passwords=reset_passwords, passwords=password
+ )
+ return password[1:]
+
+ @classmethod
+ def get_new_user_settings(cls, username, password):
+ d = {}
+ d['username'] = username
+ d['passwords'] = '+'+password
+ d['reset_keys'] = True
+ d['enabled'] = True
+ d['keys'] = cls.get_acl_key_rules()
+ d['commands'] = cls.get_acl_command_rules()
+ return d
+
+ @classmethod
+ def get_acl_key_rules(cls, include_key_prefix=False):
+ """FIXME: Find better way
+ """
+ rules = ['rq:[^q]*', 'rq:queues', f'rq:queue:{get_bench_id()}:*']
+ if include_key_prefix:
+ return ['~'+pattern for pattern in rules]
+ return rules
+
+ @classmethod
+ def get_acl_command_rules(cls):
+ return ['+@all', '-@admin']
+
+ @classmethod
+ def gen_acl_list(cls, set_admin_password=False):
+ """Generate list of ACL users needed for this branch.
+
+ This list contains default ACL user and the bench ACL user(used by all sites incase of ACL is enabled).
+ """
+ bench_username = get_bench_id()
+ bench_user_rules = cls.get_acl_key_rules(include_key_prefix=True) + cls.get_acl_command_rules()
+ bench_user_rule_str = ' '.join(bench_user_rules).strip()
+ bench_user_password = random_string(20)
+
+ default_username = 'default'
+ _default_user_password = random_string(20) if set_admin_password else ''
+ default_user_password = '>'+_default_user_password if _default_user_password else 'nopass'
+
+ return [
+ f'user {default_username} on {default_user_password} ~* &* +@all',
+ f'user {bench_username} on >{bench_user_password} {bench_user_rule_str}'
+ ], {'bench': (bench_username, bench_user_password), 'default': (default_username, _default_user_password)}
diff --git a/frappe/utils/testutils.py b/frappe/utils/testutils.py
index c451d090f1..9a2b2da791 100644
--- a/frappe/utils/testutils.py
+++ b/frappe/utils/testutils.py
@@ -12,5 +12,5 @@ def add_custom_field(doctype, fieldname, fieldtype='Data', options=None):
}).insert()
def clear_custom_fields(doctype):
- frappe.db.sql('delete from `tabCustom Field` where dt=%s', doctype)
+ frappe.db.delete("Custom Field", {"dt": doctype})
frappe.clear_cache(doctype=doctype)
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index 6e4200b54b..32f7e030a6 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -542,7 +542,7 @@ def get_form_data(doctype, docname=None, web_form_name=None):
# For Table fields, server-side processing for meta
for field in out.web_form.web_form_fields:
if field.fieldtype == "Table":
- field.fields = get_in_list_view_fields(field.options)
+ field.fields = frappe.get_meta(field.options).fields
out.update({field.fieldname: field.fields})
if field.fieldtype == "Link":
diff --git a/frappe/website/page_renderers/not_found_page.py b/frappe/website/page_renderers/not_found_page.py
index af510fecfc..de631f5dfe 100644
--- a/frappe/website/page_renderers/not_found_page.py
+++ b/frappe/website/page_renderers/not_found_page.py
@@ -8,11 +8,11 @@ from frappe.website.utils import can_cache
HOMEPAGE_PATHS = ('/', '/index', 'index')
class NotFoundPage(TemplatePage):
- def __init__(self, path, http_status_code):
+ def __init__(self, path, http_status_code=None):
self.request_path = path
self.request_url = frappe.local.request.url if hasattr(frappe.local, 'request') else ''
path = '404'
- http_status_code = 404
+ http_status_code = http_status_code or 404
super().__init__(path=path, http_status_code=http_status_code)
def can_render(self):
diff --git a/frappe/website/report/website_analytics/website_analytics.py b/frappe/website/report/website_analytics/website_analytics.py
index d141972679..6cc0fb7f97 100644
--- a/frappe/website/report/website_analytics/website_analytics.py
+++ b/frappe/website/report/website_analytics/website_analytics.py
@@ -1,11 +1,14 @@
# Copyright (c) 2013, Frappe Technologies and contributors
# For license information, please see license.txt
-import frappe
from datetime import datetime
+
+import frappe
+from frappe.query_builder.functions import Coalesce, Count
from frappe.utils import getdate
from frappe.utils.dateutils import get_dates_from_timegrain
+
def execute(filters=None):
return WebsiteAnalytics(filters).run()
@@ -56,33 +59,21 @@ class WebsiteAnalytics(object):
]
def get_data(self):
- pg_query = """
- SELECT
- path,
- COUNT(*) as count,
- COUNT(CASE WHEN CAST(is_unique as Integer) = 1 THEN 1 END) as unique_count
- FROM `tabWeb Page View`
- WHERE coalesce("tabWeb Page View".creation, '0001-01-01') BETWEEN %s AND %s
- GROUP BY path
- ORDER BY count desc
- """
+ WebPageView = frappe.qb.Table("Web Page View")
+ count_all = Count("*").as_("count")
+ case = frappe.qb.terms.Case().when(WebPageView.is_unique == "1", "1")
+ count_is_unique = Count(case).as_("unique_count")
- mariadb_query = """
- SELECT
- path,
- COUNT(*) as count,
- COUNT(CASE WHEN is_unique = 1 THEN 1 END) as unique_count
- FROM `tabWeb Page View`
- WHERE creation BETWEEN %s AND %s
- GROUP BY path
- ORDER BY count desc
- """
-
- data = frappe.db.multisql({
- "mariadb": mariadb_query,
- "postgres": pg_query
- }, (self.filters.from_date, self.filters.to_date))
- return data
+ query = (
+ frappe.qb.from_(WebPageView)
+ .select("path", count_all, count_is_unique)
+ .where(
+ Coalesce(WebPageView.creation, "0001-01-01")[self.filters.from_date:self.filters.to_date]
+ )
+ .groupby(WebPageView.path)
+ .orderby("count", Order=frappe.qb.desc)
+ )
+ return frappe.db.sql(query)
def _get_query_for_mariadb(self):
filters_range = self.filters.range
diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py
index b70ffb2406..5eedc27d9c 100644
--- a/frappe/workflow/doctype/workflow_action/workflow_action.py
+++ b/frappe/workflow/doctype/workflow_action/workflow_action.py
@@ -133,9 +133,12 @@ def return_link_expired_page(doc, doc_workflow_state):
def clear_old_workflow_actions(doc, user=None):
user = user if user else frappe.session.user
- frappe.db.sql("""DELETE FROM `tabWorkflow Action`
- WHERE `reference_doctype`=%s AND `reference_name`=%s AND `user`!=%s AND `status`='Open'""",
- (doc.get('doctype'), doc.get('name'), user))
+ frappe.db.delete("Workflow Action", {
+ "reference_doctype": doc.get("doctype"),
+ "reference_name": doc.get("name"),
+ "user": ("!=", user),
+ "status": "Open"
+ })
def update_completed_workflow_actions(doc, user=None):
user = user if user else frappe.session.user
@@ -253,11 +256,10 @@ def is_workflow_action_already_created(doc):
def clear_workflow_actions(doctype, name):
if not (doctype and name):
return
-
- frappe.db.sql('''delete from `tabWorkflow Action`
- where reference_doctype=%s and reference_name=%s''',
- (doctype, name))
-
+ frappe.db.delete("Workflow Action", {
+ "reference_doctype": doctype,
+ "reference_name": name
+ })
def get_doc_workflow_state(doc):
workflow_name = get_workflow_name(doc.get('doctype'))
workflow_state_field = get_workflow_state_field(workflow_name)
diff --git a/frappe/www/app.html b/frappe/www/app.html
index c8172693b9..68a6dc8e86 100644
--- a/frappe/www/app.html
+++ b/frappe/www/app.html
@@ -1,5 +1,5 @@
-
+
diff --git a/frappe/www/app.py b/frappe/www/app.py
index 27505c8131..acf6dde000 100644
--- a/frappe/www/app.py
+++ b/frappe/www/app.py
@@ -6,6 +6,7 @@ import os, re
import frappe
from frappe import _
import frappe.sessions
+from frappe.utils.jinja_globals import is_rtl
def get_context(context):
if frappe.session.user == "Guest":
@@ -40,6 +41,8 @@ def get_context(context):
"build_version": frappe.utils.get_build_version(),
"include_js": hooks["app_include_js"],
"include_css": hooks["app_include_css"],
+ "layout_direction": "rtl" if is_rtl() else "ltr",
+ "lang": frappe.local.lang,
"sounds": hooks["sounds"],
"boot": boot if context.get("for_mobile") else boot_json,
"desk_theme": desk_theme or "Light",
diff --git a/frappe/www/printview.html b/frappe/www/printview.html
index 8bc6e0cb80..3f8d4201fb 100644
--- a/frappe/www/printview.html
+++ b/frappe/www/printview.html
@@ -1,14 +1,11 @@
-
+
{{ title }}
{{ include_style('print.bundle.css') }}
- {%- if has_rtl -%}
- {{ include_style('frappe-rtl.bundle.css') }}
- {%- endif -%}
diff --git a/frappe/www/printview.py b/frappe/www/printview.py
index 226d5b048a..cdf47790eb 100644
--- a/frappe/www/printview.py
+++ b/frappe/www/printview.py
@@ -7,6 +7,7 @@ from frappe import _
from frappe.modules import get_doc_path
from frappe.core.doctype.access_log.access_log import make_access_log
from frappe.utils import cint, sanitize_html, strip_html
+from frappe.utils.jinja_globals import is_rtl
no_cache = 1
@@ -44,7 +45,8 @@ def get_context(context):
"css": get_print_style(frappe.form_dict.style, print_format),
"comment": frappe.session.user,
"title": doc.get(meta.title_field) if meta.title_field else doc.name,
- "has_rtl": True if frappe.local.lang in ["ar", "he", "fa", "ps"] else False
+ "lang": frappe.local.lang,
+ "layout_direction": "rtl" if is_rtl() else "ltr"
}
def get_print_format_doc(print_format_name, meta):
diff --git a/package.json b/package.json
index 8b22a6e92c..e84fa1b581 100644
--- a/package.json
+++ b/package.json
@@ -67,6 +67,7 @@
"fast-glob": "^3.2.5",
"launch-editor": "^2.2.1",
"md5": "^2.3.0",
+ "rtlcss": "^3.2.1",
"yargs": "^16.2.0"
},
"snyk": true
diff --git a/requirements.txt b/requirements.txt
index 0791f01b27..51327953d5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -49,6 +49,7 @@ pyngrok~=5.0.5
pyOpenSSL~=20.0.1
pyotp~=2.6.0
PyPDF2~=1.26.0
+PyPika~=0.48.6
pypng~=0.0.20
PyQRCode~=1.2.1
python-dateutil~=2.8.1
diff --git a/yarn.lock b/yarn.lock
index ddb5623e5e..f21f493aef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2413,6 +2413,14 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
follow-redirects@^1.10.0:
version "1.13.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.3.tgz#e5598ad50174c1bc4e872301e82ac2cd97f90267"
@@ -3690,6 +3698,13 @@ locate-path@^3.0.0:
p-locate "^3.0.0"
path-exists "^3.0.0"
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
lodash.assign@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
@@ -4148,16 +4163,11 @@ minimatch@^3.0.4, minimatch@~3.0.2:
dependencies:
brace-expansion "^1.1.7"
-minimist@^1.1.3, minimist@^1.2.5:
+minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
-minimist@^1.2.0:
- version "1.2.0"
- resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
- integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
-
minipass@^3.0.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd"
@@ -4227,6 +4237,11 @@ nanoid@^3.1.22:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844"
integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ==
+nanoid@^3.1.23:
+ version "3.1.23"
+ resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
+ integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
+
native-request@^1.0.5:
version "1.0.8"
resolved "https://registry.yarnpkg.com/native-request/-/native-request-1.0.8.tgz#8f66bf606e0f7ea27c0e5995eb2f5d03e33ae6fb"
@@ -4576,6 +4591,13 @@ p-limit@^2.0.0:
dependencies:
p-try "^2.0.0"
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
@@ -4583,6 +4605,13 @@ p-locate@^3.0.0:
dependencies:
p-limit "^2.0.0"
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
p-map@2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175"
@@ -4705,6 +4734,11 @@ path-exists@^3.0.0:
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@@ -5163,6 +5197,15 @@ postcss@^7.0.32:
source-map "^0.6.1"
supports-color "^6.1.0"
+postcss@^8.2.4:
+ version "8.3.5"
+ resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.3.5.tgz#982216b113412bc20a86289e91eb994952a5b709"
+ integrity sha512-NxTuJocUhYGsMiMFHDUkmjSKT3EdH4/WbGF6GCi1NDGk+vbcUTun4fpbOqaPtD8IIsztA2ilZm2DhYCuyN58gA==
+ dependencies:
+ colorette "^1.2.2"
+ nanoid "^3.1.23"
+ source-map-js "^0.6.2"
+
prepend-http@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
@@ -5845,6 +5888,17 @@ roarr@^2.15.3:
semver-compare "^1.0.0"
sprintf-js "^1.1.2"
+rtlcss@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/rtlcss/-/rtlcss-3.2.1.tgz#654e55ea2f46991f9738d952ba77ba0aa94a670d"
+ integrity sha512-S9bh35JXwPIhfun7nFu/HjlNrwELL5nvTJqA1suLfbnqY/mauIL5sBkrJNHziVppX9PA2rJ7NV82+RtzB71mJA==
+ dependencies:
+ chalk "^4.1.0"
+ find-up "^5.0.0"
+ mkdirp "^1.0.4"
+ postcss "^8.2.4"
+ strip-json-comments "^3.1.1"
+
run-async@^2.4.0:
version "2.4.1"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
@@ -6452,6 +6506,11 @@ sortablejs@^1.7.0:
resolved "https://registry.yarnpkg.com/sortablejs/-/sortablejs-1.8.3.tgz#5ae908ef96300966e95440a143340f5dd565a0df"
integrity sha512-AftvD4hdKcR5QlGi7L/JST506zGNGrysE8/QohDpwKXJarHWqCt+TUlrtoMk/wkECB607Q019/OZlJViyWiD6A==
+source-map-js@^0.6.2:
+ version "0.6.2"
+ resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-0.6.2.tgz#0bb5de631b41cfbda6cfba8bd05a80efdfd2385e"
+ integrity sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==
+
source-map-resolve@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a"
@@ -6749,6 +6808,11 @@ strip-indent@^1.0.1:
dependencies:
get-stdin "^4.0.1"
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@@ -7561,3 +7625,8 @@ yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk=
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==