Merge branch 'develop' into feat-desk-refresh

This commit is contained in:
Maharshi Patel 2023-08-24 23:55:42 +05:30 committed by GitHub
commit 0307dd4cca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
378 changed files with 3876 additions and 2623 deletions

2
.github/stale.yml vendored
View file

@ -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.

View file

@ -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"

View file

@ -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]

View file

@ -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

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -0,0 +1 @@
10

View file

@ -0,0 +1 @@
11

View file

@ -0,0 +1 @@
2

View file

@ -0,0 +1 @@
3

View file

@ -0,0 +1 @@
4

View file

@ -0,0 +1 @@
5

View file

@ -0,0 +1 @@
6

View file

@ -0,0 +1 @@
7

View file

@ -0,0 +1 @@
8

View file

@ -0,0 +1 @@
9

View file

@ -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);
});

View file

@ -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",

View file

@ -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"])

View file

@ -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()

View file

@ -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"])

View file

@ -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(
{

View file

@ -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:

View file

@ -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):

View file

@ -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():

View file

@ -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"])

View file

@ -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

View file

@ -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)

View file

@ -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 = ""

View file

@ -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) + " )"

View file

@ -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):

View file

@ -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))

View file

@ -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):

View file

@ -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

View file

@ -33,7 +33,6 @@
{
"fieldname": "subject",
"fieldtype": "Small Text",
"in_global_search": 1,
"in_list_view": 1,
"label": "Subject",
"reqd": 1

View file

@ -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})

View file

@ -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

View file

@ -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,
)

View file

@ -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

View file

@ -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",))

View file

@ -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):

View file

@ -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")

View file

@ -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()

View file

@ -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 = []

View file

@ -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 ""

View file

@ -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",

View file

@ -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})

View file

@ -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",

View file

@ -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",

View file

@ -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:

View file

@ -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">

View file

@ -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",

View 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()

View file

@ -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

View file

@ -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")

View file

@ -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()

View file

@ -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");
});
},
});

View file

@ -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():

View file

@ -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):

View 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;
}
});

View 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"
}

View 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

View 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");
}
},
};

View 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)

View 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) {
// },
// });

View 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": []
}

View 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

View file

@ -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

View file

@ -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):

View file

@ -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")

View file

@ -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

View file

@ -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();
});
}

View file

@ -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");

View file

@ -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:

View file

@ -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\`

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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"""

View file

@ -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):

View file

@ -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]}
):

View file

@ -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(

View file

@ -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" });
}
}

View file

@ -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"
}

View file

@ -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)
]

View file

@ -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

View file

@ -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):

View file

@ -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')]}`"""

View file

@ -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",

View file

@ -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)

View file

@ -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.

View file

@ -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":

View file

@ -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(

View file

@ -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):
"""

View file

@ -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:

View file

@ -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):

View file

@ -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