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/api.py b/frappe/api.py index 36d51e894c..636c6b2888 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -82,7 +82,7 @@ def handle(): if frappe.local.request.method=="PUT": data = get_request_form_data() - doc = frappe.get_doc(doctype, name) + doc = frappe.get_doc(doctype, name, for_update=True) if "flags" in data: del data["flags"] 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 212642e01b..8fc6877d4f 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -551,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, @@ -786,7 +770,7 @@ def get_version(output): "table": lambda: render_table( [["App", "Version", "Branch", "Commit"]] + [ - [app_info.app, app_info.version, app_info.branch, app_info.commit] + [app_info.app, app_info.version, app_info.branch, app_info.commit] for app_info in data ] ), 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/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 09a8a0ac22..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)) @@ -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/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.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 e86edaa96f..d4a119804b 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): @@ -124,6 +126,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 9265d701bf..00e60fb8d2 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( @@ -171,6 +173,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, @@ -298,6 +313,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/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 f63c76d2da..362f4c79b3 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -153,32 +153,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/patches/v14_0/__init__.py b/frappe/patches/v14_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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( `` ); 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/footer/version_timeline_content_builder.js b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js index cbfd620e4c..751d48031c 100644 --- a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js +++ b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js @@ -152,6 +152,7 @@ function get_version_comment(version_doc, text) { let unlinked_content = ""; try { + text += ''; Array.from($(text)).forEach(element => { if ($(element).is('a')) { version_comment += unlinked_content ? frappe.utils.get_form_link('Version', version_doc.name, true, unlinked_content) : ""; 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/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/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue index 2e995bca39..471306cba5 100644 --- a/frappe/public/js/frappe/recorder/RequestDetail.vue +++ b/frappe/public/js/frappe/recorder/RequestDetail.vue @@ -64,7 +64,7 @@
-
+
{{ call.index }}
{{ call.query }}
@@ -76,16 +76,13 @@
{{ call.exact_copies }}
- +
{{ __("SQL Query") }} #{{ call.index }} -
- -
@@ -116,7 +113,7 @@
-
+