Merge branch 'frappe:develop' into feat-bulk-rename-merge
This commit is contained in:
commit
2c9f08997a
55 changed files with 609 additions and 255 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -59,4 +59,4 @@ cd ../..
|
|||
bench start &
|
||||
bench --site test_site reinstall --yes
|
||||
if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi
|
||||
bench build --app frappe
|
||||
CI=Yes bench build --app frappe
|
||||
|
|
|
|||
2
.github/workflows/patch-mariadb-tests.yml
vendored
2
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
node-version: 14
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ jobs:
|
|||
python-version: '12.x'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.7'
|
||||
python-version: '3.9'
|
||||
- name: Set up bench and build assets
|
||||
run: |
|
||||
npm install -g yarn
|
||||
|
|
|
|||
5
.github/workflows/server-mariadb-tests.yml
vendored
5
.github/workflows/server-mariadb-tests.yml
vendored
|
|
@ -38,7 +38,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
|
|
@ -127,4 +127,5 @@ jobs:
|
|||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
verbose: true
|
||||
flags: server
|
||||
3
.github/workflows/server-postgres-tests.yml
vendored
3
.github/workflows/server-postgres-tests.yml
vendored
|
|
@ -41,7 +41,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
|
|
@ -131,3 +131,4 @@ jobs:
|
|||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
flags: server
|
||||
|
|
|
|||
28
.github/workflows/ui-tests.yml
vendored
28
.github/workflows/ui-tests.yml
vendored
|
|
@ -37,7 +37,7 @@ jobs:
|
|||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: '3.9'
|
||||
|
||||
- name: Check if build should be run
|
||||
id: check-build
|
||||
|
|
@ -122,12 +122,36 @@ jobs:
|
|||
DB: mariadb
|
||||
TYPE: ui
|
||||
|
||||
- name: Instrument Source Code
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/apps/frappe/ && npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe
|
||||
|
||||
- name: Build
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench build --apps frappe
|
||||
|
||||
- name: Site Setup
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard
|
||||
|
||||
- name: UI Tests
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' }}
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb
|
||||
|
||||
- name: Check If Coverage Report Exists
|
||||
id: check_coverage
|
||||
uses: andstor/file-existence-action@v1
|
||||
with:
|
||||
files: "/home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml"
|
||||
|
||||
- name: Upload Coverage Data
|
||||
if: ${{ steps.check-build.outputs.build == 'strawberry' && steps.check_coverage.outputs.files_exists == 'true' }}
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: Cypress
|
||||
fail_ci_if_error: true
|
||||
directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/
|
||||
verbose: true
|
||||
flags: ui-tests
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -67,6 +67,7 @@ coverage.xml
|
|||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
.cypress-coverage
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -27,7 +27,7 @@
|
|||
<img src='https://www.codetriage.com/frappe/frappe/badges/users.svg'>
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/frappe/frappe">
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj"/>
|
||||
<img src="https://codecov.io/gh/frappe/frappe/branch/develop/graph/badge.svg?token=XoTa679hIj&flag=server"/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
|
@ -35,25 +35,29 @@
|
|||
|
||||
Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com)
|
||||
|
||||
### Table of Contents
|
||||
* [Installation](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Documentation](https://frappeframework.com/docs)
|
||||
## Table of Contents
|
||||
* [Installation](#installation)
|
||||
* [Contributing](#contributing)
|
||||
* [Resources](#resources)
|
||||
* [License](#license)
|
||||
|
||||
### Installation
|
||||
## Installation
|
||||
|
||||
* [Install via Docker](https://github.com/frappe/frappe_docker)
|
||||
* [Install via Frappe Bench](https://github.com/frappe/bench)
|
||||
* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
|
||||
|
||||
## Contributing
|
||||
|
||||
1. [Code of Conduct](CODE_OF_CONDUCT.md)
|
||||
1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
|
||||
1. [Security Policy](SECURITY.md)
|
||||
1. [Translations](https://translate.erpnext.com)
|
||||
|
||||
### Website
|
||||
## Resources
|
||||
|
||||
For details and documentation, see the website
|
||||
[https://frappeframework.com](https://frappeframework.com)
|
||||
1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework.
|
||||
1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community.
|
||||
|
||||
### License
|
||||
## License
|
||||
This repository has been released under the [MIT License](LICENSE).
|
||||
|
|
|
|||
22
codecov.yml
22
codecov.yml
|
|
@ -4,10 +4,28 @@ codecov:
|
|||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
default: false
|
||||
server:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
flags:
|
||||
- server
|
||||
ui-tests:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
flags:
|
||||
- ui-tests
|
||||
|
||||
comment:
|
||||
layout: "diff"
|
||||
layout: "diff, flags"
|
||||
require_changes: true
|
||||
|
||||
flags:
|
||||
server:
|
||||
paths:
|
||||
- ".*\\.py"
|
||||
carryforward: true
|
||||
ui-tests:
|
||||
paths:
|
||||
- ".*\\.js"
|
||||
carryforward: true
|
||||
|
|
@ -54,13 +54,12 @@ context('Dashboard links', () => {
|
|||
cur_frm.dashboard.data.reports = [
|
||||
{
|
||||
'label': 'Reports',
|
||||
'items': ['Permitted Documents For User']
|
||||
'items': ['Website Analytics']
|
||||
}
|
||||
];
|
||||
cur_frm.dashboard.render_report_links();
|
||||
cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click();
|
||||
cy.findByText('Permitted Documents For User');
|
||||
cy.findByPlaceholderText('User').should("have.value", "Administrator");
|
||||
cy.get('[data-report="Website Analytics"]').contains('Website Analytics').click();
|
||||
cy.findByText('Website Analytics');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -44,13 +44,14 @@ context('Timeline', () => {
|
|||
cy.get('.timeline-content').should('contain', 'Testing Timeline 123');
|
||||
|
||||
//Deleting the added comment
|
||||
cy.get('.actions > .btn > .icon').first().click();
|
||||
cy.get('.more-actions > .action-btn').click();
|
||||
cy.get('.more-actions .dropdown-item').contains('Delete').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
cy.click_modal_primary_button('Yes');
|
||||
|
||||
//Deleting the added ToDo
|
||||
cy.get('.menu-btn-group button').eq(1).click();
|
||||
cy.get('.menu-btn-group [data-label="Delete"]').click();
|
||||
cy.get('.menu-btn-group [data-original-title="Menu"]').click();
|
||||
cy.get('.menu-btn-group .dropdown-item').contains('Delete').click();
|
||||
cy.findByRole('button', {name: 'Yes'}).click();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
module.exports = () => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
module.exports = (on, config) => {
|
||||
require('@cypress/code-coverage/task')(on, config);
|
||||
return config;
|
||||
};
|
||||
|
|
@ -353,5 +353,5 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => {
|
|||
});
|
||||
|
||||
Cypress.Commands.add('click_timeline_action_btn', (btn_name) => {
|
||||
cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click();
|
||||
cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click();
|
||||
});
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import '@cypress/code-coverage/support';
|
||||
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
|
|
|
|||
|
|
@ -104,6 +104,9 @@ async function execute() {
|
|||
log_error("There were some problems during build");
|
||||
log();
|
||||
log(chalk.dim(e.stack));
|
||||
if (process.env.CI) {
|
||||
process.kill(process.pid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -528,4 +531,4 @@ function log_rebuilt_assets(prev_assets, new_assets) {
|
|||
log(" " + filename);
|
||||
}
|
||||
log();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver
|
|||
|
||||
check_node_executable()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
|
||||
|
||||
|
||||
def watch(apps=None):
|
||||
|
|
|
|||
|
|
@ -679,9 +679,10 @@ def run_parallel_tests(context, app, build_number, total_builds, with_coverage=F
|
|||
@click.argument('app')
|
||||
@click.option('--headless', is_flag=True, help="Run UI Test in headless mode")
|
||||
@click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode")
|
||||
@click.option('--with-coverage', is_flag=True, help="Generate coverage report")
|
||||
@click.option('--ci-build-id')
|
||||
@pass_context
|
||||
def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
||||
def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=False, ci_build_id=None):
|
||||
"Run UI tests"
|
||||
site = get_site(context)
|
||||
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..'))
|
||||
|
|
@ -691,6 +692,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
|||
# override baseUrl using env variable
|
||||
site_env = f'CYPRESS_baseUrl={site_url}'
|
||||
password_env = f'CYPRESS_adminPassword={admin_password}' if admin_password else ''
|
||||
coverage_env = f'CYPRESS_coverage={str(with_coverage).lower()}'
|
||||
|
||||
os.chdir(app_base_path)
|
||||
|
||||
|
|
@ -698,22 +700,23 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None):
|
|||
cypress_path = f"{node_bin}/cypress"
|
||||
plugin_path = f"{node_bin}/../cypress-file-upload"
|
||||
testing_library_path = f"{node_bin}/../@testing-library"
|
||||
coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage"
|
||||
|
||||
# check if cypress in path...if not, install it.
|
||||
if not (
|
||||
os.path.exists(cypress_path)
|
||||
and os.path.exists(plugin_path)
|
||||
and os.path.exists(testing_library_path)
|
||||
and os.path.exists(coverage_plugin_path)
|
||||
and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6
|
||||
):
|
||||
# install cypress
|
||||
click.secho("Installing Cypress...", fg="yellow")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile")
|
||||
frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile")
|
||||
|
||||
# run for headless mode
|
||||
run_or_open = 'run --browser firefox --record' if headless else 'open'
|
||||
command = '{site_env} {password_env} {cypress} {run_or_open}'
|
||||
formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open)
|
||||
formatted_command = f'{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}'
|
||||
|
||||
if parallel:
|
||||
formatted_command += ' --parallel'
|
||||
|
|
|
|||
|
|
@ -813,7 +813,7 @@ def extract_images_from_doc(doc, fieldname):
|
|||
doc.set(fieldname, content)
|
||||
|
||||
|
||||
def extract_images_from_html(doc, content):
|
||||
def extract_images_from_html(doc, content, is_private=False):
|
||||
frappe.flags.has_dataurl = False
|
||||
|
||||
def _save_file(match):
|
||||
|
|
@ -846,7 +846,8 @@ def extract_images_from_html(doc, content):
|
|||
"attached_to_doctype": doctype,
|
||||
"attached_to_name": name,
|
||||
"content": content,
|
||||
"decode": False
|
||||
"decode": False,
|
||||
"is_private": is_private
|
||||
})
|
||||
_file.save(ignore_permissions=True)
|
||||
file_url = _file.file_url
|
||||
|
|
|
|||
|
|
@ -6,16 +6,27 @@ from frappe.model.document import Document
|
|||
from frappe.modules.export_file import export_doc
|
||||
import os
|
||||
import subprocess
|
||||
from frappe.query_builder.functions import Max
|
||||
|
||||
|
||||
class PackageRelease(Document):
|
||||
def set_version(self):
|
||||
# set the next patch release by default
|
||||
doctype = frappe.qb.DocType("Package Release")
|
||||
if not self.major:
|
||||
self.major = frappe.db.max('Package Release', 'major', dict(package=self.package))
|
||||
self.major = frappe.qb.from_(doctype) \
|
||||
.where(doctype.package == self.package) \
|
||||
.select(Max(doctype.minor)).run()[0][0] or 0
|
||||
|
||||
if not self.minor:
|
||||
self.minor = frappe.db.max('Package Release', 'minor', dict(package=self.package))
|
||||
self.minor = frappe.qb.from_(doctype) \
|
||||
.where(doctype.package == self.package) \
|
||||
.select(Max("minor")).run()[0][0] or 0
|
||||
if not self.patch:
|
||||
self.patch = frappe.db.max('Package Release', 'patch', dict(package=self.package)) + 1
|
||||
value = frappe.qb.from_(doctype) \
|
||||
.where(doctype.package == self.package) \
|
||||
.select(Max("patch")).run()[0][0] or 0
|
||||
self.patch = value + 1
|
||||
|
||||
def autoname(self):
|
||||
self.set_version()
|
||||
|
|
|
|||
|
|
@ -325,15 +325,15 @@ frappe.PermissionEngine = class PermissionEngine {
|
|||
.attr("data-doctype", d.parent)
|
||||
.attr("data-role", d.role)
|
||||
.attr("data-permlevel", d.permlevel)
|
||||
.click(function () {
|
||||
.on("click", () => {
|
||||
return frappe.call({
|
||||
module: "frappe.core",
|
||||
page: "permission_manager",
|
||||
method: "remove",
|
||||
args: {
|
||||
doctype: $(this).attr("data-doctype"),
|
||||
role: $(this).attr("data-role"),
|
||||
permlevel: $(this).attr("data-permlevel")
|
||||
doctype: d.parent,
|
||||
role: d.role,
|
||||
permlevel: d.permlevel
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.exc) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,13 @@ import frappe.model.meta
|
|||
|
||||
from frappe import _
|
||||
from time import time
|
||||
from frappe.utils import now, getdate, cast, get_datetime, get_table_name
|
||||
from frappe.utils import now, getdate, cast, get_datetime
|
||||
from frappe.model.utils.link_count import flush_local_link_count
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.query_builder.functions import Min, Max, Avg, Sum
|
||||
from frappe.query_builder.utils import Column
|
||||
from .query import Query
|
||||
from pypika.terms import PseudoColumn
|
||||
|
||||
|
||||
class Database(object):
|
||||
|
|
@ -55,6 +60,7 @@ class Database(object):
|
|||
|
||||
self.password = password or frappe.conf.db_password
|
||||
self.value_cache = {}
|
||||
self.query = Query()
|
||||
|
||||
def setup_type_map(self):
|
||||
pass
|
||||
|
|
@ -77,7 +83,7 @@ class Database(object):
|
|||
pass
|
||||
|
||||
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
|
||||
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False):
|
||||
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False, run=True):
|
||||
"""Execute a SQL query and fetch all rows.
|
||||
|
||||
:param query: SQL query.
|
||||
|
|
@ -90,7 +96,7 @@ class Database(object):
|
|||
:param as_utf8: Encode values as UTF 8.
|
||||
:param auto_commit: Commit after executing the query.
|
||||
:param update: Update this dict to all rows (if returned `as_dict`).
|
||||
|
||||
:param run: Returns query without executing it if False.
|
||||
Examples:
|
||||
|
||||
# return customer names as dicts
|
||||
|
|
@ -105,6 +111,8 @@ class Database(object):
|
|||
|
||||
"""
|
||||
query = str(query)
|
||||
if not run:
|
||||
return 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)
|
||||
|
|
@ -310,59 +318,6 @@ class Database(object):
|
|||
nres.append(nr)
|
||||
return nres
|
||||
|
||||
def build_conditions(self, filters):
|
||||
"""Convert filters sent as dict, lists to SQL conditions. filter's key
|
||||
is passed by map function, build conditions like:
|
||||
|
||||
* ifnull(`fieldname`, default_value) = %(fieldname)s
|
||||
* `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
|
||||
"""
|
||||
conditions = []
|
||||
values = {}
|
||||
def _build_condition(key):
|
||||
"""
|
||||
filter's key is passed by map function
|
||||
build conditions like:
|
||||
* ifnull(`fieldname`, default_value) = %(fieldname)s
|
||||
* `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s
|
||||
"""
|
||||
_operator = "="
|
||||
_rhs = " %(" + key + ")s"
|
||||
value = filters.get(key)
|
||||
values[key] = value
|
||||
if isinstance(value, (list, tuple)):
|
||||
# value is a tuple like ("!=", 0)
|
||||
_operator = value[0].lower()
|
||||
values[key] = value[1]
|
||||
if isinstance(value[1], (tuple, list)):
|
||||
# value is a list in tuple ("in", ("A", "B"))
|
||||
_rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
|
||||
del values[key]
|
||||
|
||||
if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
|
||||
_operator = "="
|
||||
|
||||
if "[" in key:
|
||||
split_key = key.split("[")
|
||||
condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \
|
||||
+ _operator + _rhs
|
||||
else:
|
||||
condition = "`" + key + "` " + _operator + _rhs
|
||||
|
||||
conditions.append(condition)
|
||||
|
||||
if isinstance(filters, int):
|
||||
# docname is a number, convert to string
|
||||
filters = str(filters)
|
||||
|
||||
if isinstance(filters, str):
|
||||
filters = { "name": filters }
|
||||
|
||||
for f in filters:
|
||||
_build_condition(f)
|
||||
|
||||
return " and ".join(conditions), values
|
||||
|
||||
def get(self, doctype, filters=None, as_dict=True, cache=False):
|
||||
"""Returns `get_value` with fieldname='*'"""
|
||||
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
|
||||
|
|
@ -424,9 +379,8 @@ class Database(object):
|
|||
(doctype, filters, fieldname) in self.value_cache:
|
||||
return self.value_cache[(doctype, filters, fieldname)]
|
||||
|
||||
if not order_by: order_by = 'modified desc'
|
||||
|
||||
if isinstance(filters, list):
|
||||
order_by = order_by or "modified_desc"
|
||||
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug)
|
||||
|
||||
else:
|
||||
|
|
@ -439,6 +393,7 @@ class Database(object):
|
|||
|
||||
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
|
||||
try:
|
||||
order_by = order_by or "modified"
|
||||
out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update)
|
||||
except Exception as e:
|
||||
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
|
||||
|
|
@ -567,32 +522,23 @@ class Database(object):
|
|||
return self.get_single_value(*args, **kwargs)
|
||||
|
||||
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False):
|
||||
fl = []
|
||||
field_objects = []
|
||||
|
||||
for field in fields:
|
||||
if "(" in field or " as " in field:
|
||||
field_objects.append(PseudoColumn(field))
|
||||
else:
|
||||
field_objects.append(field)
|
||||
|
||||
criterion = self.query.build_conditions(table=doctype, filters=filters, orderby=order_by, for_update=for_update)
|
||||
|
||||
if isinstance(fields, (list, tuple)):
|
||||
for f in fields:
|
||||
if "(" in f or " as " in f: # function
|
||||
fl.append(f)
|
||||
else:
|
||||
fl.append("`" + f + "`")
|
||||
fl = ", ".join(fl)
|
||||
query = criterion.select(*field_objects)
|
||||
else:
|
||||
fl = fields
|
||||
if fields=="*":
|
||||
query = criterion.select(fields)
|
||||
as_dict = True
|
||||
|
||||
conditions, values = self.build_conditions(filters)
|
||||
|
||||
order_by = ("order by " + order_by) if order_by else ""
|
||||
|
||||
r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}"
|
||||
.format(
|
||||
for_update = 'for update' if for_update else '',
|
||||
fields = fl,
|
||||
doctype = doctype,
|
||||
where = "where" if conditions else "",
|
||||
conditions = conditions,
|
||||
order_by = order_by),
|
||||
values, as_dict=as_dict, debug=debug, update=update)
|
||||
r = self.sql(query, as_dict=as_dict, debug=debug, update=update)
|
||||
|
||||
return r
|
||||
|
||||
|
|
@ -819,50 +765,34 @@ class Database(object):
|
|||
except Exception:
|
||||
return None
|
||||
|
||||
def min(self, dt, fieldname, filters=None, **kwargs):
|
||||
return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0
|
||||
|
||||
def max(self, dt, fieldname, filters=None, **kwargs):
|
||||
return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0
|
||||
|
||||
def avg(self, dt, fieldname, filters=None, **kwargs):
|
||||
return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0
|
||||
|
||||
def sum(self, dt, fieldname, filters=None, **kwargs):
|
||||
return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0
|
||||
|
||||
def count(self, dt, filters=None, debug=False, cache=False):
|
||||
"""Returns `COUNT(*)` for given DocType and filters."""
|
||||
if cache and not filters:
|
||||
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
|
||||
if cache_count is not None:
|
||||
return cache_count
|
||||
query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
|
||||
if filters:
|
||||
conditions, filters = self.build_conditions(filters)
|
||||
count = self.sql("""select count(*)
|
||||
from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0]
|
||||
count = self.sql(query, debug=debug)[0][0]
|
||||
return count
|
||||
else:
|
||||
count = self.sql("""select count(*)
|
||||
from `tab%s`""" % (dt,))[0][0]
|
||||
|
||||
count = self.sql(query, debug=debug)[0][0]
|
||||
if cache:
|
||||
frappe.cache().set_value('doctype:count:{}'.format(dt), count, expires_in_sec = 86400)
|
||||
|
||||
return count
|
||||
|
||||
def sum(self, dt, fieldname, filters=None):
|
||||
return self._get_aggregation('SUM', dt, fieldname, filters)
|
||||
|
||||
def avg(self, dt, fieldname, filters=None):
|
||||
return self._get_aggregation('AVG', dt, fieldname, filters)
|
||||
|
||||
def min(self, dt, fieldname, filters=None):
|
||||
return self._get_aggregation('MIN', dt, fieldname, filters)
|
||||
|
||||
def max(self, dt, fieldname, filters=None):
|
||||
return self._get_aggregation('MAX', dt, fieldname, filters)
|
||||
|
||||
def _get_aggregation(self, function, dt, fieldname, filters=None):
|
||||
if not self.has_column(dt, fieldname):
|
||||
frappe.throw(frappe._('Invalid column'), self.InvalidColumnName)
|
||||
|
||||
query = f'SELECT {function}({fieldname}) AS value FROM `tab{dt}`'
|
||||
values = ()
|
||||
if filters:
|
||||
conditions, values = self.build_conditions(filters)
|
||||
query = f"{query} WHERE {conditions}"
|
||||
|
||||
return self.sql(query, values)[0][0] or 0
|
||||
|
||||
@staticmethod
|
||||
def format_date(date):
|
||||
return getdate(date).strftime("%Y-%m-%d")
|
||||
|
|
@ -984,16 +914,9 @@ class Database(object):
|
|||
"""
|
||||
values = ()
|
||||
filters = filters or kwargs.get("conditions")
|
||||
table = get_table_name(doctype)
|
||||
query = f"DELETE FROM `{table}`"
|
||||
|
||||
query = self.query.build_conditions(table=doctype, filters=filters).delete()
|
||||
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):
|
||||
|
|
|
|||
267
frappe/database/query.py
Normal file
267
frappe/database/query.py
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
import operator
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
import frappe
|
||||
from frappe.query_builder import Criterion, Order, Field
|
||||
|
||||
|
||||
def like(key: str, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `LIKE`
|
||||
"""
|
||||
return Field(key).like(value)
|
||||
|
||||
|
||||
def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
|
||||
"""Wrapper method for `IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `IN`
|
||||
"""
|
||||
return Field(key).isin(value)
|
||||
|
||||
|
||||
def not_like(key: str, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `NOT LIKE`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT LIKE`
|
||||
"""
|
||||
return Field(key).not_like(value)
|
||||
|
||||
|
||||
def func_not_in(key: str, value: Union[List, Tuple]):
|
||||
"""Wrapper method for `NOT IN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `NOT IN`
|
||||
"""
|
||||
return Field(key).notin(value)
|
||||
|
||||
|
||||
def func_regex(key: str, value: str) -> frappe.qb:
|
||||
"""Wrapper method for `REGEX`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (str): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `REGEX`
|
||||
"""
|
||||
return Field(key).regex(value)
|
||||
|
||||
|
||||
def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
|
||||
"""Wrapper method for `BETWEEN`
|
||||
|
||||
Args:
|
||||
key (str): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: `frappe.qb object with `BETWEEN`
|
||||
"""
|
||||
return Field(key)[slice(*value)]
|
||||
|
||||
def make_function(key: Any, value: Union[int, str]):
|
||||
"""returns fucntion query
|
||||
|
||||
Args:
|
||||
key (Any): field
|
||||
value (Union[int, str]): criterion
|
||||
|
||||
Returns:
|
||||
frappe.qb: frappe.qb object
|
||||
"""
|
||||
return OPERATOR_MAP[value[0]](key, value[1])
|
||||
|
||||
|
||||
def change_orderby(order: str):
|
||||
"""Convert orderby to standart Order object
|
||||
|
||||
Args:
|
||||
order (str): Field, order
|
||||
|
||||
Returns:
|
||||
tuple: field, order
|
||||
"""
|
||||
order = order.split()
|
||||
if order[1].lower() == "asc":
|
||||
orderby, order = order[0], Order.asc
|
||||
return orderby, order
|
||||
orderby, order = order[0], Order.desc
|
||||
return orderby, order
|
||||
|
||||
|
||||
OPERATOR_MAP = {
|
||||
"+": operator.add,
|
||||
"=": operator.eq,
|
||||
"-": operator.sub,
|
||||
"!=": operator.ne,
|
||||
"<": operator.lt,
|
||||
">": operator.gt,
|
||||
"<=": operator.le,
|
||||
">=": operator.ge,
|
||||
"in": func_in,
|
||||
"not in": func_not_in,
|
||||
"like": like,
|
||||
"not like": not_like,
|
||||
"regex": func_regex,
|
||||
"between": func_between
|
||||
}
|
||||
|
||||
|
||||
class Query:
|
||||
def get_condition(self, table: str, **kwargs) -> frappe.qb:
|
||||
"""Get initial table object
|
||||
|
||||
Args:
|
||||
table (str): DocType
|
||||
|
||||
Returns:
|
||||
frappe.qb: DocType with initial condition
|
||||
"""
|
||||
if kwargs.get("update"):
|
||||
return frappe.qb.update(table)
|
||||
if kwargs.get("into"):
|
||||
return frappe.qb.into(table)
|
||||
return frappe.qb.from_(table)
|
||||
|
||||
def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
|
||||
"""Generate filters from Criterion objects
|
||||
|
||||
Args:
|
||||
table (str): DocType
|
||||
criterion (Criterion): Filters
|
||||
|
||||
Returns:
|
||||
frappe.qb: condition object
|
||||
"""
|
||||
condition = self.get_condition(table, **kwargs)
|
||||
return condition.where(criterion)
|
||||
|
||||
def add_conditions(self, conditions: frappe.qb, **kwargs):
|
||||
"""Adding additional conditions
|
||||
|
||||
Args:
|
||||
conditions (frappe.qb): built conditions
|
||||
|
||||
Returns:
|
||||
conditions (frappe.qb): frappe.qb object
|
||||
"""
|
||||
if kwargs.get("orderby"):
|
||||
orderby = kwargs.get("orderby")
|
||||
order = kwargs.get("order") if kwargs.get("order") else Order.desc
|
||||
if isinstance(orderby, str) and len(orderby.split()) > 1:
|
||||
orderby, order = change_orderby(orderby)
|
||||
conditions = conditions.orderby(orderby, order=order)
|
||||
|
||||
if kwargs.get("limit"):
|
||||
conditions = conditions.limit(kwargs.get("limit"))
|
||||
|
||||
if kwargs.get("distinct"):
|
||||
conditions = conditions.distinct()
|
||||
|
||||
if kwargs.get("for_update"):
|
||||
conditions = conditions.for_update()
|
||||
|
||||
return conditions
|
||||
|
||||
def misc_query(self, table: str, filters: Union[List, Tuple] = None, **kwargs):
|
||||
"""Build conditions using the given Lists or Tuple filters
|
||||
|
||||
Args:
|
||||
table (str): DocType
|
||||
filters (Union[List, Tuple], optional): Filters. Defaults to None.
|
||||
"""
|
||||
conditions = self.get_condition(table, **kwargs)
|
||||
if not filters:
|
||||
return conditions
|
||||
if isinstance(filters, list):
|
||||
for f in filters:
|
||||
if not isinstance(f, (list, tuple)):
|
||||
_operator = OPERATOR_MAP[filters[1]]
|
||||
if not isinstance(filters[0], str):
|
||||
conditions = make_function(filters[0], filters[2])
|
||||
break
|
||||
conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
|
||||
break
|
||||
else:
|
||||
_operator = OPERATOR_MAP[f[1]]
|
||||
conditions = conditions.where(_operator(Field(f[0]), f[2]))
|
||||
|
||||
conditions = self.add_conditions(conditions, **kwargs)
|
||||
return conditions
|
||||
|
||||
def dict_query(self, table: str, filters: Dict[str, Union[str, int]] = None, **kwargs) -> frappe.qb:
|
||||
"""Build conditions using the given dictionary filters
|
||||
|
||||
Args:
|
||||
table (str): DocType
|
||||
filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None.
|
||||
|
||||
Returns:
|
||||
frappe.qb: conditions object
|
||||
"""
|
||||
conditions = self.get_condition(table, **kwargs)
|
||||
if not filters:
|
||||
return conditions
|
||||
|
||||
for key in filters:
|
||||
value = filters.get(key)
|
||||
_operator = OPERATOR_MAP["="]
|
||||
|
||||
if not isinstance(key, str):
|
||||
conditions = conditions.where(make_function(key, value))
|
||||
continue
|
||||
if isinstance(value, (list, tuple)):
|
||||
if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
|
||||
_operator = OPERATOR_MAP[value[0]]
|
||||
conditions = conditions.where(_operator(key, value[1]))
|
||||
else:
|
||||
_operator = OPERATOR_MAP[value[0]]
|
||||
conditions = conditions.where(_operator(Field(key), value[1]))
|
||||
else:
|
||||
conditions = conditions.where(_operator(Field(key), value))
|
||||
conditions = self.add_conditions(conditions, **kwargs)
|
||||
return conditions
|
||||
|
||||
def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb:
|
||||
"""Build conditions for sql query
|
||||
|
||||
Args:
|
||||
filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict
|
||||
table (str): DocType
|
||||
|
||||
Returns:
|
||||
frappe.qb: frappe.qb conditions object
|
||||
"""
|
||||
if isinstance(filters, Criterion):
|
||||
return self.criterion_query(table, filters, **kwargs)
|
||||
|
||||
if isinstance(filters, int) or isinstance(filters, str):
|
||||
filters = {"name": str(filters)}
|
||||
|
||||
if isinstance(filters, (list, tuple)):
|
||||
return self.misc_query(table, filters, **kwargs)
|
||||
|
||||
return self.dict_query(filters=filters, table=table, **kwargs)
|
||||
|
|
@ -66,7 +66,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme
|
|||
comment_type='Comment',
|
||||
comment_by=comment_by
|
||||
))
|
||||
doc.content = extract_images_from_html(doc, content)
|
||||
doc.content = extract_images_from_html(doc, content, is_private=True)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user)
|
||||
|
|
|
|||
|
|
@ -274,4 +274,7 @@ class TestNotification(unittest.TestCase):
|
|||
self.assertTrue('test2@example.com' in recipients)
|
||||
self.assertTrue('test1@example.com' in recipients)
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
frappe.delete_doc_if_exists("Notification", "ToDo Status Update")
|
||||
frappe.delete_doc_if_exists("Notification", "Contact Status Update")
|
||||
|
|
@ -29,6 +29,10 @@ def _new_site(
|
|||
):
|
||||
"""Install a new Frappe site"""
|
||||
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.utils import get_site_path, scheduler, touch_file
|
||||
|
||||
|
||||
if not force and os.path.exists(site):
|
||||
print("Site {0} already exists".format(site))
|
||||
sys.exit(1)
|
||||
|
|
@ -37,14 +41,11 @@ def _new_site(
|
|||
print("--no-mariadb-socket requires db_type to be set to mariadb.")
|
||||
sys.exit(1)
|
||||
|
||||
if not db_name:
|
||||
import hashlib
|
||||
db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16]
|
||||
|
||||
frappe.init(site=site)
|
||||
|
||||
from frappe.commands.scheduler import _is_scheduler_enabled
|
||||
from frappe.utils import get_site_path, scheduler, touch_file
|
||||
if not db_name:
|
||||
import hashlib
|
||||
db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16]
|
||||
|
||||
try:
|
||||
# enable scheduler post install?
|
||||
|
|
|
|||
|
|
@ -267,7 +267,12 @@ class BaseDocument(object):
|
|||
if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
|
||||
frappe.throw(_('Value for {0} cannot be a list').format(_(df.label)))
|
||||
|
||||
if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)):
|
||||
if convert_dates_to_str and isinstance(d[fieldname], (
|
||||
datetime.datetime,
|
||||
datetime.date,
|
||||
datetime.time,
|
||||
datetime.timedelta
|
||||
)):
|
||||
d[fieldname] = str(d[fieldname])
|
||||
|
||||
if d[fieldname] == None and ignore_nulls:
|
||||
|
|
|
|||
|
|
@ -41,10 +41,11 @@
|
|||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-17 11:30:16.781655",
|
||||
"modified": "2021-10-07 11:23:13.799402",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Printing",
|
||||
"name": "Network Printer Settings",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -58,6 +59,15 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
|
|
|||
|
|
@ -177,7 +177,7 @@ frappe.ui.form.PrintView = class {
|
|||
);
|
||||
}
|
||||
|
||||
if (this.print_settings.enable_print_server) {
|
||||
if (cint(this.print_settings.enable_print_server)) {
|
||||
this.page.add_menu_item(__('Select Network Printer'), () =>
|
||||
this.network_printer_setting_dialog()
|
||||
);
|
||||
|
|
@ -464,7 +464,7 @@ frappe.ui.form.PrintView = class {
|
|||
printit() {
|
||||
let me = this;
|
||||
|
||||
if (me.print_settings.enable_print_server) {
|
||||
if (cint(me.print_settings.enable_print_server)) {
|
||||
if (localStorage.getItem('network_printer')) {
|
||||
me.print_by_server();
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -97,9 +97,13 @@ class BaseTimeline {
|
|||
}
|
||||
|
||||
timeline_item.append(`<div class="timeline-content ${item.is_card ? 'frappe-card' : ''}">`);
|
||||
timeline_item.find('.timeline-content').append(item.content);
|
||||
let timeline_content = timeline_item.find('.timeline-content');
|
||||
timeline_content.append(item.content);
|
||||
if (!item.hide_timestamp && !item.is_card) {
|
||||
timeline_item.find('.timeline-content').append(`<span> - ${comment_when(item.creation)}</span>`);
|
||||
timeline_content.append(`<span> - ${comment_when(item.creation)}</span>`);
|
||||
}
|
||||
if (item.id) {
|
||||
timeline_content.attr("id", item.id);
|
||||
}
|
||||
return timeline_item;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ class FormTimeline extends BaseTimeline {
|
|||
render_timeline_items() {
|
||||
super.render_timeline_items();
|
||||
this.set_document_info();
|
||||
frappe.utils.bind_actions_with_object(this.timeline_items_wrapper, this);
|
||||
}
|
||||
|
||||
set_document_info() {
|
||||
|
|
@ -179,6 +180,7 @@ class FormTimeline extends BaseTimeline {
|
|||
is_card: true,
|
||||
content: this.get_communication_timeline_content(communication),
|
||||
doctype: "Communication",
|
||||
id: `communication-${communication.name}`,
|
||||
name: communication.name
|
||||
});
|
||||
});
|
||||
|
|
@ -246,6 +248,7 @@ class FormTimeline extends BaseTimeline {
|
|||
creation: comment.creation,
|
||||
is_card: true,
|
||||
doctype: "Comment",
|
||||
id: `comment-${comment.name}`,
|
||||
name: comment.name,
|
||||
content: this.get_comment_timeline_content(comment),
|
||||
};
|
||||
|
|
@ -394,7 +397,7 @@ class FormTimeline extends BaseTimeline {
|
|||
}
|
||||
|
||||
setup_reply(communication_box, communication_doc) {
|
||||
let actions = communication_box.find('.actions');
|
||||
let actions = communication_box.find('.custom-actions');
|
||||
let reply = $(`<a class="action-btn reply">${frappe.utils.icon('reply', 'md')}</a>`).click(() => {
|
||||
this.compose_mail(communication_doc);
|
||||
});
|
||||
|
|
@ -446,14 +449,16 @@ class FormTimeline extends BaseTimeline {
|
|||
let edit_wrapper = $(`<div class="comment-edit-box">`).hide();
|
||||
let edit_box = this.make_editable(edit_wrapper);
|
||||
let content_wrapper = comment_wrapper.find('.content');
|
||||
|
||||
let delete_button = $();
|
||||
let more_actions_wrapper = comment_wrapper.find('.more-actions');
|
||||
if (frappe.model.can_delete("Comment")) {
|
||||
delete_button = $(`
|
||||
<button class="btn btn-link action-btn">
|
||||
${frappe.utils.icon('close', 'sm')}
|
||||
</button>
|
||||
const delete_option = $(`
|
||||
<li>
|
||||
<a class="dropdown-item">
|
||||
${__("Delete")}
|
||||
</a>
|
||||
</li>
|
||||
`).click(() => this.delete_comment(doc.name));
|
||||
more_actions_wrapper.find('.dropdown-menu').append(delete_option);
|
||||
}
|
||||
|
||||
let dismiss_button = $(`
|
||||
|
|
@ -493,15 +498,14 @@ class FormTimeline extends BaseTimeline {
|
|||
edit_button.toggle_edit_mode = () => {
|
||||
edit_button.edit_mode = !edit_button.edit_mode;
|
||||
edit_button.text(edit_button.edit_mode ? __('Save') : __('Edit'));
|
||||
delete_button.toggle(!edit_button.edit_mode);
|
||||
more_actions_wrapper.toggle(!edit_button.edit_mode);
|
||||
dismiss_button.toggle(edit_button.edit_mode);
|
||||
edit_wrapper.toggle(edit_button.edit_mode);
|
||||
content_wrapper.toggle(!edit_button.edit_mode);
|
||||
};
|
||||
|
||||
comment_wrapper.find('.actions').append(edit_button);
|
||||
comment_wrapper.find('.actions').append(dismiss_button);
|
||||
comment_wrapper.find('.actions').append(delete_button);
|
||||
let actions_wrapper = comment_wrapper.find('.custom-actions');
|
||||
actions_wrapper.append(edit_button);
|
||||
actions_wrapper.append(dismiss_button);
|
||||
}
|
||||
|
||||
make_editable(container) {
|
||||
|
|
@ -559,6 +563,14 @@ class FormTimeline extends BaseTimeline {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
copy_link(ev) {
|
||||
let doc_link = frappe.urllib.get_full_url(
|
||||
frappe.utils.get_form_link(this.frm.doctype, this.frm.docname)
|
||||
);
|
||||
let element_id = $(ev.currentTarget).closest(".timeline-content").attr("id");
|
||||
frappe.utils.copy_to_clipboard(`${doc_link}#${element_id}`);
|
||||
}
|
||||
}
|
||||
|
||||
export default FormTimeline;
|
||||
|
|
|
|||
|
|
@ -480,7 +480,11 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.layout.show_empty_form_message();
|
||||
}
|
||||
|
||||
this.scroll_to_element();
|
||||
frappe.after_ajax(() => {
|
||||
$(document).ready(() => {
|
||||
this.scroll_to_element();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
set_first_tab_as_active() {
|
||||
|
|
@ -598,6 +602,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
this.validate_form_action(save_action, resolve);
|
||||
|
||||
var after_save = function(r) {
|
||||
// to remove hash from URL to avoid scroll after save
|
||||
history.replaceState(null, null, ' ');
|
||||
if(!r.exc) {
|
||||
if (["Save", "Update", "Amend"].indexOf(save_action)!==-1) {
|
||||
frappe.utils.play_sound("click");
|
||||
|
|
@ -1195,6 +1201,8 @@ frappe.ui.form.Form = class FrappeForm {
|
|||
if (selector.length) {
|
||||
frappe.utils.scroll_to(selector);
|
||||
}
|
||||
} else if (window.location.hash && $(window.location.hash).length) {
|
||||
frappe.utils.scroll_to(window.location.hash, true, 200, null, null, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -773,16 +773,18 @@ export default class Grid {
|
|||
}
|
||||
|
||||
setup_user_defined_columns() {
|
||||
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView');
|
||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
|
||||
this.user_defined_columns = user_settings[this.doctype].map(row => {
|
||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
|
||||
if (column) {
|
||||
column.in_list_view = 1;
|
||||
column.columns = row.columns;
|
||||
return column;
|
||||
}
|
||||
});
|
||||
if (this.frm) {
|
||||
let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView');
|
||||
if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) {
|
||||
this.user_defined_columns = user_settings[this.doctype].map(row => {
|
||||
let column = frappe.meta.get_docfield(this.doctype, row.fieldname);
|
||||
if (column) {
|
||||
column.in_list_view = 1;
|
||||
column.columns = row.columns;
|
||||
return column;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -497,7 +497,7 @@ export default class GridRow {
|
|||
}
|
||||
|
||||
update_user_settings_for_grid() {
|
||||
if (!this.selected_columns_for_grid) {
|
||||
if (!this.selected_columns_for_grid || !this.frm) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.dialog = new frappe.ui.Dialog({
|
||||
title: title,
|
||||
fields: this.fields,
|
||||
size: this.size,
|
||||
primary_action_label: this.primary_action_label || __("Get Items"),
|
||||
secondary_action_label: __("Make {0}", [__(this.doctype)]),
|
||||
primary_action: () => {
|
||||
|
|
@ -135,7 +136,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
|
|||
this.get_child_result().then(r => {
|
||||
this.child_results = r.message || [];
|
||||
this.render_child_datatable();
|
||||
|
||||
|
||||
this.$wrapper.addClass('hidden');
|
||||
this.$child_wrapper.removeClass('hidden');
|
||||
this.dialog.fields_dict.more_btn.$wrapper.hide();
|
||||
|
|
|
|||
|
|
@ -193,7 +193,7 @@ frappe.ui.form.ScriptManager = class ScriptManager {
|
|||
|
||||
function setup_add_fetch(df) {
|
||||
if ((['Data', 'Read Only', 'Text', 'Small Text', 'Currency', 'Check',
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select'].includes(df.fieldtype) || df.read_only==1)
|
||||
'Text Editor', 'Code', 'Link', 'Float', 'Int', 'Date', 'Select', 'Duration'].includes(df.fieldtype) || df.read_only==1)
|
||||
&& df.fetch_from && df.fetch_from.indexOf(".")!=-1) {
|
||||
var parts = df.fetch_from.split(".");
|
||||
me.frm.add_fetch(parts[0], parts[1], df.fieldname);
|
||||
|
|
|
|||
|
|
@ -63,6 +63,20 @@
|
|||
</svg>
|
||||
</a>
|
||||
{% } %}
|
||||
<div class="custom-actions"></div>
|
||||
<div class="more-actions">
|
||||
<a type="button" class="action-btn"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<svg class="icon icon-sm">
|
||||
<use xlink:href="#icon-dot-horizontal"></use>
|
||||
</svg>
|
||||
</a>
|
||||
<ul class="dropdown-menu small">
|
||||
<li>
|
||||
<a class="dropdown-item" data-action="copy_link">{{ __('Copy Link') }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</span>
|
||||
</span>
|
||||
<div class="content">
|
||||
|
|
|
|||
|
|
@ -114,14 +114,14 @@ export default class ListSettings {
|
|||
|
||||
<div class="row">
|
||||
<div class="col-md-1">
|
||||
<i class="fa fa-bars text-muted sortable-handle ${show_sortable_handle}" aria-hidden="true"></i>
|
||||
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle " + show_sortable_handle)}
|
||||
</div>
|
||||
<div class="col-md-10" style="padding-left:0px;">
|
||||
${me.fields[idx].label}
|
||||
</div>
|
||||
<div class="col-md-1 ${can_remove}">
|
||||
<a class="text-muted remove-field" data-fieldname="${me.fields[idx].fieldname}">
|
||||
<i class="fa fa-trash-o" aria-hidden="true"></i>
|
||||
${frappe.utils.icon("delete", "xs")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -907,7 +907,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
return this.settings.get_form_link(doc);
|
||||
}
|
||||
|
||||
const docname = doc.name.match(/[%'"\s]/)
|
||||
const docname = doc.name.match(/[%'"#\s]/)
|
||||
? encodeURIComponent(doc.name)
|
||||
: doc.name;
|
||||
|
||||
|
|
|
|||
|
|
@ -56,10 +56,6 @@ $('body').on('click', 'a', function(e) {
|
|||
return override(e.currentTarget.hash);
|
||||
}
|
||||
|
||||
if (frappe.router.is_app_route(e.currentTarget.pathname)) {
|
||||
// target has "/app, this is a v2 style route.
|
||||
return override(e.currentTarget.pathname + e.currentTarget.hash);
|
||||
}
|
||||
});
|
||||
|
||||
frappe.router = {
|
||||
|
|
@ -263,7 +259,9 @@ frappe.router = {
|
|||
return new Promise(resolve => {
|
||||
route = this.get_route_from_arguments(route);
|
||||
route = this.convert_from_standard_route(route);
|
||||
const sub_path = this.make_url(route);
|
||||
let sub_path = this.make_url(route);
|
||||
// replace each # occurrences in the URL with encoded character except for last
|
||||
// sub_path = sub_path.replace(/[#](?=.*[#])/g, "%23");
|
||||
this.push_state(sub_path);
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
@ -347,7 +345,7 @@ frappe.router = {
|
|||
return null;
|
||||
} else {
|
||||
a = String(a);
|
||||
if (a && a.match(/[%'"\s\t]/)) {
|
||||
if (a && a.match(/[%'"#\s\t]/)) {
|
||||
// if special chars, then encode
|
||||
a = encodeURIComponent(a);
|
||||
}
|
||||
|
|
@ -374,7 +372,7 @@ frappe.router = {
|
|||
// return clean sub_path from hash or url
|
||||
// supports both v1 and v2 routing
|
||||
if (!route) {
|
||||
route = window.location.pathname + window.location.hash + window.location.search;
|
||||
route = window.location.pathname;
|
||||
if (route.includes('app#')) {
|
||||
// to support v1
|
||||
route = window.location.hash;
|
||||
|
|
|
|||
|
|
@ -536,8 +536,8 @@ frappe.ui.filter_utils = {
|
|||
if (condition === 'is') {
|
||||
df.fieldtype = 'Select';
|
||||
df.options = [
|
||||
{ label: __('Set'), value: 'set' },
|
||||
{ label: __('Not Set'), value: 'not set' },
|
||||
{ label: __('Set', null, 'Field value is set'), value: 'set' },
|
||||
{ label: __('Not Set', null, 'Field value is not set'), value: 'not set' },
|
||||
];
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -242,18 +242,21 @@ Object.defineProperties(window, {
|
|||
get: function() {
|
||||
console.warn('Please use `frappe.datetime` instead of `dateutil`. It will be deprecated soon.');
|
||||
return frappe.datetime;
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
},
|
||||
'date': {
|
||||
get: function() {
|
||||
console.warn('Please use `frappe.datetime` instead of `date`. It will be deprecated soon.');
|
||||
return frappe.datetime;
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
},
|
||||
'get_today': {
|
||||
get: function() {
|
||||
console.warn('Please use `frappe.datetime.get_today` instead of `get_today`. It will be deprecated soon.');
|
||||
return frappe.datetime.get_today;
|
||||
}
|
||||
},
|
||||
configurable: true
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@ function prettyDate(date, mini) {
|
|||
if (day_diff < 7) {
|
||||
return __("{0} d", [day_diff]);
|
||||
} else if (day_diff < 31) {
|
||||
return __("{0} w", [Math.ceil(day_diff / 7)]);
|
||||
return __("{0} w", [Math.floor(day_diff / 7)]);
|
||||
} else if (day_diff < 365) {
|
||||
return __("{0} M", [Math.ceil(day_diff / 30)]);
|
||||
return __("{0} M", [Math.floor(day_diff / 30)]);
|
||||
} else {
|
||||
return __("{0} y", [Math.ceil(day_diff / 365)]);
|
||||
return __("{0} y", [Math.floor(day_diff / 365)]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -54,15 +54,15 @@ function prettyDate(date, mini) {
|
|||
} else if (day_diff < 14) {
|
||||
return __("1 week ago");
|
||||
} else if (day_diff < 31) {
|
||||
return __("{0} weeks ago", [Math.ceil(day_diff / 7)]);
|
||||
return __("{0} weeks ago", [Math.floor(day_diff / 7)]);
|
||||
} else if (day_diff < 62) {
|
||||
return __("1 month ago");
|
||||
} else if (day_diff < 365) {
|
||||
return __("{0} months ago", [Math.ceil(day_diff / 30)]);
|
||||
return __("{0} months ago", [Math.floor(day_diff / 30)]);
|
||||
} else if (day_diff < 730) {
|
||||
return __("1 year ago");
|
||||
} else {
|
||||
return __("{0} years ago", [Math.ceil(day_diff / 365)]);
|
||||
return __("{0} years ago", [Math.floor(day_diff / 365)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -268,7 +268,8 @@ Object.assign(frappe.utils, {
|
|||
</a></p>');
|
||||
return content.html();
|
||||
},
|
||||
scroll_to: function(element, animate=true, additional_offset, element_to_be_scrolled, callback) {
|
||||
scroll_to: function(element, animate=true, additional_offset,
|
||||
element_to_be_scrolled, callback, highlight_element=false) {
|
||||
if (frappe.flags.disable_auto_scroll) return;
|
||||
|
||||
element_to_be_scrolled = element_to_be_scrolled || $("html, body");
|
||||
|
|
@ -291,11 +292,20 @@ Object.assign(frappe.utils, {
|
|||
}
|
||||
|
||||
if (animate) {
|
||||
element_to_be_scrolled.animate({ scrollTop: scroll_top }).promise().then(callback);
|
||||
element_to_be_scrolled.animate({
|
||||
scrollTop: scroll_top
|
||||
}).promise().then(() => {
|
||||
if (highlight_element) {
|
||||
$(element).addClass('highlight');
|
||||
document.addEventListener("click", function() {
|
||||
$(element).removeClass('highlight');
|
||||
}, {once: true});
|
||||
}
|
||||
callback && callback();
|
||||
});
|
||||
} else {
|
||||
element_to_be_scrolled.scrollTop(scroll_top);
|
||||
}
|
||||
|
||||
},
|
||||
get_scroll_position: function(element, additional_offset) {
|
||||
let header_offset = $(".navbar").height() + $(".page-head:visible").height();
|
||||
|
|
@ -1123,7 +1133,7 @@ Object.assign(frappe.utils, {
|
|||
}
|
||||
},
|
||||
|
||||
icon(icon_name, size="sm", icon_class="", icon_style="") {
|
||||
icon(icon_name, size="sm", icon_class="", icon_style="", svg_class="") {
|
||||
let size_class = "";
|
||||
|
||||
if (typeof size == "object") {
|
||||
|
|
@ -1131,7 +1141,7 @@ Object.assign(frappe.utils, {
|
|||
} else {
|
||||
size_class = `icon-${size}`;
|
||||
}
|
||||
return `<svg class="icon ${size_class}" style="${icon_style}">
|
||||
return `<svg class="icon ${svg_class} ${size_class}" style="${icon_style}">
|
||||
<use class="${icon_class}" href="#icon-${icon_name}"></use>
|
||||
</svg>`;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -209,6 +209,8 @@
|
|||
--highlight-color: var(--gray-50);
|
||||
--yellow-highlight-color: var(--yellow-50);
|
||||
|
||||
--highlight-shadow: 1px 1px 10px var(--blue-50), 0px 0px 4px var(--blue-600);
|
||||
|
||||
// Border Sizes
|
||||
--border-radius-sm: 4px;
|
||||
--border-radius: 6px;
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@
|
|||
--highlight-color: var(--gray-700);
|
||||
--yellow-highlight-color: var(--yellow-700);
|
||||
|
||||
--highlight-shadow: 1px 1px 10px var(--blue-900), 0px 0px 4px var(--blue-500);
|
||||
|
||||
// input
|
||||
--input-disabled-bg: none;
|
||||
|
||||
|
|
|
|||
|
|
@ -164,12 +164,11 @@ body {
|
|||
|
||||
.drag-handle {
|
||||
cursor: all-scroll;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: grabbing;
|
||||
|
||||
&:active {
|
||||
cursor: all-scroll;
|
||||
cursor: grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -813,7 +812,7 @@ body {
|
|||
|
||||
.drag-handle {
|
||||
cursor: all-scroll;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: grabbing;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -966,7 +965,7 @@ body {
|
|||
|
||||
.drag-handle {
|
||||
cursor: all-scroll;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -561,6 +561,19 @@ details > summary:focus {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
transition: 0.5s ease background-color;
|
||||
box-shadow: var(--highlight-shadow) !important;
|
||||
}
|
||||
|
||||
.dropdown-menu.small {
|
||||
font-size: var(--text-sm);
|
||||
min-width: 140px;
|
||||
.dropdown-item {
|
||||
padding: var(--padding-xs);
|
||||
}
|
||||
}
|
||||
|
||||
// REDESIGN TODO: Handling of broken images?
|
||||
// img.no-image:before {
|
||||
// .img-background();
|
||||
|
|
|
|||
|
|
@ -228,6 +228,11 @@ input.list-check-all, input.list-row-checkbox {
|
|||
z-index: 500;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sortable-handle {
|
||||
cursor: all-scroll;
|
||||
cursor: grabbing;
|
||||
}
|
||||
}
|
||||
|
||||
.list-items {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@ $threshold: 34;
|
|||
|
||||
.actions {
|
||||
display: flex;
|
||||
> * {
|
||||
> *:not(.indicator-pill) {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
from pypika import MySQLQuery, Order, PostgreSQLQuery, terms
|
||||
from pypika.queries import Schema, Table
|
||||
from frappe.utils import get_table_name
|
||||
|
||||
|
||||
from pypika.terms import Function
|
||||
class Base:
|
||||
terms = terms
|
||||
desc = Order.desc
|
||||
Schema = Schema
|
||||
Table = Table
|
||||
|
||||
@staticmethod
|
||||
def functions(name: str, *args, **kwargs) -> Function:
|
||||
return Function(name, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def DocType(table_name: str, *args, **kwargs) -> Table:
|
||||
table_name = get_table_name(table_name)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, get_type_hints
|
||||
from typing import Any, Callable, Dict, Union, get_type_hints
|
||||
from importlib import import_module
|
||||
|
||||
from pypika import Query
|
||||
|
|
@ -26,7 +26,7 @@ class BuilderIdentificationFailed(Exception):
|
|||
def __init__(self):
|
||||
super().__init__("Couldn't guess builder")
|
||||
|
||||
def get_query_builder(type_of_db: str) -> Query:
|
||||
def get_query_builder(type_of_db: str) -> Union[Postgres, MariaDB]:
|
||||
"""[return the query builder object]
|
||||
|
||||
Args:
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ class TestDB(unittest.TestCase):
|
|||
self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
|
||||
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
|
||||
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
|
||||
self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"]), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
|
||||
self.assertEqual(frappe.db.get_value("User", {}, "Min(name)"), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
|
||||
|
||||
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
|
||||
frappe.db.get_value("User", {"name": [">", "s"]}))
|
||||
|
|
@ -46,12 +48,6 @@ class TestDB(unittest.TestCase):
|
|||
def test_escape(self):
|
||||
frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8"))
|
||||
|
||||
def test_aggregation(self):
|
||||
self.assertTrue(type(frappe.db.sum('DocField', 'permlevel', dict(parent=('like', 'doc')))) in (int, float))
|
||||
self.assertTrue(type(frappe.db.avg('DocField', 'permlevel')) in (int, float))
|
||||
self.assertTrue(type(frappe.db.min('DocField', 'permlevel')) in (int, float))
|
||||
self.assertTrue(type(frappe.db.max('DocField', 'permlevel')) in (int, float))
|
||||
|
||||
def test_get_single_value(self):
|
||||
#setup
|
||||
values_dict = {
|
||||
|
|
|
|||
|
|
@ -147,13 +147,14 @@ def get_safe_globals():
|
|||
set_value = frappe.db.set_value,
|
||||
get_single_value = frappe.db.get_single_value,
|
||||
get_default = frappe.db.get_default,
|
||||
escape = frappe.db.escape,
|
||||
sql = read_sql,
|
||||
sum = frappe.db.sum,
|
||||
avg = frappe.db.avg,
|
||||
exists = frappe.db.exists,
|
||||
count = frappe.db.count,
|
||||
min = frappe.db.min,
|
||||
max = frappe.db.max
|
||||
max = frappe.db.max,
|
||||
avg = frappe.db.avg,
|
||||
sum = frappe.db.sum,
|
||||
escape = frappe.db.escape,
|
||||
sql = read_sql
|
||||
)
|
||||
|
||||
if frappe.response:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@
|
|||
"build": "node esbuild",
|
||||
"production": "node esbuild --production",
|
||||
"watch": "node esbuild --watch",
|
||||
"snyk-protect": "snyk protect"
|
||||
"snyk-protect": "snyk protect",
|
||||
"coverage:report": "npx nyc report --reporter=clover"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -74,5 +75,8 @@
|
|||
"rtlcss": "^3.2.1",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
"snyk": true
|
||||
"snyk": true,
|
||||
"nyc": {
|
||||
"report-dir": ".cypress-coverage"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue