Merge branch 'develop' into bump-pydantic-v2

This commit is contained in:
gavin 2023-06-26 13:12:24 +05:30 committed by GitHub
commit faab26ce4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
271 changed files with 3248 additions and 3031 deletions

View file

@ -37,3 +37,6 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
# minor formatting fix in `user.py`
f223bc02490902dfcc32892058f13f343d51fbaf
# frappe.cache() -> frappe.cache
fa6dc03cc87ad74e11609e7373078366fdcb3e1b

View file

@ -54,7 +54,9 @@ fi
echo "Starting Bench..."
bench start &> bench_start.log &
export FRAPPE_TUNE_GC=True
bench start &> ~/frappe-bench/bench_start.log &
if [ "$TYPE" == "server" ]
then

View file

@ -25,7 +25,7 @@ jobs:
fetch-depth: 200
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Check commit titles

View file

@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- uses: actions/setup-python@v4
with:

View file

@ -62,14 +62,16 @@ jobs:
fi
- name: Setup Python
uses: "gabrielfalcao/pyenv-action@v10"
uses: actions/setup-python@v4
with:
versions: 3.10:latest, 3.7:latest
python-version: |
3.7
3.10
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts
@ -100,7 +102,6 @@ jobs:
run: |
bash ${GITHUB_WORKSPACE}/.github/helper/install_dependencies.sh
pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
BEFORE: ${{ env.GITHUB_EVENT_PATH.before }}
@ -120,26 +121,39 @@ jobs:
function update_to_version() {
version=$1
py=$2
branch_name="version-$version-hotfix"
echo "Updating to v$version"
git fetch --depth 1 upstream $branch_name:$branch_name
git checkout -q -f $branch_name
pip install -U frappe-bench
pgrep honcho | xargs kill
rm -rf ~/frappe-bench/env
bench -v setup env
bench -v setup env --python $py
bench start &> ~/frappe-bench/bench_start.log &
bench --site test_site migrate
}
pyenv global $(pyenv versions | grep '3.7')
update_to_version 12
update_to_version 13
update_to_version 12 python3.7
update_to_version 13 python3.7
pyenv global $(pyenv versions | grep '3.10')
update_to_version 14
update_to_version 14 python3.10
echo "Updating to last commit"
git checkout -q -f "$GITHUB_SHA"
rm -rf ~/frappe-bench/env
git checkout -q -f "$GITHUB_SHA"
bench -v setup env
bench --site test_site migrate
- name: Show bench output
if: ${{ always() }}
run: |
cd ~/frappe-bench
cat bench_start.log || true
cd logs
for f in ./*.log*; do
echo "Printing log: $f";
cat $f
done

View file

@ -16,7 +16,7 @@ jobs:
path: 'frappe'
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
- uses: actions/setup-python@v4
with:
python-version: '3.11'

View file

@ -90,7 +90,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts
@ -136,6 +136,10 @@ jobs:
BUILD_NUMBER: ${{ matrix.container }}
TOTAL_BUILDS: 2
- name: Show bench output
if: ${{ always() }}
run: cat ~/frappe-bench/bench_start.log || true
- name: Upload coverage data
uses: actions/upload-artifact@v3
with:

View file

@ -78,7 +78,7 @@ jobs:
- uses: actions/setup-node@v3
with:
node-version: 16
node-version: 18
check-latest: true
- name: Add to Hosts

0
.semgrepignore Normal file
View file

View file

@ -8,6 +8,8 @@ module.exports = defineConfig({
pageLoadTimeout: 15000,
video: true,
videoUploadOnPasses: false,
viewportHeight: 960,
viewportWidth: 1400,
retries: {
runMode: 2,
openMode: 2,

View file

@ -7,50 +7,41 @@ context("Awesome Bar", () => {
beforeEach(() => {
cy.get(".navbar .navbar-home").click();
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").clear();
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").as("awesome_bar");
cy.get("@awesome_bar").type("{selectall}");
});
it("navigates to doctype list", () => {
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type("todo", {
delay: 700,
});
cy.get("@awesome_bar").type("todo");
cy.wait(100);
cy.get(".awesomplete").findByRole("listbox").should("be.visible");
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type("{enter}", {
delay: 700,
});
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.location("pathname").should("eq", "/app/todo");
});
it("find text in doctype list", () => {
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type(
"test in todo{enter}",
{ delay: 700 }
);
cy.get("@awesome_bar").type("test in todo");
cy.wait(100);
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text").should("contain", "To Do");
cy.findByPlaceholderText("ID").should("have.value", "%test%");
cy.wait(200);
const name_filter = cy.findByPlaceholderText("ID");
name_filter.should("have.value", "%test%");
cy.clear_filters();
});
it("navigates to new form", () => {
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type(
"new blog post{enter}",
{ delay: 700 }
);
cy.get("@awesome_bar").type("new blog post");
cy.wait(100);
cy.get("@awesome_bar").type("{enter}");
cy.get(".title-text:visible").should("have.text", "New Blog Post");
});
it("calculates math expressions", () => {
cy.findByPlaceholderText("Search or type a command (Ctrl + G)").type(
"55 + 32{downarrow}{enter}",
{ delay: 700 }
);
cy.get("@awesome_bar").type("55 + 32");
cy.wait(100);
cy.get("@awesome_bar").type("{downarrow}{enter}");
cy.get(".modal-title").should("contain", "Result");
cy.get(".msgprint").should("contain", "55 + 32 = 87");
});

View file

@ -0,0 +1,74 @@
context("Control Currency", () => {
const fieldname = "currency_field";
before(() => {
cy.login();
cy.visit("/app/website");
});
function get_dialog_with_currency(df_options = {}) {
return cy.dialog({
title: "Currency Check",
fields: [
{
fieldname: fieldname,
fieldtype: "Currency",
Label: "Currency",
...df_options,
},
],
});
}
it("check value changes", () => {
const TEST_CASES = [
{
input: "10.101",
df_options: { precision: 1 },
blur_expected: "10.1",
},
{
input: "10.101",
df_options: { precision: "3" },
blur_expected: "10.101",
},
{
input: "10.101",
df_options: { precision: "" }, // default assumed to be 2;
blur_expected: "10.10",
},
{
input: "10.101",
df_options: { precision: "0" },
blur_expected: "10",
},
{
input: "10.101",
df_options: { precision: 0 },
blur_expected: "10",
},
{
input: "10.101",
df_options: { precision: "" },
blur_expected: "10.1",
default_precision: 1,
},
];
TEST_CASES.forEach((test_case) => {
cy.window()
.its("frappe")
.then((frappe) => {
frappe.boot.sysdefaults.currency = test_case.currency;
frappe.boot.sysdefaults.currency_precision = test_case.default_precision ?? 2;
});
get_dialog_with_currency(test_case.df_options).as("dialog");
cy.get_field(fieldname, "Currency").clear();
cy.wait(300);
cy.fill_field(fieldname, test_case.input, "Currency").blur();
cy.get_field(fieldname, "Currency").should("have.value", test_case.blur_expected);
cy.hide_dialog();
});
});
});

View file

@ -42,14 +42,14 @@ context("Control Icon", () => {
it("search for icon and clear search input", () => {
let search_text = "ed";
cy.get(".icon-picker").findByRole("searchbox").click().type(search_text);
cy.get(".icon-picker").get(".search-icons > input").click().type(search_text);
cy.get(".icon-section .icon-wrapper:not(.hidden)").then((i) => {
cy.get(`.icon-section .icon-wrapper[id*='${search_text}']`).then((icons) => {
expect(i.length).to.equal(icons.length);
});
});
cy.get(".icon-picker").findByRole("searchbox").clear().blur();
cy.get(".icon-picker").get(".search-icons > input").clear().blur();
cy.get(".icon-section .icon-wrapper").should("not.have.class", "hidden");
});
});

View file

@ -133,8 +133,7 @@ context("Control Link", () => {
true
);
cy.clear_cache();
cy.wait(500);
cy.reload();
get_dialog_with_link().as("dialog");
cy.window()
@ -177,7 +176,7 @@ context("Control Link", () => {
cy.intercept("POST", "/api/method/frappe.client.validate_link").as("validate_link");
cy.get(".frappe-control[data-fieldname=assigned_by] input").focus().as("input");
cy.get("@input").type(cy.config("testUser"), { delay: 100 }).blur();
cy.get("@input").clear().type(cy.config("testUser"), { delay: 300 }).blur();
cy.wait("@validate_link");
cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should(
"contain",

View file

@ -47,7 +47,7 @@ context("Control Phone", () => {
it("case insensitive search for country and clear search", () => {
let search_text = "india";
cy.get(".selected-phone").click().first();
cy.get(".phone-picker").findByRole("searchbox").click().type(search_text);
cy.get(".phone-picker").get(".search-phones").click().type(search_text);
cy.get(".phone-section .phone-wrapper:not(.hidden)").then((i) => {
cy.get(`.phone-section .phone-wrapper[id*="${search_text.toLowerCase()}"]`).then(
(countries) => {
@ -56,7 +56,7 @@ context("Control Phone", () => {
);
});
cy.get(".phone-picker").findByRole("searchbox").clear().blur();
cy.get(".phone-picker").get(".search-phones").clear();
cy.get(".phone-section .phone-wrapper").should("not.have.class", "hidden");
});

View file

@ -11,9 +11,9 @@ context("Folder Navigation", () => {
cy.click_filter_button();
cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click();
cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}");
cy.get(
".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback"
).type("Home{enter}");
cy.get(".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback")
.first()
.type("Home{enter}");
cy.get(".filter-action-buttons > div > .btn-primary").findByText("Apply Filters").click();
//Adding folder (Test Folder)
@ -24,6 +24,7 @@ context("Folder Navigation", () => {
it("Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct", () => {
//Navigating inside the Attachments folder
cy.clear_filters();
cy.wait(500);
cy.get('[title="Attachments"] > span').click();

View file

@ -59,11 +59,13 @@ context("Form", () => {
.blur();
cy.click_listview_row_item_with_text("Test Form Contact 3");
cy.scrollTo(0);
cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist");
cy.get(".prev-doc").should("be.visible").click();
cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible");
cy.hide_dialog();
cy.scrollTo(0);
cy.get("#page-Contact .page-head").findByTitle("Test Form Contact 3").should("exist");
cy.get(".next-doc").should("be.visible").click();
cy.get(".msgprint-dialog .modal-body").contains("No further records").should("be.visible");

View file

@ -100,15 +100,15 @@ context("Kanban Board", () => {
it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => {
cy.switch_to_user("Administrator");
const noSystemManager = "nosysmanager@example.com";
const not_system_manager = "nosysmanager@example.com";
cy.call("frappe.tests.ui_test_helpers.create_test_user", {
username: noSystemManager,
username: not_system_manager,
});
cy.remove_role(noSystemManager, "System Manager");
cy.remove_role(not_system_manager, "System Manager");
cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
cy.switch_to_user(noSystemManager);
cy.switch_to_user(not_system_manager);
cy.visit("/app/todo/view/kanban/Admin Kanban");
@ -125,7 +125,7 @@ context("Kanban Board", () => {
cy.get(".kanban .column-options").should("have.length", 0);
cy.switch_to_user("Administrator");
cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager });
cy.call("frappe.client.delete", { doctype: "User", name: not_system_manager });
});
after(() => {

View file

@ -13,15 +13,8 @@ context("List View", () => {
it("Keep checkbox checked after Refresh", { scrollBehavior: false }, () => {
cy.go_to_list("ToDo");
cy.clear_filters();
cy.get(".list-row-container .list-row-checkbox").click({
multiple: true,
force: true,
});
cy.get(".actions-btn-group button").contains("Actions").should("be.visible");
cy.intercept("/api/method/frappe.desk.reportview.get").as("list-refresh");
cy.wait(3000); // wait before you hit another refresh
cy.get('button[data-original-title="Refresh"]').click();
cy.wait("@list-refresh");
cy.get(".list-header-subject > .list-subject > .list-check-all").click();
cy.get("button[data-original-title='Refresh']").click();
cy.get(".list-row-container .list-row-checkbox:checked").should("be.visible");
});
@ -39,11 +32,8 @@ context("List View", () => {
];
cy.go_to_list("ToDo");
cy.clear_filters();
cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({
multiple: true,
force: true,
});
cy.get(".actions-btn-group button").contains("Actions").should("be.visible").click();
cy.get(".list-header-subject > .list-subject > .list-check-all").click();
cy.findByRole("button", { name: "Actions" }).click();
cy.get(".dropdown-menu li:visible .dropdown-item")
.should("have.length", 9)
.each((el, index) => {
@ -56,8 +46,7 @@ context("List View", () => {
}).as("bulk-approval");
cy.wrap(elements).contains("Approve").click();
cy.wait("@bulk-approval");
cy.wait(300);
cy.get_open_dialog().find(".btn-modal-close").click();
cy.hide_dialog();
cy.reload();
cy.clear_filters();
cy.get(".list-row-container:visible").should("contain", "Approved");

View file

@ -1,21 +1,26 @@
context("Navigation", () => {
before(() => {
cy.visit("/login");
cy.login();
cy.visit("/app/website");
});
it("Navigate to route with hash in document name", () => {
cy.insert_doc("ToDo", {
__newname: "ABC#123",
description: "Test this",
ignore_duplicate: true,
});
cy.visit("/app/todo/ABC#123");
cy.insert_doc(
"ToDo",
{
__newname: "ABC#123",
description: "Test this",
},
true
);
cy.visit(`/app/todo/${encodeURIComponent("ABC#123")}`);
cy.title().should("eq", "Test this - ABC#123");
cy.get_field("description", "Text Editor").contains("Test this");
cy.go("back");
cy.title().should("eq", "Website");
});
it.only("Navigate to previous page after login", () => {
it("Navigate to previous page after login", () => {
cy.visit("/app/todo");
cy.get(".page-head").findByTitle("To Do").should("be.visible");
cy.clear_filters();

View file

@ -34,7 +34,7 @@ Cypress.Commands.add("login", (email, password) => {
if (!password) {
password = Cypress.env("adminPassword");
}
cy.request({
return cy.request({
url: "/api/method/login",
method: "POST",
body: {
@ -373,7 +373,9 @@ Cypress.Commands.add("update_doc", (doctype, docname, args) => {
Cypress.Commands.add("switch_to_user", (user) => {
cy.call("logout");
cy.wait(200);
cy.login(user);
cy.reload();
});
Cypress.Commands.add("add_role", (user, role) => {

View file

@ -60,6 +60,11 @@ const argv = yargs
type: "boolean",
description: "Run build command for apps",
})
.option("save-metafiles", {
type: "boolean",
description:
"Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
})
.example("node esbuild --apps frappe,erpnext", "Run build only for frappe and erpnext")
.example(
"node esbuild --files frappe/website.bundle.js,frappe/desk.bundle.js",
@ -89,7 +94,7 @@ execute()
.then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
.catch((e) => {
console.error(e);
throw e;
process.exit(1);
});
if (WATCH_MODE) {
@ -401,6 +406,13 @@ async function write_assets_json(metafile) {
await fs.promises.writeFile(assets_json_path, JSON.stringify(new_assets_json, null, 4));
await update_assets_json_in_cache();
if (argv["save-metafiles"]) {
// use current timestamp in readable formate as a suffix for filename
let current_timestamp = new Date().getTime();
const metafile_name = `meta-${current_timestamp}.json`;
await fs.promises.writeFile(`${metafile_name}`, JSON.stringify(metafile));
log(`Saved metafile as ${metafile_name}`);
}
return {
new_assets_json,
prev_assets_json,
@ -446,9 +458,9 @@ function run_build_command_for_apps(apps) {
async function notify_redis({ error, success, changed_files }) {
// notify redis which in turns tells socketio to publish this to browser
let subscriber = get_redis_subscriber("redis_socketio");
let subscriber = get_redis_subscriber("redis_queue");
subscriber.on("error", (_) => {
log_warn("Cannot connect to redis_socketio for browser events");
log_warn("Cannot connect to redis_queue for browser events");
});
let payload = null;
@ -482,9 +494,9 @@ async function notify_redis({ error, success, changed_files }) {
}
function open_in_editor() {
let subscriber = get_redis_subscriber("redis_socketio");
let subscriber = get_redis_subscriber("redis_queue");
subscriber.on("error", (_) => {
log_warn("Cannot connect to redis_socketio for open_in_editor events");
log_warn("Cannot connect to redis_queue for open_in_editor events");
});
subscriber.on("message", (event, file) => {
if (event === "open_in_editor") {

View file

@ -11,6 +11,7 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
import functools
import gc
import importlib
import inspect
import json
@ -57,6 +58,7 @@ re._MAXCACHE = (
50 # reduced from default 512 given we are already maintaining this on parent worker
)
_tune_gc = bool(os.environ.get("FRAPPE_TUNE_GC", False))
if _dev_server:
warnings.simplefilter("always", DeprecationWarning)
@ -380,7 +382,7 @@ def errprint(msg: str) -> None:
def print_sql(enable: bool = True) -> None:
return cache().set_value("flag_print_sql", enable)
return cache.set_value("flag_print_sql", enable)
def log(msg: str) -> None:
@ -925,7 +927,6 @@ def has_permission(
ptype="read",
doc=None,
user=None,
verbose=False,
throw=False,
*,
parent_doctype=None,
@ -938,7 +939,6 @@ def has_permission(
:param ptype: Permission type (`read`, `write`, `create`, `submit`, `cancel`, `amend`). Default: `read`.
:param doc: [optional] Checks User permissions for given doc.
:param user: [optional] Check for given user. Default: current user.
:param verbose: DEPRECATED, will be removed in a future release.
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified).
"""
import frappe.permissions
@ -1016,7 +1016,7 @@ def is_table(doctype: str) -> bool:
def get_tables():
return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True)
tables = cache().get_value("is_table", get_tables)
tables = cache.get_value("is_table", get_tables)
return doctype in tables
@ -1043,7 +1043,7 @@ def generate_hash(txt: str | None = None, length: int = 56) -> str:
def reset_metadata_version():
"""Reset `metadata_version` (Client (Javascript) build ID) hash."""
v = generate_hash()
cache().set_value("metadata_version", v)
cache.set_value("metadata_version", v)
return v
@ -1079,7 +1079,7 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
if (key := can_cache_doc(args)) and (doc := cache().get_value(key)):
if (key := can_cache_doc(args)) and (doc := cache.get_value(key)):
return doc
# Not found in cache, fetch from DB
@ -1095,7 +1095,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
def _set_document_in_cache(key: str, doc: "Document") -> None:
cache().set_value(key, doc)
cache.set_value(key, doc)
def can_cache_doc(args) -> str | None:
@ -1122,9 +1122,9 @@ def get_document_cache_key(doctype: str, name: str):
def clear_document_cache(doctype: str, name: str | None = None) -> None:
def clear_in_redis():
if name is not None:
cache().delete_value(get_document_cache_key(doctype, name))
cache.delete_value(get_document_cache_key(doctype, name))
else:
cache().delete_keys(get_document_cache_key(doctype, ""))
cache.delete_keys(get_document_cache_key(doctype, ""))
clear_in_redis()
if hasattr(db, "after_commit"):
@ -1214,7 +1214,7 @@ def get_doc(*args, **kwargs):
doc = frappe.model.document.get_doc(*args, **kwargs)
# Replace cache if stale one exists
if (key := can_cache_doc(args)) and cache().exists(key):
if (key := can_cache_doc(args)) and cache.exists(key):
_set_document_in_cache(key, doc)
return doc
@ -1428,7 +1428,7 @@ def get_all_apps(with_internal_apps=True, sites_path=None):
@request_cache
def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False):
def get_installed_apps(*, _ensure_on_bench=False):
"""
Get list of installed apps in current site.
@ -1436,8 +1436,6 @@ def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False)
:param frappe_last: [DEPRECATED] Keep frappe last. Do not use this, reverse the app list instead.
:param ensure_on_bench: Only return apps that are present on bench.
"""
from frappe.utils.deprecations import deprecation_warning
if getattr(flags, "in_install_db", True):
return []
@ -1446,23 +1444,10 @@ def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False)
installed = json.loads(db.get_global("installed_apps") or "[]")
if sort:
if not local.all_apps:
local.all_apps = cache().get_value("all_apps", get_all_apps)
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
installed = [app for app in local.all_apps if app in installed]
if _ensure_on_bench:
all_apps = cache().get_value("all_apps", get_all_apps)
all_apps = cache.get_value("all_apps", get_all_apps)
installed = [app for app in installed if app in all_apps]
if frappe_last:
deprecation_warning("`frappe_last` argument is deprecated and will be removed in v15.")
if "frappe" in installed:
installed.remove("frappe")
installed.append("frappe")
return installed
@ -1525,7 +1510,7 @@ def get_hooks(
if conf.developer_mode:
hooks = _dict(_load_app_hooks())
else:
hooks = _dict(cache().get_value("app_hooks", _load_app_hooks))
hooks = _dict(cache.get_value("app_hooks", _load_app_hooks))
if hook:
return hooks.get(hook, ([] if default == "_KEEP_DEFAULT_LIST" else default))
@ -1555,11 +1540,9 @@ def append_hook(target, key, value):
def setup_module_map():
"""Rebuild map of all modules (internal)."""
_cache = cache()
if conf.db_name:
local.app_modules = _cache.get_value("app_modules")
local.module_app = _cache.get_value("module_app")
local.app_modules = cache.get_value("app_modules")
local.module_app = cache.get_value("module_app")
if not (local.app_modules and local.module_app):
local.module_app, local.app_modules = {}, {}
@ -1571,8 +1554,8 @@ def setup_module_map():
local.app_modules[app].append(module)
if conf.db_name:
_cache.set_value("app_modules", local.app_modules)
_cache.set_value("module_app", local.module_app)
cache.set_value("app_modules", local.app_modules)
cache.set_value("module_app", local.module_app)
def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
@ -1861,7 +1844,7 @@ def redirect_to_message(title, html, http_status_code=None, context=None, indica
if indicator_color:
message["context"].update({"indicator_color": indicator_color})
cache().set_value(f"message_id:{message_id}", message, expires_in_sec=60)
cache.set_value(f"message_id:{message_id}", message, expires_in_sec=60)
location = f"/message?id={message_id}"
if not getattr(local, "is_ajax", False):
@ -2437,4 +2420,30 @@ def mock(type, size=1, locale="en"):
return squashify(results)
from frappe.desk.search import validate_and_sanitize_search_inputs # noqa
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"])
kwargs["start"] = cint(kwargs["start"])
kwargs["page_len"] = cint(kwargs["page_len"])
if kwargs["doctype"] and not db.exists("DocType", kwargs["doctype"]):
return []
return fn(**kwargs)
return wrapper
if _tune_gc:
# generational GC gets triggered after certain allocs (g0) which is 700 by default.
# This number is quite small for frappe where a single query can potentially create 700+
# objects easily.
# Bump this number higher, this will make GC less aggressive but that improves performance of
# everything else.
g0, g1, g2 = gc.get_threshold() # defaults are 700, 10, 10.
gc.set_threshold(g0 * 10, g1 * 2, g2 * 2)

View file

@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import gc
import logging
import os
@ -30,6 +31,30 @@ _site = None
_sites_path = os.environ.get("SITES_PATH", ".")
# If gc.freeze is done then importing modules before forking allows us to share the memory
if frappe._tune_gc:
import frappe.boot
import frappe.client
import frappe.core.doctype.user.user
import frappe.database.mariadb.database # Load database related utils
import frappe.database.query
import frappe.desk.desktop # workspace
import frappe.model.db_query
import frappe.query_builder
import frappe.utils.background_jobs # Enqueue is very common
import frappe.utils.data # common utils
import frappe.utils.jinja # web page rendering
import frappe.utils.jinja_globals
import frappe.utils.redis_wrapper # Exact redis_wrapper
import frappe.utils.safe_exec
import frappe.utils.typing_validations # any whitelisted method uses this
import frappe.website.path_resolver # all the page types and resolver
import frappe.website.router # Website router
import frappe.website.website_generator # web page doctypes
# end: module pre-loading
@local_manager.middleware
@Request.application
def application(request: Request):
@ -157,6 +182,8 @@ def log_request(request, response):
{
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"pid": os.getpid(),
"user": getattr(frappe.local.session, "user", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
@ -392,3 +419,17 @@ def serve(
use_evalex=not in_test_env,
threaded=not no_threading,
)
# Both Gunicorn and RQ use forking to spawn workers. In an ideal world, the fork should be sharing
# most of the memory if there are no writes made to data because of Copy on Write, however,
# python's GC is not CoW friendly and writes to data even if user-code doesn't. Specifically, the
# generational GC which stores and mutates every python object: `PyGC_Head`
#
# Calling gc.freeze() moves all the objects imported so far into permanant generation and hence
# doesn't mutate `PyGC_Head`
#
# Refer to issue for more info: https://github.com/frappe/frappe/issues/18927
if frappe._tune_gc:
gc.collect() # clean up any garbage created so far before freeze
gc.freeze()

View file

@ -188,10 +188,10 @@ class LoginManager:
frappe.response["full_name"] = self.full_name
# redirect information
redirect_to = frappe.cache().hget("redirect_after_login", self.user)
redirect_to = frappe.cache.hget("redirect_after_login", self.user)
if redirect_to:
frappe.local.response["redirect_to"] = redirect_to
frappe.cache().hdel("redirect_after_login", self.user)
frappe.cache.hdel("redirect_after_login", self.user)
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user)
@ -482,15 +482,15 @@ class LoginAttemptTracker:
@property
def login_failed_count(self):
return frappe.cache().hget("login_failed_count", self.user_name)
return frappe.cache.hget("login_failed_count", self.user_name)
@login_failed_count.setter
def login_failed_count(self, count):
frappe.cache().hset("login_failed_count", self.user_name, count)
frappe.cache.hset("login_failed_count", self.user_name, count)
@login_failed_count.deleter
def login_failed_count(self):
frappe.cache().hdel("login_failed_count", self.user_name)
frappe.cache.hdel("login_failed_count", self.user_name)
@property
def login_failed_time(self):
@ -498,15 +498,15 @@ class LoginAttemptTracker:
For every user we track only First failed login attempt time within lock interval of time.
"""
return frappe.cache().hget("login_failed_time", self.user_name)
return frappe.cache.hget("login_failed_time", self.user_name)
@login_failed_time.setter
def login_failed_time(self, timestamp):
frappe.cache().hset("login_failed_time", self.user_name, timestamp)
frappe.cache.hset("login_failed_time", self.user_name, timestamp)
@login_failed_time.deleter
def login_failed_time(self):
frappe.cache().hdel("login_failed_time", self.user_name)
frappe.cache.hdel("login_failed_time", self.user_name)
def add_failure_attempt(self):
"""Log user failure attempts into the system.

View file

@ -9,7 +9,7 @@ class TestMilestoneTracker(FrappeTestCase):
def test_milestone(self):
frappe.db.delete("Milestone Tracker")
frappe.cache().delete_key("milestone_tracker_map")
frappe.cache.delete_key("milestone_tracker_map")
milestone_tracker = frappe.get_doc(
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status")

View file

@ -21,7 +21,6 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p
from frappe.social.doctype.energy_point_settings.energy_point_settings import (
is_energy_point_enabled,
)
from frappe.translate import get_lang_dict, get_messages_for_boot, get_translated_doctypes
from frappe.utils import add_user_info, cstr, get_system_timezone
from frappe.utils.change_log import get_versions
from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled
@ -29,6 +28,8 @@ from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabl
def get_bootinfo():
"""build and return boot info"""
from frappe.translate import get_lang_dict, get_translated_doctypes
frappe.set_user_lang(frappe.session.user)
bootinfo = frappe._dict()
hooks = frappe.get_hooks()
@ -149,10 +150,8 @@ def get_allowed_report_names(cache=False) -> set[str]:
def get_user_pages_or_reports(parent, cache=False):
_cache = frappe.cache()
if cache:
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
has_role = frappe.cache.get_value("has_role:" + parent, user=frappe.session.user)
if has_role:
return has_role
@ -254,11 +253,13 @@ def get_user_pages_or_reports(parent, cache=False):
has_role.pop(r, None)
# Expire every six hours
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
frappe.cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role
def load_translations(bootinfo):
from frappe.translate import get_messages_for_boot
bootinfo["lang"] = frappe.lang
bootinfo["__messages"] = get_messages_for_boot()

View file

@ -9,9 +9,6 @@ from tempfile import mkdtemp, mktemp
from urllib.parse import urlparse
import click
import psutil
from requests import head
from requests.exceptions import HTTPError
from semantic_version import Version
import frappe
@ -27,7 +24,7 @@ class AssetsNotDownloadedError(Exception):
pass
class AssetsDontExistError(HTTPError):
class AssetsDontExistError(Exception):
pass
@ -78,6 +75,8 @@ def build_missing_files():
def get_assets_link(frappe_head) -> str:
import requests
tag = getoutput(
r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*"
r" refs/tags/,,' -e 's/\^{}//'" % frappe_head
@ -89,7 +88,7 @@ def get_assets_link(frappe_head) -> str:
else:
url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz"
if not head(url):
if not requests.head(url):
reference = f"Release {tag}" if tag else f"Commit {frappe_head}"
raise AssetsDontExistError(f"Assets for {reference} don't exist")
@ -227,11 +226,10 @@ def bundle(
mode,
apps=None,
hard_link=False,
make_copy=False,
restore=False,
verbose=False,
skip_frappe=False,
files=None,
save_metafiles=False,
):
"""concat / minify js files"""
setup()
@ -251,6 +249,9 @@ def bundle(
command += " --run-build-command"
if save_metafiles:
command += " --save-metafiles"
check_node_executable()
frappe_app_path = frappe.get_app_path("frappe", "..")
frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True)
@ -277,8 +278,8 @@ def watch(apps=None):
def check_node_executable():
node_version = Version(subprocess.getoutput("node -v")[1:])
warn = "⚠️ "
if node_version.major < 14:
click.echo(f"{warn} Please update your node version to 14")
if node_version.major < 18:
click.echo(f"{warn} Please update your node version to 18")
if not shutil.which("yarn"):
click.echo(f"{warn} Please install yarn using below command and try again.\nnpm install -g yarn")
click.echo()
@ -290,6 +291,8 @@ def get_node_env():
def get_safe_max_old_space_size():
import psutil
safe_max_old_space_size = 0
try:
total_memory = psutil.virtual_memory().total / (1024 * 1024)

View file

@ -1,10 +1,7 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe.desk.notifications import clear_notifications, delete_notification_count_for
common_default_keys = ["__default", "__global"]
@ -79,7 +76,7 @@ doctype_cache_keys = (
def clear_user_cache(user=None):
cache = frappe.cache()
from frappe.desk.notifications import clear_notifications
# this will automatically reload the global cache
# so it is important to clear this first
@ -87,20 +84,19 @@ def clear_user_cache(user=None):
if user:
for name in user_cache_keys:
cache.hdel(name, user)
cache.delete_keys("user:" + user)
frappe.cache.hdel(name, user)
frappe.cache.delete_keys("user:" + user)
clear_defaults_cache(user)
else:
for name in user_cache_keys:
cache.delete_key(name)
frappe.cache.delete_key(name)
clear_defaults_cache()
clear_global_cache()
def clear_domain_cache(user=None):
cache = frappe.cache()
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
cache.delete_value(domain_cache_keys)
frappe.cache.delete_value(domain_cache_keys)
def clear_global_cache():
@ -108,17 +104,17 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(global_cache_keys)
frappe.cache().delete_value(bench_cache_keys)
frappe.cache.delete_value(global_cache_keys)
frappe.cache.delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
if user:
for p in [user] + common_default_keys:
frappe.cache().hdel("defaults", p)
frappe.cache.hdel("defaults", p)
elif frappe.flags.in_install != "frappe":
frappe.cache().delete_key("defaults")
frappe.cache.delete_key("defaults")
def clear_doctype_cache(doctype=None):
@ -131,15 +127,15 @@ def clear_doctype_cache(doctype=None):
def _clear_doctype_cache_form_redis(doctype: str | None = None):
cache = frappe.cache()
from frappe.desk.notifications import delete_notification_count_for
for key in ("is_table", "doctype_modules"):
cache.delete_value(key)
frappe.cache.delete_value(key)
def clear_single(dt):
frappe.clear_document_cache(dt)
for name in doctype_cache_keys:
cache.hdel(name, dt)
frappe.cache.hdel(name, dt)
if doctype:
clear_single(doctype)
@ -163,8 +159,8 @@ def _clear_doctype_cache_form_redis(doctype: str | None = None):
else:
# clear all
for name in doctype_cache_keys:
cache.delete_value(name)
cache.delete_keys("document_cache::")
frappe.cache.delete_value(name)
frappe.cache.delete_keys("document_cache::")
def clear_controller_cache(doctype=None):
@ -177,7 +173,7 @@ def clear_controller_cache(doctype=None):
def get_doctype_map(doctype, name, filters=None, order_by=None):
return frappe.cache().hget(
return frappe.cache.hget(
get_doctype_map_key(doctype),
name,
lambda: frappe.get_all(doctype, filters=filters, order_by=order_by, ignore_ddl=True),
@ -185,7 +181,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
def clear_doctype_map(doctype, name):
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)
frappe.cache.hdel(frappe.scrub(doctype) + "_map", name)
def build_table_count_cache():
@ -198,7 +194,6 @@ def build_table_count_cache():
):
return
_cache = frappe.cache()
table_name = frappe.qb.Field("table_name").as_("name")
table_rows = frappe.qb.Field("table_rows").as_("count")
information_schema = frappe.qb.Schema("information_schema")
@ -207,7 +202,7 @@ def build_table_count_cache():
as_dict=True
)
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
_cache.set_value("information_schema:counts", counts)
frappe.cache.set_value("information_schema:counts", counts)
return counts
@ -221,11 +216,10 @@ def build_domain_restriced_doctype_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
doctypes = [doc.name for doc in doctypes]
_cache.set_value("domain_restricted_doctypes", doctypes)
frappe.cache.set_value("domain_restricted_doctypes", doctypes)
return doctypes
@ -239,10 +233,9 @@ def build_domain_restriced_page_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
pages = [page.name for page in pages]
_cache.set_value("domain_restricted_pages", pages)
frappe.cache.set_value("domain_restricted_pages", pages)
return pages

View file

@ -3,7 +3,6 @@ import os
import click
import frappe
from frappe.installer import update_site_config
from frappe.utils.redis_queue import RedisQueue
@ -23,6 +22,8 @@ def create_rq_users(set_admin_password=False, use_rq_auth=False):
acl config file will be used by redis server while starting the server
and app config is used by app while connecting to redis server.
"""
from frappe.installer import update_site_config
acl_file_path = os.path.abspath("../config/redis_queue.acl")
with frappe.init_site():

View file

@ -9,7 +9,6 @@ import click
# imports - module imports
import frappe
from frappe.commands import get_site, pass_context
from frappe.core.doctype.log_settings.log_settings import LOG_DOCTYPES
from frappe.exceptions import SiteNotSpecifiedError
@ -1199,11 +1198,12 @@ def build_search_index(context):
@click.command("clear-log-table")
@click.option("--doctype", required=True, type=click.Choice(LOG_DOCTYPES), help="Log DocType")
@click.option("--doctype", required=True, type=str, help="Log DocType")
@click.option("--days", type=int, help="Keep records for days")
@click.option("--no-backup", is_flag=True, default=False, help="Do not backup the table")
@pass_context
def clear_log_table(context, doctype, days, no_backup):
"""If any logtype table grows too large then clearing it with DELETE query
is not feasible in reasonable time. This command copies recent data to new
table and replaces current table with new smaller table.
@ -1211,6 +1211,7 @@ def clear_log_table(context, doctype, days, no_backup):
ref: https://mariadb.com/kb/en/big-deletes/#deleting-more-than-half-a-table
"""
from frappe.core.doctype.log_settings.log_settings import LOG_DOCTYPES
from frappe.core.doctype.log_settings.log_settings import clear_log_table as clear_logs
from frappe.utils.backups import scheduled_backup

View file

@ -102,10 +102,28 @@ def import_translations(context, lang, path):
frappe.destroy()
@click.command("migrate-translations")
@click.argument("source-app")
@click.argument("target-app")
@pass_context
def migrate_translations(context, source_app, target_app):
"Migrate target-app-specific translations from source-app to target-app"
import frappe.translate
site = get_site(context)
try:
frappe.init(site=site)
frappe.connect()
frappe.translate.migrate_translations(source_app, target_app)
finally:
frappe.destroy()
commands = [
build_message_files,
get_untranslated,
import_translations,
new_language,
update_translations,
migrate_translations,
]

View file

@ -13,10 +13,6 @@ from frappe.exceptions import SiteNotSpecifiedError
from frappe.utils import cint, update_progress_bar
find_executable = which # backwards compatibility
DATA_IMPORT_DEPRECATION = (
"[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n"
"Use `data-import` command instead to import data via 'Data Import'."
)
EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@ -30,32 +26,25 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
help="Copy the files instead of symlinking",
envvar="FRAPPE_HARD_LINK_ASSETS",
)
@click.option(
"--make-copy",
is_flag=True,
default=False,
help="[DEPRECATED] Copy the files instead of symlinking",
)
@click.option(
"--restore",
is_flag=True,
default=False,
help="[DEPRECATED] Copy the files instead of symlinking with force",
)
@click.option("--production", is_flag=True, default=False, help="Build assets in production mode")
@click.option("--verbose", is_flag=True, default=False, help="Verbose")
@click.option(
"--force", is_flag=True, default=False, help="Force build assets instead of downloading available"
)
@click.option(
"--save-metafiles",
is_flag=True,
default=False,
help="Saves esbuild metafiles for built assets. Useful for analyzing bundle size. More info: https://esbuild.github.io/api/#metafile",
)
def build(
app=None,
apps=None,
hard_link=False,
make_copy=False,
restore=False,
production=False,
verbose=False,
force=False,
save_metafiles=False,
):
"Compile JS and CSS source files"
from frappe.build import bundle, download_frappe_assets
@ -80,14 +69,14 @@ def build(
if production:
mode = "production"
if make_copy or restore:
hard_link = make_copy or restore
click.secho(
"bench build: --make-copy and --restore options are deprecated in favour of --hard-link",
fg="yellow",
)
bundle(mode, apps=apps, hard_link=hard_link, verbose=verbose, skip_frappe=skip_frappe)
bundle(
mode,
apps=apps,
hard_link=hard_link,
verbose=verbose,
skip_frappe=skip_frappe,
save_metafiles=save_metafiles,
)
@click.command("watch")
@ -409,37 +398,16 @@ def import_doc(context, path, force=False):
raise SiteNotSpecifiedError
@click.command("import-csv", help=DATA_IMPORT_DEPRECATION)
@click.argument("path")
@click.option(
"--only-insert", default=False, is_flag=True, help="Do not overwrite existing records"
)
@click.option(
"--submit-after-import", default=False, is_flag=True, help="Submit document after importing it"
)
@click.option(
"--ignore-encoding-errors",
default=False,
is_flag=True,
help="Ignore encoding errors while coverting to unicode",
)
@click.option("--no-email", default=True, is_flag=True, help="Send email if applicable")
@pass_context
def import_csv(
context,
path,
only_insert=False,
submit_after_import=False,
ignore_encoding_errors=False,
no_email=True,
):
click.secho(DATA_IMPORT_DEPRECATION, fg="yellow")
sys.exit(1)
@click.command("data-import")
@click.option(
"--file", "file_path", type=click.Path(), required=True, help="Path to import file (.csv, .xlsx)"
"--file",
"file_path",
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
required=True,
help=(
"Path to import file (.csv, .xlsx)."
"Consider that relative paths will resolve from 'sites' directory"
),
)
@click.option("--doctype", type=str, required=True)
@click.option(
@ -765,7 +733,6 @@ def transform_database(context, table, engine, row_format, failfast):
help="Path to .txt file for list of doctypes. Example erpnext/tests/server/agriculture.txt",
)
@click.option("--test", multiple=True, help="Specific test")
@click.option("--ui-tests", is_flag=True, default=False, help="Run UI Tests")
@click.option("--module", help="Run tests in a module")
@click.option("--profile", is_flag=True, default=False)
@click.option("--coverage", is_flag=True, default=False)
@ -788,7 +755,6 @@ def run_tests(
profile=False,
coverage=False,
junit_xml_output=False,
ui_tests=False,
doctype_list_path=None,
skip_test_records=False,
skip_before_tests=False,
@ -827,7 +793,6 @@ def run_tests(
force=context.force,
profile=profile,
junit_xml_output=junit_xml_output,
ui_tests=ui_tests,
doctype_list_path=doctype_list_path,
failfast=failfast,
case=case,
@ -1063,20 +1028,11 @@ def create_patch():
"-g", "--global", "global_", is_flag=True, default=False, help="Set value in bench config"
)
@click.option("-p", "--parse", is_flag=True, default=False, help="Evaluate as Python Object")
@click.option("--as-dict", is_flag=True, default=False, help="Legacy: Evaluate as Python Object")
@pass_context
def set_config(context, key, value, global_=False, parse=False, as_dict=False):
def set_config(context, key, value, global_=False, parse=False):
"Insert/Update a value in site_config.json"
from frappe.installer import update_site_config
if as_dict:
from frappe.utils.commands import warn
warn(
"--as-dict will be deprecated in v14. Use --parse instead", category=PendingDeprecationWarning
)
parse = as_dict
if parse:
import ast
@ -1200,7 +1156,6 @@ commands = [
export_fixtures,
export_json,
get_version,
import_csv,
data_import,
import_doc,
make_app,

View file

@ -0,0 +1,44 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2023-06-16 17:57:36.604672",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"document_type",
"action"
],
"fields": [
{
"default": "Amend Counter",
"fieldname": "action",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Action",
"options": "Amend Counter\nDefault Naming",
"reqd": 1
},
{
"fieldname": "document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-06-16 18:26:16.247475",
"modified_by": "Administrator",
"module": "Core",
"name": "Amended Document Naming Settings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View file

@ -0,0 +1,9 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class AmendedDocumentNamingSettings(Document):
pass

View file

@ -62,7 +62,7 @@ class Importer:
def before_import(self):
# set user lang for translations
frappe.cache().hdel("lang", frappe.session.user)
frappe.cache.hdel("lang", frappe.session.user)
frappe.set_user_lang(frappe.session.user)
# set flags
@ -579,6 +579,10 @@ class ImportFile:
file_content = None
if self.console:
file_content = frappe.read_file(file_path, True)
return file_content, extn
file_name = frappe.db.get_value("File", {"file_url": file_path})
if file_name:
file = frappe.get_doc("File", file_name)
@ -690,7 +694,7 @@ class Row:
df = col.df
if df.fieldtype == "Select":
select_options = get_select_options(df)
if select_options and value not in select_options:
if select_options and cstr(value) not in select_options:
options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
@ -1207,7 +1211,7 @@ def get_df_for_column_header(doctype, header):
def build_fields_dict_for_doctype():
return build_fields_dict_for_column_matching(doctype)
df_by_labels_and_fieldname = frappe.cache().hget(
df_by_labels_and_fieldname = frappe.cache.hget(
"data_import_column_header_map", doctype, generator=build_fields_dict_for_doctype
)
return df_by_labels_and_fieldname.get(header)

View file

@ -21,6 +21,7 @@
"search_index",
"column_break_18",
"options",
"sort_options",
"show_dashboard",
"defaults_section",
"default",
@ -102,7 +103,8 @@
"oldfieldtype": "Select",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
"search_index": 1,
"sort_options": 1
},
{
"bold": 1,
@ -550,13 +552,20 @@
"fieldtype": "Data",
"label": "Documentation URL",
"options": "URL"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype === 'Select'",
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-02-20 12:07:29.552523",
"modified": "2023-06-08 19:05:10.778371",
"modified_by": "Administrator",
"module": "Core",
"name": "DocField",

View file

@ -67,7 +67,8 @@
"default": "0",
"fieldname": "everyone",
"fieldtype": "Check",
"label": "Everyone"
"label": "Everyone",
"search_index": 1
},
{
"default": "1",
@ -85,10 +86,11 @@
],
"in_create": 1,
"links": [],
"modified": "2021-04-04 11:38:50.813312",
"modified": "2023-06-15 18:02:51.877533",
"modified_by": "Administrator",
"module": "Core",
"name": "DocShare",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [
{
@ -106,5 +108,6 @@
"read_only": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -755,4 +755,4 @@
"states": [],
"track_changes": 1,
"translated_doctype": 1
}
}

View file

@ -337,7 +337,7 @@ class DocType(Document):
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=self.name)
)
for p in parent_list:
frappe.db.set_value("DocType", p.parent, {}, for_update=False)
frappe.db.set_value("DocType", p.parent, {})
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
@ -1710,7 +1710,7 @@ def check_fieldname_conflicts(docfield):
def clear_linked_doctype_cache():
frappe.cache().delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
frappe.cache.delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
def check_email_append_to(doc):

View file

@ -2,6 +2,16 @@
// For license information, please see license.txt
frappe.ui.form.on("Document Naming Settings", {
setup: function (frm) {
frm.set_query("document_type", "amend_naming_override", () => {
return {
filters: {
is_submittable: 1,
},
};
});
},
refresh: function (frm) {
frm.trigger("setup_transaction_autocomplete");
frm.disable_save();

View file

@ -18,7 +18,11 @@
"update_series",
"prefix",
"current_value",
"update_series_start"
"update_series_start",
"amended_documents_section",
"default_amend_naming",
"amend_naming_override",
"update_amendment_naming"
],
"fields": [
{
@ -105,13 +109,41 @@
"fieldtype": "Text",
"label": "Preview of generated names",
"read_only": 1
},
{
"collapsible": 1,
"description": "Configure how amended documents will be named.<br>\n\nDefault behaviour is to follow an amend counter which adds a number to the end of the original name indicating the amended version. <br>\n\nDefault Naming will make the amended document to behave same as new documents.",
"fieldname": "amended_documents_section",
"fieldtype": "Section Break",
"label": "Amended Documents"
},
{
"default": "Amend Counter",
"fieldname": "default_amend_naming",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Default Amendment Naming",
"options": "Amend Counter\nDefault Naming",
"reqd": 1
},
{
"fieldname": "amend_naming_override",
"fieldtype": "Table",
"label": "Amendment Naming Override",
"options": "Amended Document Naming Settings"
},
{
"fieldname": "update_amendment_naming",
"fieldtype": "Button",
"label": "Update Amendment Naming",
"options": "update_amendment_rule"
}
],
"hide_toolbar": 1,
"icon": "fa fa-sort-by-order",
"issingle": 1,
"links": [],
"modified": "2023-02-20 13:11:56.662100",
"modified": "2023-06-20 17:47:52.204139",
"modified_by": "Administrator",
"module": "Core",
"name": "Document Naming Settings",

View file

@ -169,6 +169,23 @@ class DocumentNamingSettings(Document):
self.current_value = NamingSeries(self.prefix).get_current_value()
return self.current_value
@frappe.whitelist()
def update_amendment_rule(self):
self.db_set("default_amend_naming", self.default_amend_naming)
existing_overrides = frappe.db.get_all(
"Amended Document Naming Settings",
filters={"name": ["not in", [d.name for d in self.amend_naming_override]]},
pluck="name",
)
for override in existing_overrides:
frappe.delete_doc("Amended Document Naming Settings", override)
for row in self.amend_naming_override:
row.save()
frappe.msgprint(_("Amendment naming rules updated."), indicator="green", alert=True)
@frappe.whitelist()
def update_series_start(self):
frappe.only_for("System Manager")

View file

@ -26,6 +26,7 @@ class TestNamingSeries(FrappeTestCase):
}
],
autoname="naming_series:",
is_submittable=1,
)
.insert()
.name
@ -82,3 +83,36 @@ class TestNamingSeries(FrappeTestCase):
self.dns.update_series_start()
self.assertEqual(self.dns.get_current(), new_count, f"Incorrect update for {series}")
def test_amended_naming(self):
self.dns.amend_naming_override = []
self.dns.default_amend_naming = "Amend Counter"
self.dns.update_amendment_rule()
submittable_doc = frappe.get_doc(
dict(doctype=self.ns_doctype, some_fieldname="test doc with submit")
).submit()
submittable_doc.cancel()
amended_doc = frappe.get_doc(
dict(
doctype=self.ns_doctype,
some_fieldname="test doc with submit",
amended_from=submittable_doc.name,
)
).insert()
self.assertIn(submittable_doc.name, amended_doc.name)
amended_doc.delete()
self.dns.default_amend_naming = "Default Naming"
self.dns.update_amendment_rule()
new_amended_doc = frappe.get_doc(
dict(
doctype=self.ns_doctype,
some_fieldname="test doc with submit",
amended_from=submittable_doc.name,
)
).insert()
self.assertNotIn(submittable_doc.name, new_amended_doc.name)

View file

@ -73,7 +73,7 @@ def get_active_domains():
active_domains.append("")
return active_domains
return frappe.cache().get_value("active_domains", _get_active_domains)
return frappe.cache.get_value("active_domains", _get_active_domains)
def get_active_modules():
@ -87,4 +87,4 @@ def get_active_modules():
active_modules.append(m.name)
return active_modules
return frappe.cache().get_value("active_modules", _get_active_modules)
return frappe.cache.get_value("active_modules", _get_active_modules)

View file

@ -1,7 +1,10 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
from ldap3.core.exceptions import LDAPException, LDAPInappropriateAuthenticationResult
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils.error import _is_ldap_exception
# test_records = frappe.get_test_records('Error Log')
@ -12,3 +15,9 @@ class TestErrorLog(FrappeTestCase):
doc = frappe.new_doc("Error Log")
error = doc.log_error("This is an error")
self.assertEqual(error.doctype, "Error Log")
def test_ldap_exceptions(self):
exc = [LDAPException, LDAPInappropriateAuthenticationResult]
for e in exc:
self.assertTrue(_is_ldap_exception(e()))

View file

@ -236,12 +236,19 @@ class File(Document):
):
return
frappe.db.set_value(
self.attached_to_doctype,
self.attached_to_name,
self.attached_to_field,
self.file_url,
)
if frappe.get_meta(self.attached_to_doctype).issingle:
frappe.db.set_single_value(
self.attached_to_doctype,
self.attached_to_field,
self.file_url,
)
else:
frappe.db.set_value(
self.attached_to_doctype,
self.attached_to_name,
self.attached_to_field,
self.file_url,
)
def fetch_attached_to_field(self, old_file_url):
if self.attached_to_field:

View file

@ -137,7 +137,7 @@ class Report(Document):
if execution_time > threshold and not self.prepared_report:
self.db_set("prepared_report", 1)
frappe.cache().hset("report_execution_time", self.name, execution_time)
frappe.cache.hset("report_execution_time", self.name, execution_time)
return res

View file

@ -24,7 +24,7 @@ class Role(Document):
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
frappe.cache().hdel("roles", "Administrator")
frappe.cache.hdel("roles", "Administrator")
def validate(self):
if self.disabled:

View file

@ -124,6 +124,20 @@ class TestRQJob(FrappeTestCase):
frappe.db.commit()
self.assertIsNone(get_job_status(job_id))
@timeout(20)
def test_memory_usage(self):
job = frappe.enqueue("frappe.utils.data._get_rss_memory_usage")
self.check_status(job, "finished")
rss = job.latest_result().return_value
msg = """Memory usage of simple background job increased. Potential root cause can be a newly added python module import. Check and move them to approriate file/function to avoid loading the module by default."""
# If this starts failing analyze memory usage using memray or some equivalent tool to find
# offending imports/function calls.
# Refer this PR: https://github.com/frappe/frappe/pull/21467
LAST_MEASURED_USAGE = 40
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
def test_func(fail=False, sleep=0):
if fail:

View file

@ -18,7 +18,7 @@ class TestScheduledJobType(FrappeTestCase):
self.assertEqual(all_job.frequency, "All")
daily_job = frappe.get_doc(
"Scheduled Job Type", dict(method="frappe.email.queue.set_expiry_for_email_queue")
"Scheduled Job Type", dict(method="frappe.desk.notifications.clear_notifications")
)
self.assertEqual(daily_job.frequency, "Daily")
@ -37,7 +37,7 @@ class TestScheduledJobType(FrappeTestCase):
def test_daily_job(self):
job = frappe.get_doc(
"Scheduled Job Type", dict(method="frappe.email.queue.set_expiry_for_email_queue")
"Scheduled Job Type", dict(method="frappe.desk.notifications.clear_notifications")
)
job.db_set("last_execution", "2019-01-01 00:00:00")
self.assertTrue(job.is_event_due(get_datetime("2019-01-02 00:00:06")))

View file

@ -19,7 +19,7 @@ class ServerScript(Document):
self.check_if_compilable_in_restricted_context()
def on_update(self):
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
self.sync_scheduler_events()
def on_trash(self):
@ -168,11 +168,11 @@ class ServerScript(Document):
out.append([key, score])
return out
items = frappe.cache().get_value("server_script_autocompletion_items")
items = frappe.cache.get_value("server_script_autocompletion_items")
if not items:
items = get_keys(get_safe_globals())
items = [{"value": d[0], "score": d[1]} for d in items]
frappe.cache().set_value("server_script_autocompletion_items", items)
frappe.cache.set_value("server_script_autocompletion_items", items)
return items

View file

@ -55,7 +55,7 @@ def get_server_script_map():
if frappe.flags.in_patch and not frappe.db.table_exists("Server Script"):
return {}
script_map = frappe.cache().get_value("server_script_map")
script_map = frappe.cache.get_value("server_script_map")
if script_map is None:
script_map = {"permission_query": {}}
enabled_server_scripts = frappe.get_all(
@ -73,6 +73,6 @@ def get_server_script_map():
else:
script_map.setdefault("_api", {})[script.api_method] = script.name
frappe.cache().set_value("server_script_map", script_map)
frappe.cache.set_value("server_script_map", script_map)
return script_map

View file

@ -104,10 +104,10 @@ class TestServerScript(FrappeTestCase):
def tearDownClass(cls):
frappe.db.commit()
frappe.db.truncate("Server Script")
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
def setUp(self):
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
def test_doctype_event(self):
todo = frappe.get_doc(dict(doctype="ToDo", description="hello")).insert()

View file

@ -5,14 +5,13 @@ import frappe
from frappe import _
from frappe.model import no_value_fields
from frappe.model.document import Document
from frappe.translate import set_default_language
from frappe.twofactor import toggle_two_factor_auth
from frappe.utils import cint, today
from frappe.utils.momentjs import get_all_timezones
class SystemSettings(Document):
def validate(self):
from frappe.twofactor import toggle_two_factor_auth
enable_password_policy = cint(self.enable_password_policy) and True or False
minimum_password_score = cint(getattr(self, "minimum_password_score", 0)) or 0
if enable_password_policy and minimum_password_score <= 0:
@ -64,13 +63,15 @@ class SystemSettings(Document):
def on_update(self):
self.set_defaults()
frappe.cache().delete_value("system_settings")
frappe.cache().delete_value("time_zone")
frappe.cache.delete_value("system_settings")
frappe.cache.delete_value("time_zone")
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()
def set_defaults(self):
from frappe.translate import set_default_language
for df in self.meta.get("fields"):
if df.fieldtype not in no_value_fields and self.has_value_changed(df.fieldname):
frappe.db.set_default(df.fieldname, self.get(df.fieldname))
@ -92,6 +93,8 @@ def update_last_reset_password_date():
@frappe.whitelist()
def load():
from frappe.utils.momentjs import get_all_timezones
if not "System Manager" in frappe.get_roles():
frappe.throw(_("Not permitted"), frappe.PermissionError)

View file

@ -23,71 +23,7 @@ class Translation(Document):
def on_trash(self):
clear_user_translation_cache(self.language)
def contribute(self):
pass
def get_contribution_status(self):
pass
@frappe.whitelist()
def create_translations(translation_map, language):
from frappe.frappeclient import FrappeClient
translation_map = json.loads(translation_map)
translation_map_to_send = frappe._dict({})
# first create / update local user translations
for source_id, translation_dict in translation_map.items():
translation_dict = frappe._dict(translation_dict)
existing_doc_name = frappe.get_all(
"Translation",
{
"source_text": translation_dict.source_text,
"context": translation_dict.context or "",
"language": language,
},
)
translation_map_to_send[source_id] = translation_dict
if existing_doc_name:
frappe.db.set_value(
"Translation",
existing_doc_name[0].name,
{
"translated_text": translation_dict.translated_text,
"contributed": 1,
"contribution_status": "Pending",
},
)
translation_map_to_send[source_id].name = existing_doc_name[0].name
else:
doc = frappe.get_doc(
{
"doctype": "Translation",
"source_text": translation_dict.source_text,
"contributed": 1,
"contribution_status": "Pending",
"translated_text": translation_dict.translated_text,
"context": translation_dict.context,
"language": language,
}
)
doc.insert()
translation_map_to_send[source_id].name = doc.name
params = {
"language": language,
"contributor_email": frappe.session.user,
"contributor_name": frappe.utils.get_fullname(frappe.session.user),
"translation_map": json.dumps(translation_map_to_send),
}
translator = FrappeClient(get_translator_url())
added_translations = translator.post_api("translator.api.add_translations", params=params)
for local_docname, remote_docname in added_translations.items():
frappe.db.set_value("Translation", local_docname, "contribution_docname", remote_docname)
def clear_user_translation_cache(lang):
frappe.cache().hdel(USER_TRANSLATION_KEY, lang)
frappe.cache().hdel(MERGED_TRANSLATION_KEY, lang)
frappe.cache.hdel(USER_TRANSLATION_KEY, lang)
frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang)

View file

@ -283,7 +283,7 @@ class TestUser(FrappeTestCase):
# Clear rate limit tracker to start fresh
key = f"rl:{data['cmd']}:{data['user']}"
frappe.cache().delete(key)
frappe.cache.delete(key)
c = FrappeClient(url)
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
@ -330,7 +330,7 @@ class TestUser(FrappeTestCase):
sign_up(random_user, random_user_name, "/welcome"),
(1, "Please check your email for verification"),
)
self.assertEqual(frappe.cache().hget("redirect_after_login", random_user), "/welcome")
self.assertEqual(frappe.cache.hget("redirect_after_login", random_user), "/welcome")
# re-register
self.assertTupleEqual(

View file

@ -28,6 +28,7 @@ from frappe.utils import (
now_datetime,
today,
)
from frappe.utils.deprecations import deprecated
from frappe.utils.password import check_password, get_password_reset_limit
from frappe.utils.password import update_password as _update_password
from frappe.utils.user import get_system_managers
@ -60,8 +61,8 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
frappe.cache().delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("users_for_mentions")
frappe.cache.delete_key("enabled_users")
def validate(self):
# clear new password
@ -75,6 +76,7 @@ class User(Document):
self.validate_email_type(self.email)
self.validate_email_type(self.name)
self.add_system_manager_role()
self.populate_role_profile_roles()
self.check_roles_added()
self.set_system_user()
self.set_full_name()
@ -85,7 +87,6 @@ class User(Document):
self.remove_disabled_roles()
self.validate_user_email_inbox()
ask_pass_update()
self.validate_roles()
self.validate_allowed_modules()
self.validate_user_image()
self.set_time_zone()
@ -98,12 +99,16 @@ class User(Document):
):
self.set_social_login_userid("frappe", frappe.generate_hash(length=39))
def validate_roles(self):
def populate_role_profile_roles(self):
if self.role_profile_name:
role_profile = frappe.get_doc("Role Profile", self.role_profile_name)
self.set("roles", [])
self.append_roles(*[role.role for role in role_profile.roles])
@deprecated
def validate_roles(self):
self.populate_role_profile_roles()
def validate_allowed_modules(self):
if self.module_profile:
module_profile = frappe.get_doc("Module Profile", self.module_profile)
@ -143,10 +148,10 @@ class User(Document):
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
if self.has_value_changed("enabled"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("users_for_mentions")
frappe.cache.delete_key("enabled_users")
elif self.has_value_changed("allow_in_mentions") or self.has_value_changed("user_type"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache.delete_key("users_for_mentions")
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
@ -462,9 +467,9 @@ class User(Document):
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
if self.get("allow_in_mentions"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache.delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("enabled_users")
# delete user permissions
frappe.db.delete("User Permission", {"user": self.name})
@ -760,10 +765,10 @@ def update_password(
user_doc, redirect_url = reset_user_data(user)
# get redirect url from cache
redirect_to = frappe.cache().hget("redirect_after_login", user)
redirect_to = frappe.cache.hget("redirect_after_login", user)
if redirect_to:
redirect_url = redirect_to
frappe.cache().hdel("redirect_after_login", user)
frappe.cache.hdel("redirect_after_login", user)
frappe.local.login_manager.login_as(user)
@ -921,7 +926,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
user.add_roles(default_role)
if redirect_to:
frappe.cache().hset("redirect_after_login", user.name, redirect_to)
frappe.cache.hset("redirect_after_login", user.name, redirect_to)
if user.flags.email_sent:
return 1, _("Please check your email for verification")
@ -1234,4 +1239,4 @@ def get_enabled_users():
enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
return enabled_users
return frappe.cache().get_value("enabled_users", _get_enabled_users)
return frappe.cache.get_value("enabled_users", _get_enabled_users)

View file

@ -9,7 +9,7 @@ from frappe.model.document import Document
class UserGroup(Document):
def after_insert(self):
frappe.cache().delete_key("user_groups")
frappe.cache.delete_key("user_groups")
def on_trash(self):
frappe.cache().delete_key("user_groups")
frappe.cache.delete_key("user_groups")

View file

@ -178,7 +178,7 @@ class TestUserPermission(FrappeTestCase):
frappe.db.set_value(
"User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1
)
frappe.cache().delete_value("user_permissions")
frappe.cache.delete_value("user_permissions")
# check if adding perm on a group record with hide_descendants enabled,
# hides child records

View file

@ -17,11 +17,11 @@ class UserPermission(Document):
self.validate_default_permission()
def on_update(self):
frappe.cache().hdel("user_permissions", self.user)
frappe.cache.hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def on_trash(self):
frappe.cache().hdel("user_permissions", self.user)
frappe.cache.hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def validate_user_permission(self):
@ -74,7 +74,7 @@ def get_user_permissions(user=None):
if not user or user in ("Administrator", "Guest"):
return {}
cached_user_permissions = frappe.cache().hget("user_permissions", user)
cached_user_permissions = frappe.cache.hget("user_permissions", user)
if cached_user_permissions is not None:
return cached_user_permissions
@ -110,7 +110,7 @@ def get_user_permissions(user=None):
add_doc_to_perm(perm, doc, False)
out = frappe._dict(out)
frappe.cache().hset("user_permissions", user, out)
frappe.cache.hset("user_permissions", user, out)
except frappe.db.SQLError as e:
if frappe.db.is_table_missing(e):
# called from patch

View file

@ -18,7 +18,7 @@ class UserType(Document):
super().clear_cache()
if not self.is_standard:
frappe.cache().delete_value("non_standard_user_types")
frappe.cache.delete_value("non_standard_user_types")
def on_update(self):
if self.is_standard:
@ -290,7 +290,7 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
if not frappe.db.table_exists("User Type") or frappe.flags.in_migrate:
return
user_types = frappe.cache().get_value(
user_types = frappe.cache.get_value(
"non_standard_user_types",
get_non_standard_user_types,
)

View file

@ -123,8 +123,15 @@ def update(doctype, role, permlevel, ptype, value=None):
Returns:
str: Refresh flag is permission is updated successfully
"""
def clear_cache():
frappe.clear_cache(doctype=doctype)
frappe.only_for("System Manager")
out = update_permission_property(doctype, role, permlevel, ptype, value)
frappe.db.after_commit.add(clear_cache)
return "refresh" if out else None

View file

@ -21,6 +21,7 @@
"hide_seconds",
"hide_days",
"options",
"sort_options",
"fetch_from",
"fetch_if_empty",
"options_help",
@ -126,7 +127,8 @@
"oldfieldname": "fieldtype",
"oldfieldtype": "Select",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nJSON\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRead Only\nRating\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1
"reqd": 1,
"sort_options": 1
},
{
"depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
@ -435,13 +437,20 @@
"fieldtype": "Check",
"label": "Is System Generated",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype === 'Select'",
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
}
],
"icon": "fa fa-glass",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-06-13 06:39:03.319667",
"modified": "2023-06-08 19:05:51.737234",
"modified_by": "Administrator",
"module": "Custom",
"name": "Custom Field",

View file

@ -40,8 +40,9 @@ class CustomField(Document):
# remove special characters from fieldname
self.fieldname = "".join(
filter(lambda x: x.isdigit() or x.isalpha() or "_", cstr(label).replace(" ", "_"))
[c for c in cstr(label).replace(" ", "_") if c.isdigit() or c.isalpha() or c == "_"]
)
self.fieldname = f"custom_{self.fieldname}"
# fieldnames should be lowercase
self.fieldname = self.fieldname.lower()

View file

@ -49,14 +49,6 @@ frappe.ui.form.on("Customize Form", {
grid_row.row.addClass("highlight");
}
});
$(frm.wrapper).on("grid-make-sortable", function (e, frm) {
frm.trigger("setup_sortable");
});
$(frm.wrapper).on("grid-move-row", function (e, frm) {
frm.trigger("setup_sortable");
});
},
doc_type: function (frm) {
@ -71,7 +63,7 @@ frappe.ui.form.on("Customize Form", {
frm.set_value("doc_type", "");
} else {
frm.refresh();
frm.trigger("setup_sortable");
frm.trigger("add_customize_child_table_button");
frm.trigger("setup_default_views");
}
}
@ -87,23 +79,16 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_default_views");
},
setup_sortable: function (frm) {
add_customize_child_table_button: function (frm) {
frm.doc.fields.forEach(function (f) {
if (!f.is_custom_field || f.is_system_generated) {
f._sortable = false;
}
if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return;
if (f.fieldtype == "Table") {
frm.add_custom_button(
f.options,
function () {
frm.set_value("doc_type", f.options);
},
__("Customize Child Table")
);
}
frm.add_custom_button(
f.options,
() => frm.set_value("doc_type", f.options),
__("Customize Child Table")
);
});
frm.fields_dict.fields.grid.refresh();
},
refresh: function (frm) {
@ -125,6 +110,14 @@ frappe.ui.form.on("Customize Form", {
__("Actions")
);
frm.add_custom_button(
__("Set Permissions"),
function () {
frappe.set_route("permission-manager", frm.doc.doc_type);
},
__("Actions")
);
frm.add_custom_button(
__("Reload"),
function () {
@ -134,17 +127,17 @@ frappe.ui.form.on("Customize Form", {
);
frm.add_custom_button(
__("Reset to defaults"),
function () {
frappe.customize_form.confirm(__("Remove all customizations?"), frm);
__("Reset Layout"),
() => {
frm.trigger("reset_layout");
},
__("Actions")
);
frm.add_custom_button(
__("Set Permissions"),
__("Reset All Customizations"),
function () {
frappe.set_route("permission-manager", frm.doc.doc_type);
frappe.customize_form.confirm(__("Remove all customizations?"), frm);
},
__("Actions")
);
@ -179,6 +172,27 @@ frappe.ui.form.on("Customize Form", {
}
},
reset_layout(frm) {
frappe.confirm(
__("Layout will be reset to standard layout, are you sure you want to do this?"),
() => {
return frm.call({
doc: frm.doc,
method: "reset_layout",
callback: function (r) {
if (!r.exc) {
frappe.show_alert({
message: __("Layout Reset"),
indicator: "green",
});
frappe.customize_form.clear_locals_and_refresh(frm);
}
},
});
}
);
},
setup_export(frm) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(

View file

@ -35,7 +35,7 @@ class CustomizeForm(Document):
if not self.doc_type:
return
meta = frappe.get_meta(self.doc_type)
meta = frappe.get_meta(self.doc_type, cached=False)
self.validate_doctype(meta)
@ -214,11 +214,39 @@ class CustomizeForm(Document):
# action and links
self.set_property_setters_for_actions_and_links(meta)
def set_property_setter_for_field_order(self, meta):
new_order = [df.fieldname for df in self.fields]
existing_order = getattr(meta, "field_order", None)
default_order = [
fieldname for fieldname, df in meta._fields.items() if not getattr(df, "is_custom_field", False)
]
if new_order == default_order:
if existing_order:
delete_property_setter(self.doc_type, "field_order")
return
if existing_order and new_order == json.loads(existing_order):
return
frappe.make_property_setter(
{
"doctype": self.doc_type,
"doctype_or_field": "DocType",
"property": "field_order",
"value": json.dumps(new_order),
},
is_system_generated=False,
)
def set_property_setters_for_doctype(self, meta):
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)
self.set_property_setter_for_field_order(meta)
def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""):
@ -540,6 +568,24 @@ class CustomizeForm(Document):
reset_customization(self.doc_type)
self.fetch_to_customize()
@frappe.whitelist()
def reset_layout(self):
if not self.doc_type:
return
property_setters = frappe.get_all(
"Property Setter",
filters={"doc_type": self.doc_type, "property": ("in", ("field_order", "insert_after"))},
pluck="name",
)
if not property_setters:
return
frappe.db.delete("Property Setter", {"name": ("in", property_setters)})
frappe.clear_cache(doctype=self.doc_type)
self.fetch_to_customize()
@classmethod
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
"""allow type change, if both old_type and new_type are in same field group.
@ -619,6 +665,7 @@ docfield_properties = {
"label": "Data",
"fieldtype": "Select",
"options": "Text",
"sort_options": "Check",
"fetch_from": "Small Text",
"fetch_if_empty": "Check",
"show_dashboard": "Check",

View file

@ -14,10 +14,11 @@ test_dependencies = ["Custom Field", "Property Setter"]
class TestCustomizeForm(FrappeTestCase):
def insert_custom_field(self):
frappe.delete_doc_if_exists("Custom Field", "Event-test_custom_field")
frappe.get_doc(
frappe.delete_doc_if_exists("Custom Field", "Event-custom_test_field")
self.field = frappe.get_doc(
{
"doctype": "Custom Field",
"fieldname": "custom_test_field",
"dt": "Event",
"label": "Test Custom Field",
"description": "A Custom Field for Testing",
@ -36,7 +37,7 @@ class TestCustomizeForm(FrappeTestCase):
frappe.clear_cache(doctype="Event")
def tearDown(self):
frappe.delete_doc("Custom Field", "Event-test_custom_field")
frappe.delete_doc("Custom Field", self.field.name)
frappe.db.commit()
frappe.clear_cache(doctype="Event")
@ -60,7 +61,7 @@ class TestCustomizeForm(FrappeTestCase):
self.assertEqual(d.doc_type, "Event")
self.assertEqual(len(d.get("fields")), len(frappe.get_doc("DocType", d.doc_type).fields) + 1)
self.assertEqual(d.get("fields")[-1].fieldname, "test_custom_field")
self.assertEqual(d.get("fields")[-1].fieldname, self.field.fieldname)
self.assertEqual(d.get("fields", {"fieldname": "event_type"})[0].in_list_view, 1)
return d
@ -129,21 +130,21 @@ class TestCustomizeForm(FrappeTestCase):
def test_save_customization_custom_field_property(self):
d = self.get_customize_form("Event")
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
self.assertEqual(frappe.db.get_value("Custom Field", self.field.name, "reqd"), 0)
custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0]
custom_field = d.get("fields", {"fieldname": self.field.fieldname})[0]
custom_field.reqd = 1
custom_field.no_copy = 1
d.run_method("save_customization")
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 1)
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 1)
self.assertEqual(frappe.db.get_value("Custom Field", self.field.name, "reqd"), 1)
self.assertEqual(frappe.db.get_value("Custom Field", self.field.name, "no_copy"), 1)
custom_field = d.get("fields", {"is_custom_field": True})[0]
custom_field.reqd = 0
custom_field.no_copy = 0
d.run_method("save_customization")
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "reqd"), 0)
self.assertEqual(frappe.db.get_value("Custom Field", "Event-test_custom_field", "no_copy"), 0)
self.assertEqual(frappe.db.get_value("Custom Field", self.field.name, "reqd"), 0)
self.assertEqual(frappe.db.get_value("Custom Field", self.field.name, "no_copy"), 0)
def test_save_customization_new_field(self):
d = self.get_customize_form("Event")
@ -157,28 +158,24 @@ class TestCustomizeForm(FrappeTestCase):
},
)
d.run_method("save_customization")
custom_field_name = "Event-custom_test_add_custom_field_via_customize_form"
self.assertEqual(
frappe.db.get_value(
"Custom Field", "Event-test_add_custom_field_via_customize_form", "fieldtype"
),
frappe.db.get_value("Custom Field", custom_field_name, "fieldtype"),
"Data",
)
self.assertEqual(
frappe.db.get_value(
"Custom Field", "Event-test_add_custom_field_via_customize_form", "insert_after"
),
frappe.db.get_value("Custom Field", custom_field_name, "insert_after"),
last_fieldname,
)
frappe.delete_doc("Custom Field", "Event-test_add_custom_field_via_customize_form")
self.assertEqual(
frappe.db.get_value("Custom Field", "Event-test_add_custom_field_via_customize_form"), None
)
frappe.delete_doc("Custom Field", custom_field_name)
self.assertEqual(frappe.db.get_value("Custom Field", custom_field_name), None)
def test_save_customization_remove_field(self):
d = self.get_customize_form("Event")
custom_field = d.get("fields", {"fieldname": "test_custom_field"})[0]
custom_field = d.get("fields", {"fieldname": self.field.fieldname})[0]
d.get("fields").remove(custom_field)
d.run_method("save_customization")
@ -200,7 +197,7 @@ class TestCustomizeForm(FrappeTestCase):
def test_set_allow_on_submit(self):
d = self.get_customize_form("Event")
d.get("fields", {"fieldname": "subject"})[0].allow_on_submit = 1
d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit = 1
d.get("fields", {"fieldname": "custom_test_field"})[0].allow_on_submit = 1
d.run_method("save_customization")
d = self.get_customize_form("Event")
@ -209,7 +206,7 @@ class TestCustomizeForm(FrappeTestCase):
self.assertEqual(d.get("fields", {"fieldname": "subject"})[0].allow_on_submit or 0, 0)
# allow for custom field
self.assertEqual(d.get("fields", {"fieldname": "test_custom_field"})[0].allow_on_submit, 1)
self.assertEqual(d.get("fields", {"fieldname": "custom_test_field"})[0].allow_on_submit, 1)
def test_title_field_pattern(self):
d = self.get_customize_form("Web Form")
@ -406,7 +403,7 @@ class TestCustomizeForm(FrappeTestCase):
def test_system_generated_fields(self):
doctype = "Event"
custom_field_name = "test_custom_field"
custom_field_name = "custom_test_field"
custom_field = frappe.get_doc("Custom Field", {"dt": doctype, "fieldname": custom_field_name})
custom_field.is_system_generated = 1
@ -425,3 +422,15 @@ class TestCustomizeForm(FrappeTestCase):
self.assertEqual(
frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
)
def test_custom_field_order(self):
# shuffle fields
customize_form = self.get_customize_form(doctype="ToDo")
customize_form.fields.insert(0, customize_form.fields.pop())
customize_form.save_customization()
field_order_property = json.loads(
frappe.db.get_value("Property Setter", {"doc_type": "ToDo", "property": "field_order"}, "value")
)
self.assertEqual(field_order_property, [df.fieldname for df in frappe.get_meta("ToDo").fields])

View file

@ -29,6 +29,7 @@
"precision",
"length",
"options",
"sort_options",
"fetch_from",
"fetch_if_empty",
"show_dashboard",
@ -89,7 +90,8 @@
"oldfieldtype": "Select",
"options": "Autocomplete\nAttach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nPhone\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTab Break\nTable\nTable MultiSelect\nText\nText Editor\nTime",
"reqd": 1,
"search_index": 1
"search_index": 1,
"sort_options": 1
},
{
"fieldname": "fieldname",
@ -462,13 +464,20 @@
"fieldname": "ignore_xss_filter",
"fieldtype": "Check",
"label": "Ignore XSS Filter"
},
{
"default": "0",
"depends_on": "eval: doc.fieldtype === 'Select'",
"fieldname": "sort_options",
"fieldtype": "Check",
"label": "Sort Options"
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2023-02-20 12:07:40.242470",
"modified": "2023-06-08 19:05:37.767838",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form Field",

View file

@ -17,7 +17,6 @@ from pypika.terms import Criterion, NullValue
import frappe
import frappe.defaults
import frappe.model.meta
from frappe import _
from frappe.database.utils import (
DefaultOrderBy,
@ -33,7 +32,7 @@ from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager
from frappe.utils import cast as cast_fieldtype
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
from frappe.utils.deprecations import deprecated, deprecation_warning
from frappe.utils.deprecations import deprecation_warning
IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE)
INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*")
@ -302,7 +301,7 @@ class Database:
"""Takes the query and logs it to various interfaces according to the settings."""
_query = None
if frappe.conf.allow_tests and frappe.cache().get_value("flag_print_sql"):
if frappe.conf.allow_tests and frappe.cache.get_value("flag_print_sql"):
_query = _query or str(mogrified_query)
print(_query)
@ -419,7 +418,7 @@ class Database:
@staticmethod
def clear_db_table_cache(query):
if query and is_query_type(query, ("drop", "create")):
frappe.cache().delete_key("db_tables")
frappe.cache.delete_key("db_tables")
def get_description(self):
"""Returns result metadata."""
@ -874,7 +873,6 @@ class Database:
modified_by=None,
update_modified=True,
debug=False,
for_update=True,
):
"""Set a single value in the database, do not call the ORM triggers
but update the modified timestamp (unless specified not to).
@ -890,10 +888,11 @@ class Database:
:param update_modified: default True. Set as false, if you don't want to update the timestamp.
:param debug: Print the query in the developer / js console.
"""
from frappe.model.utils import is_single_doctype
if _is_single_doctype := not (dn and dt != dn):
if (dn is None or dt == dn) and is_single_doctype(dt):
deprecation_warning(
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in version 15. Use db.set_single_value instead."
"Calling db.set_value on single doctype is deprecated. This behaviour will be removed in future. Use db.set_single_value instead."
)
self.set_single_value(
doctype=dt,
@ -1067,7 +1066,7 @@ class Database:
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
cache_count = frappe.cache.get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
count = frappe.qb.get_query(
@ -1078,7 +1077,7 @@ class Database:
validate_filters=True,
).run(debug=debug)[0][0]
if not filters and cache:
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
frappe.cache.set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@staticmethod
@ -1109,7 +1108,7 @@ class Database:
def get_db_table_columns(self, table) -> list[str]:
"""Returns list of column names from given table."""
columns = frappe.cache().hget("table_columns", table)
columns = frappe.cache.hget("table_columns", table)
if columns is None:
information_schema = frappe.qb.Schema("information_schema")
@ -1121,7 +1120,7 @@ class Database:
)
if columns:
frappe.cache().hset("table_columns", table, columns)
frappe.cache.hset("table_columns", table, columns)
return columns

View file

@ -435,7 +435,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
to_query = not cached
if cached:
tables = frappe.cache().get_value("db_tables")
tables = frappe.cache.get_value("db_tables")
to_query = not tables
if to_query:
@ -447,7 +447,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
.where(information_schema.tables.table_schema != "information_schema")
.run(pluck=True)
)
frappe.cache().set_value("db_tables", tables)
frappe.cache.set_value("db_tables", tables)
return tables

View file

@ -40,7 +40,7 @@ class DBTable:
if self.is_new():
self.create()
else:
frappe.cache().hdel("table_columns", self.table_name)
frappe.cache.hdel("table_columns", self.table_name)
self.alter()
def create(self):

View file

@ -3,7 +3,6 @@
import frappe
from frappe.cache_manager import clear_defaults_cache, common_default_keys
from frappe.desk.notifications import clear_notifications
from frappe.query_builder import DocType
# Note: DefaultValue records are identified by parent (e.g. __default, __global)
@ -230,7 +229,7 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
def get_defaults_for(parent="__default"):
"""get all defaults"""
defaults = frappe.cache().hget("defaults", parent)
defaults = frappe.cache.hget("defaults", parent)
if defaults is None:
# sort descending because first default must get precedence
@ -256,7 +255,7 @@ def get_defaults_for(parent="__default"):
elif d.defvalue is not None:
defaults[d.defkey] = d.defvalue
frappe.cache().hset("defaults", parent, defaults)
frappe.cache.hset("defaults", parent, defaults)
return defaults

View file

@ -19,20 +19,20 @@ def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str):
_records = records
try:
frappe.cache().rpush(f"{queue_prefix}{doctype}", _records)
frappe.cache.rpush(f"{queue_prefix}{doctype}", _records)
except redis.exceptions.ConnectionError:
for record in records:
insert_record(record, doctype)
def save_to_db():
queue_keys = frappe.cache().get_keys(queue_prefix)
queue_keys = frappe.cache.get_keys(queue_prefix)
for key in queue_keys:
record_count = 0
queue_key = get_key_name(key)
doctype = get_doctype_name(key)
while frappe.cache().llen(queue_key) > 0 and record_count <= 500:
records = frappe.cache().lpop(queue_key)
while frappe.cache.llen(queue_key) > 0 and record_count <= 500:
records = frappe.cache.lpop(queue_key)
records = json.loads(records.decode("utf-8"))
if isinstance(records, dict):
record_count += 1

View file

@ -62,10 +62,10 @@ class Workspace:
self.table_counts = get_table_with_counts()
self.restricted_doctypes = (
frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
)
self.restricted_pages = (
frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
frappe.cache.get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
)
def is_permitted(self):
@ -88,16 +88,14 @@ class Workspace:
return True
def get_cached(self, cache_key, fallback_fn):
_cache = frappe.cache()
value = _cache.get_value(cache_key, user=frappe.session.user)
value = frappe.cache.get_value(cache_key, user=frappe.session.user)
if value:
return value
value = fallback_fn()
# Expire every six hour
_cache.set_value(cache_key, value, frappe.session.user, 21600)
frappe.cache.set_value(cache_key, value, frappe.session.user, 21600)
return value
def get_can_read_items(self):
@ -469,7 +467,7 @@ def get_workspace_sidebar_items():
def get_table_with_counts():
counts = frappe.cache().get_value("information_schema:counts")
counts = frappe.cache.get_value("information_schema:counts")
if not counts:
counts = build_table_count_cache()

View file

@ -340,7 +340,7 @@ def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
class DashboardChart(Document):
def on_update(self):
frappe.cache().delete_key(f"chart-data:{self.name}")
frappe.cache.delete_key(f"chart-data:{self.name}")
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[["Dashboard Chart", self.name]], record_module=self.module)

View file

@ -28,7 +28,7 @@ def get_desktop_icons(user=None):
if not user:
user = frappe.session.user
user_icons = frappe.cache().hget("desktop_icons", user)
user_icons = frappe.cache.hget("desktop_icons", user)
if not user_icons:
fields = [
@ -120,7 +120,7 @@ def get_desktop_icons(user=None):
if d.label:
d.label = _(d.label)
frappe.cache().hset("desktop_icons", user, user_icons)
frappe.cache.hset("desktop_icons", user, user_icons)
return user_icons
@ -313,8 +313,8 @@ def get_all_icons():
def clear_desktop_icons_cache(user=None):
frappe.cache().hdel("desktop_icons", user or frappe.session.user)
frappe.cache().hdel("bootinfo", user or frappe.session.user)
frappe.cache.hdel("desktop_icons", user or frappe.session.user)
frappe.cache.hdel("bootinfo", user or frappe.session.user)
def get_user_copy(module_name, user=None):
@ -445,7 +445,7 @@ def get_module_icons(user=None):
if not user:
icons = frappe.get_all("Desktop Icon", fields="*", filters={"standard": 1}, order_by="idx")
else:
frappe.cache().hdel("desktop_icons", user)
frappe.cache.hdel("desktop_icons", user)
icons = get_user_icons(user)
for icon in icons:

View file

@ -34,13 +34,13 @@ class FormTour(Document):
step.fieldtype = field_df.fieldtype
def on_update(self):
frappe.cache().delete_key("bootinfo")
frappe.cache.delete_key("bootinfo")
if frappe.conf.developer_mode and self.is_standard:
export_to_files([["Form Tour", self.name]], self.module)
def on_trash(self):
frappe.cache().delete_key("bootinfo")
frappe.cache.delete_key("bootinfo")
@frappe.whitelist()
@ -51,7 +51,7 @@ def reset_tour(tour_name):
frappe.db.set_value(
"User", user, "onboarding_status", frappe.as_json(onboarding_status), update_modified=False
)
frappe.cache().hdel("bootinfo", user)
frappe.cache.hdel("bootinfo", user)
frappe.msgprint(_("Successfully reset onboarding status for all users."), alert=True)
@ -72,7 +72,7 @@ def update_user_status(value, step):
"User", frappe.session.user, "onboarding_status", value, update_modified=False
)
frappe.cache().hdel("bootinfo", frappe.session.user)
frappe.cache.hdel("bootinfo", frappe.session.user)
def get_onboarding_ui_tours():

View file

@ -28,7 +28,7 @@ class GlobalSearchSettings(Document):
frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts))
# reset cache
frappe.cache().hdel("global_search", "search_priorities")
frappe.cache.hdel("global_search", "search_priorities")
def get_doctypes_for_global_search():
@ -36,7 +36,7 @@ def get_doctypes_for_global_search():
doctypes = frappe.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
return frappe.cache().hget("global_search", "search_priorities", get_from_db)
return frappe.cache.hget("global_search", "search_priorities", get_from_db)
@frappe.whitelist()

View file

@ -14,7 +14,7 @@ class KanbanBoard(Document):
def on_change(self):
frappe.clear_cache(doctype=self.reference_doctype)
frappe.cache().delete_keys("_user_settings")
frappe.cache.delete_keys("_user_settings")
def before_insert(self):
for column in self.columns:

View file

@ -13,6 +13,10 @@ frappe.ui.form.on("Module Onboarding", {
if (!frappe.boot.developer_mode) {
frm.trigger("disable_form");
}
frm.add_custom_button(__("Reset"), () => {
frm.call("reset_progress");
});
},
disable_form: function (frm) {

View file

@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
@ -37,6 +38,16 @@ class ModuleOnboarding(Document):
return False
@frappe.whitelist()
def reset_progress(self):
self.db_set("is_complete", 0)
for step in self.get_steps():
step.db_set("is_complete", 0)
step.db_set("is_skipped", 0)
frappe.msgprint(_("Module onboarding progress reset"), alert=True)
def before_export(self, doc):
doc.is_complete = 0

View file

@ -29,7 +29,8 @@
"fieldtype": "Link",
"hidden": 1,
"label": "For User",
"options": "User"
"options": "User",
"search_index": 1
},
{
"fieldname": "type",
@ -64,8 +65,7 @@
"fieldtype": "Link",
"hidden": 1,
"label": "From User",
"options": "User",
"search_index": 1
"options": "User"
},
{
"default": "0",
@ -96,7 +96,7 @@
"hide_toolbar": 1,
"in_create": 1,
"links": [],
"modified": "2022-09-13 16:08:48.153934",
"modified": "2023-06-14 21:20:51.197943",
"modified_by": "Administrator",
"module": "Desk",
"name": "Notification Log",

View file

@ -81,13 +81,21 @@ class ToDo(Document):
)
assignments.reverse()
frappe.db.set_value(
self.reference_type,
self.reference_name,
"_assign",
json.dumps(assignments),
update_modified=False,
)
if frappe.get_meta(self.reference_type).issingle:
frappe.db.set_single_value(
self.reference_type,
"_assign",
json.dumps(assignments),
update_modified=False,
)
else:
frappe.db.set_value(
self.reference_type,
self.reference_name,
"_assign",
json.dumps(assignments),
update_modified=False,
)
except Exception as e:
if frappe.db.is_table_missing(e) and frappe.flags.in_install:

View file

@ -211,7 +211,7 @@
],
"in_create": 1,
"links": [],
"modified": "2023-05-17 14:52:38.110224",
"modified": "2023-06-08 14:52:38.110224",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",

View file

@ -531,13 +531,13 @@ def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
{"Address": {"fieldname": "customer"}..}
"""
if without_ignore_user_permissions_enabled:
return frappe.cache().hget(
return frappe.cache.hget(
"linked_doctypes_without_ignore_user_permissions_enabled",
doctype,
lambda: _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled),
)
else:
return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
return frappe.cache.hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):

View file

@ -80,7 +80,7 @@ def get_meta_bundle(doctype):
bundle = [frappe.desk.form.meta.get_meta(doctype)]
for df in bundle[0].fields:
if df.fieldtype in frappe.model.table_fields:
bundle.append(frappe.desk.form.meta.get_meta(df.options, not frappe.conf.developer_mode))
bundle.append(frappe.desk.form.meta.get_meta(df.options))
return bundle
@ -202,11 +202,13 @@ def get_versions(doc):
@frappe.whitelist()
def get_communications(doctype, name, start=0, limit=20):
from frappe.utils import cint
doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"):
raise frappe.PermissionError
return _get_communications(doctype, name, start, limit)
return _get_communications(doctype, name, cint(start), cint(limit))
def get_comments(

View file

@ -9,7 +9,6 @@ from frappe.build import scrub_html_template
from frappe.model.meta import Meta
from frappe.model.utils import render_include
from frappe.modules import get_module_path, load_doctype_module, scrub
from frappe.translate import extract_messages_from_code, make_dict_from_messages
from frappe.utils import get_html_format
from frappe.utils.data import get_link_to_form
@ -34,13 +33,15 @@ ASSET_KEYS = (
)
def get_meta(doctype, cached=True):
def get_meta(doctype, cached=True) -> "FormMeta":
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
meta = frappe.cache().hget("doctype_form_meta", doctype)
cached = cached and not frappe.conf.developer_mode
if cached:
meta = frappe.cache.hget("doctype_form_meta", doctype)
if not meta:
meta = FormMeta(doctype)
frappe.cache().hset("doctype_form_meta", doctype, meta)
# Cache miss - explicitly get meta from DB to avoid
meta = FormMeta(doctype, cached=False)
frappe.cache.hset("doctype_form_meta", doctype, meta)
else:
meta = FormMeta(doctype)
@ -51,8 +52,8 @@ def get_meta(doctype, cached=True):
class FormMeta(Meta):
def __init__(self, doctype):
self.__dict__.update(frappe.get_meta(doctype).__dict__)
def __init__(self, doctype, *, cached=True):
self.__dict__.update(frappe.get_meta(doctype, cached=cached).__dict__)
self.load_assets()
def load_assets(self):
@ -258,6 +259,8 @@ class FormMeta(Meta):
self.set("__form_grid_templates", templates)
def set_translations(self, lang):
from frappe.translate import extract_messages_from_code, make_dict_from_messages
self.set("__messages", frappe.get_lang_dict("doctype", self.name))
# set translations for grid templates

View file

@ -16,7 +16,7 @@ from frappe.utils.telemetry import capture_doc
def savedocs(doc, action):
"""save / submit / update doclist"""
doc = frappe.get_doc(json.loads(doc))
capture_doc(doc)
capture_doc(doc, action)
set_local_name(doc)
# action
@ -47,6 +47,8 @@ def savedocs(doc, action):
def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_state=None):
"""cancel a doclist"""
doc = frappe.get_doc(doctype, name)
capture_doc(doc, "Cancel")
if workflow_state_fieldname and workflow_state:
doc.set(workflow_state_fieldname, workflow_state)
doc.cancel()

View file

@ -52,7 +52,10 @@ def _toggle_like(doctype, name, add, user=None):
liked_by.remove(user)
remove_like(doctype, name)
frappe.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False)
if frappe.get_meta(doctype).issingle:
frappe.db.set_single_value(doctype, "_liked_by", json.dumps(liked_by), update_modified=False)
else:
frappe.db.set_value(doctype, name, "_liked_by", json.dumps(liked_by), update_modified=False)
except frappe.db.ProgrammingError as e:
if frappe.db.is_column_missing(e):

View file

@ -34,13 +34,12 @@ def get_notifications():
return out
groups = list(config.get("for_doctype")) + list(config.get("for_module"))
cache = frappe.cache()
notification_count = {}
notification_percent = {}
for name in groups:
count = cache.hget("notification_count:" + name, frappe.session.user)
count = frappe.cache.hget("notification_count:" + name, frappe.session.user)
if count is not None:
notification_count[name] = count
@ -83,7 +82,7 @@ def get_notifications_for_doctypes(config, notification_count):
else:
open_count_doctype[d] = result
frappe.cache().hset("notification_count:" + d, frappe.session.user, result)
frappe.cache.hset("notification_count:" + d, frappe.session.user, result)
return open_count_doctype
@ -139,7 +138,6 @@ def get_notifications_for_targets(config, notification_percent):
def clear_notifications(user=None):
if frappe.flags.in_install:
return
cache = frappe.cache()
config = get_notification_config()
if not config:
@ -151,17 +149,17 @@ def clear_notifications(user=None):
for name in groups:
if user:
cache.hdel("notification_count:" + name, user)
frappe.cache.hdel("notification_count:" + name, user)
else:
cache.delete_key("notification_count:" + name)
frappe.cache.delete_key("notification_count:" + name)
def clear_notification_config(user):
frappe.cache().hdel("notification_config", user)
frappe.cache.hdel("notification_config", user)
def delete_notification_count_for(doctype):
frappe.cache().delete_key("notification_count:" + doctype)
frappe.cache.delete_key("notification_count:" + doctype)
def clear_doctype_notifications(doc, method=None, *args, **kwargs):
@ -230,7 +228,7 @@ def get_notification_config():
config[key].update(nc.get(key, {}))
return config
return frappe.cache().hget("notification_config", user, _get)
return frappe.cache.hget("notification_config", user, _get)
def get_filters_for(doctype):

View file

@ -325,8 +325,8 @@ def load_country():
@frappe.whitelist()
def load_user_details():
return {
"full_name": frappe.cache().hget("full_name", "signup"),
"email": frappe.cache().hget("email", "signup"),
"full_name": frappe.cache.hget("full_name", "signup"),
"email": frappe.cache.hget("email", "signup"),
}

View file

@ -119,7 +119,7 @@ def generate_report_result(
"report_summary": report_summary,
"skip_total_row": skip_total_row or 0,
"status": None,
"execution_time": frappe.cache().hget("report_execution_time", report.name) or 0,
"execution_time": frappe.cache.hget("report_execution_time", report.name) or 0,
}
@ -170,7 +170,8 @@ def get_script(report_name):
return {
"script": render_include(script),
"html_format": html_format,
"execution_time": frappe.cache().hget("report_execution_time", report_name) or 0,
"execution_time": frappe.cache.hget("report_execution_time", report_name) or 0,
"filters": report.filters,
}
@ -348,6 +349,13 @@ def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=F
datetime.timedelta,
)
if len(visible_idx) == len(data.result):
# It's not possible to have same length and different content.
ignore_visible_idx = True
else:
# Note: converted for faster lookups
visible_idx = set(visible_idx)
result = [[]]
column_widths = []

View file

@ -479,6 +479,7 @@ def delete_items():
def delete_bulk(doctype, items):
undeleted_items = []
for i, d in enumerate(items):
try:
frappe.delete_doc(doctype, d)
@ -493,7 +494,11 @@ def delete_bulk(doctype, items):
except Exception:
# rollback if any record failed to delete
# if not rollbacked, queries get committed on after_request method in app.py
undeleted_items.append(d)
frappe.db.rollback()
if undeleted_items and len(items) != len(undeleted_items):
frappe.clear_messages()
delete_bulk(doctype, undeleted_items)
@frappe.whitelist()

View file

@ -6,7 +6,9 @@ import json
import re
import frappe
from frappe import _, is_whitelisted
# Backward compatbility
from frappe import _, is_whitelisted, validate_and_sanitize_search_inputs
from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.permissions import has_permission
from frappe.utils import cint, cstr, unique
@ -293,26 +295,10 @@ def relevance_sorter(key, query, as_dict):
return (cstr(value).casefold().startswith(query.casefold()) is not True, value)
def validate_and_sanitize_search_inputs(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
kwargs.update(dict(zip(fn.__code__.co_varnames, args)))
sanitize_searchfield(kwargs["searchfield"])
kwargs["start"] = cint(kwargs["start"])
kwargs["page_len"] = cint(kwargs["page_len"])
if kwargs["doctype"] and not frappe.db.exists("DocType", kwargs["doctype"]):
return []
return fn(**kwargs)
return wrapper
@frappe.whitelist()
def get_names_for_mentions(search_term):
users_for_mentions = frappe.cache().get_value("users_for_mentions", get_users_for_mentions)
user_groups = frappe.cache().get_value("user_groups", get_user_groups)
users_for_mentions = frappe.cache.get_value("users_for_mentions", get_users_for_mentions)
user_groups = frappe.cache.get_value("user_groups", get_user_groups)
filtered_mentions = []
for mention_data in users_for_mentions + user_groups:

View file

@ -96,7 +96,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
def get_cached_contacts(txt):
contacts = frappe.cache().hget("contacts", frappe.session.user) or []
contacts = frappe.cache.hget("contacts", frappe.session.user) or []
if not contacts:
return
@ -113,9 +113,9 @@ def get_cached_contacts(txt):
def update_contact_cache(contacts):
cached_contacts = frappe.cache().hget("contacts", frappe.session.user) or []
cached_contacts = frappe.cache.hget("contacts", frappe.session.user) or []
uncached_contacts = [d for d in contacts if d not in cached_contacts]
cached_contacts.extend(uncached_contacts)
frappe.cache().hset("contacts", frappe.session.user, cached_contacts)
frappe.cache.hset("contacts", frappe.session.user, cached_contacts)

View file

@ -508,7 +508,7 @@
},
{
"default": "0",
"depends_on": "eval:!doc.domain && doc.enable_outgoing",
"depends_on": "eval:!doc.domain && doc.enable_outgoing && doc.enable_incoming && doc.use_imap",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"hide_days": 1,
@ -616,7 +616,7 @@
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-12-28 14:56:18.754804",
"modified": "2023-06-05 15:03:08.538819",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@ -639,4 +639,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View file

@ -176,7 +176,7 @@ class EmailAccount(Document):
def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"):
"""Returns logged in POP3/IMAP connection object."""
if frappe.cache().get_value("workers:no-internet") == True:
if frappe.cache.get_value("workers:no-internet") == True:
return None
oauth_token = self.get_oauth_token()
@ -253,7 +253,7 @@ class EmailAccount(Document):
if self.no_failed > 2:
self.handle_incoming_connect_error(description=description)
else:
frappe.cache().set_value("workers:no-internet", True)
frappe.cache.set_value("workers:no-internet", True)
return None
else:
raise
@ -384,6 +384,10 @@ class EmailAccount(Document):
"name": {"conf_names": ("email_sender_name",), "default": "Frappe"},
"auth_method": {"conf_names": ("auth_method"), "default": "Basic"},
"from_site_config": {"default": True},
"no_smtp_authentication": {
"conf_names": ("disable_mail_smtp_authentication",),
"default": 0,
},
}
account_details = {}
@ -436,13 +440,13 @@ class EmailAccount(Document):
else:
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
else:
frappe.cache().set_value("workers:no-internet", True)
frappe.cache.set_value("workers:no-internet", True)
def set_failed_attempts_count(self, value):
frappe.cache().set(f"{self.name}:email-account-failed-attempts", value)
frappe.cache.set(f"{self.name}:email-account-failed-attempts", value)
def get_failed_attempts_count(self):
return cint(frappe.cache().get(f"{self.name}:email-account-failed-attempts"))
return cint(frappe.cache.get(f"{self.name}:email-account-failed-attempts"))
def receive(self):
"""Called by scheduler to receive emails from this EMail account using POP3/IMAP."""
@ -648,21 +652,16 @@ class EmailAccount(Document):
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
def append_email_to_sent_folder(self, message):
email_server = None
try:
email_server = self.get_incoming_server(in_receive=True)
except Exception:
self.log_error("Email Connection Error")
if not email_server:
if not (self.enable_incoming and self.use_imap):
# don't try appending if enable incoming and imap is not set
return
if email_server.imap:
try:
message = safe_encode(message)
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
except Exception:
self.log_error("Unable to add to Sent folder")
try:
email_server = self.get_incoming_server(in_receive=True)
message = safe_encode(message)
email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
except Exception:
self.log_error("Unable to add to Sent folder")
def get_oauth_token(self):
if self.auth_method == "OAuth":
@ -766,9 +765,9 @@ def pull(now=False):
"""Will be called via scheduler, pull emails from all enabled Email accounts."""
from frappe.integrations.doctype.connected_app.connected_app import has_token
if frappe.cache().get_value("workers:no-internet") == True:
if frappe.cache.get_value("workers:no-internet") == True:
if test_internet():
frappe.cache().set_value("workers:no-internet", False)
frappe.cache.set_value("workers:no-internet", False)
return
doctype = frappe.qb.DocType("Email Account")

Some files were not shown because too many files have changed in this diff Show more