Merge branch 'develop' into feature/force-web-capture-setting
This commit is contained in:
commit
bffc1af985
91 changed files with 781 additions and 308 deletions
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
|
|
@ -54,6 +54,8 @@ fi
|
|||
|
||||
echo "Starting Bench..."
|
||||
|
||||
export FRAPPE_TUNE_GC=True
|
||||
|
||||
bench start &> ~/frappe-bench/bench_start.log &
|
||||
|
||||
if [ "$TYPE" == "server" ]
|
||||
|
|
|
|||
21
.github/workflows/patch-mariadb-tests.yml
vendored
21
.github/workflows/patch-mariadb-tests.yml
vendored
|
|
@ -62,9 +62,11 @@ 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
|
||||
|
|
@ -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,25 +121,25 @@ 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"
|
||||
rm -rf ~/frappe-bench/env
|
||||
|
|
|
|||
0
.semgrepignore
Normal file
0
.semgrepignore
Normal file
|
|
@ -8,6 +8,8 @@ module.exports = defineConfig({
|
|||
pageLoadTimeout: 15000,
|
||||
video: true,
|
||||
videoUploadOnPasses: false,
|
||||
viewportHeight: 960,
|
||||
viewportWidth: 1400,
|
||||
retries: {
|
||||
runMode: 2,
|
||||
openMode: 2,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -89,7 +89,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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -2418,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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
@ -394,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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -257,6 +258,8 @@ def get_user_pages_or_reports(parent, cache=False):
|
|||
|
||||
|
||||
def load_translations(bootinfo):
|
||||
from frappe.translate import get_messages_for_boot
|
||||
|
||||
bootinfo["lang"] = frappe.lang
|
||||
bootinfo["__messages"] = get_messages_for_boot()
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
@ -288,6 +287,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)
|
||||
|
|
|
|||
|
|
@ -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,6 +76,8 @@ doctype_cache_keys = (
|
|||
|
||||
|
||||
def clear_user_cache(user=None):
|
||||
from frappe.desk.notifications import clear_notifications
|
||||
|
||||
# this will automatically reload the global cache
|
||||
# so it is important to clear this first
|
||||
clear_notifications(user)
|
||||
|
|
@ -128,6 +127,8 @@ def clear_doctype_cache(doctype=None):
|
|||
|
||||
|
||||
def _clear_doctype_cache_form_redis(doctype: str | None = None):
|
||||
from frappe.desk.notifications import delete_notification_count_for
|
||||
|
||||
for key in ("is_table", "doctype_modules"):
|
||||
frappe.cache.delete_value(key)
|
||||
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
]
|
||||
|
|
|
|||
|
|
@ -386,7 +386,14 @@ def import_doc(context, path, force=False):
|
|||
|
||||
@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(
|
||||
|
|
|
|||
|
|
@ -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": []
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
@ -71,6 +70,8 @@ class SystemSettings(Document):
|
|||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ ASSET_KEYS = (
|
|||
|
||||
def get_meta(doctype, cached=True) -> "FormMeta":
|
||||
# don't cache for developer mode as js files, templates may be edited
|
||||
cached = cached and not frappe._dev_server
|
||||
cached = cached and not frappe.conf.developer_mode
|
||||
if cached:
|
||||
meta = frappe.cache.hget("doctype_form_meta", doctype)
|
||||
if not meta:
|
||||
|
|
@ -260,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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -349,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 = []
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,22 +295,6 @@ 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)
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@ FrappeClient is a library that helps you connect with other frappe systems
|
|||
import base64
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
import frappe
|
||||
from frappe.utils.data import cstr
|
||||
|
||||
|
|
@ -37,6 +35,8 @@ class FrappeClient:
|
|||
api_secret=None,
|
||||
frappe_authorization_source=None,
|
||||
):
|
||||
import requests
|
||||
|
||||
self.headers = {
|
||||
"Accept": "application/json",
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
|
|
@ -390,42 +390,13 @@ class FrappeClient:
|
|||
|
||||
class FrappeOAuth2Client(FrappeClient):
|
||||
def __init__(self, url, access_token, verify=True):
|
||||
import requests
|
||||
|
||||
self.access_token = access_token
|
||||
self.headers = {
|
||||
"Authorization": "Bearer " + access_token,
|
||||
"content-type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
self.verify = verify
|
||||
self.session = OAuth2Session(self.headers)
|
||||
self.session = requests.session()
|
||||
self.url = url
|
||||
|
||||
def get_request(self, params):
|
||||
res = requests.get(
|
||||
self.url, params=self.preprocess(params), headers=self.headers, verify=self.verify
|
||||
)
|
||||
res = self.post_process(res)
|
||||
return res
|
||||
|
||||
def post_request(self, data):
|
||||
res = requests.post(
|
||||
self.url, data=self.preprocess(data), headers=self.headers, verify=self.verify
|
||||
)
|
||||
res = self.post_process(res)
|
||||
return res
|
||||
|
||||
|
||||
class OAuth2Session:
|
||||
def __init__(self, headers):
|
||||
self.headers = headers
|
||||
|
||||
def get(self, url, params, verify):
|
||||
res = requests.get(url, params=params, headers=self.headers, verify=verify)
|
||||
return res
|
||||
|
||||
def post(self, url, data, verify):
|
||||
res = requests.post(url, data=data, headers=self.headers, verify=verify)
|
||||
return res
|
||||
|
||||
def put(self, url, data, verify):
|
||||
res = requests.put(url, data=data, headers=self.headers, verify=verify)
|
||||
return res
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
import json
|
||||
from contextlib import contextmanager
|
||||
|
||||
import responses
|
||||
from responses.matchers import json_params_matcher
|
||||
|
||||
import frappe
|
||||
from frappe.integrations.doctype.webhook.webhook import (
|
||||
enqueue_webhook,
|
||||
|
|
@ -94,9 +97,15 @@ class TestWebhook(FrappeTestCase):
|
|||
self.test_user.email = "user1@integration.webhooks.test.com"
|
||||
self.test_user.first_name = "user1"
|
||||
|
||||
self.responses = responses.RequestsMock()
|
||||
self.responses.start()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.user.delete()
|
||||
self.test_user.delete()
|
||||
|
||||
self.responses.stop()
|
||||
self.responses.reset()
|
||||
super().tearDown()
|
||||
|
||||
def test_webhook_trigger_with_enabled_webhooks(self):
|
||||
|
|
@ -172,6 +181,13 @@ class TestWebhook(FrappeTestCase):
|
|||
self.assertEqual(data, {"name": self.user.name})
|
||||
|
||||
def test_webhook_req_log_creation(self):
|
||||
self.responses.add(
|
||||
responses.POST,
|
||||
"https://httpbin.org/post",
|
||||
status=200,
|
||||
json={},
|
||||
)
|
||||
|
||||
if not frappe.db.get_value("User", "user2@integration.webhooks.test.com"):
|
||||
user = frappe.get_doc(
|
||||
{"doctype": "User", "email": "user2@integration.webhooks.test.com", "first_name": "user2"}
|
||||
|
|
@ -185,6 +201,7 @@ class TestWebhook(FrappeTestCase):
|
|||
self.assertTrue(frappe.get_all("Webhook Request Log", pluck="name"))
|
||||
|
||||
def test_webhook_with_array_body(self):
|
||||
|
||||
"""Check if array request body are supported."""
|
||||
wh_config = {
|
||||
"doctype": "Webhook",
|
||||
|
|
@ -194,7 +211,7 @@ class TestWebhook(FrappeTestCase):
|
|||
"request_url": "https://httpbin.org/post",
|
||||
"request_method": "POST",
|
||||
"request_structure": "JSON",
|
||||
"webhook_json": '[\r\n{% for n in range(3) %}\r\n {\r\n "title": "{{ doc.title }}",\r\n "n": {{ n }}\r\n }\r\n {%- if not loop.last -%}\r\n , \r\n {%endif%}\r\n{%endfor%}\r\n]',
|
||||
"webhook_json": '[\r\n{% for n in range(3) %}\r\n {\r\n "title": "{{ doc.title }}" }\r\n {%- if not loop.last -%}\r\n , \r\n {%endif%}\r\n{%endfor%}\r\n]',
|
||||
"meets_condition": "Yes",
|
||||
"webhook_headers": [
|
||||
{
|
||||
|
|
@ -204,13 +221,22 @@ class TestWebhook(FrappeTestCase):
|
|||
],
|
||||
}
|
||||
|
||||
with get_test_webhook(wh_config) as wh:
|
||||
doc = frappe.new_doc("Note")
|
||||
doc.title = "Test Webhook Note"
|
||||
doc = frappe.new_doc("Note")
|
||||
doc.title = "Test Webhook Note"
|
||||
|
||||
expected_req = [{"title": doc.title} for _ in range(3)]
|
||||
self.responses.add(
|
||||
responses.POST,
|
||||
"https://httpbin.org/post",
|
||||
status=200,
|
||||
json=expected_req,
|
||||
match=[json_params_matcher(expected_req)],
|
||||
)
|
||||
|
||||
with get_test_webhook(wh_config) as wh:
|
||||
enqueue_webhook(doc, wh)
|
||||
log = frappe.get_last_doc("Webhook Request Log")
|
||||
self.assertEqual(len(json.loads(log.response)["json"]), 3)
|
||||
self.assertEqual(len(json.loads(log.response)), 3)
|
||||
|
||||
def test_webhook_with_dynamic_url_enabled(self):
|
||||
wh_config = {
|
||||
|
|
@ -232,12 +258,16 @@ class TestWebhook(FrappeTestCase):
|
|||
],
|
||||
}
|
||||
|
||||
self.responses.add(
|
||||
responses.POST,
|
||||
"https://httpbin.org/anything/Note",
|
||||
status=200,
|
||||
)
|
||||
|
||||
with get_test_webhook(wh_config) as wh:
|
||||
doc = frappe.new_doc("Note")
|
||||
doc.title = "Test Webhook Note"
|
||||
enqueue_webhook(doc, wh)
|
||||
log = frappe.get_last_doc("Webhook Request Log")
|
||||
self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/Note")
|
||||
|
||||
def test_webhook_with_dynamic_url_disabled(self):
|
||||
wh_config = {
|
||||
|
|
@ -259,11 +289,13 @@ class TestWebhook(FrappeTestCase):
|
|||
],
|
||||
}
|
||||
|
||||
self.responses.add(
|
||||
responses.POST,
|
||||
"https://httpbin.org/anything/{{doc.doctype}}",
|
||||
status=200,
|
||||
)
|
||||
|
||||
with get_test_webhook(wh_config) as wh:
|
||||
doc = frappe.new_doc("Note")
|
||||
doc.title = "Test Webhook Note"
|
||||
enqueue_webhook(doc, wh)
|
||||
log = frappe.get_last_doc("Webhook Request Log")
|
||||
self.assertEqual(
|
||||
json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"html_condition",
|
||||
"sb_webhook",
|
||||
"request_url",
|
||||
"timeout",
|
||||
"is_dynamic_url",
|
||||
"cb_webhook",
|
||||
"request_method",
|
||||
|
|
@ -204,6 +205,14 @@
|
|||
"fieldname": "is_dynamic_url",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Dynamic URL?"
|
||||
},
|
||||
{
|
||||
"default": "5",
|
||||
"description": "The number of seconds until the request expires",
|
||||
"fieldname": "timeout",
|
||||
"fieldtype": "Int",
|
||||
"label": "Request Timeout",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
|
|
@ -212,7 +221,7 @@
|
|||
"link_fieldname": "webhook"
|
||||
}
|
||||
],
|
||||
"modified": "2023-06-02 17:25:12.598232",
|
||||
"modified": "2023-06-16 10:21:00.971833",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Webhook",
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ def enqueue_webhook(doc, webhook) -> None:
|
|||
url=request_url,
|
||||
data=json.dumps(data, default=str),
|
||||
headers=headers,
|
||||
timeout=5,
|
||||
timeout=webhook.timeout or 5,
|
||||
)
|
||||
r.raise_for_status()
|
||||
frappe.logger().debug({"webhook_success": r.text})
|
||||
|
|
|
|||
|
|
@ -151,7 +151,8 @@ def set_new_name(doc):
|
|||
|
||||
if getattr(doc, "amended_from", None):
|
||||
_set_amended_name(doc)
|
||||
return
|
||||
if doc.name:
|
||||
return
|
||||
|
||||
elif getattr(doc.meta, "issingle", False):
|
||||
doc.name = doc.doctype
|
||||
|
|
@ -506,6 +507,17 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-"
|
|||
|
||||
|
||||
def _set_amended_name(doc):
|
||||
amend_naming_rule = frappe.db.get_value(
|
||||
"Amended Document Naming Settings", {"document_type": doc.doctype}, "action", cache=True
|
||||
)
|
||||
if not amend_naming_rule:
|
||||
amend_naming_rule = frappe.db.get_single_value(
|
||||
"Document Naming Settings", "default_amend_naming", cache=True
|
||||
)
|
||||
|
||||
if amend_naming_rule == "Default Naming":
|
||||
return
|
||||
|
||||
am_id = 1
|
||||
am_prefix = doc.amended_from
|
||||
if frappe.db.get_value(doc.doctype, doc.amended_from, "amended_from"):
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ def update_document_title(
|
|||
# handle bad API usages
|
||||
merge = sbool(merge)
|
||||
enqueue = sbool(enqueue)
|
||||
action_enqueued = enqueue and not is_scheduler_inactive()
|
||||
|
||||
doc = frappe.get_doc(doctype, docname)
|
||||
doc.check_permission(permtype="write")
|
||||
|
|
@ -65,7 +66,7 @@ def update_document_title(
|
|||
name_updated = updated_name and (updated_name != doc.name)
|
||||
|
||||
if name_updated:
|
||||
if enqueue and not is_scheduler_inactive():
|
||||
if action_enqueued:
|
||||
current_name = doc.name
|
||||
|
||||
# before_name hook may have DocType specific validations or transformations
|
||||
|
|
@ -90,18 +91,27 @@ def update_document_title(
|
|||
doc.rename(updated_name, merge=merge)
|
||||
|
||||
if title_updated:
|
||||
try:
|
||||
setattr(doc, title_field, updated_title)
|
||||
doc.save()
|
||||
frappe.msgprint(_("Saved"), alert=True, indicator="green")
|
||||
except Exception as e:
|
||||
if frappe.db.is_duplicate_entry(e):
|
||||
frappe.throw(
|
||||
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
|
||||
title=_("Duplicate Name"),
|
||||
exc=frappe.DuplicateEntryError,
|
||||
)
|
||||
raise
|
||||
if action_enqueued and name_updated:
|
||||
frappe.enqueue(
|
||||
"frappe.client.set_value",
|
||||
doctype=doc.doctype,
|
||||
name=updated_name,
|
||||
fieldname=title_field,
|
||||
value=updated_title,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
setattr(doc, title_field, updated_title)
|
||||
doc.save()
|
||||
frappe.msgprint(_("Saved"), alert=True, indicator="green")
|
||||
except Exception as e:
|
||||
if frappe.db.is_duplicate_entry(e):
|
||||
frappe.throw(
|
||||
_("{0} {1} already exists").format(doctype, frappe.bold(docname)),
|
||||
title=_("Duplicate Name"),
|
||||
exc=frappe.DuplicateEntryError,
|
||||
)
|
||||
raise
|
||||
|
||||
return doc.name
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ ignore_values = {
|
|||
"Print Style": ["disabled"],
|
||||
"Module Onboarding": ["is_complete"],
|
||||
"Onboarding Step": ["is_complete", "is_skipped"],
|
||||
"Workspace": ["is_hidden"],
|
||||
}
|
||||
|
||||
ignore_doctypes = [""]
|
||||
|
|
|
|||
|
|
@ -226,3 +226,4 @@ frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
|
|||
frappe.patches.v15_0.remove_background_jobs_from_dropdown
|
||||
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
|
||||
execute:frappe.delete_doc_if_exists("Workspace", "Customization")
|
||||
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali
|
|||
|
||||
out = setup_custom_perms(doctype)
|
||||
|
||||
name = frappe.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel))
|
||||
name = frappe.db.get_value("Custom DocPerm", dict(parent=doctype, role=role, permlevel=permlevel))
|
||||
table = DocType("Custom DocPerm")
|
||||
frappe.qb.update(table).set(ptype, value).where(table.name == name).run()
|
||||
|
||||
|
|
|
|||
|
|
@ -84,13 +84,15 @@
|
|||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-arrow-up-right">
|
||||
<path d="M2.5 9.5L9.5 2.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.50002 8V2.5H4.00002" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.5 9.5L9.5 2.5M9.50002 8V2.5H4.00002" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-arrow-down-left">
|
||||
<path d="M9.5 2.5L2.5 9.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M2.49999 4L2.49998 9.5L7.99998 9.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.5 2.5L2.5 9.5M2.49999 4L2.49998 9.5L7.99998 9.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg" id="icon-arrow-down-right">
|
||||
<path d="M2.5 2.5L9.5 9.5M4 9.5h5.5v-5.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</symbol>
|
||||
|
||||
<symbol viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg" id="icon-expand">
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
|
@ -60,7 +60,11 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
|
|||
this.clear_editable_layers();
|
||||
|
||||
const data_layers = new L.FeatureGroup().addLayer(
|
||||
L.geoJson(JSON.parse(value), { pointToLayer: this.point_to_layer })
|
||||
L.geoJson(JSON.parse(value), {
|
||||
pointToLayer: this.point_to_layer,
|
||||
style: this.set_style,
|
||||
onEachFeature: this.on_each_feature,
|
||||
})
|
||||
);
|
||||
this.add_non_group_layers(data_layers, this.editableLayers);
|
||||
this.editableLayers.addTo(this.map);
|
||||
|
|
@ -70,6 +74,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
|
|||
/**
|
||||
* Defines custom rules for how geoJSON data is rendered on the map.
|
||||
*
|
||||
* Can be inherited in custom map controllers.
|
||||
*
|
||||
* @param {Object} geoJsonPoint - The geoJSON object to be rendered on the map.
|
||||
* @param {Object} latlng - The latitude and longitude where the geoJSON data should be rendered on the map.
|
||||
* @returns {Object} - Returns the Leaflet layer object to be rendered on the map.
|
||||
|
|
@ -85,6 +91,28 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines custom styles for how geoJSON Line and LineString data is rendered on the map.
|
||||
*
|
||||
* Can be inherited in custom map controllers.
|
||||
*
|
||||
* @param {Object} geoJsonFeature - The geoJSON object to be rendered on the map.
|
||||
* @returns {Object} - Returns the style object for the geoJSON object.
|
||||
*/
|
||||
set_style(geoJsonFeature) {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Is called after each feature is rendered and styles, can be used to attache popups, tooltips and other events
|
||||
*
|
||||
* Can be inherited in custom map controllers.
|
||||
*
|
||||
* @param {Object} feature - The leaflet object representing a geojson feature.
|
||||
* @param {Object} layer - The leaflet layer object.
|
||||
*/
|
||||
on_each_feature(feature, layer) {}
|
||||
|
||||
bind_leaflet_map() {
|
||||
const circleToGeoJSON = L.Circle.prototype.toGeoJSON;
|
||||
L.Circle.include({
|
||||
|
|
|
|||
|
|
@ -267,15 +267,17 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
|
|||
r.results = me.merge_duplicates(r.results);
|
||||
|
||||
// show filter description in awesomplete
|
||||
if (args.filters) {
|
||||
let filter_string = me.get_filter_description(args.filters);
|
||||
if (filter_string) {
|
||||
r.results.push({
|
||||
html: `<span class="text-muted" style="line-height: 1.5">${filter_string}</span>`,
|
||||
value: "",
|
||||
action: () => {},
|
||||
});
|
||||
}
|
||||
let filter_string = me.df.filter_description
|
||||
? me.df.filter_description
|
||||
: args.filters
|
||||
? me.get_filter_description(args.filters)
|
||||
: null;
|
||||
if (filter_string) {
|
||||
r.results.push({
|
||||
html: `<span class="text-muted" style="line-height: 1.5">${filter_string}</span>`,
|
||||
value: "",
|
||||
action: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
if (!me.df.only_select) {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ frappe.ui.form.QuickEntryForm = class QuickEntryForm {
|
|||
this.mandatory = [
|
||||
{
|
||||
fieldname: "__newname",
|
||||
label: __("{0} Name", [this.meta.name]),
|
||||
label: __("{0} Name", [__(this.meta.name)]),
|
||||
reqd: 1,
|
||||
fieldtype: "Data",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ export default class Section {
|
|||
|
||||
add_field(fieldobj) {
|
||||
this.fields_list.push(fieldobj);
|
||||
this.fields_dict[fieldobj.fieldname] = fieldobj;
|
||||
this.fields_dict[fieldobj.df.fieldname] = fieldobj;
|
||||
fieldobj.section = this;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
}
|
||||
|
||||
let rename_document = () => {
|
||||
if (input_name != docname) frappe.socketio.doctype_subscribe(doctype, input_name);
|
||||
return frappe
|
||||
.xcall("frappe.model.rename_doc.update_document_title", {
|
||||
doctype,
|
||||
|
|
@ -129,9 +130,8 @@ frappe.ui.form.Toolbar = class Toolbar {
|
|||
};
|
||||
|
||||
// handle document renaming queued action
|
||||
if (input_name && new_docname == docname) {
|
||||
frappe.socketio.doc_subscribe(doctype, input_name);
|
||||
frappe.realtime.on("doc_update", (data) => {
|
||||
if (input_name != docname) {
|
||||
frappe.realtime.on("list_update", (data) => {
|
||||
if (data.doctype == doctype && data.name == input_name) {
|
||||
reload_form(input_name);
|
||||
frappe.show_alert({
|
||||
|
|
|
|||
|
|
@ -456,22 +456,20 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
|
|||
get_no_result_message() {
|
||||
let help_link = this.get_documentation_link();
|
||||
let filters = this.filter_area && this.filter_area.get();
|
||||
let no_result_message =
|
||||
filters && filters.length
|
||||
? __("No {0} found", [__(this.doctype)])
|
||||
: __("You haven't created a {0} yet", [__(this.doctype)]);
|
||||
let new_button_label =
|
||||
filters && filters.length
|
||||
? __(
|
||||
"Create a new {0}",
|
||||
[__(this.doctype)],
|
||||
"Create a new document from list view"
|
||||
)
|
||||
: __(
|
||||
"Create your first {0}",
|
||||
[__(this.doctype)],
|
||||
"Create a new document from list view"
|
||||
);
|
||||
|
||||
let has_filters_set = filters && filters.length;
|
||||
let no_result_message = has_filters_set
|
||||
? __("No {0} found with matching filters. Clear filters to see all {0}.", [
|
||||
__(this.doctype),
|
||||
])
|
||||
: __("You haven't created a {0} yet", [__(this.doctype)]);
|
||||
let new_button_label = has_filters_set
|
||||
? __("Create a new {0}", [__(this.doctype)], "Create a new document from list view")
|
||||
: __(
|
||||
"Create your first {0}",
|
||||
[__(this.doctype)],
|
||||
"Create a new document from list view"
|
||||
);
|
||||
let empty_state_image =
|
||||
this.settings.empty_state_image ||
|
||||
"/assets/frappe/images/ui-states/list-empty-state.svg";
|
||||
|
|
|
|||
|
|
@ -163,7 +163,8 @@ $.extend(frappe.model, {
|
|||
|
||||
if (!user_default) {
|
||||
user_default = frappe.defaults.get_user_default(df.fieldname);
|
||||
} else if (
|
||||
}
|
||||
if (
|
||||
!user_default &&
|
||||
df.remember_last_selected_value &&
|
||||
frappe.boot.user.last_selected_values
|
||||
|
|
|
|||
|
|
@ -836,9 +836,9 @@ $.extend(frappe.model, {
|
|||
}
|
||||
|
||||
if (
|
||||
(frm.doc.fields.find((i) => i.fieldname === "latitude") &&
|
||||
frm.doc.fields.find((i) => i.fieldname === "longitude")) ||
|
||||
frm.doc.fields.find(
|
||||
(frm.doc.fields?.find((i) => i.fieldname === "latitude") &&
|
||||
frm.doc.fields?.find((i) => i.fieldname === "longitude")) ||
|
||||
frm.doc.fields?.find(
|
||||
(i) => i.fieldname === "location" && i.fieldtype == "Geolocation"
|
||||
)
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -324,9 +324,9 @@ frappe.ui.GroupBy = class {
|
|||
);
|
||||
|
||||
if (this.aggregate_function === "sum") {
|
||||
docfield.label = __("Sum of {0}", [docfield.label]);
|
||||
docfield.label = __("Sum of {0}", [__(docfield.label)]);
|
||||
} else {
|
||||
docfield.label = __("Average of {0}", [docfield.label]);
|
||||
docfield.label = __("Average of {0}", [__(docfield.label)]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,7 +367,9 @@ frappe.ui.GroupBy = class {
|
|||
["Select", "Link", "Data", "Int", "Check"].includes(f.fieldtype)
|
||||
);
|
||||
const tag_field = { fieldname: "_user_tags", fieldtype: "Data", label: __("Tags") };
|
||||
this.group_by_fields[this.doctype] = fields.concat(tag_field);
|
||||
this.group_by_fields[this.doctype] = fields
|
||||
.concat(tag_field)
|
||||
.sort((a, b) => __(a.label).localeCompare(__(b.label)));
|
||||
this.all_fields[this.doctype] = this.report_view.meta.fields;
|
||||
|
||||
const standard_fields_filter = (df) =>
|
||||
|
|
@ -379,7 +381,8 @@ frappe.ui.GroupBy = class {
|
|||
const cdt = df.options;
|
||||
const child_table_fields = frappe.meta
|
||||
.get_docfields(cdt)
|
||||
.filter(standard_fields_filter);
|
||||
.filter(standard_fields_filter)
|
||||
.sort((a, b) => __(a.label).localeCompare(__(b.label)));
|
||||
this.group_by_fields[cdt] = child_table_fields;
|
||||
this.all_fields[cdt] = child_table_fields;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -376,6 +376,7 @@ frappe.show_progress = (title, count, total = 100, description, hide_on_completi
|
|||
// timeout to avoid abrupt hide
|
||||
setTimeout(frappe.hide_progress, 500);
|
||||
}
|
||||
frappe.cur_progress.$wrapper.css("z-index", 2000);
|
||||
return dialog;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
this.menu_items.push({
|
||||
label: __("Delete Kanban Board"),
|
||||
action: () => {
|
||||
frappe.confirm("Are you sure you want to proceed?", () => {
|
||||
frappe.confirm(__("Are you sure you want to proceed?"), () => {
|
||||
frappe.db.delete_doc("Kanban Board", this.board_name).then(() => {
|
||||
frappe.show_alert(`Kanban Board ${this.board_name} deleted.`);
|
||||
frappe.set_route("List", this.doctype, "List");
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ frappe.report_utils = {
|
|||
return frappe.after_ajax(() => {
|
||||
if (
|
||||
frappe.query_reports[report_name] &&
|
||||
!frappe.query_reports[report_name].filter &&
|
||||
!frappe.query_reports[report_name].filters &&
|
||||
r.filters
|
||||
) {
|
||||
return (frappe.query_reports[report_name].filters = r.filters);
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ export default class NumberCardWidget extends Widget {
|
|||
color_class = "green-stat";
|
||||
} else {
|
||||
caret_html = `<span class="indicator-pill-round red">
|
||||
${frappe.utils.icon("arrow-down-left", "xs")}
|
||||
${frappe.utils.icon("arrow-down-right", "xs")}
|
||||
</span>`;
|
||||
color_class = "red-stat";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,27 +106,28 @@ def patch_query_execute():
|
|||
def prepare_query(query):
|
||||
import inspect
|
||||
|
||||
from frappe.utils.safe_exec import check_safe_sql_query
|
||||
|
||||
param_collector = NamedParameterWrapper()
|
||||
query = query.get_sql(param_wrapper=param_collector)
|
||||
if frappe.flags.in_safe_exec and not check_safe_sql_query(query, throw=False):
|
||||
callstack = inspect.stack()
|
||||
if len(callstack) >= 3 and ".py" in callstack[2].filename:
|
||||
# ignore any query builder methods called from python files
|
||||
# assumption is that those functions are whitelisted already.
|
||||
if frappe.flags.in_safe_exec:
|
||||
from frappe.utils.safe_exec import check_safe_sql_query
|
||||
|
||||
# since query objects are patched everywhere any query.run()
|
||||
# will have callstack like this:
|
||||
# frame0: this function prepare_query()
|
||||
# frame1: execute_query()
|
||||
# frame2: frame that called `query.run()`
|
||||
#
|
||||
# if frame2 is server script <serverscript> is set as the filename
|
||||
# it shouldn't be allowed.
|
||||
pass
|
||||
else:
|
||||
raise frappe.PermissionError("Only SELECT SQL allowed in scripting")
|
||||
if not check_safe_sql_query(query, throw=False):
|
||||
callstack = inspect.stack()
|
||||
if len(callstack) >= 3 and ".py" in callstack[2].filename:
|
||||
# ignore any query builder methods called from python files
|
||||
# assumption is that those functions are whitelisted already.
|
||||
|
||||
# since query objects are patched everywhere any query.run()
|
||||
# will have callstack like this:
|
||||
# frame0: this function prepare_query()
|
||||
# frame1: execute_query()
|
||||
# frame2: frame that called `query.run()`
|
||||
#
|
||||
# if frame2 is server script <serverscript> is set as the filename
|
||||
# it shouldn't be allowed.
|
||||
pass
|
||||
else:
|
||||
raise frappe.PermissionError("Only SELECT SQL allowed in scripting")
|
||||
return query, param_collector.get_parameters()
|
||||
|
||||
builder_class = frappe.qb._BuilderClasss
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ def get_shared(doctype, user=None, rights=None):
|
|||
or_filters += [["everyone", "=", 1]]
|
||||
|
||||
shared_docs = frappe.get_all(
|
||||
"DocShare", fields=["share_name"], filters=filters, or_filters=or_filters
|
||||
"DocShare", fields=["share_name"], filters=filters, or_filters=or_filters, order_by=None
|
||||
)
|
||||
|
||||
return [doc.share_name for doc in shared_docs]
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import parse_qs, urljoin, urlparse
|
||||
|
||||
import jwt
|
||||
import requests
|
||||
from werkzeug.test import TestResponse
|
||||
|
||||
|
|
@ -362,6 +361,8 @@ class TestOAuth20(FrappeRequestTestCase):
|
|||
self.assertTrue(payload.get("nonce") == nonce)
|
||||
|
||||
def decode_id_token(self, id_token):
|
||||
import jwt
|
||||
|
||||
return jwt.decode(
|
||||
id_token,
|
||||
audience=self.client_id,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ from PIL import Image
|
|||
import frappe
|
||||
from frappe.installer import parse_app_name
|
||||
from frappe.model.document import Document
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.tests.utils import FrappeTestCase, MockedRequestTestCase, change_settings
|
||||
from frappe.utils import (
|
||||
ceil,
|
||||
dict_to_str,
|
||||
|
|
@ -815,8 +815,14 @@ class TestLinkTitle(FrappeTestCase):
|
|||
prop_setter.delete()
|
||||
|
||||
|
||||
class TestAppParser(FrappeTestCase):
|
||||
class TestAppParser(MockedRequestTestCase):
|
||||
def test_app_name_parser(self):
|
||||
self.responses.add(
|
||||
"HEAD",
|
||||
"https://api.github.com/repos/frappe/healthcare",
|
||||
status=200,
|
||||
json={},
|
||||
)
|
||||
bench_path = get_bench_path()
|
||||
frappe_app = os.path.join(bench_path, "apps", "frappe")
|
||||
self.assertEqual("frappe", parse_app_name(frappe_app))
|
||||
|
|
@ -1096,6 +1102,8 @@ class TestRounding(FrappeTestCase):
|
|||
rounding_method = "Banker's Rounding"
|
||||
|
||||
self.assertEqual(rounded(0, 0, rounding_method=rounding_method), 0)
|
||||
self.assertEqual(rounded(5.551115123125783e-17, 2, rounding_method=rounding_method), 0.0)
|
||||
|
||||
self.assertEqual(flt("0.5", 0, rounding_method=rounding_method), 0)
|
||||
self.assertEqual(flt("0.3", rounding_method=rounding_method), 0.3)
|
||||
|
||||
|
|
|
|||
|
|
@ -93,6 +93,19 @@ class FrappeTestCase(unittest.TestCase):
|
|||
frappe.db.sql = orig_sql
|
||||
|
||||
|
||||
class MockedRequestTestCase(FrappeTestCase):
|
||||
def setUp(self):
|
||||
import responses
|
||||
|
||||
self.responses = responses.RequestsMock()
|
||||
self.responses.start()
|
||||
|
||||
self.addCleanup(self.responses.stop)
|
||||
self.addCleanup(self.responses.reset)
|
||||
|
||||
return super().setUp()
|
||||
|
||||
|
||||
def _commit_watcher():
|
||||
import traceback
|
||||
|
||||
|
|
|
|||
|
|
@ -15,10 +15,8 @@ import operator
|
|||
import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from csv import reader
|
||||
from csv import reader, writer
|
||||
|
||||
from babel.messages.extract import extract_python
|
||||
from babel.messages.jslexer import Token, tokenize, unquote_string
|
||||
from pypika.terms import PseudoColumn
|
||||
|
||||
import frappe
|
||||
|
|
@ -737,6 +735,7 @@ def get_messages_from_file(path: str) -> list[tuple[str, str, str | None, int]]:
|
|||
|
||||
def extract_messages_from_python_code(code: str) -> list[tuple[int, str, str | None]]:
|
||||
"""Extracts translatable strings from Python code using babel."""
|
||||
from babel.messages.extract import extract_python
|
||||
|
||||
messages = []
|
||||
|
||||
|
|
@ -809,6 +808,8 @@ def extract_javascript(code, keywords=("__",), options=None):
|
|||
* `template_string` -- set to false to disable ES6
|
||||
template string support.
|
||||
"""
|
||||
from babel.messages.jslexer import Token, tokenize, unquote_string
|
||||
|
||||
if options is None:
|
||||
options = {}
|
||||
|
||||
|
|
@ -997,7 +998,6 @@ def write_csv_file(path, app_messages, lang_dict):
|
|||
:param lang_dict: Full translated dict.
|
||||
"""
|
||||
app_messages.sort(key=lambda x: x[1])
|
||||
from csv import writer
|
||||
|
||||
with open(path, "w", newline="") as msgfile:
|
||||
w = writer(msgfile, lineterminator="\n")
|
||||
|
|
@ -1118,6 +1118,50 @@ def import_translations(lang, path):
|
|||
write_translations_file(app, lang, full_dict)
|
||||
|
||||
|
||||
def migrate_translations(source_app, target_app):
|
||||
"""Migrate target-app-specific translations from source-app to target-app"""
|
||||
clear_cache()
|
||||
strings_in_source_app = [m[1] for m in frappe.translate.get_messages_for_app(source_app)]
|
||||
strings_in_target_app = [m[1] for m in frappe.translate.get_messages_for_app(target_app)]
|
||||
|
||||
strings_in_target_app_but_not_in_source_app = list(
|
||||
set(strings_in_target_app) - set(strings_in_source_app)
|
||||
)
|
||||
|
||||
languages = frappe.translate.get_all_languages()
|
||||
|
||||
source_app_translations_dir = os.path.join(frappe.get_pymodule_path(source_app), "translations")
|
||||
target_app_translations_dir = os.path.join(frappe.get_pymodule_path(target_app), "translations")
|
||||
|
||||
if not os.path.exists(target_app_translations_dir):
|
||||
os.makedirs(target_app_translations_dir)
|
||||
|
||||
for lang in languages:
|
||||
source_csv = os.path.join(source_app_translations_dir, lang + ".csv")
|
||||
|
||||
if not os.path.exists(source_csv):
|
||||
continue
|
||||
|
||||
target_csv = os.path.join(target_app_translations_dir, lang + ".csv")
|
||||
temp_csv = os.path.join(source_app_translations_dir, "_temp.csv")
|
||||
|
||||
with open(source_csv) as s, open(target_csv, "a+") as t, open(temp_csv, "a+") as temp:
|
||||
source_reader = reader(s, lineterminator="\n")
|
||||
target_writer = writer(t, lineterminator="\n")
|
||||
temp_writer = writer(temp, lineterminator="\n")
|
||||
|
||||
for row in source_reader:
|
||||
if row[0] in strings_in_target_app_but_not_in_source_app:
|
||||
target_writer.writerow(row)
|
||||
else:
|
||||
temp_writer.writerow(row)
|
||||
|
||||
if not os.path.getsize(target_csv):
|
||||
os.remove(target_csv)
|
||||
os.remove(source_csv)
|
||||
os.rename(temp_csv, source_csv)
|
||||
|
||||
|
||||
def rebuild_all_translation_files():
|
||||
"""Rebuild all translation files: `[app]/translations/[lang].csv`."""
|
||||
for lang in get_all_languages():
|
||||
|
|
|
|||
|
|
@ -1208,6 +1208,7 @@ Google Calendar ID,Google Kalender-ID,
|
|||
Google Font,Google Font,
|
||||
Google Services,Google-Dienste,
|
||||
Grant Type,Grant Typ,
|
||||
Group By {0},Gruppieren nach {0},
|
||||
Group Name,Gruppenname,
|
||||
Group name cannot be empty.,Der Gruppenname darf nicht leer sein.,
|
||||
Groups of DocTypes,Gruppen von DocTypes,
|
||||
|
|
@ -3238,6 +3239,7 @@ Change User,Benutzer wechseln,
|
|||
Check the Error Log for more information: {0},Überprüfen Sie das Fehlerprotokoll auf weitere Informationen: {0},
|
||||
Clear Cache and Reload,Cache leeren und neu laden,
|
||||
Clear Filters,Filter löschen,
|
||||
Clear all filters,Alle Filter löschen,
|
||||
Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access.,"Klicken Sie auf <b>Google Drive Access autorisieren, um Google Drive Access</b> zu autorisieren.",
|
||||
Click on a file to select it.,"Klicken Sie auf eine Datei, um sie auszuwählen.",
|
||||
Click on the link below to approve the request,"Klicken Sie auf den folgenden Link, um die Anfrage zu genehmigen",
|
||||
|
|
@ -3559,7 +3561,7 @@ Select Field...,Feld auswählen ...,
|
|||
Select Filters,Wählen Sie Filter,
|
||||
Select Google Calendar to which event should be synced.,"Wählen Sie Google Kalender aus, mit dem das Ereignis synchronisiert werden soll.",
|
||||
Select Google Contacts to which contact should be synced.,"Wählen Sie Google-Kontakte aus, mit denen der Kontakt synchronisiert werden soll.",
|
||||
Select Group By...,Wählen Sie Gruppieren nach ...,
|
||||
Select Group By...,Gruppieren nach ...,
|
||||
Select Mandatory,Verpflichtende auswählen,
|
||||
Select atleast 2 actions,Wählen Sie mindestens 2 Aktionen aus,
|
||||
Select list item,Listenelement auswählen,
|
||||
|
|
|
|||
|
|
|
@ -5,7 +5,6 @@ from base64 import b32encode, b64encode
|
|||
from io import BytesIO
|
||||
|
||||
import pyotp
|
||||
from pyqrcode import create as qrcreate
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
|
@ -387,6 +386,8 @@ def send_token_via_email(user, token, otp_secret, otp_issuer, subject=None, mess
|
|||
|
||||
def get_qr_svg_code(totp_uri):
|
||||
"""Get SVG code to display Qrcode for OTP."""
|
||||
from pyqrcode import create as qrcreate
|
||||
|
||||
url = qrcreate(totp_uri)
|
||||
svg = ""
|
||||
stream = BytesIO()
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ from collections.abc import (
|
|||
)
|
||||
from email.header import decode_header, make_header
|
||||
from email.utils import formataddr, parseaddr
|
||||
from gzip import GzipFile
|
||||
from typing import Any, Callable, Literal
|
||||
from urllib.parse import quote, urlparse
|
||||
|
||||
|
|
@ -873,6 +872,8 @@ def gzip_compress(data, compresslevel=9):
|
|||
"""Compress data in one shot and return the compressed string.
|
||||
Optional argument is the compression level, in range of 0-9.
|
||||
"""
|
||||
from gzip import GzipFile
|
||||
|
||||
buf = io.BytesIO()
|
||||
with GzipFile(fileobj=buf, mode="wb", compresslevel=compresslevel) as f:
|
||||
f.write(data)
|
||||
|
|
@ -883,6 +884,8 @@ def gzip_decompress(data):
|
|||
"""Decompress a gzip compressed string in one shot.
|
||||
Return the decompressed string.
|
||||
"""
|
||||
from gzip import GzipFile
|
||||
|
||||
with GzipFile(fileobj=io.BytesIO(data)) as f:
|
||||
return f.read()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import gc
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
|
|
@ -234,6 +235,10 @@ def start_worker(
|
|||
"""Wrapper to start rq worker. Connects to redis and monitors these queues."""
|
||||
DEQUEUE_STRATEGIES = {"round_robin": RoundRobinWorker, "random": RandomWorker}
|
||||
|
||||
if frappe._tune_gc:
|
||||
gc.collect()
|
||||
gc.freeze()
|
||||
|
||||
with frappe.init_site():
|
||||
# empty init is required to get redis_queue from common_site_config.json
|
||||
redis_connection = get_redis_conn(username=rq_username, password=rq_password)
|
||||
|
|
|
|||
|
|
@ -298,14 +298,13 @@ __version__ = '0.0.1'
|
|||
|
||||
"""
|
||||
|
||||
hooks_template = """from . import __version__ as app_version
|
||||
|
||||
app_name = "{app_name}"
|
||||
hooks_template = """app_name = "{app_name}"
|
||||
app_title = "{app_title}"
|
||||
app_publisher = "{app_publisher}"
|
||||
app_description = "{app_description}"
|
||||
app_email = "{app_email}"
|
||||
app_license = "{app_license}"
|
||||
# required_apps = []
|
||||
|
||||
# Includes in <head>
|
||||
# ------------------
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import json
|
|||
import os
|
||||
import subprocess # nosec
|
||||
|
||||
import requests
|
||||
from semantic_version import Version
|
||||
|
||||
import frappe
|
||||
|
|
@ -231,6 +230,7 @@ def check_release_on_github(app: str):
|
|||
organization name, if the application exists, otherwise None.
|
||||
"""
|
||||
|
||||
import requests
|
||||
from giturlparse import parse
|
||||
from giturlparse.parser import ParserError
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ from typing import Any, Literal, Optional, TypeVar, Union
|
|||
from urllib.parse import parse_qsl, quote, urlencode, urljoin, urlparse, urlunparse
|
||||
|
||||
from click import secho
|
||||
from dateutil import parser
|
||||
from dateutil.parser import ParserError
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
import frappe
|
||||
from frappe.desk.utils import slug
|
||||
|
|
@ -80,9 +83,6 @@ def getdate(
|
|||
Converts string date (yyyy-mm-dd) to datetime.date object.
|
||||
If no input is provided, current date is returned.
|
||||
"""
|
||||
from dateutil import parser
|
||||
from dateutil.parser._parser import ParserError
|
||||
|
||||
if not string_date:
|
||||
return get_datetime().date()
|
||||
if isinstance(string_date, datetime.datetime):
|
||||
|
|
@ -105,7 +105,6 @@ def getdate(
|
|||
def get_datetime(
|
||||
datetime_str: Optional["DateTimeLikeObject"] = None,
|
||||
) -> datetime.datetime | None:
|
||||
from dateutil import parser
|
||||
|
||||
if datetime_str is None:
|
||||
return now_datetime()
|
||||
|
|
@ -141,9 +140,6 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None:
|
|||
Returns:
|
||||
datetime.timedelta: Timedelta object equivalent of the passed `time` string
|
||||
"""
|
||||
from dateutil import parser
|
||||
from dateutil.parser import ParserError
|
||||
|
||||
time = time or "0:0:0"
|
||||
|
||||
try:
|
||||
|
|
@ -161,8 +157,6 @@ def get_timedelta(time: str | None = None) -> datetime.timedelta | None:
|
|||
|
||||
|
||||
def to_timedelta(time_str: str | datetime.time) -> datetime.timedelta:
|
||||
from dateutil import parser
|
||||
|
||||
if isinstance(time_str, datetime.time):
|
||||
time_str = str(time_str)
|
||||
|
||||
|
|
@ -237,9 +231,6 @@ def add_to_date(
|
|||
as_datetime=False,
|
||||
) -> DateTimeLikeObject:
|
||||
"""Adds `days` to the given date"""
|
||||
from dateutil import parser
|
||||
from dateutil.parser._parser import ParserError
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
if date is None:
|
||||
date = now_datetime()
|
||||
|
|
@ -500,9 +491,6 @@ def get_year_ending(date) -> datetime.date:
|
|||
|
||||
|
||||
def get_time(time_str: str) -> datetime.time:
|
||||
from dateutil import parser
|
||||
from dateutil.parser import ParserError
|
||||
|
||||
if isinstance(time_str, datetime.datetime):
|
||||
return time_str.time()
|
||||
elif isinstance(time_str, datetime.time):
|
||||
|
|
@ -1116,12 +1104,12 @@ def _round_away_from_zero(num, precision):
|
|||
|
||||
|
||||
def _bankers_rounding(num, precision):
|
||||
if num == 0:
|
||||
return 0.0
|
||||
|
||||
multiplier = 10**precision
|
||||
num = round(num * multiplier, 12)
|
||||
|
||||
if num == 0:
|
||||
return 0.0
|
||||
|
||||
floor_num = math.floor(num)
|
||||
decimal_part = num - floor_num
|
||||
|
||||
|
|
@ -2234,3 +2222,11 @@ def add_trackers_to_url(url: str, source: str, campaign: str, medium: str = "ema
|
|||
|
||||
url_parts[4] = urlencode(query)
|
||||
return urlunparse(url_parts)
|
||||
|
||||
|
||||
# This is used in test to count memory overhead of default imports.
|
||||
def _get_rss_memory_usage():
|
||||
import psutil
|
||||
|
||||
rss = psutil.Process().memory_info().rss // (1024 * 1024)
|
||||
return rss
|
||||
|
|
|
|||
|
|
@ -7,12 +7,9 @@ import inspect
|
|||
import json
|
||||
import linecache
|
||||
import os
|
||||
import pydoc
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from ldap3.core.exceptions import LDAPException
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cstr, encode
|
||||
|
||||
|
|
@ -20,16 +17,31 @@ EXCLUDE_EXCEPTIONS = (
|
|||
frappe.AuthenticationError,
|
||||
frappe.CSRFTokenError, # CSRF covers OAuth too
|
||||
frappe.SecurityException,
|
||||
LDAPException,
|
||||
frappe.InReadOnlyMode,
|
||||
)
|
||||
|
||||
LDAP_BASE_EXCEPTION = "LDAPException"
|
||||
|
||||
|
||||
def _is_ldap_exception(e):
|
||||
"""Check if exception is from LDAP library.
|
||||
|
||||
This is a hack but ensures that LDAP is not imported unless it's required. This is tested in
|
||||
unittests in case the exception changes in future.
|
||||
"""
|
||||
|
||||
for t in type(e).__mro__:
|
||||
if t.__name__ == LDAP_BASE_EXCEPTION:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def make_error_snapshot(exception):
|
||||
if frappe.conf.disable_error_snapshot:
|
||||
return
|
||||
|
||||
if isinstance(exception, EXCLUDE_EXCEPTIONS):
|
||||
if isinstance(exception, EXCLUDE_EXCEPTIONS) or _is_ldap_exception(exception):
|
||||
return
|
||||
|
||||
logger = frappe.logger(with_more_info=True)
|
||||
|
|
@ -56,6 +68,8 @@ def make_error_snapshot(exception):
|
|||
|
||||
|
||||
def get_snapshot(exception, context=10):
|
||||
import pydoc
|
||||
|
||||
"""
|
||||
Return a dict describing a given traceback (based on cgitb.text)
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ import base64
|
|||
import json
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
import jwt
|
||||
|
||||
import frappe
|
||||
import frappe.utils
|
||||
from frappe import _
|
||||
|
|
@ -126,6 +124,9 @@ def login_via_oauth2_id_token(
|
|||
def get_info_via_oauth(
|
||||
provider: str, code: str, decoder: Callable | None = None, id_token: bool = False
|
||||
):
|
||||
|
||||
import jwt
|
||||
|
||||
flow = get_oauth2_flow(provider)
|
||||
oauth2_providers = get_oauth2_providers()
|
||||
|
||||
|
|
|
|||
|
|
@ -195,7 +195,13 @@ def decrypt(txt, encryption_key=None):
|
|||
return cstr(cipher_suite.decrypt(encode(txt)))
|
||||
except InvalidToken:
|
||||
# encryption_key in site_config is changed and not valid
|
||||
frappe.throw(_("Encryption key is invalid! Please check site_config.json"))
|
||||
frappe.throw(
|
||||
_("Encryption key is invalid! Please check site_config.json")
|
||||
+ "<br>"
|
||||
+ _(
|
||||
"If you have recently restored the site you may need to copy the site config contaning original Encryption Key."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_encryption_key():
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ from typing import NoReturn
|
|||
|
||||
# imports - module imports
|
||||
import frappe
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.utils import cint, get_datetime, get_sites, now_datetime
|
||||
from frappe.utils.background_jobs import get_jobs
|
||||
|
||||
|
|
@ -176,6 +175,8 @@ def _get_last_modified_timestamp(doctype):
|
|||
|
||||
@frappe.whitelist()
|
||||
def activate_scheduler():
|
||||
from frappe.installer import update_site_config
|
||||
|
||||
frappe.only_for("Administrator")
|
||||
|
||||
if frappe.local.conf.maintenance_mode:
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ removed without any warning.
|
|||
"""
|
||||
from contextlib import suppress
|
||||
|
||||
from posthog import Posthog
|
||||
|
||||
import frappe
|
||||
from frappe.utils import getdate
|
||||
from frappe.utils.caching import site_cache
|
||||
|
||||
from posthog import Posthog # isort: skip
|
||||
|
||||
|
||||
POSTHOG_PROJECT_FIELD = "posthog_project_id"
|
||||
POSTHOG_HOST_FIELD = "posthog_host"
|
||||
|
||||
|
|
@ -59,11 +60,13 @@ def capture(event, app, **kwargs):
|
|||
ph and ph.capture(distinct_id=frappe.local.site, event=f"{app}_{event}", **kwargs)
|
||||
|
||||
|
||||
def capture_doc(doc):
|
||||
def capture_doc(doc, action):
|
||||
with suppress(Exception):
|
||||
age = site_age()
|
||||
if not age or age > 15:
|
||||
return
|
||||
|
||||
if doc.get("__islocal") or not doc.get("name"):
|
||||
capture("document_created", "frappe", properties={"doctype": doc.doctype})
|
||||
capture("document_created", "frappe", properties={"doctype": doc.doctype, "action": "Insert"})
|
||||
else:
|
||||
capture("document_modified", "frappe", properties={"doctype": doc.doctype, "action": action})
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ from urllib.parse import quote
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.google_oauth import GoogleOAuth
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import encode, get_request_site_address
|
||||
from frappe.website.utils import get_boot_data
|
||||
|
|
@ -100,6 +99,8 @@ class WebsiteSettings(Document):
|
|||
frappe.clear_cache()
|
||||
|
||||
def get_access_token(self):
|
||||
from frappe.integrations.google_oauth import GoogleOAuth
|
||||
|
||||
if not self.indexing_refresh_token:
|
||||
button_label = frappe.bold(_("Allow API Indexing Access"))
|
||||
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label))
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import frappe
|
|||
import frappe.utils
|
||||
from frappe import _
|
||||
from frappe.auth import LoginManager
|
||||
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
|
||||
from frappe.rate_limiter import rate_limit
|
||||
from frappe.utils import cint, get_url
|
||||
from frappe.utils.data import escape_html
|
||||
|
|
@ -85,7 +84,10 @@ def get_context(context):
|
|||
)
|
||||
context["social_login"] = True
|
||||
|
||||
context["ldap_settings"] = LDAPSettings.get_ldap_client_settings()
|
||||
if cint(frappe.db.get_value("LDAP Settings", "LDAP Settings", "enabled")):
|
||||
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
|
||||
|
||||
context["ldap_settings"] = LDAPSettings.get_ldap_client_settings()
|
||||
|
||||
login_label = [_("Email")]
|
||||
|
||||
|
|
|
|||
|
|
@ -105,3 +105,4 @@ pyngrok = "~=6.0.0"
|
|||
unittest-xml-reporting = "~=3.2.0"
|
||||
watchdog = "~=3.0.0"
|
||||
hypothesis = "~=6.77.0"
|
||||
responses = "~=0.23.1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue