Merge branch 'develop' into feat-desk-refresh
This commit is contained in:
commit
0307dd4cca
378 changed files with 3876 additions and 2623 deletions
2
.github/stale.yml
vendored
2
.github/stale.yml
vendored
|
|
@ -1,7 +1,7 @@
|
|||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 7
|
||||
daysUntilStale: 14
|
||||
|
||||
# Number of days of inactivity before a stale Issue or Pull Request is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
|
|
|
|||
10
.github/workflows/patch-mariadb-tests.yml
vendored
10
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -157,3 +157,13 @@ jobs:
|
|||
echo "Printing log: $f";
|
||||
cat $f
|
||||
done
|
||||
|
||||
faux-test:
|
||||
name: Patch
|
||||
runs-on: ubuntu-latest
|
||||
needs: checkrun
|
||||
if: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
||||
|
|
|
|||
16
.github/workflows/server-tests.yml
vendored
16
.github/workflows/server-tests.yml
vendored
|
|
@ -147,6 +147,22 @@ jobs:
|
|||
name: coverage-${{ matrix.db }}-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
|
||||
# This is required because github still doesn't understand knowingly skipped tests
|
||||
faux-test:
|
||||
name: Unit Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: checkrun
|
||||
if: ${{ needs.checkrun.outputs.build != 'strawberry' }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
db: ["mariadb", "postgres"]
|
||||
container: [1, 2]
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
||||
|
||||
coverage:
|
||||
name: Coverage Wrap Up
|
||||
needs: [test, checkrun]
|
||||
|
|
|
|||
12
.github/workflows/ui-tests.yml
vendored
12
.github/workflows/ui-tests.yml
vendored
|
|
@ -167,6 +167,18 @@ jobs:
|
|||
if: ${{ always() }}
|
||||
run: cat ~/frappe-bench/bench_start.log || true
|
||||
|
||||
faux-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: checkrun
|
||||
if: ${{ needs.checkrun.outputs.build != 'strawberry' && github.repository_owner == 'frappe' }}
|
||||
name: UI Tests (Cypress)
|
||||
strategy:
|
||||
matrix:
|
||||
container: [1, 2, 3]
|
||||
|
||||
steps:
|
||||
- name: Pass skipped tests unconditionally
|
||||
run: "echo Skipped"
|
||||
|
||||
coverage:
|
||||
name: Coverage Wrap Up
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ Full-stack web application framework that uses Python and MariaDB on the server
|
|||
|
||||
### Development
|
||||
* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
|
||||
* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation)
|
||||
* [Development installation on bare metal](https://frappeframework.com/docs/user/en/installation)
|
||||
|
||||
|
||||
## Contributing
|
||||
|
|
|
|||
BIN
cypress/fixtures/sample_attachments/attachment-1.jpg
Normal file
BIN
cypress/fixtures/sample_attachments/attachment-1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
1
cypress/fixtures/sample_attachments/attachment-10.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-10.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
10
|
||||
1
cypress/fixtures/sample_attachments/attachment-11.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-11.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
11
|
||||
1
cypress/fixtures/sample_attachments/attachment-2.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-2.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
2
|
||||
1
cypress/fixtures/sample_attachments/attachment-3.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-3.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
3
|
||||
1
cypress/fixtures/sample_attachments/attachment-4.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-4.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
4
|
||||
1
cypress/fixtures/sample_attachments/attachment-5.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-5.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
5
|
||||
1
cypress/fixtures/sample_attachments/attachment-6.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-6.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
6
|
||||
1
cypress/fixtures/sample_attachments/attachment-7.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-7.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
7
|
||||
1
cypress/fixtures/sample_attachments/attachment-8.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-8.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
8
|
||||
1
cypress/fixtures/sample_attachments/attachment-9.txt
Normal file
1
cypress/fixtures/sample_attachments/attachment-9.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
9
|
||||
|
|
@ -24,9 +24,9 @@ context("Discussions", () => {
|
|||
.should("have.value", "Discussion from tests");
|
||||
|
||||
// Enter comment
|
||||
cy.get(".modal .comment-field")
|
||||
.type("This is a discussion from the cypress ui tests.")
|
||||
.should("have.value", "This is a discussion from the cypress ui tests.");
|
||||
cy.get(".modal .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
|
||||
// Submit
|
||||
cy.get(".modal .submit-discussion").click();
|
||||
|
|
@ -38,21 +38,16 @@ context("Discussions", () => {
|
|||
"Discussion from tests"
|
||||
);
|
||||
cy.get(".discussion-on-page:visible").should("have.class", "show");
|
||||
cy.get(".discussion-on-page:visible .reply-card .reply-text").should(
|
||||
cy.get(".discussion-on-page:visible .reply-card .reply-text .ql-editor p").should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests.\n"
|
||||
"This is a discussion from the cypress ui tests."
|
||||
);
|
||||
};
|
||||
|
||||
const reply_through_comment_box = () => {
|
||||
cy.get(".discussion-form:visible .comment-field")
|
||||
.type(
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
|
||||
)
|
||||
.should(
|
||||
"have.value",
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
|
||||
);
|
||||
cy.get(".discussion-form:visible .discussions-comment").type(
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page."
|
||||
);
|
||||
|
||||
cy.get(".discussion-form:visible .submit-discussion").click();
|
||||
cy.wait(3000);
|
||||
|
|
@ -63,28 +58,18 @@ context("Discussions", () => {
|
|||
.find(".reply-text")
|
||||
.should(
|
||||
"have.text",
|
||||
"This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n"
|
||||
"This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n"
|
||||
);
|
||||
};
|
||||
|
||||
const cancel_and_clear_comment_box = () => {
|
||||
cy.get(".discussion-form:visible .comment-field")
|
||||
.type("This is a discussion from the cypress ui tests.")
|
||||
.should("have.value", "This is a discussion from the cypress ui tests.");
|
||||
|
||||
cy.get(".discussion-form:visible .cancel-comment").click();
|
||||
cy.get(".discussion-form:visible .comment-field").should("have.value", "");
|
||||
};
|
||||
|
||||
const single_thread_discussion = () => {
|
||||
cy.visit("/test-single-thread");
|
||||
cy.get(".discussions-sidebar").should("have.length", 0);
|
||||
cy.get(".reply").should("have.length", 0);
|
||||
|
||||
cy.get(".discussion-form:visible .comment-field")
|
||||
.type("This comment is being made on a single thread discussion.")
|
||||
.should("have.value", "This comment is being made on a single thread discussion.");
|
||||
|
||||
cy.get(".discussion-form:visible .discussions-comment").type(
|
||||
"This comment is being made on a single thread discussion."
|
||||
);
|
||||
cy.get(".discussion-form:visible .submit-discussion").click();
|
||||
cy.wait(3000);
|
||||
cy.get(".discussion-on-page")
|
||||
|
|
@ -96,6 +81,5 @@ context("Discussions", () => {
|
|||
|
||||
it("reply through modal", reply_through_modal);
|
||||
it("reply through comment box", reply_through_comment_box);
|
||||
it("cancel and clear comment box", cancel_and_clear_comment_box);
|
||||
it("single thread discussion", single_thread_discussion);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -13,6 +13,27 @@ const verify_attachment_visibility = (document, is_private) => {
|
|||
cy.get_open_dialog().findByRole("checkbox", { name: "Private" }).should(assertion);
|
||||
};
|
||||
|
||||
const attach_file = (file, no_of_files = 1) => {
|
||||
let files = [];
|
||||
if (file) {
|
||||
files = [file];
|
||||
} else if (no_of_files > 1) {
|
||||
// attach n files
|
||||
files = [...Array(no_of_files)].map(
|
||||
(el, idx) =>
|
||||
"cypress/fixtures/sample_attachments/attachment-" +
|
||||
(idx + 1) +
|
||||
(idx == 0 ? ".jpg" : ".txt")
|
||||
);
|
||||
}
|
||||
|
||||
cy.findByRole("button", { name: "Attach File" }).click();
|
||||
cy.get_open_dialog().find(".file-upload-area").selectFile(files, {
|
||||
action: "drag-drop",
|
||||
});
|
||||
cy.get_open_dialog().findByRole("button", { name: "Upload" }).click();
|
||||
};
|
||||
|
||||
context("Sidebar", () => {
|
||||
before(() => {
|
||||
cy.visit("/login");
|
||||
|
|
@ -35,6 +56,36 @@ context("Sidebar", () => {
|
|||
verify_attachment_visibility("blog-post/test-blog-attachment-post", false);
|
||||
});
|
||||
|
||||
it("Verify attachment accessibility UX", () => {
|
||||
cy.call("frappe.tests.ui_test_helpers.create_todo_with_attachment_limit", {
|
||||
description: "Sidebar Attachment Access Test ToDo",
|
||||
}).then((todo) => {
|
||||
cy.visit(`/app/todo/${todo.message.name}`);
|
||||
|
||||
// explore icon btn should be hidden as there are no attachments
|
||||
cy.get(".explore-btn").should("be.hidden");
|
||||
|
||||
attach_file("cypress/fixtures/sample_image.jpg");
|
||||
cy.get(".explore-btn").should("be.visible");
|
||||
cy.get(".show-all-btn").should("be.hidden");
|
||||
|
||||
// attach 10 images
|
||||
attach_file(null, 10);
|
||||
cy.get(".show-all-btn").should("be.visible");
|
||||
|
||||
// attach 1 more image to reach attachment limit
|
||||
attach_file("cypress/fixtures/sample_attachments/attachment-11.txt");
|
||||
cy.get(".explore-full-btn").should("be.visible");
|
||||
cy.get(".attachments-actions").should("be.hidden");
|
||||
cy.get(".explore-btn").should("be.hidden");
|
||||
|
||||
// test "Show All" button
|
||||
cy.get(".attachment-row").should("have.length", 10);
|
||||
cy.get(".show-all-btn").click();
|
||||
cy.get(".attachment-row").should("have.length", 12);
|
||||
});
|
||||
});
|
||||
|
||||
it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => {
|
||||
cy.call("frappe.tests.ui_test_helpers.create_todo", {
|
||||
description: "Sidebar Attachment ToDo",
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ from frappe.query_builder import (
|
|||
patch_query_execute,
|
||||
)
|
||||
from frappe.utils.caching import request_cache
|
||||
from frappe.utils.data import cstr, sbool
|
||||
from frappe.utils.data import cint, cstr, sbool
|
||||
|
||||
# Local application imports
|
||||
from .exceptions import *
|
||||
|
|
@ -302,19 +302,13 @@ def connect_replica() -> bool:
|
|||
def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]:
|
||||
"""Returns `site_config.json` combined with `sites/common_site_config.json`.
|
||||
`site_config` is a set of site wide settings like database name, password, email etc."""
|
||||
config = {}
|
||||
config = _dict()
|
||||
|
||||
sites_path = sites_path or getattr(local, "sites_path", None)
|
||||
site_path = site_path or getattr(local, "site_path", None)
|
||||
|
||||
if sites_path:
|
||||
common_site_config = os.path.join(sites_path, "common_site_config.json")
|
||||
if os.path.exists(common_site_config):
|
||||
try:
|
||||
config.update(get_file_json(common_site_config))
|
||||
except Exception as error:
|
||||
click.secho("common_site_config.json is invalid", fg="red")
|
||||
print(error)
|
||||
config.update(get_common_site_config(sites_path))
|
||||
|
||||
if site_path:
|
||||
site_config = os.path.join(site_path, "site_config.json")
|
||||
|
|
@ -348,7 +342,26 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None)
|
|||
os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"])
|
||||
)
|
||||
|
||||
return _dict(config)
|
||||
return config
|
||||
|
||||
|
||||
def get_common_site_config(sites_path: str | None = None) -> dict[str, Any]:
|
||||
"""Returns common site config as dictionary.
|
||||
|
||||
This is useful for:
|
||||
- checking configuration which should only be allowed in common site config
|
||||
- When no site context is present and fallback is required.
|
||||
"""
|
||||
sites_path = sites_path or getattr(local, "sites_path", None)
|
||||
|
||||
common_site_config = os.path.join(sites_path, "common_site_config.json")
|
||||
if os.path.exists(common_site_config):
|
||||
try:
|
||||
return _dict(get_file_json(common_site_config))
|
||||
except Exception as error:
|
||||
click.secho("common_site_config.json is invalid", fg="red")
|
||||
print(error)
|
||||
return _dict()
|
||||
|
||||
|
||||
def get_conf(site: str | None = None) -> dict[str, Any]:
|
||||
|
|
@ -523,11 +536,7 @@ def clear_messages():
|
|||
|
||||
|
||||
def get_message_log():
|
||||
log = []
|
||||
for msg_out in local.message_log:
|
||||
log.append(json.loads(msg_out))
|
||||
|
||||
return log
|
||||
return [json.loads(msg_out) for msg_out in local.message_log]
|
||||
|
||||
|
||||
def clear_last_message():
|
||||
|
|
@ -918,11 +927,8 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
|
|||
elif user:
|
||||
frappe.cache_manager.clear_user_cache(user)
|
||||
else: # everything
|
||||
from frappe import translate
|
||||
|
||||
frappe.cache_manager.clear_user_cache()
|
||||
frappe.cache_manager.clear_domain_cache()
|
||||
translate.clear_cache()
|
||||
# Delete ALL keys associated with this site.
|
||||
frappe.cache.delete_keys("")
|
||||
reset_metadata_version()
|
||||
local.cache = {}
|
||||
local.new_doc_templates = {}
|
||||
|
|
@ -989,7 +995,9 @@ def has_permission(
|
|||
|
||||
if throw and not out:
|
||||
# mimics frappe.throw
|
||||
document_label = f"{_(doc.doctype)} {doc.name}" if doc else _(doctype)
|
||||
document_label = (
|
||||
f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype)
|
||||
)
|
||||
msgprint(
|
||||
_("No permission for {0}").format(document_label),
|
||||
raise_exception=ValidationError,
|
||||
|
|
@ -1418,11 +1426,21 @@ def get_app_path(app_name, *joins):
|
|||
return get_pymodule_path(app_name, *joins)
|
||||
|
||||
|
||||
def get_app_source_path(app_name, *joins):
|
||||
"""Return source path of given app.
|
||||
|
||||
:param app: App name.
|
||||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
return get_app_path(app_name, "..", *joins)
|
||||
|
||||
|
||||
def get_site_path(*joins):
|
||||
"""Return path of current site.
|
||||
|
||||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
return os.path.join(local.site_path, *joins)
|
||||
from os.path import abspath, join
|
||||
|
||||
return abspath(join(local.site_path, *joins))
|
||||
|
||||
|
||||
def get_pymodule_path(modulename, *joins):
|
||||
|
|
@ -1430,14 +1448,17 @@ def get_pymodule_path(modulename, *joins):
|
|||
|
||||
:param modulename: Python module name.
|
||||
:param *joins: Join additional path elements using `os.path.join`."""
|
||||
if not "public" in joins:
|
||||
from os.path import abspath, dirname, join
|
||||
|
||||
if "public" not in joins:
|
||||
joins = [scrub(part) for part in joins]
|
||||
return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ""), *joins)
|
||||
|
||||
return abspath(join(dirname(get_module(scrub(modulename)).__file__ or ""), *joins))
|
||||
|
||||
|
||||
def get_module_list(app_name):
|
||||
"""Get list of modules for given all via `app/modules.txt`."""
|
||||
return get_file_items(os.path.join(os.path.dirname(get_module(app_name).__file__), "modules.txt"))
|
||||
return get_file_items(get_app_path(app_name, "modules.txt"))
|
||||
|
||||
|
||||
def get_all_apps(with_internal_apps=True, sites_path=None):
|
||||
|
|
@ -1984,8 +2005,6 @@ def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> s
|
|||
|
||||
|
||||
def are_emails_muted():
|
||||
from frappe.utils import cint
|
||||
|
||||
return flags.mute_emails or cint(conf.get("mute_emails") or 0) or False
|
||||
|
||||
|
||||
|
|
@ -2074,53 +2093,46 @@ def attach_print(
|
|||
lang=None,
|
||||
print_letterhead=True,
|
||||
password=None,
|
||||
letterhead=None,
|
||||
):
|
||||
from frappe.translate import print_language
|
||||
from frappe.utils import scrub_urls
|
||||
from frappe.utils.pdf import get_pdf
|
||||
|
||||
if not file_name:
|
||||
file_name = name
|
||||
file_name = cstr(file_name).replace(" ", "").replace("/", "-")
|
||||
|
||||
print_settings = db.get_singles_dict("Print Settings")
|
||||
|
||||
_lang = local.lang
|
||||
|
||||
# set lang as specified in print format attachment
|
||||
if lang:
|
||||
local.lang = lang
|
||||
local.flags.ignore_print_permissions = True
|
||||
|
||||
no_letterhead = not print_letterhead
|
||||
|
||||
kwargs = dict(
|
||||
print_format=print_format,
|
||||
style=style,
|
||||
doc=doc,
|
||||
no_letterhead=no_letterhead,
|
||||
no_letterhead=not print_letterhead,
|
||||
letterhead=letterhead,
|
||||
password=password,
|
||||
)
|
||||
|
||||
content = ""
|
||||
if int(print_settings.send_print_as_pdf or 0):
|
||||
ext = ".pdf"
|
||||
kwargs["as_pdf"] = True
|
||||
content = (
|
||||
get_pdf(html, options={"password": password} if password else None)
|
||||
if html
|
||||
else get_print(doctype, name, **kwargs)
|
||||
)
|
||||
else:
|
||||
ext = ".html"
|
||||
content = html or scrub_urls(get_print(doctype, name, **kwargs)).encode("utf-8")
|
||||
local.flags.ignore_print_permissions = True
|
||||
|
||||
out = {"fname": file_name + ext, "fcontent": content}
|
||||
with print_language(lang or local.lang):
|
||||
content = ""
|
||||
if cint(print_settings.send_print_as_pdf):
|
||||
ext = ".pdf"
|
||||
kwargs["as_pdf"] = True
|
||||
content = (
|
||||
get_pdf(html, options={"password": password} if password else None)
|
||||
if html
|
||||
else get_print(doctype, name, **kwargs)
|
||||
)
|
||||
else:
|
||||
ext = ".html"
|
||||
content = html or scrub_urls(get_print(doctype, name, **kwargs)).encode("utf-8")
|
||||
|
||||
local.flags.ignore_print_permissions = False
|
||||
# reset lang to original local lang
|
||||
local.lang = _lang
|
||||
|
||||
return out
|
||||
if not file_name:
|
||||
file_name = name
|
||||
file_name = cstr(file_name).replace(" ", "").replace("/", "-") + ext
|
||||
|
||||
return {"fname": file_name, "fcontent": content}
|
||||
|
||||
|
||||
def publish_progress(*args, **kwargs):
|
||||
|
|
@ -2256,41 +2268,10 @@ def bold(text):
|
|||
|
||||
def safe_eval(code, eval_globals=None, eval_locals=None):
|
||||
"""A safer `eval`"""
|
||||
whitelisted_globals = {"int": int, "float": float, "long": int, "round": round}
|
||||
code = unicodedata.normalize("NFKC", code)
|
||||
|
||||
UNSAFE_ATTRIBUTES = {
|
||||
# Generator Attributes
|
||||
"gi_frame",
|
||||
"gi_code",
|
||||
# Coroutine Attributes
|
||||
"cr_frame",
|
||||
"cr_code",
|
||||
"cr_origin",
|
||||
# Async Generator Attributes
|
||||
"ag_code",
|
||||
"ag_frame",
|
||||
# Traceback Attributes
|
||||
"tb_frame",
|
||||
"tb_next",
|
||||
# Format Attributes
|
||||
"format",
|
||||
"format_map",
|
||||
}
|
||||
from frappe.utils.safe_exec import safe_eval
|
||||
|
||||
for attribute in UNSAFE_ATTRIBUTES:
|
||||
if attribute in code:
|
||||
throw(f'Illegal rule {bold(code)}. Cannot use "{attribute}"')
|
||||
|
||||
if "__" in code:
|
||||
throw(f'Illegal rule {bold(code)}. Cannot use "__"')
|
||||
|
||||
if not eval_globals:
|
||||
eval_globals = {}
|
||||
|
||||
eval_globals["__builtins__"] = {}
|
||||
eval_globals.update(whitelisted_globals)
|
||||
return eval(code, eval_globals, eval_locals)
|
||||
return safe_eval(code, eval_globals, eval_locals)
|
||||
|
||||
|
||||
def get_website_settings(key):
|
||||
|
|
@ -2419,7 +2400,6 @@ def validate_and_sanitize_search_inputs(fn):
|
|||
@functools.wraps(fn)
|
||||
def wrapper(*args, **kwargs):
|
||||
from frappe.desk.search import sanitize_searchfield
|
||||
from frappe.utils import cint
|
||||
|
||||
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
|
||||
sanitize_searchfield(kwargs["searchfield"])
|
||||
|
|
|
|||
|
|
@ -401,11 +401,7 @@ def serve(
|
|||
application = ProfilerMiddleware(application, sort_by=("cumtime", "calls"))
|
||||
|
||||
if not os.environ.get("NO_STATICS"):
|
||||
application = SharedDataMiddleware(
|
||||
application, {"/assets": str(os.path.join(sites_path, "assets"))}
|
||||
)
|
||||
|
||||
application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))})
|
||||
application = application_with_statics()
|
||||
|
||||
application.debug = True
|
||||
application.config = {"SERVER_NAME": "localhost:8000"}
|
||||
|
|
@ -429,6 +425,18 @@ def serve(
|
|||
)
|
||||
|
||||
|
||||
def application_with_statics():
|
||||
global application, _sites_path
|
||||
|
||||
application = SharedDataMiddleware(
|
||||
application, {"/assets": str(os.path.join(_sites_path, "assets"))}
|
||||
)
|
||||
|
||||
application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(_sites_path))})
|
||||
|
||||
return application
|
||||
|
||||
|
||||
# Remove references to pattern that are pre-compiled and loaded to global scopes.
|
||||
re.purge()
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class AssignmentRule(Document):
|
|||
priority: DF.Int
|
||||
rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"]
|
||||
unassign_condition: DF.Code | None
|
||||
users: DF.TableMultiSelect[AssignmentRuleUser] | None
|
||||
users: DF.TableMultiSelect[AssignmentRuleUser]
|
||||
# end: auto-generated types
|
||||
def validate(self):
|
||||
self.validate_document_types()
|
||||
|
|
@ -141,17 +141,20 @@ class AssignmentRule(Document):
|
|||
|
||||
def get_user_load_balancing(self):
|
||||
"""Assign to the user with least number of open assignments"""
|
||||
counts = []
|
||||
for d in self.users:
|
||||
counts.append(
|
||||
dict(
|
||||
user=d.user,
|
||||
count=frappe.db.count(
|
||||
"ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open")
|
||||
counts = [
|
||||
dict(
|
||||
user=d.user,
|
||||
count=frappe.db.count(
|
||||
"ToDo",
|
||||
dict(
|
||||
reference_type=self.document_type,
|
||||
allocated_to=d.user,
|
||||
status="Open",
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
for d in self.users
|
||||
]
|
||||
# sort by dict value
|
||||
sorted_counts = sorted(counts, key=lambda k: k["count"])
|
||||
|
||||
|
|
|
|||
|
|
@ -274,6 +274,55 @@ class TestAutoAssign(FrappeTestCase):
|
|||
assignment_rule.delete()
|
||||
frappe.db.commit() # undo changes commited by DDL
|
||||
|
||||
def test_submittable_assignment(self):
|
||||
# create a submittable doctype
|
||||
submittable_doctype = "Assignment Test Submittable"
|
||||
create_test_doctype(submittable_doctype)
|
||||
dt = frappe.get_doc("DocType", submittable_doctype)
|
||||
dt.is_submittable = 1
|
||||
dt.save()
|
||||
|
||||
# create a rule for the submittable doctype
|
||||
assignment_rule = frappe.new_doc("Assignment Rule")
|
||||
assignment_rule.name = f"For {submittable_doctype}"
|
||||
assignment_rule.document_type = submittable_doctype
|
||||
assignment_rule.rule = "Round Robin"
|
||||
assignment_rule.extend("assignment_days", self.days)
|
||||
assignment_rule.append("users", {"user": "test@example.com"})
|
||||
assignment_rule.assign_condition = "docstatus == 1"
|
||||
assignment_rule.unassign_condition = "docstatus == 2"
|
||||
assignment_rule.save()
|
||||
|
||||
# create a submittable doc
|
||||
doc = frappe.new_doc(submittable_doctype)
|
||||
doc.save()
|
||||
doc.submit()
|
||||
|
||||
# check if todo is created
|
||||
todos = frappe.get_all(
|
||||
"ToDo",
|
||||
filters={
|
||||
"reference_type": submittable_doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Open",
|
||||
"allocated_to": "test@example.com",
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(todos), 1)
|
||||
|
||||
# check if todo is closed on cancel
|
||||
doc.cancel()
|
||||
todos = frappe.get_all(
|
||||
"ToDo",
|
||||
filters={
|
||||
"reference_type": submittable_doctype,
|
||||
"reference_name": doc.name,
|
||||
"status": "Cancelled",
|
||||
"allocated_to": "test@example.com",
|
||||
},
|
||||
)
|
||||
self.assertEqual(len(todos), 1)
|
||||
|
||||
|
||||
def clear_assignments():
|
||||
frappe.db.delete("ToDo", {"reference_type": TEST_DOCTYPE})
|
||||
|
|
@ -335,7 +384,7 @@ def _make_test_record(**kwargs):
|
|||
|
||||
def create_test_doctype(doctype: str):
|
||||
"""Create custom doctype."""
|
||||
frappe.db.delete("DocType", doctype)
|
||||
frappe.delete_doc("DocType", doctype)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -569,6 +569,7 @@ def update_reference(docname, reference):
|
|||
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
|
||||
frappe.has_permission("Auto Repeat", "write", throw=True)
|
||||
doc = frappe.get_doc(reference_dt, reference_doc)
|
||||
doc.check_permission()
|
||||
subject_preview = _("Please add a subject to your email")
|
||||
msg_preview = frappe.render_template(message, {"doc": doc})
|
||||
if subject:
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ class TestAutoRepeat(FrappeTestCase):
|
|||
|
||||
def make_auto_repeat(**args):
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.get_doc(
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Auto Repeat",
|
||||
"reference_doctype": args.reference_doctype or "ToDo",
|
||||
|
|
@ -259,8 +259,6 @@ def make_auto_repeat(**args):
|
|||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
def create_submittable_doctype(doctype, submit_perms=1):
|
||||
if frappe.db.exists("DocType", doctype):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import re
|
|||
import shutil
|
||||
import subprocess
|
||||
from subprocess import getoutput
|
||||
from tempfile import mkdtemp, mktemp
|
||||
from tempfile import mkdtemp
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import click
|
||||
|
|
@ -183,7 +183,7 @@ def symlink(target, link_name, overwrite=False):
|
|||
|
||||
# Create link to target with temporary filename
|
||||
while True:
|
||||
temp_link_name = mktemp(dir=link_dir)
|
||||
temp_link_name = f"tmp{frappe.generate_hash()}"
|
||||
|
||||
# os.* functions mimic as closely as possible system functions
|
||||
# The POSIX symlink() returns EEXIST if link_name already exists
|
||||
|
|
@ -253,7 +253,7 @@ def bundle(
|
|||
command += " --save-metafiles"
|
||||
|
||||
check_node_executable()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe_app_path = frappe.get_app_source_path("frappe")
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
|
||||
|
||||
|
||||
|
|
@ -271,7 +271,7 @@ def watch(apps=None):
|
|||
command += " --live-reload"
|
||||
|
||||
check_node_executable()
|
||||
frappe_app_path = frappe.get_app_path("frappe", "..")
|
||||
frappe_app_path = frappe.get_app_source_path("frappe")
|
||||
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env())
|
||||
|
||||
|
||||
|
|
@ -286,8 +286,7 @@ def check_node_executable():
|
|||
|
||||
|
||||
def get_node_env():
|
||||
node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"}
|
||||
return node_env
|
||||
return {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"}
|
||||
|
||||
|
||||
def get_safe_max_old_space_size():
|
||||
|
|
|
|||
|
|
@ -208,11 +208,7 @@ def insert_many(docs=None):
|
|||
if len(docs) > 200:
|
||||
frappe.throw(_("Only 200 inserts allowed in one request"))
|
||||
|
||||
out = []
|
||||
for doc in docs:
|
||||
out.append(insert_doc(doc).name)
|
||||
|
||||
return out
|
||||
return [insert_doc(doc).name for doc in docs]
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
|
|
|
|||
|
|
@ -52,8 +52,7 @@ def pass_context(f):
|
|||
|
||||
def get_site(context, raise_err=True):
|
||||
try:
|
||||
site = context.sites[0]
|
||||
return site
|
||||
return context.sites[0]
|
||||
except (IndexError, TypeError):
|
||||
if raise_err:
|
||||
raise frappe.SiteNotSpecifiedError
|
||||
|
|
|
|||
|
|
@ -931,7 +931,7 @@ def _drop_site(
|
|||
drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password)
|
||||
|
||||
archived_sites_path = archived_sites_path or os.path.join(
|
||||
frappe.get_app_path("frappe"), "..", "..", "..", "archived", "sites"
|
||||
frappe.utils.get_bench_path(), "archived", "sites"
|
||||
)
|
||||
archived_sites_path = os.path.realpath(archived_sites_path)
|
||||
|
||||
|
|
|
|||
|
|
@ -254,9 +254,8 @@ def execute(context, method, args=None, kwargs=None, profile=False):
|
|||
try:
|
||||
ret = frappe.get_attr(method)(*args, **kwargs)
|
||||
except Exception:
|
||||
ret = frappe.safe_eval(
|
||||
method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals()
|
||||
)
|
||||
# eval is safe here because input is from console
|
||||
ret = eval(method + "(*args, **kwargs)", globals(), locals()) # nosemgrep
|
||||
|
||||
if profile:
|
||||
import pstats
|
||||
|
|
@ -864,7 +863,7 @@ def run_ui_tests(
|
|||
):
|
||||
"Run UI tests"
|
||||
site = get_site(context)
|
||||
app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), ".."))
|
||||
app_base_path = frappe.get_app_source_path(app)
|
||||
site_url = frappe.utils.get_site_url(site)
|
||||
admin_password = frappe.get_conf(site).admin_password
|
||||
|
||||
|
|
@ -1076,7 +1075,7 @@ def get_version(output):
|
|||
app_info = frappe._dict()
|
||||
|
||||
try:
|
||||
app_info.commit = Repo(frappe.get_app_path(app, "..")).head.object.hexsha[:7]
|
||||
app_info.commit = Repo(frappe.get_app_source_path(app)).head.object.hexsha[:7]
|
||||
except InvalidGitRepositoryError:
|
||||
app_info.commit = ""
|
||||
|
||||
|
|
|
|||
|
|
@ -51,21 +51,17 @@ def get_permission_query_conditions(doctype):
|
|||
return ""
|
||||
|
||||
elif not links.get("permitted_links"):
|
||||
conditions = []
|
||||
|
||||
# when everything is not permitted
|
||||
for df in links.get("not_permitted_links"):
|
||||
# like ifnull(customer, '')='' and ifnull(supplier, '')=''
|
||||
conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')=''")
|
||||
conditions = [
|
||||
f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')=''" for df in links.get("not_permitted_links")
|
||||
]
|
||||
|
||||
return "( " + " and ".join(conditions) + " )"
|
||||
|
||||
else:
|
||||
conditions = []
|
||||
|
||||
for df in links.get("permitted_links"):
|
||||
# like ifnull(customer, '')!='' or ifnull(supplier, '')!=''
|
||||
conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')!=''")
|
||||
conditions = [
|
||||
f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')!=''" for df in links.get("permitted_links")
|
||||
]
|
||||
|
||||
return "( " + " or ".join(conditions) + " )"
|
||||
|
||||
|
|
|
|||
|
|
@ -217,18 +217,15 @@ def invite_user(contact):
|
|||
@frappe.whitelist()
|
||||
def get_contact_details(contact):
|
||||
contact = frappe.get_doc("Contact", contact)
|
||||
out = {
|
||||
return {
|
||||
"contact_person": contact.get("name"),
|
||||
"contact_display": " ".join(
|
||||
filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")])
|
||||
),
|
||||
"contact_display": contact.get("full_name"),
|
||||
"contact_email": contact.get("email_id"),
|
||||
"contact_mobile": contact.get("mobile_no"),
|
||||
"contact_phone": contact.get("phone"),
|
||||
"contact_designation": contact.get("designation"),
|
||||
"contact_department": contact.get("department"),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def update_contact(doc, method):
|
||||
|
|
|
|||
|
|
@ -115,10 +115,7 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_
|
|||
fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, [])
|
||||
|
||||
records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True)
|
||||
temp_records = list()
|
||||
|
||||
for d in records:
|
||||
temp_records.append(d[1:])
|
||||
temp_records = [d[1:] for d in records]
|
||||
|
||||
if not reference_list:
|
||||
frappe.throw(_("No records present in {0}").format(reference_doctype))
|
||||
|
|
|
|||
|
|
@ -27,13 +27,12 @@ def get_custom_linked_doctype():
|
|||
|
||||
def get_custom_doc_for_address_and_contacts():
|
||||
get_custom_linked_doctype()
|
||||
linked_doc = frappe.get_doc(
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Test Custom Doctype",
|
||||
"test_field": "Hello",
|
||||
}
|
||||
).insert()
|
||||
return linked_doc
|
||||
|
||||
|
||||
def create_linked_address(link_list):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.file.file import File, setup_folder_path
|
||||
from frappe.core.doctype.file.file import File
|
||||
from frappe.core.doctype.file.utils import setup_folder_path
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,6 @@
|
|||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Small Text",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ class TestActivityLog(FrappeTestCase):
|
|||
)
|
||||
|
||||
name = names[0]
|
||||
auth_log = frappe.get_doc("Activity Log", name)
|
||||
return auth_log
|
||||
return frappe.get_doc("Activity Log", name)
|
||||
|
||||
def test_brute_security(self):
|
||||
update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5})
|
||||
|
|
|
|||
|
|
@ -45,8 +45,6 @@ class Comment(Document):
|
|||
]
|
||||
content: DF.HTMLEditor | None
|
||||
ip_address: DF.Data | None
|
||||
link_doctype: DF.Link | None
|
||||
link_name: DF.DynamicLink | None
|
||||
published: DF.Check
|
||||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.DynamicLink | None
|
||||
|
|
|
|||
|
|
@ -10,15 +10,6 @@ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
|
|||
|
||||
|
||||
class TestComment(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.form_dict.comment = None
|
||||
frappe.form_dict.comment_email = None
|
||||
frappe.form_dict.comment_by = None
|
||||
frappe.form_dict.reference_doctype = None
|
||||
frappe.form_dict.reference_name = None
|
||||
frappe.form_dict.route = None
|
||||
frappe.local.request_ip = None
|
||||
|
||||
def test_comment_creation(self):
|
||||
test_doc = frappe.get_doc(dict(doctype="ToDo", description="test"))
|
||||
test_doc.insert()
|
||||
|
|
@ -45,16 +36,15 @@ class TestComment(FrappeTestCase):
|
|||
test_blog = make_test_blog()
|
||||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = "test@test.com"
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
add_comment()
|
||||
add_comment_args = {
|
||||
"comment": "Good comment with 10 chars",
|
||||
"comment_email": "test@test.com",
|
||||
"comment_by": "Good Tester",
|
||||
"reference_doctype": test_blog.doctype,
|
||||
"reference_name": test_blog.name,
|
||||
"route": test_blog.route,
|
||||
}
|
||||
add_comment(**add_comment_args)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.get_all(
|
||||
|
|
@ -67,10 +57,10 @@ class TestComment(FrappeTestCase):
|
|||
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = "pleez vizits my site http://mysite.com"
|
||||
frappe.form_dict.comment_by = "bad commentor"
|
||||
|
||||
add_comment()
|
||||
add_comment_args.update(
|
||||
comment="pleez vizits my site http://mysite.com", comment_by="bad commentor"
|
||||
)
|
||||
add_comment(**add_comment_args)
|
||||
|
||||
self.assertEqual(
|
||||
len(
|
||||
|
|
@ -86,11 +76,8 @@ class TestComment(FrappeTestCase):
|
|||
# test for filtering html and css injection elements
|
||||
frappe.db.delete("Comment", {"reference_doctype": "Blog Post"})
|
||||
|
||||
frappe.form_dict.comment = "<script>alert(1)</script>Comment"
|
||||
frappe.form_dict.comment_by = "hacker"
|
||||
|
||||
add_comment()
|
||||
|
||||
add_comment_args.update(comment="<script>alert(1)</script>Comment", comment_by="hacker")
|
||||
add_comment(**add_comment_args)
|
||||
self.assertEqual(
|
||||
frappe.get_all(
|
||||
"Comment",
|
||||
|
|
@ -106,27 +93,30 @@ class TestComment(FrappeTestCase):
|
|||
def test_guest_cannot_comment(self):
|
||||
test_blog = make_test_blog()
|
||||
with set_user("Guest"):
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = "mail@example.org"
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
self.assertEqual(add_comment(), None)
|
||||
self.assertEqual(
|
||||
add_comment(
|
||||
comment="Good comment with 10 chars",
|
||||
comment_email="mail@example.org",
|
||||
comment_by="Good Tester",
|
||||
reference_doctype="Blog Post",
|
||||
reference_name=test_blog.name,
|
||||
route=test_blog.route,
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
def test_user_not_logged_in(self):
|
||||
some_system_user = frappe.db.get_value("User", {})
|
||||
some_system_user = frappe.db.get_value("User", {"name": ("not in", frappe.STANDARD_USERS)})
|
||||
|
||||
test_blog = make_test_blog()
|
||||
with set_user("Guest"):
|
||||
frappe.form_dict.comment = "Good comment with 10 chars"
|
||||
frappe.form_dict.comment_email = some_system_user
|
||||
frappe.form_dict.comment_by = "Good Tester"
|
||||
frappe.form_dict.reference_doctype = "Blog Post"
|
||||
frappe.form_dict.reference_name = test_blog.name
|
||||
frappe.form_dict.route = test_blog.route
|
||||
frappe.local.request_ip = "127.0.0.1"
|
||||
|
||||
self.assertRaises(frappe.ValidationError, add_comment)
|
||||
self.assertRaises(
|
||||
frappe.ValidationError,
|
||||
add_comment,
|
||||
comment="Good comment with 10 chars",
|
||||
comment_email=some_system_user,
|
||||
comment_by="Good Tester",
|
||||
reference_doctype="Blog Post",
|
||||
reference_name=test_blog.name,
|
||||
route=test_blog.route,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -309,12 +309,14 @@ class Communication(Document, CommunicationEmailMixin):
|
|||
return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname)
|
||||
|
||||
def get_attachments(self):
|
||||
attachments = frappe.get_all(
|
||||
return frappe.get_all(
|
||||
"File",
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE},
|
||||
filters={
|
||||
"attached_to_name": self.name,
|
||||
"attached_to_doctype": self.DOCTYPE,
|
||||
},
|
||||
)
|
||||
return attachments
|
||||
|
||||
def notify_change(self, action):
|
||||
frappe.publish_realtime(
|
||||
|
|
@ -551,9 +553,7 @@ def get_emails(email_strings: list[str]) -> list[str]:
|
|||
for email_string in email_strings:
|
||||
if email_string:
|
||||
result = getaddresses([email_string])
|
||||
for email in result:
|
||||
email_addrs.append(email[1])
|
||||
|
||||
email_addrs.extend(email[1] for email in result)
|
||||
return email_addrs
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
from collections.abc import Iterable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
|
|
@ -37,7 +38,7 @@ def make(
|
|||
send_email=False,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
attachments="[]",
|
||||
attachments=None,
|
||||
send_me_a_copy=False,
|
||||
cc=None,
|
||||
bcc=None,
|
||||
|
|
@ -60,7 +61,7 @@ def make(
|
|||
:param send_email: Send via email (default **False**).
|
||||
:param print_html: HTML Print format to be sent as attachment.
|
||||
:param print_format: Print Format name of parent document to be sent as attachment.
|
||||
:param attachments: List of attachments as list of files or JSON string.
|
||||
:param attachments: List of File names or dicts with keys "fname" and "fcontent"
|
||||
:param send_me_a_copy: Send a copy to the sender (default **False**).
|
||||
:param email_template: Template which is used to compose mail .
|
||||
"""
|
||||
|
|
@ -114,7 +115,7 @@ def _make(
|
|||
send_email=False,
|
||||
print_html=None,
|
||||
print_format=None,
|
||||
attachments="[]",
|
||||
attachments=None,
|
||||
send_me_a_copy=False,
|
||||
cc=None,
|
||||
bcc=None,
|
||||
|
|
@ -218,26 +219,41 @@ def set_incoming_outgoing_accounts(doc):
|
|||
doc.db_set("email_account", doc.outgoing_email_account.name)
|
||||
|
||||
|
||||
def add_attachments(name, attachments):
|
||||
"""Add attachments to the given Communication"""
|
||||
def add_attachments(name: str, attachments: Iterable[str | dict]) -> None:
|
||||
"""Add attachments to the given Communication
|
||||
|
||||
:param name: Communication name
|
||||
:param attachments: File names or dicts with keys "fname" and "fcontent"
|
||||
"""
|
||||
# loop through attachments
|
||||
for a in attachments:
|
||||
if isinstance(a, str):
|
||||
attach = frappe.db.get_value(
|
||||
"File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1
|
||||
)
|
||||
# save attachments to new doc
|
||||
_file = frappe.get_doc(
|
||||
{
|
||||
"doctype": "File",
|
||||
"file_url": attach.file_url,
|
||||
"attached_to_doctype": "Communication",
|
||||
"attached_to_name": name,
|
||||
"folder": "Home/Attachments",
|
||||
"is_private": attach.is_private,
|
||||
}
|
||||
)
|
||||
_file.save(ignore_permissions=True)
|
||||
attach = frappe.db.get_value("File", {"name": a}, ["file_url", "is_private"], as_dict=1)
|
||||
file_args = {
|
||||
"file_url": attach.file_url,
|
||||
"is_private": attach.is_private,
|
||||
}
|
||||
elif isinstance(a, dict) and "fcontent" in a and "fname" in a:
|
||||
# dict returned by frappe.attach_print()
|
||||
file_args = {
|
||||
"file_name": a["fname"],
|
||||
"content": a["fcontent"],
|
||||
"is_private": 1,
|
||||
}
|
||||
else:
|
||||
continue
|
||||
|
||||
file_args.update(
|
||||
{
|
||||
"attached_to_doctype": "Communication",
|
||||
"attached_to_name": name,
|
||||
"folder": "Home/Attachments",
|
||||
}
|
||||
)
|
||||
|
||||
_file = frappe.new_doc("File")
|
||||
_file.update(file_args)
|
||||
_file.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True, methods=("GET",))
|
||||
|
|
|
|||
|
|
@ -189,9 +189,7 @@ class CommunicationEmailMixin:
|
|||
}
|
||||
final_attachments.append(d)
|
||||
|
||||
for a in self.get_attachments() or []:
|
||||
final_attachments.append({"fid": a["name"]})
|
||||
|
||||
final_attachments.extend({"fid": a["name"]} for a in self.get_attachments() or [])
|
||||
return final_attachments
|
||||
|
||||
def get_unsubscribe_message(self):
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from urllib.parse import quote
|
|||
|
||||
import frappe
|
||||
from frappe.core.doctype.communication.communication import Communication, get_emails
|
||||
from frappe.core.doctype.communication.email import add_attachments
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
|
@ -374,6 +375,39 @@ class TestCommunicationEmailMixin(FrappeTestCase):
|
|||
doc.delete()
|
||||
comm.delete()
|
||||
|
||||
def test_add_attachments_by_filename(self):
|
||||
to_list = ["to <to@test.com>"]
|
||||
comm = self.new_communication(recipients=to_list)
|
||||
|
||||
file = frappe.new_doc("File")
|
||||
file.file_name = "test_add_attachments_by_filename.txt"
|
||||
file.content = "test_add_attachments_by_filename"
|
||||
file.insert(ignore_permissions=True)
|
||||
|
||||
add_attachments(comm.name, [file.name])
|
||||
|
||||
attached_file_name, attached_content_hash = frappe.db.get_value(
|
||||
"File",
|
||||
{"attached_to_name": comm.name, "attached_to_doctype": comm.doctype},
|
||||
["file_name", "content_hash"],
|
||||
)
|
||||
self.assertEqual(attached_content_hash, file.content_hash)
|
||||
self.assertEqual(attached_file_name, file.file_name)
|
||||
|
||||
def test_add_attachments_by_file_content(self):
|
||||
to_list = ["to <to@test.com>"]
|
||||
comm = self.new_communication(recipients=to_list)
|
||||
file_name = "test_add_attachments_by_file_content.txt"
|
||||
file_content = "test_add_attachments_by_file_content"
|
||||
add_attachments(comm.name, [{"fcontent": file_content, "fname": file_name}])
|
||||
attached_file_name = frappe.db.get_value(
|
||||
"File",
|
||||
{"attached_to_name": comm.name, "attached_to_doctype": comm.doctype},
|
||||
)
|
||||
attached_file = frappe.get_doc("File", attached_file_name)
|
||||
self.assertEqual(attached_file.file_name, file_name)
|
||||
self.assertEqual(attached_file.get_content(), file_content)
|
||||
|
||||
|
||||
def create_email_account() -> "EmailAccount":
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
|
|
|||
|
|
@ -120,9 +120,10 @@ class DataExporter:
|
|||
self.column_start_end = {}
|
||||
|
||||
if self.all_doctypes:
|
||||
self.child_doctypes = []
|
||||
for df in frappe.get_meta(self.doctype).get_table_fields():
|
||||
self.child_doctypes.append(dict(doctype=df.options, parentfield=df.fieldname))
|
||||
self.child_doctypes = [
|
||||
dict(doctype=df.options, parentfield=df.fieldname)
|
||||
for df in frappe.get_meta(self.doctype).get_table_fields()
|
||||
]
|
||||
|
||||
def build_response(self):
|
||||
self.writer = UnicodeWriter()
|
||||
|
|
|
|||
|
|
@ -606,8 +606,6 @@ class ImportFile:
|
|||
|
||||
|
||||
class Row:
|
||||
link_values_exist_map = {}
|
||||
|
||||
def __init__(self, index, row, doctype, header, import_type):
|
||||
self.index = index
|
||||
self.row_number = index + 1
|
||||
|
|
@ -640,8 +638,7 @@ class Row:
|
|||
return None
|
||||
|
||||
columns = self.header.get_columns(col_indexes)
|
||||
doc = self._parse_doc(doctype, columns, values, parent_doc, table_df)
|
||||
return doc
|
||||
return self._parse_doc(doctype, columns, values, parent_doc, table_df)
|
||||
|
||||
def _parse_doc(self, doctype, columns, values, parent_doc=None, table_df=None):
|
||||
doc = frappe._dict()
|
||||
|
|
@ -749,10 +746,7 @@ class Row:
|
|||
return value
|
||||
|
||||
def link_exists(self, value, df):
|
||||
key = df.options + "::" + cstr(value)
|
||||
if Row.link_values_exist_map.get(key) is None:
|
||||
Row.link_values_exist_map[key] = frappe.db.exists(df.options, value)
|
||||
return Row.link_values_exist_map.get(key)
|
||||
return bool(frappe.db.exists(df.options, value, cache=True))
|
||||
|
||||
def parse_value(self, value, col):
|
||||
df = col.df
|
||||
|
|
@ -848,9 +842,6 @@ class Header(Row):
|
|||
|
||||
|
||||
class Column:
|
||||
seen = []
|
||||
fields_column_map = {}
|
||||
|
||||
def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None):
|
||||
if seen is None:
|
||||
seen = []
|
||||
|
|
|
|||
|
|
@ -126,14 +126,17 @@ class DocField(Document):
|
|||
if self.fieldtype == "Table MultiSelect":
|
||||
table_doctype = self.options
|
||||
|
||||
link_doctype = frappe.db.get_value(
|
||||
return frappe.db.get_value(
|
||||
"DocField",
|
||||
{"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1},
|
||||
{
|
||||
"fieldtype": "Link",
|
||||
"parenttype": "DocType",
|
||||
"parent": table_doctype,
|
||||
"in_list_view": 1,
|
||||
},
|
||||
"options",
|
||||
)
|
||||
|
||||
return link_doctype
|
||||
|
||||
def get_select_options(self):
|
||||
if self.fieldtype == "Select":
|
||||
options = self.options or ""
|
||||
|
|
|
|||
|
|
@ -91,7 +91,8 @@
|
|||
"column_break_51",
|
||||
"email_append_to",
|
||||
"sender_field",
|
||||
"subject_field"
|
||||
"subject_field",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -231,6 +232,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "form_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Form Settings"
|
||||
|
|
@ -304,6 +306,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "view_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "View Settings"
|
||||
|
|
@ -414,7 +417,7 @@
|
|||
"oldfieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.custom===0",
|
||||
"depends_on": "eval:doc.custom===0 && !doc.istable",
|
||||
"fieldname": "web_view",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Web View"
|
||||
|
|
@ -482,6 +485,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "actions",
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "actions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Actions"
|
||||
|
|
@ -495,6 +499,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "links",
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "links_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Linked Documents"
|
||||
|
|
@ -526,6 +531,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "email_settings_sb",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Email Settings"
|
||||
|
|
@ -538,7 +544,6 @@
|
|||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "is_virtual",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Virtual"
|
||||
|
|
@ -579,6 +584,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.istable",
|
||||
"fieldname": "document_states_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Document States"
|
||||
|
|
@ -648,6 +654,12 @@
|
|||
"fieldname": "fields_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Fields"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
|
|
@ -730,7 +742,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2023-07-12 13:56:26.185637",
|
||||
"modified": "2023-08-23 15:09:08.789467",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ from frappe.model.meta import Meta
|
|||
from frappe.modules import get_doc_path, make_boilerplate
|
||||
from frappe.modules.import_file import get_file_path
|
||||
from frappe.query_builder.functions import Concat
|
||||
from frappe.utils import cint, flt, get_table_name, random_string
|
||||
from frappe.utils import cint, flt, is_a_property, random_string
|
||||
from frappe.website.utils import clear_cache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -1542,9 +1542,9 @@ def validate_fields(meta):
|
|||
return
|
||||
|
||||
doctype = docfield.options
|
||||
meta = frappe.get_meta(doctype)
|
||||
child_doctype_meta = frappe.get_meta(doctype)
|
||||
|
||||
if not meta.istable:
|
||||
if not child_doctype_meta.istable:
|
||||
frappe.throw(
|
||||
_("Option {0} for field {1} is not a child table").format(
|
||||
frappe.bold(doctype), frappe.bold(docfield.fieldname)
|
||||
|
|
@ -1552,6 +1552,15 @@ def validate_fields(meta):
|
|||
title=_("Invalid Option"),
|
||||
)
|
||||
|
||||
if not (meta.is_virtual == child_doctype_meta.is_virtual):
|
||||
error_msg = " should be virtual." if meta.is_virtual else " cannot be virtual."
|
||||
frappe.throw(
|
||||
_("Child Table {0} for field {1}" + error_msg).format(
|
||||
frappe.bold(doctype), frappe.bold(docfield.fieldname)
|
||||
),
|
||||
title=_("Invalid Option"),
|
||||
)
|
||||
|
||||
def check_max_height(docfield):
|
||||
if getattr(docfield, "max_height", None) and (docfield.max_height[-2:] not in ("px", "em")):
|
||||
frappe.throw(f"Max for {frappe.bold(docfield.fieldname)} height must be in px, em, rem")
|
||||
|
|
@ -1811,13 +1820,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
|
|||
raise
|
||||
|
||||
|
||||
def is_a_property(x) -> bool:
|
||||
"""Get properties (@property, @cached_property) in a controller class"""
|
||||
from functools import cached_property
|
||||
|
||||
return isinstance(x, (property, cached_property))
|
||||
|
||||
|
||||
def check_fieldname_conflicts(docfield):
|
||||
"""Checks if fieldname conflicts with methods or properties"""
|
||||
doc = frappe.get_doc({"doctype": docfield.dt})
|
||||
|
|
|
|||
|
|
@ -567,20 +567,10 @@ class TestDocType(FrappeTestCase):
|
|||
"options": "Test Virtual DocType as Child Table",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.exceptions.ValidationError, parent_doc.insert)
|
||||
parent_doc.is_virtual = 1
|
||||
parent_doc.insert(ignore_permissions=True)
|
||||
|
||||
# create entry for parent doctype
|
||||
parent_doc_entry = frappe.get_doc(
|
||||
{"doctype": "Test Parent Virtual DocType", "some_fieldname": "Test"}
|
||||
)
|
||||
parent_doc_entry.insert(ignore_permissions=True)
|
||||
|
||||
# update the parent doc (should not abort because of any DB query to a virtual child table, as there is none)
|
||||
parent_doc_entry.some_fieldname = "Test update"
|
||||
parent_doc_entry.save(ignore_permissions=True)
|
||||
|
||||
# delete the parent doc (should not abort because of any DB query to a virtual child table, as there is none)
|
||||
parent_doc_entry.delete()
|
||||
self.assertFalse(frappe.db.table_exists("Test Parent Virtual DocType"))
|
||||
|
||||
def test_default_fieldname(self):
|
||||
fields = [
|
||||
|
|
@ -777,6 +767,7 @@ def new_doctype(
|
|||
unique: bool = False,
|
||||
depends_on: str = "",
|
||||
fields: list[dict] | None = None,
|
||||
custom: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
if not name:
|
||||
|
|
@ -787,7 +778,7 @@ def new_doctype(
|
|||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"custom": custom,
|
||||
"fields": [
|
||||
{
|
||||
"label": "Some Field",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"reference_name",
|
||||
"section_break_5",
|
||||
"method",
|
||||
"error"
|
||||
"error",
|
||||
"trace_id"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -57,13 +58,19 @@
|
|||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "trace_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Trace ID",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-warning-sign",
|
||||
"idx": 1,
|
||||
"in_create": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-13 06:34:05.158606",
|
||||
"modified": "2023-08-23 14:20:15.343339",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Error Log",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ class ErrorLog(Document):
|
|||
reference_doctype: DF.Link | None
|
||||
reference_name: DF.Data | None
|
||||
seen: DF.Check
|
||||
trace_id: DF.Data | None
|
||||
# end: auto-generated types
|
||||
def onload(self):
|
||||
if not self.seen and not frappe.flags.read_only:
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ frappe.ui.form.on("File", {
|
|||
|
||||
preview_file: function (frm) {
|
||||
let $preview = "";
|
||||
let file_name = frm.doc.file_name.split("?")[0];
|
||||
let file_extension = file_name.split(".").pop()?.toLowerCase();
|
||||
let file_extension = frm.doc.file_type.toLowerCase();
|
||||
|
||||
if (frappe.utils.is_image_file(frm.doc.file_url)) {
|
||||
$preview = $(`<div class="img_preview">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@
|
|||
"field_order": [
|
||||
"file_name",
|
||||
"is_private",
|
||||
"column_break_7jmm",
|
||||
"file_type",
|
||||
"preview",
|
||||
"preview_html",
|
||||
"section_break_5",
|
||||
|
|
@ -169,13 +171,25 @@
|
|||
"fieldtype": "Check",
|
||||
"label": "Uploaded To Google Drive",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7jmm",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "file_type",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "File Type",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"force_re_route_to_default_view": 1,
|
||||
"icon": "fa fa-file",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-02 09:43:51.178011",
|
||||
"modified": "2023-08-02 09:43:51.178012",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "File",
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ class File(Document):
|
|||
content_hash: DF.Data | None
|
||||
file_name: DF.Data | None
|
||||
file_size: DF.Int
|
||||
file_type: DF.Data | None
|
||||
file_url: DF.Code | None
|
||||
folder: DF.Link | None
|
||||
is_attachments_folder: DF.Check
|
||||
|
|
@ -86,6 +87,7 @@ class File(Document):
|
|||
self.set_folder_name()
|
||||
self.set_file_name()
|
||||
self.validate_attachment_limit()
|
||||
self.set_file_type()
|
||||
|
||||
if self.is_folder:
|
||||
return
|
||||
|
|
@ -330,6 +332,17 @@ class File(Document):
|
|||
elif not self.is_home_folder:
|
||||
self.folder = "Home"
|
||||
|
||||
def set_file_type(self):
|
||||
if self.is_folder:
|
||||
return
|
||||
|
||||
file_type = mimetypes.guess_type(self.file_name)[0]
|
||||
if not file_type:
|
||||
return
|
||||
|
||||
file_extension = mimetypes.guess_extension(file_type)
|
||||
self.file_type = file_extension.lstrip(".").upper() if file_extension else None
|
||||
|
||||
def validate_file_on_disk(self):
|
||||
"""Validates existence file"""
|
||||
full_path = self.get_full_path()
|
||||
|
|
@ -734,6 +747,8 @@ class File(Document):
|
|||
continue
|
||||
if _file.is_folder:
|
||||
continue
|
||||
if not has_permission(_file, "read"):
|
||||
continue
|
||||
zf.writestr(_file.file_name, _file.get_content())
|
||||
zf.close()
|
||||
return zip_file.getvalue()
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ class ModuleDef(Document):
|
|||
module_name: DF.Data
|
||||
package: DF.Link | None
|
||||
restrict_to_domain: DF.Link | None
|
||||
|
||||
# end: auto-generated types
|
||||
def on_update(self):
|
||||
"""If in `developer_mode`, create folder for module and
|
||||
|
|
|
|||
|
|
@ -53,5 +53,4 @@ def get_app_logo():
|
|||
|
||||
|
||||
def get_navbar_settings():
|
||||
navbar_settings = frappe.get_single("Navbar Settings")
|
||||
return navbar_settings
|
||||
return frappe.get_single("Navbar Settings")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ import os
|
|||
import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
LICENSES = (
|
||||
"GNU Affero General Public License",
|
||||
"GNU General Public License",
|
||||
"MIT License",
|
||||
)
|
||||
|
||||
|
||||
class Package(Document):
|
||||
# begin: auto-generated types
|
||||
|
|
@ -29,6 +35,7 @@ class Package(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_license_text(license_type):
|
||||
with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile:
|
||||
return textfile.read()
|
||||
def get_license_text(license_type: str) -> str | None:
|
||||
if license_type in LICENSES:
|
||||
with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile:
|
||||
return textfile.read()
|
||||
|
|
|
|||
|
|
@ -4,9 +4,5 @@
|
|||
frappe.ui.form.on("Patch Log", {
|
||||
refresh: function (frm) {
|
||||
frm.disable_save();
|
||||
|
||||
frm.add_custom_button(__("Re-Run Patch"), () => {
|
||||
frm.call("rerun_patch");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
|
|
@ -21,15 +20,7 @@ class PatchLog(Document):
|
|||
skipped: DF.Check
|
||||
traceback: DF.Code | None
|
||||
# end: auto-generated types
|
||||
@frappe.whitelist()
|
||||
def rerun_patch(self):
|
||||
from frappe.modules.patch_handler import run_single
|
||||
|
||||
if not frappe.conf.developer_mode:
|
||||
frappe.throw(_("Re-running patch is only allowed in developer mode."))
|
||||
|
||||
run_single(self.patch, force=True)
|
||||
frappe.msgprint(_("Successfully re-ran patch: {0}").format(self.patch), alert=True)
|
||||
pass
|
||||
|
||||
|
||||
def before_migrate():
|
||||
|
|
|
|||
|
|
@ -61,13 +61,13 @@ class PreparedReport(Document):
|
|||
self.status = "Queued"
|
||||
|
||||
def on_trash(self):
|
||||
# If job is running then send stop signal.
|
||||
if self.status != "Started":
|
||||
"""Remove pending job from queue, if already running then kill the job."""
|
||||
if self.status not in ("Started", "Queued"):
|
||||
return
|
||||
|
||||
with suppress(Exception):
|
||||
job = frappe.get_doc("RQ Job", self.job_id)
|
||||
job.stop_job()
|
||||
job.stop_job() if self.status == "Started" else job.delete()
|
||||
|
||||
def after_insert(self):
|
||||
enqueue(
|
||||
|
|
@ -168,7 +168,7 @@ def process_filters_for_prepared_report(filters: dict[str, Any] | str) -> str:
|
|||
|
||||
@frappe.whitelist()
|
||||
def get_reports_in_queued_state(report_name, filters):
|
||||
reports = frappe.get_all(
|
||||
return frappe.get_all(
|
||||
"Prepared Report",
|
||||
filters={
|
||||
"report_name": report_name,
|
||||
|
|
@ -177,7 +177,6 @@ def get_reports_in_queued_state(report_name, filters):
|
|||
"owner": frappe.session.user,
|
||||
},
|
||||
)
|
||||
return reports
|
||||
|
||||
|
||||
def get_completed_prepared_report(filters, user, report_name):
|
||||
|
|
@ -211,9 +210,9 @@ def expire_stalled_report():
|
|||
def delete_prepared_reports(reports):
|
||||
reports = frappe.parse_json(reports)
|
||||
for report in reports:
|
||||
frappe.delete_doc(
|
||||
"Prepared Report", report["name"], ignore_permissions=True, delete_permanently=True
|
||||
)
|
||||
prepared_report = frappe.get_doc("Prepared Report", report["name"])
|
||||
if prepared_report.has_permission():
|
||||
prepared_report.delete(ignore_permissions=True, delete_permanently=True)
|
||||
|
||||
|
||||
def create_json_gz_file(data, dt, dn):
|
||||
|
|
|
|||
58
frappe/core/doctype/recorder/recorder.js
Normal file
58
frappe/core/doctype/recorder/recorder.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Copyright (c) 2023, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Recorder Query", "form_render", function (frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let stack = JSON.parse(row.stack);
|
||||
render_html_field(stack, "stack_html", __("Stack Trace"));
|
||||
|
||||
let explain_result = JSON.parse(row.explain_result);
|
||||
render_html_field(explain_result, "sql_explain_html", __("SQL Explain"));
|
||||
|
||||
function render_html_field(parsed_json, fieldname, label) {
|
||||
let html =
|
||||
"<div class='clearfix'><label class='control-label'>" + label + "</label></div>";
|
||||
if (parsed_json.length == 0) {
|
||||
html += "<label class='control-label'>None</label>";
|
||||
} else {
|
||||
html = create_html_table(parsed_json, html);
|
||||
}
|
||||
|
||||
let field_wrapper =
|
||||
frm.fields_dict[row.parentfield].grid.grid_rows_by_docname[cdn].grid_form.fields_dict[
|
||||
fieldname
|
||||
].wrapper;
|
||||
$(html).appendTo(field_wrapper);
|
||||
}
|
||||
|
||||
function create_html_table(table_content, html) {
|
||||
html += `
|
||||
<div class='control-value like-disabled-input for-description'
|
||||
style='overflow:auto; padding:0px'>
|
||||
<table class='table table-striped' style='margin:0px'>
|
||||
<thead>
|
||||
<tr>
|
||||
${Object.keys(table_content[0])
|
||||
.map((key) => `<th>${key}<th>`)
|
||||
.join("")}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${table_content
|
||||
.map((content) => {
|
||||
return `
|
||||
<tr>
|
||||
${Object.values(content)
|
||||
.map((key) => `<td>${key}<td>`)
|
||||
.join("")}
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
return html;
|
||||
}
|
||||
});
|
||||
126
frappe/core/doctype/recorder/recorder.json
Normal file
126
frappe/core/doctype/recorder/recorder.json
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2023-08-01 12:06:49.630877",
|
||||
"default_view": "List",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"path",
|
||||
"number_of_queries",
|
||||
"time_in_queries",
|
||||
"method",
|
||||
"column_break_qo53",
|
||||
"cmd",
|
||||
"time",
|
||||
"duration",
|
||||
"section_break_1skt",
|
||||
"request_headers",
|
||||
"section_break_sgro",
|
||||
"form_dict",
|
||||
"section_break_9jhm",
|
||||
"sql_queries"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "path",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Path"
|
||||
},
|
||||
{
|
||||
"fieldname": "cmd",
|
||||
"fieldtype": "Data",
|
||||
"in_standard_filter": 1,
|
||||
"label": "CMD"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "time",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "number_of_queries",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Number of Queries"
|
||||
},
|
||||
{
|
||||
"fieldname": "time_in_queries",
|
||||
"fieldtype": "Float",
|
||||
"label": "Time in Queries"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qo53",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_1skt",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "request_headers",
|
||||
"fieldtype": "Code",
|
||||
"label": "Request Headers"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_sgro",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "form_dict",
|
||||
"fieldtype": "Code",
|
||||
"label": "Form Dict"
|
||||
},
|
||||
{
|
||||
"fieldname": "method",
|
||||
"fieldtype": "Select",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Method",
|
||||
"options": "GET\nPOST\nPUT\nDELETE\nPATCH\nHEAD\nOPTIONS"
|
||||
},
|
||||
{
|
||||
"fieldname": "sql_queries",
|
||||
"fieldtype": "Table",
|
||||
"label": "SQL Queries",
|
||||
"options": "Recorder Query"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_9jhm",
|
||||
"fieldtype": "Section Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_virtual": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 12:01:03.456643",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Recorder",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Administrator",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "duration",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "path"
|
||||
}
|
||||
102
frappe/core/doctype/recorder/recorder.py
Normal file
102
frappe/core/doctype/recorder/recorder.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.recorder import get as get_recorder_data
|
||||
from frappe.utils import cint, evaluate_filters, make_filter_dict
|
||||
|
||||
|
||||
class Recorder(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.core.doctype.recorder_query.recorder_query import RecorderQuery
|
||||
from frappe.types import DF
|
||||
|
||||
cmd: DF.Data | None
|
||||
duration: DF.Float
|
||||
form_dict: DF.Code | None
|
||||
method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]
|
||||
number_of_queries: DF.Int
|
||||
path: DF.Data | None
|
||||
request_headers: DF.Code | None
|
||||
sql_queries: DF.Table[RecorderQuery]
|
||||
time: DF.Datetime | None
|
||||
time_in_queries: DF.Float
|
||||
# end: auto-generated types
|
||||
|
||||
def load_from_db(self):
|
||||
request_data = get_recorder_data(self.name)
|
||||
if not request_data:
|
||||
raise frappe.DoesNotExistError
|
||||
request = serialize_request(request_data)
|
||||
super(Document, self).__init__(request)
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
start = cint(args.get("start")) or 0
|
||||
page_length = cint(args.get("page_length")) or 20
|
||||
requests = Recorder.get_filtered_requests(args)[start : start + page_length]
|
||||
|
||||
if order_by_statment := args.get("order_by"):
|
||||
if "." in order_by_statment:
|
||||
order_by_statment = order_by_statment.split(".")[1]
|
||||
|
||||
if " " in order_by_statment:
|
||||
sort_key, sort_order = order_by_statment.split(" ", 1)
|
||||
else:
|
||||
sort_key = order_by_statment
|
||||
sort_order = "desc"
|
||||
|
||||
sort_key = sort_key.replace("`", "")
|
||||
return sorted(requests, key=lambda r: r.get(sort_key) or 0, reverse=bool(sort_order == "desc"))
|
||||
|
||||
return sorted(requests, key=lambda r: r.duration, reverse=1)
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
return len(Recorder.get_filtered_requests(args))
|
||||
|
||||
@staticmethod
|
||||
def get_filtered_requests(args):
|
||||
filters = args.get("filters")
|
||||
requests = [serialize_request(request) for request in get_recorder_data()]
|
||||
return [req for req in requests if evaluate_filters(req, filters)]
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def delete(self):
|
||||
pass
|
||||
|
||||
def db_insert(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def db_update(self):
|
||||
pass
|
||||
|
||||
|
||||
def serialize_request(request):
|
||||
request = frappe._dict(request)
|
||||
if request.get("calls"):
|
||||
for i in request.calls:
|
||||
i["stack"] = frappe.as_json(i["stack"])
|
||||
i["explain_result"] = frappe.as_json(i["explain_result"])
|
||||
request.update(
|
||||
name=request.get("uuid"),
|
||||
number_of_queries=request.get("queries"),
|
||||
time_in_queries=request.get("time_queries"),
|
||||
request_headers=frappe.as_json(request.get("headers"), indent=4),
|
||||
form_dict=frappe.as_json(request.get("form_dict"), indent=4),
|
||||
sql_queries=request.get("calls"),
|
||||
modified=request.get("time"),
|
||||
creation=request.get("time"),
|
||||
)
|
||||
|
||||
return request
|
||||
110
frappe/core/doctype/recorder/recorder_list.js
Normal file
110
frappe/core/doctype/recorder/recorder_list.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
frappe.listview_settings["Recorder"] = {
|
||||
hide_name_column: true,
|
||||
|
||||
onload(listview) {
|
||||
listview.page.sidebar.remove();
|
||||
if (!has_common(frappe.user_roles, ["Administrator", "System Manager"])) return;
|
||||
|
||||
if (listview.list_view_settings) {
|
||||
listview.list_view_settings.disable_comment_count = true;
|
||||
}
|
||||
|
||||
listview.page.add_button(__("Clear"), () => {
|
||||
frappe.call({
|
||||
method: "frappe.recorder.delete",
|
||||
callback: function () {
|
||||
listview.refresh();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
listview.page.add_menu_item(__("Import"), () => {
|
||||
new frappe.ui.FileUploader({
|
||||
folder: this.current_folder,
|
||||
on_success: (file) => {
|
||||
if (cur_list.data.length > 0) {
|
||||
// don't replace existing capture
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "frappe.recorder.import_data",
|
||||
args: {
|
||||
file: file.file_url,
|
||||
},
|
||||
callback: function () {
|
||||
listview.refresh();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
listview.page.add_menu_item(__("Export"), () => {
|
||||
frappe.call({
|
||||
method: "frappe.recorder.export_data",
|
||||
callback: function (r) {
|
||||
const data = r.message;
|
||||
const filename = `${data[0]["uuid"]}..${data[data.length - 1]["uuid"]}.json`;
|
||||
|
||||
const el = document.createElement("a");
|
||||
el.setAttribute(
|
||||
"href",
|
||||
"data:application/json," + encodeURIComponent(JSON.stringify(data))
|
||||
);
|
||||
el.setAttribute("download", filename);
|
||||
el.click();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
if (listview.list_view_settings.disable_auto_refresh) {
|
||||
return;
|
||||
}
|
||||
if (!listview.enabled) return;
|
||||
|
||||
const route = frappe.get_route() || [];
|
||||
if (route[0] != "List" || "Recorder" != route[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
listview.refresh();
|
||||
}, 5000);
|
||||
},
|
||||
|
||||
refresh(listview) {
|
||||
this.fetch_recorder_status(listview).then(() => this.refresh_controls(listview));
|
||||
},
|
||||
|
||||
refresh_controls(listview) {
|
||||
this.setup_recorder_controls(listview);
|
||||
this.update_indicators(listview);
|
||||
},
|
||||
|
||||
fetch_recorder_status(listview) {
|
||||
return frappe.xcall("frappe.recorder.status").then((status) => {
|
||||
listview.enabled = Boolean(status);
|
||||
});
|
||||
},
|
||||
|
||||
setup_recorder_controls(listview) {
|
||||
listview.page.set_primary_action(listview.enabled ? __("Stop") : __("Start"), () => {
|
||||
frappe.call({
|
||||
method: listview.enabled ? "frappe.recorder.stop" : "frappe.recorder.start",
|
||||
callback: function () {
|
||||
listview.refresh();
|
||||
},
|
||||
});
|
||||
listview.enabled = !listview.enabled;
|
||||
this.refresh_controls(listview);
|
||||
});
|
||||
},
|
||||
|
||||
update_indicators(listview) {
|
||||
if (listview.enabled) {
|
||||
listview.page.set_indicator(__("Active"), "green");
|
||||
} else {
|
||||
listview.page.set_indicator(__("Inactive"), "red");
|
||||
}
|
||||
},
|
||||
};
|
||||
77
frappe/core/doctype/recorder/test_recorder.py
Normal file
77
frappe/core/doctype/recorder/test_recorder.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
import re
|
||||
|
||||
import frappe
|
||||
import frappe.recorder
|
||||
from frappe.core.doctype.recorder.recorder import serialize_request
|
||||
from frappe.recorder import get as get_recorder_data
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import set_request
|
||||
|
||||
|
||||
class TestRecorder(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.start_recoder()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.recorder.stop()
|
||||
|
||||
def start_recoder(self):
|
||||
frappe.recorder.stop()
|
||||
frappe.recorder.delete()
|
||||
set_request(path="/api/method/ping")
|
||||
frappe.recorder.start()
|
||||
frappe.recorder.record()
|
||||
|
||||
def stop_recorder(self):
|
||||
frappe.recorder.dump()
|
||||
|
||||
def test_recorder_list(self):
|
||||
frappe.get_all("User") # trigger one query
|
||||
self.stop_recorder()
|
||||
requests = frappe.get_all("Recorder")
|
||||
self.assertGreaterEqual(len(requests), 1)
|
||||
request = frappe.get_doc("Recorder", requests[0].name)
|
||||
self.assertGreaterEqual(len(request.sql_queries), 1)
|
||||
queries = [sql_query.query for sql_query in request.sql_queries]
|
||||
match_flag = 0
|
||||
for query in queries:
|
||||
if bool(re.match("^[select.*from `tabUser`]", query, flags=re.IGNORECASE)):
|
||||
match_flag = 1
|
||||
break
|
||||
self.assertEqual(match_flag, 1)
|
||||
|
||||
def test_recorder_list_filters(self):
|
||||
user = frappe.qb.DocType("User")
|
||||
frappe.qb.from_(user).select("name").run()
|
||||
self.stop_recorder()
|
||||
|
||||
set_request(path="/api/method/abc")
|
||||
frappe.recorder.start()
|
||||
frappe.recorder.record()
|
||||
frappe.get_all("User")
|
||||
self.stop_recorder()
|
||||
|
||||
requests = frappe.get_list(
|
||||
"Recorder", filters={"path": ("like", "/api/method/ping"), "number_of_queries": 1}
|
||||
)
|
||||
self.assertGreaterEqual(len(requests), 1)
|
||||
requests = frappe.get_list("Recorder", filters={"path": ("like", "/api/method/test")})
|
||||
self.assertEqual(len(requests), 0)
|
||||
|
||||
requests = frappe.get_list("Recorder", filters={"method": "GET"})
|
||||
self.assertGreaterEqual(len(requests), 1)
|
||||
requests = frappe.get_list("Recorder", filters={"method": "POST"})
|
||||
self.assertEqual(len(requests), 0)
|
||||
|
||||
requests = frappe.get_list("Recorder", order_by="path desc")
|
||||
self.assertEqual(requests[0].path, "/api/method/ping")
|
||||
|
||||
def test_recorder_serialization(self):
|
||||
frappe.get_all("User") # trigger one query
|
||||
self.stop_recorder()
|
||||
requests = frappe.get_all("Recorder")
|
||||
request_doc = get_recorder_data(requests[0].name)
|
||||
self.assertIsInstance(serialize_request(request_doc), dict)
|
||||
0
frappe/core/doctype/recorder_query/__init__.py
Normal file
0
frappe/core/doctype/recorder_query/__init__.py
Normal file
8
frappe/core/doctype/recorder_query/recorder_query.js
Normal file
8
frappe/core/doctype/recorder_query/recorder_query.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2023, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("Recorder Query", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
106
frappe/core/doctype/recorder_query/recorder_query.json
Normal file
106
frappe/core/doctype/recorder_query/recorder_query.json
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2023-08-01 17:04:12.173774",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"index",
|
||||
"query",
|
||||
"duration",
|
||||
"column_break_qmju",
|
||||
"exact_copies",
|
||||
"normalized_query",
|
||||
"normalized_copies",
|
||||
"section_break_dygy",
|
||||
"stack_html",
|
||||
"stack",
|
||||
"section_break_kvkb",
|
||||
"sql_explain_html",
|
||||
"explain_result"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "query",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Query",
|
||||
"length": 2
|
||||
},
|
||||
{
|
||||
"fieldname": "normalized_query",
|
||||
"fieldtype": "Data",
|
||||
"label": "Normalized Query"
|
||||
},
|
||||
{
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "exact_copies",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Exact Copies"
|
||||
},
|
||||
{
|
||||
"fieldname": "normalized_copies",
|
||||
"fieldtype": "Int",
|
||||
"label": "Normalized Copies"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_qmju",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_dygy",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "stack",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "stack_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Stack Trace"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_kvkb",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "explain_result",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sql_explain_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "SQL Explain"
|
||||
},
|
||||
{
|
||||
"fieldname": "index",
|
||||
"fieldtype": "Int",
|
||||
"in_list_view": 1,
|
||||
"label": "Index"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-07 13:12:23.496002",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Recorder Query",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
53
frappe/core/doctype/recorder_query/recorder_query.py
Normal file
53
frappe/core/doctype/recorder_query/recorder_query.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class RecorderQuery(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
duration: DF.Float
|
||||
exact_copies: DF.Int
|
||||
explain_result: DF.Text | None
|
||||
index: DF.Int
|
||||
normalized_copies: DF.Int
|
||||
normalized_query: DF.Data | None
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
query: DF.Data
|
||||
stack: DF.Text | None
|
||||
# end: auto-generated types
|
||||
pass
|
||||
|
||||
def db_insert(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def load_from_db(self):
|
||||
pass
|
||||
|
||||
def db_update(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_list(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_count(args):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def get_stats(args):
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
pass
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# Copyright (c) 2023, Frappe Technologies and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestRecorderQuery(FrappeTestCase):
|
||||
pass
|
||||
|
|
@ -290,10 +290,11 @@ class Report(Document):
|
|||
columns = params.get("fields")
|
||||
else:
|
||||
columns = [["name", self.ref_doctype]]
|
||||
for df in frappe.get_meta(self.ref_doctype).fields:
|
||||
if df.in_list_view:
|
||||
columns.append([df.fieldname, self.ref_doctype])
|
||||
|
||||
columns.extend(
|
||||
[df.fieldname, self.ref_doctype]
|
||||
for df in frappe.get_meta(self.ref_doctype).fields
|
||||
if df.in_list_view
|
||||
)
|
||||
return columns
|
||||
|
||||
def get_standard_report_filters(self, params, filters):
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ test_dependencies = ["User"]
|
|||
|
||||
|
||||
class TestReport(FrappeTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.enable_safe_exec()
|
||||
return super().setUpClass()
|
||||
|
||||
def test_report_builder(self):
|
||||
if frappe.db.exists("Report", "User Activity Report"):
|
||||
frappe.delete_doc("Report", "User Activity Report")
|
||||
|
|
|
|||
|
|
@ -95,11 +95,9 @@ class RolePermissionforPageandReport(Document):
|
|||
return {check_for_field: name}
|
||||
|
||||
def get_roles(self):
|
||||
roles = []
|
||||
for data in self.roles:
|
||||
if data.role != "All":
|
||||
roles.append({"role": data.role, "parenttype": "Custom Role"})
|
||||
return roles
|
||||
return [
|
||||
{"role": data.role, "parenttype": "Custom Role"} for data in self.roles if data.role != "All"
|
||||
]
|
||||
|
||||
def update_status(self):
|
||||
return frappe.render_template
|
||||
|
|
|
|||
|
|
@ -6,20 +6,22 @@ frappe.ui.form.on("RQ Job", {
|
|||
// Nothing in this form is supposed to be editable.
|
||||
frm.disable_form();
|
||||
frm.dashboard.set_headline_alert(
|
||||
"This is a virtual doctype and data is cleared periodically."
|
||||
__("This is a virtual doctype and data is cleared periodically.")
|
||||
);
|
||||
|
||||
if (["started", "queued"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(__("Force Stop job"), () => {
|
||||
frappe.confirm(
|
||||
"This will terminate the job immediately and might be dangerous, are you sure? ",
|
||||
__(
|
||||
"This will terminate the job immediately and might be dangerous, are you sure? "
|
||||
),
|
||||
() => {
|
||||
frappe
|
||||
.xcall("frappe.core.doctype.rq_job.rq_job.stop_job", {
|
||||
job_id: frm.doc.name,
|
||||
})
|
||||
.then((r) => {
|
||||
frappe.show_alert("Job Stopped Succefully");
|
||||
frappe.show_alert(__("Job Stopped Successfully"));
|
||||
frm.reload_doc();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ frappe.listview_settings["RQ Job"] = {
|
|||
__("Actions")
|
||||
);
|
||||
|
||||
if (listview.list_view_settings) {
|
||||
listview.list_view_settings.disable_sidebar_stats = 1;
|
||||
}
|
||||
|
||||
frappe.xcall("frappe.utils.scheduler.get_scheduler_status").then(({ status }) => {
|
||||
if (status === "active") {
|
||||
listview.page.set_indicator(__("Scheduler: Active"), "green");
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@ from rq.job import Job
|
|||
|
||||
import frappe
|
||||
from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs, stop_job
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.tests.utils import FrappeTestCase, timeout
|
||||
from frappe.utils import cstr, execute_in_shell
|
||||
from frappe.utils.background_jobs import get_job_status, is_job_enqueued
|
||||
|
||||
|
||||
class TestRQJob(FrappeTestCase):
|
||||
|
||||
BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func"
|
||||
|
||||
@timeout(seconds=20)
|
||||
|
|
@ -163,6 +163,17 @@ class TestRQJob(FrappeTestCase):
|
|||
LAST_MEASURED_USAGE = 40
|
||||
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
|
||||
|
||||
@timeout(20)
|
||||
def test_clear_failed_jobs(self):
|
||||
limit = 10
|
||||
update_site_config("rq_failed_jobs_limit", limit)
|
||||
|
||||
jobs = [frappe.enqueue(method=self.BG_JOB, queue="short", fail=True) for _ in range(limit * 2)]
|
||||
self.check_status(jobs[-1], "failed")
|
||||
self.assertLessEqual(
|
||||
RQJob.get_count({"filters": [["RQ Job", "status", "=", "failed"]]}), limit * 1.1
|
||||
)
|
||||
|
||||
|
||||
def test_func(fail=False, sleep=0):
|
||||
if fail:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,20 @@ frappe.ui.form.on("Server Script", {
|
|||
.then((items) => {
|
||||
frm.set_df_property("script", "autocompletions", items);
|
||||
});
|
||||
|
||||
frm.trigger("check_safe_exec");
|
||||
},
|
||||
|
||||
check_safe_exec(frm) {
|
||||
frappe.xcall("frappe.core.doctype.server_script.server_script.enabled").then((enabled) => {
|
||||
if (enabled === false) {
|
||||
frm.dashboard.clear_comment();
|
||||
let msg = __("Server Scripts feature is not available on this site.") + " ";
|
||||
msg += __("Please contact your system administrator to enable this feature.");
|
||||
frm.dashboard.add_comment(msg, "yellow", true);
|
||||
frm.disable_form();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setup_help(frm) {
|
||||
|
|
@ -68,7 +82,7 @@ else:
|
|||
<pre><code>
|
||||
# generate dynamic conditions and set it in the conditions variable
|
||||
tenant_id = frappe.db.get_value(...)
|
||||
conditions = 'tenant_id = {}'.format(tenant_id)
|
||||
conditions = f'tenant_id = {tenant_id}'
|
||||
|
||||
# resulting select query
|
||||
select name from \`tabPerson\`
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import frappe
|
|||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, safe_exec
|
||||
from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, is_safe_exec_enabled, safe_exec
|
||||
|
||||
|
||||
class ServerScript(Document):
|
||||
|
|
@ -277,3 +277,9 @@ def execute_api_server_script(script=None, *args, **kwargs):
|
|||
_globals, _locals = safe_exec(script.script)
|
||||
|
||||
return _globals.frappe.flags
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def enabled() -> bool | None:
|
||||
if frappe.has_permission("Server Script"):
|
||||
return is_safe_exec_enabled()
|
||||
|
|
|
|||
|
|
@ -97,8 +97,9 @@ class TestServerScript(FrappeTestCase):
|
|||
script_doc = frappe.get_doc(doctype="Server Script")
|
||||
script_doc.update(script)
|
||||
script_doc.insert()
|
||||
|
||||
cls.enable_safe_exec()
|
||||
frappe.db.commit()
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
|
|
@ -269,13 +270,13 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
|
|||
site = frappe.utils.get_site_url(frappe.local.site)
|
||||
client = FrappeClient(site)
|
||||
|
||||
# Exhaust rate limti
|
||||
# Exhaust rate limit
|
||||
for _ in range(5):
|
||||
client.get_api(script1.api_method)
|
||||
|
||||
self.assertRaises(FrappeException, client.get_api, script1.api_method)
|
||||
|
||||
# Exhaust rate limti
|
||||
# Exhaust rate limit
|
||||
for _ in range(5):
|
||||
client.get_api(script2.api_method)
|
||||
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ class SystemSettings(Document):
|
|||
]
|
||||
float_precision: DF.Literal["", "2", "3", "4", "5", "6", "7", "8", "9"]
|
||||
force_user_to_reset_password: DF.Int
|
||||
force_web_capture_mode_for_uploads: DF.Check
|
||||
hide_footer_in_auto_email_reports: DF.Check
|
||||
language: DF.Link
|
||||
lifespan_qrcode_image: DF.Int
|
||||
|
|
|
|||
|
|
@ -367,6 +367,9 @@ class TestUser(FrappeTestCase):
|
|||
set_request(path="/random")
|
||||
frappe.local.cookie_manager = CookieManager()
|
||||
frappe.local.login_manager = LoginManager()
|
||||
# used by rate limiter when calling reset_password
|
||||
frappe.local.request_ip = "127.0.0.69"
|
||||
frappe.db.set_single_value("System Settings", "password_reset_limit", 6)
|
||||
|
||||
frappe.set_user("testpassword@example.com")
|
||||
test_user = frappe.get_doc("User", "testpassword@example.com")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from collections.abc import Sequence
|
||||
|
||||
from collections.abc import Iterable
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -54,7 +54,7 @@ class User(Document):
|
|||
api_key: DF.Data | None
|
||||
api_secret: DF.Password | None
|
||||
banner_image: DF.AttachImage | None
|
||||
bio: DF.Text | None
|
||||
bio: DF.SmallText | None
|
||||
birth_date: DF.Date | None
|
||||
block_modules: DF.Table[BlockModule]
|
||||
bypass_restrict_ip_check_if_2fa_enabled: DF.Check
|
||||
|
|
@ -569,10 +569,7 @@ class User(Document):
|
|||
tables = frappe.db.get_tables()
|
||||
for tab in tables:
|
||||
desc = frappe.db.get_table_columns_description(tab)
|
||||
has_fields = []
|
||||
for d in desc:
|
||||
if d.get("name") in ["owner", "modified_by"]:
|
||||
has_fields.append(d.get("name"))
|
||||
has_fields = [d.get("name") for d in desc if d.get("name") in ["owner", "modified_by"]]
|
||||
for field in has_fields:
|
||||
frappe.db.sql(
|
||||
"""UPDATE `%s`
|
||||
|
|
@ -1010,7 +1007,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
|
|||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"])
|
||||
@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60)
|
||||
def reset_password(user: str) -> str:
|
||||
if user == "Administrator":
|
||||
return "not allowed"
|
||||
|
|
@ -1042,7 +1039,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
|
|||
conditions = []
|
||||
|
||||
user_type_condition = "and user_type != 'Website User'"
|
||||
if filters and filters.get("ignore_user_type"):
|
||||
if filters and filters.get("ignore_user_type") and frappe.session.data.user_type == "System User":
|
||||
user_type_condition = ""
|
||||
filters.pop("ignore_user_type")
|
||||
|
||||
|
|
@ -1090,29 +1087,24 @@ def get_total_users():
|
|||
)
|
||||
|
||||
|
||||
def get_system_users(exclude_users=None, limit=None):
|
||||
if not exclude_users:
|
||||
exclude_users = []
|
||||
elif not isinstance(exclude_users, (list, tuple)):
|
||||
exclude_users = [exclude_users]
|
||||
def get_system_users(exclude_users: Iterable[str] | str | None = None, limit: int | None = None):
|
||||
_excluded_users = list(STANDARD_USERS)
|
||||
if isinstance(exclude_users, str):
|
||||
_excluded_users.append(exclude_users)
|
||||
elif isinstance(exclude_users, Iterable):
|
||||
_excluded_users.extend(exclude_users)
|
||||
|
||||
limit_cond = ""
|
||||
if limit:
|
||||
limit_cond = f"limit {limit}"
|
||||
|
||||
exclude_users += list(STANDARD_USERS)
|
||||
|
||||
system_users = frappe.db.sql_list(
|
||||
"""select name from `tabUser`
|
||||
where enabled=1 and user_type != 'Website User'
|
||||
and name not in ({}) {}""".format(
|
||||
", ".join(["%s"] * len(exclude_users)), limit_cond
|
||||
),
|
||||
exclude_users,
|
||||
return frappe.get_all(
|
||||
"User",
|
||||
filters={
|
||||
"enabled": 1,
|
||||
"user_type": ("!=", "Website User"),
|
||||
"name": ("not in", _excluded_users),
|
||||
},
|
||||
pluck="name",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
return system_users
|
||||
|
||||
|
||||
def get_active_users():
|
||||
"""Returns No. of system users who logged in, in the last 3 days"""
|
||||
|
|
|
|||
|
|
@ -144,13 +144,11 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
|
|||
user_permissions = get_user_permissions(user).get(allow, [])
|
||||
if not user_permissions:
|
||||
return None
|
||||
has_same_user_permission = find(
|
||||
return find(
|
||||
user_permissions,
|
||||
lambda perm: perm["doc"] == for_value and perm.get("applicable_for") == applicable_for,
|
||||
)
|
||||
|
||||
return has_same_user_permission
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
|
|
@ -171,11 +169,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
|
|||
|
||||
linked_doctypes.sort()
|
||||
|
||||
return_list = []
|
||||
for doctype in linked_doctypes[start:page_len]:
|
||||
return_list.append([doctype])
|
||||
|
||||
return return_list
|
||||
return [[doctype] for doctype in linked_doctypes[start:page_len]]
|
||||
|
||||
|
||||
def get_permitted_documents(doctype):
|
||||
|
|
|
|||
|
|
@ -193,9 +193,7 @@ class UserType(Document):
|
|||
doctypes.append("File")
|
||||
|
||||
for doctype in ["select_doctypes", "custom_select_doctypes"]:
|
||||
for dt in self.get(doctype):
|
||||
doctypes.append(dt.document_type)
|
||||
|
||||
doctypes.extend(dt.document_type for dt in self.get(doctype))
|
||||
for perm in frappe.get_all(
|
||||
"Custom DocPerm", filters={"role": self.role, "parent": ["not in", doctypes]}
|
||||
):
|
||||
|
|
|
|||
|
|
@ -43,9 +43,7 @@ def get_roles_and_doctypes():
|
|||
restricted_roles = ["Administrator"]
|
||||
if frappe.session.user != "Administrator":
|
||||
custom_user_type_roles = frappe.get_all("User Type", filters={"is_standard": 0}, fields=["role"])
|
||||
for row in custom_user_type_roles:
|
||||
restricted_roles.append(row.role)
|
||||
|
||||
restricted_roles.extend(row.role for row in custom_user_type_roles)
|
||||
restricted_roles.append("All")
|
||||
|
||||
roles = frappe.get_all(
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
frappe.pages["recorder"].on_page_load = function (wrapper) {
|
||||
frappe.ui.make_app_page({
|
||||
parent: wrapper,
|
||||
title: __("Recorder"),
|
||||
single_column: true,
|
||||
card_layout: true,
|
||||
});
|
||||
|
||||
frappe.recorder = new Recorder(wrapper);
|
||||
$(wrapper).bind("show", function () {
|
||||
frappe.recorder.show();
|
||||
});
|
||||
|
||||
frappe.require("recorder.bundle.js");
|
||||
};
|
||||
|
||||
class Recorder {
|
||||
constructor(wrapper) {
|
||||
this.wrapper = $(wrapper);
|
||||
this.container = this.wrapper.find(".layout-main-section");
|
||||
this.container.append($('<div class="recorder-container"></div>'));
|
||||
}
|
||||
|
||||
show() {
|
||||
if (!this.route || this.route.name == "RecorderDetail") return;
|
||||
this.router?.replace({ name: "RecorderDetail" });
|
||||
}
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
{
|
||||
"content": null,
|
||||
"creation": "2019-02-08 08:17:45.392739",
|
||||
"docstatus": 0,
|
||||
"doctype": "Page",
|
||||
"idx": 0,
|
||||
"modified": "2019-02-08 08:23:04.416426",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "recorder",
|
||||
"owner": "Administrator",
|
||||
"page_name": "Recorder",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Administrator"
|
||||
}
|
||||
],
|
||||
"script": null,
|
||||
"standard": "Yes",
|
||||
"style": null,
|
||||
"system_page": 0,
|
||||
"title": "Recorder"
|
||||
}
|
||||
|
|
@ -56,11 +56,9 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
|
|||
|
||||
single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})]
|
||||
|
||||
out = []
|
||||
for dt in can_read:
|
||||
if txt.lower().replace("%", "") in dt.lower() and (
|
||||
include_single_doctypes or dt not in single_doctypes
|
||||
):
|
||||
out.append([dt])
|
||||
|
||||
return out
|
||||
return [
|
||||
[dt]
|
||||
for dt in can_read
|
||||
if txt.lower().replace("%", "") in dt.lower()
|
||||
and (include_single_doctypes or dt not in single_doctypes)
|
||||
]
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ def calculate_chain(transaction_hash, previous_hash):
|
|||
|
||||
|
||||
def get_columns(filters=None):
|
||||
columns = [
|
||||
return [
|
||||
{
|
||||
"label": _("Chain Integrity"),
|
||||
"fieldname": "chain_integrity",
|
||||
|
|
@ -90,9 +90,28 @@ def get_columns(filters=None):
|
|||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{"label": _("Reference Name"), "fieldname": "reference_name", "fieldtype": "Data", "width": 150},
|
||||
{"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("Modified By"), "fieldname": "modified_by", "fieldtype": "Data", "width": 100},
|
||||
{"label": _("Timestamp"), "fieldname": "timestamp", "fieldtype": "Data", "width": 100},
|
||||
{
|
||||
"label": _("Reference Name"),
|
||||
"fieldname": "reference_name",
|
||||
"fieldtype": "Data",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Owner"),
|
||||
"fieldname": "owner",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Modified By"),
|
||||
"fieldname": "modified_by",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
{
|
||||
"label": _("Timestamp"),
|
||||
"fieldname": "timestamp",
|
||||
"fieldtype": "Data",
|
||||
"width": 100,
|
||||
},
|
||||
]
|
||||
return columns
|
||||
|
|
|
|||
|
|
@ -66,11 +66,7 @@ def find_all(list_of_dict, match_function):
|
|||
|
||||
red_shapes = find_all(colored_shapes, lambda d: d['color'] == 'red')
|
||||
"""
|
||||
found = []
|
||||
for entry in list_of_dict:
|
||||
if match_function(entry):
|
||||
found.append(entry)
|
||||
return found
|
||||
return [entry for entry in list_of_dict if match_function(entry)]
|
||||
|
||||
|
||||
def ljust_list(_list, length, fill_word=None):
|
||||
|
|
|
|||
|
|
@ -293,7 +293,7 @@ def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=
|
|||
return custom_field
|
||||
|
||||
|
||||
def create_custom_fields(custom_fields, ignore_validate=False, update=True):
|
||||
def create_custom_fields(custom_fields: dict, ignore_validate=False, update=True):
|
||||
"""Add / update multiple custom fields
|
||||
|
||||
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`"""
|
||||
|
|
|
|||
|
|
@ -600,11 +600,8 @@ class CustomizeForm(Document):
|
|||
),
|
||||
as_dict=True,
|
||||
)
|
||||
links = []
|
||||
label = df.label
|
||||
for doc in docs:
|
||||
links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name))
|
||||
links_str = ", ".join(links)
|
||||
links_str = ", ".join(frappe.utils.get_link_to_form(self.doc_type, doc.name) for doc in docs)
|
||||
|
||||
if docs:
|
||||
frappe.throw(
|
||||
|
|
@ -710,7 +707,6 @@ doctype_properties = {
|
|||
"naming_rule": "Data",
|
||||
"autoname": "Data",
|
||||
"show_title_field_in_link": "Check",
|
||||
"translate_link_fields": "Check",
|
||||
"is_calendar_and_gantt": "Check",
|
||||
"default_view": "Select",
|
||||
"force_re_route_to_default_view": "Check",
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ from frappe.database.utils import (
|
|||
is_query_type,
|
||||
)
|
||||
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
|
||||
from frappe.monitor import get_trace_id
|
||||
from frappe.query_builder.functions import Count
|
||||
from frappe.utils import CallbackManager
|
||||
from frappe.utils import cast as cast_fieldtype
|
||||
|
|
@ -113,6 +114,10 @@ class Database:
|
|||
self.before_rollback = CallbackManager()
|
||||
self.after_rollback = CallbackManager()
|
||||
|
||||
self._trace_comment = ""
|
||||
if trace_id := get_trace_id():
|
||||
self._trace_comment = f" /* FRAPPE_TRACE_ID: {trace_id} */"
|
||||
|
||||
# self.db_type: str
|
||||
# self.last_query (lazy) attribute of last sql query executed
|
||||
|
||||
|
|
@ -223,7 +228,9 @@ class Database:
|
|||
values = None
|
||||
elif not isinstance(values, (tuple, dict, list)):
|
||||
values = (values,)
|
||||
|
||||
query, values = self._transform_query(query, values)
|
||||
query += self._trace_comment
|
||||
|
||||
try:
|
||||
self._cursor.execute(query, values)
|
||||
|
|
|
|||
|
|
@ -78,4 +78,6 @@ class DbManager:
|
|||
source=source,
|
||||
port=frappe.conf.db_port,
|
||||
)
|
||||
|
||||
os.system(command)
|
||||
frappe.cache.delete_keys("") # Delete all keys associated with this site.
|
||||
|
|
|
|||
|
|
@ -70,29 +70,26 @@ class MariaDBTable(DBTable):
|
|||
for col in self.columns.values():
|
||||
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
|
||||
|
||||
add_column_query = []
|
||||
modify_column_query = []
|
||||
add_index_query = []
|
||||
drop_index_query = []
|
||||
|
||||
for col in self.add_column:
|
||||
add_column_query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}")
|
||||
|
||||
add_column_query = [
|
||||
f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column
|
||||
]
|
||||
columns_to_modify = set(self.change_type + self.set_default)
|
||||
for col in columns_to_modify:
|
||||
modify_column_query.append(
|
||||
f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
|
||||
)
|
||||
|
||||
for col in self.add_unique:
|
||||
modify_column_query.append(
|
||||
modify_column_query = [
|
||||
f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
|
||||
for col in columns_to_modify
|
||||
]
|
||||
modify_column_query.extend(
|
||||
[
|
||||
f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)"
|
||||
)
|
||||
|
||||
for col in self.add_index:
|
||||
# if index key does not exists
|
||||
if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False):
|
||||
add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)")
|
||||
for col in self.add_unique
|
||||
]
|
||||
)
|
||||
add_index_query = [
|
||||
f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)"
|
||||
for col in self.add_index
|
||||
if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False)
|
||||
]
|
||||
drop_index_query = []
|
||||
|
||||
for col in {*self.drop_index, *self.drop_unique}:
|
||||
if col.fieldname == "name":
|
||||
|
|
|
|||
|
|
@ -76,10 +76,7 @@ class PostgresTable(DBTable):
|
|||
for col in self.columns.values():
|
||||
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
|
||||
|
||||
query = []
|
||||
|
||||
for col in self.add_column:
|
||||
query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}")
|
||||
query = [f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column]
|
||||
|
||||
for col in self.change_type:
|
||||
using_clause = ""
|
||||
|
|
@ -88,7 +85,7 @@ class PostgresTable(DBTable):
|
|||
# involving the old values of the row
|
||||
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
|
||||
using_clause = f"USING {col.fieldname}::timestamp without time zone"
|
||||
elif col.fieldtype in ("Check"):
|
||||
elif col.fieldtype == "Check":
|
||||
using_clause = f"USING {col.fieldname}::smallint"
|
||||
|
||||
query.append(
|
||||
|
|
|
|||
|
|
@ -58,16 +58,16 @@ class DBTable:
|
|||
return ret
|
||||
|
||||
def get_index_definitions(self):
|
||||
ret = []
|
||||
for key, col in self.columns.items():
|
||||
return [
|
||||
"index `" + key + "`(`" + key + "`)"
|
||||
for key, col in self.columns.items()
|
||||
if (
|
||||
col.set_index
|
||||
and not col.unique
|
||||
and col.fieldtype in frappe.db.type_map
|
||||
and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext")
|
||||
):
|
||||
ret.append("index `" + key + "`(`" + key + "`)")
|
||||
return ret
|
||||
)
|
||||
]
|
||||
|
||||
def get_columns_from_docfields(self):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -39,8 +39,7 @@ def get_doctype_name(table_name: str) -> str:
|
|||
if table_name.startswith(("tab", "`tab", '"tab')):
|
||||
table_name = table_name.replace("tab", "", 1)
|
||||
table_name = table_name.replace("`", "")
|
||||
table_name = table_name.replace('"', "")
|
||||
return table_name
|
||||
return table_name.replace('"', "")
|
||||
|
||||
|
||||
class LazyString:
|
||||
|
|
|
|||
|
|
@ -71,9 +71,7 @@ def get_user_default_as_list(key, user=None):
|
|||
d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d))
|
||||
|
||||
# filter default values if not found in user permission
|
||||
values = [value for value in d if not not_in_user_permission(key, value)]
|
||||
|
||||
return values
|
||||
return [value for value in d if not not_in_user_permission(key, value)]
|
||||
|
||||
|
||||
def is_a_user_permission_key(key):
|
||||
|
|
|
|||
|
|
@ -493,11 +493,15 @@ def get_custom_doctype_list(module):
|
|||
order_by="name",
|
||||
)
|
||||
|
||||
out = []
|
||||
for d in doctypes:
|
||||
out.append({"type": "Link", "link_type": "doctype", "link_to": d.name, "label": _(d.name)})
|
||||
|
||||
return out
|
||||
return [
|
||||
{
|
||||
"type": "Link",
|
||||
"link_type": "doctype",
|
||||
"link_to": d.name,
|
||||
"label": _(d.name),
|
||||
}
|
||||
for d in doctypes
|
||||
]
|
||||
|
||||
|
||||
def get_custom_report_list(module):
|
||||
|
|
@ -509,23 +513,20 @@ def get_custom_report_list(module):
|
|||
order_by="name",
|
||||
)
|
||||
|
||||
out = []
|
||||
for r in reports:
|
||||
out.append(
|
||||
{
|
||||
"type": "Link",
|
||||
"link_type": "report",
|
||||
"doctype": r.ref_doctype,
|
||||
"dependencies": r.ref_doctype,
|
||||
"is_query_report": 1
|
||||
if r.report_type in ("Query Report", "Script Report", "Custom Report")
|
||||
else 0,
|
||||
"label": _(r.name),
|
||||
"link_to": r.name,
|
||||
}
|
||||
)
|
||||
|
||||
return out
|
||||
return [
|
||||
{
|
||||
"type": "Link",
|
||||
"link_type": "report",
|
||||
"doctype": r.ref_doctype,
|
||||
"dependencies": r.ref_doctype,
|
||||
"is_query_report": 1
|
||||
if r.report_type in ("Query Report", "Script Report", "Custom Report")
|
||||
else 0,
|
||||
"label": _(r.name),
|
||||
"link_to": r.name,
|
||||
}
|
||||
for r in reports
|
||||
]
|
||||
|
||||
|
||||
def save_new_widget(doc, page, blocks, new_widgets):
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue