Merge branch 'develop' into file-permissions

This commit is contained in:
Raffael Meyer 2023-06-08 18:21:23 +02:00 committed by GitHub
commit 01d8a4ab28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
237 changed files with 4083 additions and 1852 deletions

View file

@ -34,3 +34,9 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
# db.get_all -> get_all
2eec621e95564c359ad22da79501a855c1f32b03
# minor formatting fix in `user.py`
f223bc02490902dfcc32892058f13f343d51fbaf
# frappe.cache() -> frappe.cache
fa6dc03cc87ad74e11609e7373078366fdcb3e1b

View file

@ -97,4 +97,4 @@ jobs:
pip install pip-audit
cd ${GITHUB_WORKSPACE}
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
pip-audit --desc on .
pip-audit --desc on --ignore-vuln GHSA-4xqq-73wg-5mjp .

View file

@ -56,10 +56,15 @@ Full-stack web application framework that uses Python and MariaDB on the server
## Installation
* [Install via Docker](https://github.com/frappe/frappe_docker)
* [Install via Frappe Bench](https://github.com/frappe/bench)
* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/frappe/signup)
### Production
* [Managed Hosting on Frappe Cloud](https://frappecloud.com/)
* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
* [Manual install using Docker images](https://github.com/frappe/frappe_docker)
### Development
* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation)
## Contributing

View file

@ -26,11 +26,11 @@ context("Form Builder", () => {
cy.get(".page-title").click();
cy.get(".frappe-control[data-fieldname='doctype'] input").click().as("input");
cy.get("@input").type("{rightArrow} Field", { delay: 200 });
cy.get("@input").type("{rightArrow}Web Form Field", { delay: 200 });
cy.wait("@search_link");
cy.get("@input").type("{enter}").blur();
cy.click_modal_primary_button("Change");
cy.click_modal_primary_button("Edit");
cy.get(".page-title .title-text").should("have.text", "Web Form Field");
});

View file

@ -87,7 +87,10 @@ const NODE_PATHS = [].concat(
execute()
.then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
.catch((e) => console.error(e));
.catch((e) => {
console.error(e);
throw e;
});
if (WATCH_MODE) {
// listen for open files in editor event

View file

@ -16,8 +16,9 @@ import inspect
import json
import os
import re
import unicodedata
import warnings
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, overload
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeAlias, overload
import click
from werkzeug.local import Local, release_local
@ -47,6 +48,7 @@ __title__ = "Frappe Framework"
controllers = {}
local = Local()
cache = None
STANDARD_USERS = ("Guest", "Administrator")
_dev_server = int(sbool(os.environ.get("DEV_SERVER", False)))
@ -177,6 +179,7 @@ if TYPE_CHECKING:
db: MariaDBDatabase | PostgresDatabase
qb: MariaDB | Postgres
cache: RedisWrapper
# end: static analysis hack
@ -190,7 +193,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.error_log = []
local.message_log = []
local.debug_log = []
local.realtime_log = []
local.flags = _dict(
{
"currently_saving": [],
@ -207,9 +209,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
"read_only": False,
}
)
local.rollback_observers = []
local.locked_documents = []
local.before_commit = []
local.test_objects = {}
local.site = site
@ -233,7 +233,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.role_permissions = {}
local.valid_columns = {}
local.new_doc_templates = {}
local.link_count = {}
local.jenv = None
local.jloader = None
@ -244,6 +243,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type or "mariadb")
local.qb.get_query = get_query
setup_redis_cache_connection()
setup_module_map()
if not _qb_patched.get(local.conf.db_type):
@ -351,17 +351,14 @@ def destroy():
release_local(local)
redis_server = None
def setup_redis_cache_connection():
"""Defines `frappe.cache` as `RedisWrapper` instance"""
global cache
def cache() -> "RedisWrapper":
"""Returns redis connection."""
global redis_server
if not redis_server:
if not cache:
from frappe.utils.redis_wrapper import RedisWrapper
redis_server = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
return redis_server
cache = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
def get_traceback(with_context: bool = False) -> str:
@ -383,7 +380,7 @@ def errprint(msg: str) -> None:
def print_sql(enable: bool = True) -> None:
return cache().set_value("flag_print_sql", enable)
return cache.set_value("flag_print_sql", enable)
def log(msg: str) -> None:
@ -879,6 +876,7 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
:param doctype: If doctype is given, only DocType cache is cleared."""
import frappe.cache_manager
import frappe.utils.caching
from frappe.website.router import clear_routing_cache
if doctype:
frappe.cache_manager.clear_doctype_cache(doctype)
@ -907,6 +905,8 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
if hasattr(local, "website_settings"):
del local.website_settings
clear_routing_cache()
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
if ignore_permissions:
@ -1016,7 +1016,7 @@ def is_table(doctype: str) -> bool:
def get_tables():
return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True)
tables = cache().get_value("is_table", get_tables)
tables = cache.get_value("is_table", get_tables)
return doctype in tables
@ -1043,24 +1043,32 @@ def generate_hash(txt: str | None = None, length: int = 56) -> str:
def reset_metadata_version():
"""Reset `metadata_version` (Client (Javascript) build ID) hash."""
v = generate_hash()
cache().set_value("metadata_version", v)
cache.set_value("metadata_version", v)
return v
def new_doc(
doctype: str,
*,
parent_doc: Optional["Document"] = None,
parentfield: str | None = None,
as_dict: bool = False,
**kwargs,
) -> "Document":
"""Returns a new document of the given DocType with defaults set.
:param doctype: DocType of the new document.
:param parent_doc: [optional] add to parent document.
:param parentfield: [optional] add against this `parentfield`."""
:param parentfield: [optional] add against this `parentfield`.
:param as_dict: [optional] return as dictionary instead of Document.
:param kwargs: [optional] You can specify fields as field=value pairs in function call.
"""
from frappe.model.create_new import get_new_doc
return get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
new_doc = get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
return new_doc.update(kwargs)
def set_value(doctype, docname, fieldname, value=None):
@ -1071,7 +1079,7 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
if (key := can_cache_doc(args)) and (doc := cache().hget("document_cache", key)):
if (key := can_cache_doc(args)) and (doc := cache.get_value(key)):
return doc
# Not found in cache, fetch from DB
@ -1087,7 +1095,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
def _set_document_in_cache(key: str, doc: "Document") -> None:
cache().hset("document_cache", key, doc)
cache.set_value(key, doc)
def can_cache_doc(args) -> str | None:
@ -1108,12 +1116,20 @@ def can_cache_doc(args) -> str | None:
def get_document_cache_key(doctype: str, name: str):
return f"{doctype}::{name}"
return f"document_cache::{doctype}::{name}"
def clear_document_cache(doctype, name):
cache().hdel("last_modified", doctype)
cache().hdel("document_cache", get_document_cache_key(doctype, name))
def clear_document_cache(doctype: str, name: str | None = None) -> None:
def clear_in_redis():
if name is not None:
cache.delete_value(get_document_cache_key(doctype, name))
else:
cache.delete_keys(get_document_cache_key(doctype, ""))
clear_in_redis()
if hasattr(db, "after_commit"):
db.after_commit.add(clear_in_redis)
db.after_rollback.add(clear_in_redis)
if doctype == "System Settings" and hasattr(local, "system_settings"):
delattr(local, "system_settings")
@ -1142,7 +1158,42 @@ def get_cached_value(
return values
def get_doc(*args, **kwargs) -> "Document":
_SingleDocument: TypeAlias = "Document"
_NewDocument: TypeAlias = "Document"
@overload
def get_doc(document: "Document", /) -> "Document":
pass
@overload
def get_doc(doctype: str, /) -> _SingleDocument:
"""Retrieve Single DocType from DB, doctype must be positional argument."""
pass
@overload
def get_doc(doctype: str, name: str, /, for_update: bool | None = None) -> "Document":
"""Retrieve DocType from DB, doctype and name must be positional argument."""
pass
@overload
def get_doc(**kwargs: dict) -> "_NewDocument":
"""Initialize document from kwargs.
Not recommended. Use `frappe.new_doc` instead."""
pass
@overload
def get_doc(documentdict: dict) -> "_NewDocument":
"""Create document from dict.
Not recommended. Use `frappe.new_doc` instead."""
pass
def get_doc(*args, **kwargs):
"""Return a `frappe.model.document.Document` object of the given type and name.
:param arg1: DocType name as string **or** document JSON.
@ -1163,7 +1214,7 @@ def get_doc(*args, **kwargs) -> "Document":
doc = frappe.model.document.get_doc(*args, **kwargs)
# Replace cache if stale one exists
if (key := can_cache_doc(args)) and cache().hexists("document_cache", key):
if (key := can_cache_doc(args)) and cache.exists(key):
_set_document_in_cache(key, doc)
return doc
@ -1397,13 +1448,13 @@ def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False)
if sort:
if not local.all_apps:
local.all_apps = cache().get_value("all_apps", get_all_apps)
local.all_apps = cache.get_value("all_apps", get_all_apps)
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
installed = [app for app in local.all_apps if app in installed]
if _ensure_on_bench:
all_apps = cache().get_value("all_apps", get_all_apps)
all_apps = cache.get_value("all_apps", get_all_apps)
installed = [app for app in installed if app in all_apps]
if frappe_last:
@ -1474,7 +1525,7 @@ def get_hooks(
if conf.developer_mode:
hooks = _dict(_load_app_hooks())
else:
hooks = _dict(cache().get_value("app_hooks", _load_app_hooks))
hooks = _dict(cache.get_value("app_hooks", _load_app_hooks))
if hook:
return hooks.get(hook, ([] if default == "_KEEP_DEFAULT_LIST" else default))
@ -1504,11 +1555,9 @@ def append_hook(target, key, value):
def setup_module_map():
"""Rebuild map of all modules (internal)."""
_cache = cache()
if conf.db_name:
local.app_modules = _cache.get_value("app_modules")
local.module_app = _cache.get_value("module_app")
local.app_modules = cache.get_value("app_modules")
local.module_app = cache.get_value("module_app")
if not (local.app_modules and local.module_app):
local.module_app, local.app_modules = {}, {}
@ -1520,8 +1569,8 @@ def setup_module_map():
local.app_modules[app].append(module)
if conf.db_name:
_cache.set_value("app_modules", local.app_modules)
_cache.set_value("module_app", local.module_app)
cache.set_value("app_modules", local.app_modules)
cache.set_value("module_app", local.module_app)
def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
@ -1810,7 +1859,7 @@ def redirect_to_message(title, html, http_status_code=None, context=None, indica
if indicator_color:
message["context"].update({"indicator_color": indicator_color})
cache().set_value(f"message_id:{message_id}", message, expires_in_sec=60)
cache.set_value(f"message_id:{message_id}", message, expires_in_sec=60)
location = f"/message?id={message_id}"
if not getattr(local, "is_ajax", False):
@ -2228,6 +2277,7 @@ def bold(text):
def safe_eval(code, eval_globals=None, eval_locals=None):
"""A safer `eval`"""
whitelisted_globals = {"int": int, "float": float, "long": int, "round": round}
code = unicodedata.normalize("NFKC", code)
UNSAFE_ATTRIBUTES = {
# Generator Attributes

View file

@ -19,7 +19,6 @@ import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import cint, get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot
@ -158,6 +157,8 @@ def log_request(request, response):
{
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
"pid": os.getpid(),
"user": getattr(frappe.local.session, "user", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),
@ -351,8 +352,6 @@ def sync_database(rollback: bool) -> bool:
frappe.db.commit()
rollback = False
update_comments_in_parent_after_request()
return rollback

View file

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

View file

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

View file

@ -1,13 +1,15 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
"content": "[{\"id\":\"-P-RG1wVHg\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"LdZrgvxxo7\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yNSSTIaDWZ\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Documents</b></span>\",\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
"creation": "2020-03-02 14:53:24.980279",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_hidden": 0,
"label": "Tools",
"links": [
{
@ -132,28 +134,89 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Data",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Import Data",
"link_count": 0,
"link_to": "Data Import",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Export Data",
"link_count": 0,
"link_to": "Data Export",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Bulk Update",
"link_count": 0,
"link_to": "Bulk Update",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Download Backups",
"link_count": 0,
"link_to": "backups",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Deleted Documents",
"link_count": 0,
"link_to": "Deleted Document",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-12-12 14:58:44.733393",
"modified": "2023-05-24 14:47:24.740856",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 26.0,
"sequence_id": 17.0,
"shortcuts": [
{
"label": "ToDo",
"link_to": "ToDo",
"color": "Grey",
"doc_view": "List",
"label": "Import Data",
"link_to": "Data Import",
"type": "DocType"
},
{
"label": "Note",
"link_to": "Note",
"label": "ToDo",
"link_to": "ToDo",
"type": "DocType"
},
{
@ -165,11 +228,6 @@
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"type": "DocType"
},
{
"label": "Auto Repeat",
"link_to": "Auto Repeat",
"type": "DocType"
}
],
"title": "Tools"

View file

@ -8,6 +8,7 @@ import frappe
import frappe.defaults
import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
from frappe.email.inbox import get_email_accounts
@ -68,6 +69,7 @@ def get_bootinfo():
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
bootinfo.onboarding_tours = get_onboarding_ui_tours()
set_time_zone(bootinfo)
# ipinfo
@ -147,10 +149,8 @@ def get_allowed_report_names(cache=False) -> set[str]:
def get_user_pages_or_reports(parent, cache=False):
_cache = frappe.cache()
if cache:
has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
has_role = frappe.cache.get_value("has_role:" + parent, user=frappe.session.user)
if has_role:
return has_role
@ -252,7 +252,7 @@ def get_user_pages_or_reports(parent, cache=False):
has_role.pop(r, None)
# Expire every six hours
_cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
frappe.cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role

View file

@ -79,28 +79,25 @@ doctype_cache_keys = (
def clear_user_cache(user=None):
cache = frappe.cache()
# this will automatically reload the global cache
# so it is important to clear this first
clear_notifications(user)
if user:
for name in user_cache_keys:
cache.hdel(name, user)
cache.delete_keys("user:" + user)
frappe.cache.hdel(name, user)
frappe.cache.delete_keys("user:" + user)
clear_defaults_cache(user)
else:
for name in user_cache_keys:
cache.delete_key(name)
frappe.cache.delete_key(name)
clear_defaults_cache()
clear_global_cache()
def clear_domain_cache(user=None):
cache = frappe.cache()
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
cache.delete_value(domain_cache_keys)
frappe.cache.delete_value(domain_cache_keys)
def clear_global_cache():
@ -108,29 +105,36 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
frappe.cache().delete_value(global_cache_keys)
frappe.cache().delete_value(bench_cache_keys)
frappe.cache.delete_value(global_cache_keys)
frappe.cache.delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
if user:
for p in [user] + common_default_keys:
frappe.cache().hdel("defaults", p)
frappe.cache.hdel("defaults", p)
elif frappe.flags.in_install != "frappe":
frappe.cache().delete_key("defaults")
frappe.cache.delete_key("defaults")
def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
cache = frappe.cache()
for key in ("is_table", "doctype_modules", "document_cache"):
cache.delete_value(key)
_clear_doctype_cache_form_redis()
if hasattr(frappe.db, "after_commit"):
frappe.db.after_commit.add(_clear_doctype_cache_form_redis)
frappe.db.after_rollback.add(_clear_doctype_cache_form_redis)
def _clear_doctype_cache_form_redis(doctype: str | None = None):
for key in ("is_table", "doctype_modules"):
frappe.cache.delete_value(key)
def clear_single(dt):
frappe.clear_document_cache(dt)
for name in doctype_cache_keys:
cache.hdel(name, dt)
frappe.cache.hdel(name, dt)
if doctype:
clear_single(doctype)
@ -154,7 +158,8 @@ def clear_doctype_cache(doctype=None):
else:
# clear all
for name in doctype_cache_keys:
cache.delete_value(name)
frappe.cache.delete_value(name)
frappe.cache.delete_keys("document_cache::")
def clear_controller_cache(doctype=None):
@ -167,7 +172,7 @@ def clear_controller_cache(doctype=None):
def get_doctype_map(doctype, name, filters=None, order_by=None):
return frappe.cache().hget(
return frappe.cache.hget(
get_doctype_map_key(doctype),
name,
lambda: frappe.get_all(doctype, filters=filters, order_by=order_by, ignore_ddl=True),
@ -175,7 +180,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
def clear_doctype_map(doctype, name):
frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)
frappe.cache.hdel(frappe.scrub(doctype) + "_map", name)
def build_table_count_cache():
@ -188,7 +193,6 @@ def build_table_count_cache():
):
return
_cache = frappe.cache()
table_name = frappe.qb.Field("table_name").as_("name")
table_rows = frappe.qb.Field("table_rows").as_("count")
information_schema = frappe.qb.Schema("information_schema")
@ -197,7 +201,7 @@ def build_table_count_cache():
as_dict=True
)
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
_cache.set_value("information_schema:counts", counts)
frappe.cache.set_value("information_schema:counts", counts)
return counts
@ -211,11 +215,10 @@ def build_domain_restriced_doctype_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
doctypes = [doc.name for doc in doctypes]
_cache.set_value("domain_restricted_doctypes", doctypes)
frappe.cache.set_value("domain_restricted_doctypes", doctypes)
return doctypes
@ -229,10 +232,9 @@ def build_domain_restriced_page_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
_cache = frappe.cache()
active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
pages = [page.name for page in pages]
_cache.set_value("domain_restricted_pages", pages)
frappe.cache.set_value("domain_restricted_pages", pages)
return pages

View file

@ -623,7 +623,7 @@ frappe.db.connect()
def _console_cleanup():
# Execute rollback_observers on console close
# Execute after_rollback on console close
frappe.db.rollback()
frappe.destroy()

View file

@ -1,77 +1,17 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import functools
import re
import frappe
from frappe import _
def load_address_and_contact(doc, key=None):
def load_address_and_contact(doc, key=None) -> None:
"""Loads address list and contact list in `__onload`"""
from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address
from frappe.contacts.doctype.address.address import get_address_display_list
from frappe.contacts.doctype.contact.contact import get_contact_display_list
filters = [
["Dynamic Link", "link_doctype", "=", doc.doctype],
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Address"],
]
address_list = frappe.get_list("Address", filters=filters, fields=["*"], order_by="creation asc")
address_list = [a.update({"display": get_address_display(a)}) for a in address_list]
address_list = sorted(
address_list,
key=functools.cmp_to_key(
lambda a, b: (int(a.is_primary_address - b.is_primary_address))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)
doc.set_onload("addr_list", address_list)
contact_list = []
filters = [
["Dynamic Link", "link_doctype", "=", doc.doctype],
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Contact"],
]
contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])
for contact in contact_list:
contact["email_ids"] = frappe.get_all(
"Contact Email",
filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
fields=["email_id"],
)
contact["phone_nos"] = frappe.get_all(
"Contact Phone",
filters={
"parenttype": "Contact",
"parent": contact.name,
"is_primary_phone": 0,
"is_primary_mobile_no": 0,
},
fields=["phone"],
)
if contact.address:
address = frappe.get_doc("Address", contact.address)
contact["address"] = get_condensed_address(address)
contact_list = sorted(
contact_list,
key=functools.cmp_to_key(
lambda a, b: (int(a.is_primary_contact - b.is_primary_contact))
or (1 if a.modified - b.modified else 0)
),
reverse=True,
)
doc.set_onload("contact_list", contact_list)
doc.set_onload("addr_list", get_address_display_list(doc.doctype, doc.name))
doc.set_onload("contact_list", get_contact_display_list(doc.doctype, doc.name))
def has_permission(doc, ptype, user):

View file

@ -291,3 +291,23 @@ def get_condensed_address(doc):
def update_preferred_address(address, field):
frappe.db.set_value("Address", address, field, 0)
def get_address_display_list(doctype: str, name: str) -> list[dict]:
if not frappe.has_permission("Address", "read"):
return []
address_list = frappe.get_list(
"Address",
filters=[
["Dynamic Link", "link_doctype", "=", doctype],
["Dynamic Link", "link_name", "=", name],
["Dynamic Link", "parenttype", "=", "Address"],
],
fields=["*"],
order_by="is_primary_address DESC, creation ASC",
)
for a in address_list:
a["display"] = get_address_display(a)
return address_list

View file

@ -341,3 +341,45 @@ def get_full_name(
full_name = company
return full_name
def get_contact_display_list(doctype: str, name: str) -> list[dict]:
from frappe.contacts.doctype.address.address import get_condensed_address
if not frappe.has_permission("Contact", "read"):
return []
contact_list = frappe.get_list(
"Contact",
filters=[
["Dynamic Link", "link_doctype", "=", doctype],
["Dynamic Link", "link_name", "=", name],
["Dynamic Link", "parenttype", "=", "Contact"],
],
fields=["*"],
order_by="is_primary_contact DESC, creation ASC",
)
for contact in contact_list:
contact["email_ids"] = frappe.get_all(
"Contact Email",
filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
fields=["email_id"],
)
contact["phone_nos"] = frappe.get_all(
"Contact Phone",
filters={
"parenttype": "Contact",
"parent": contact.name,
"is_primary_phone": 0,
"is_primary_mobile_no": 0,
},
fields=["phone"],
)
if contact.address and frappe.has_permission("Address", "read"):
address = frappe.get_doc("Address", contact.address)
contact["address"] = get_condensed_address(address)
return contact_list

View file

@ -152,14 +152,9 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
# missing column and in request, add column and update after commit
frappe.local._comments = getattr(frappe.local, "_comments", []) + [
(reference_doctype, reference_name, _comments)
]
pass
elif frappe.db.is_data_too_long(e):
raise frappe.DataTooLongException
else:
raise
else:
@ -169,13 +164,3 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
# Clear route cache
if route := frappe.get_cached_value(reference_doctype, reference_name, "route"):
clear_cache(route)
def update_comments_in_parent_after_request():
"""update _comments in parent if _comments column is missing"""
if hasattr(frappe.local, "_comments"):
for (reference_doctype, reference_name, _comments) in frappe.local._comments:
add_column(reference_doctype, "_comments", "Text")
update_comments_in_parent(reference_doctype, reference_name, _comments)
frappe.db.commit()

View file

@ -62,7 +62,7 @@ class Importer:
def before_import(self):
# set user lang for translations
frappe.cache().hdel("lang", frappe.session.user)
frappe.cache.hdel("lang", frappe.session.user)
frappe.set_user_lang(frappe.session.user)
# set flags
@ -1207,7 +1207,7 @@ def get_df_for_column_header(doctype, header):
def build_fields_dict_for_doctype():
return build_fields_dict_for_column_matching(doctype)
df_by_labels_and_fieldname = frappe.cache().hget(
df_by_labels_and_fieldname = frappe.cache.hget(
"data_import_column_header_map", doctype, generator=build_fields_dict_for_doctype
)
return df_by_labels_and_fieldname.get(header)

View file

@ -33,7 +33,7 @@ from frappe.model.meta import Meta
from frappe.modules import get_doc_path, make_boilerplate
from frappe.modules.import_file import get_file_path
from frappe.query_builder.functions import Concat
from frappe.utils import cint, random_string
from frappe.utils import cint, flt, random_string
from frappe.website.utils import clear_cache
if TYPE_CHECKING:
@ -1679,7 +1679,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
for role in list(set(roles)):
if frappe.db.table_exists("Role", cached=False) and not frappe.db.exists("Role", role):
r = frappe.get_doc(dict(doctype="Role", role_name=role, desk_access=1))
r = frappe.new_doc("Role")
r.role_name = role
r.desk_access = 1
r.flags.ignore_mandatory = r.flags.ignore_permissions = True
r.insert()
except frappe.DoesNotExistError as e:
@ -1708,7 +1710,7 @@ def check_fieldname_conflicts(docfield):
def clear_linked_doctype_cache():
frappe.cache().delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
frappe.cache.delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
def check_email_append_to(doc):
@ -1749,3 +1751,14 @@ def get_field(doc, fieldname):
for field in doc.fields:
if field.fieldname == fieldname:
return field
@frappe.whitelist()
def get_row_size_utilization(doctype: str) -> float:
"""Get row size utilization in percentage"""
frappe.has_permission("DocType", throw=True)
try:
return flt(frappe.db.get_row_size(doctype) / frappe.db.MAX_ROW_SIZE_LIMIT * 100, 2)
except Exception:
return 0.0

View file

@ -0,0 +1,28 @@
frappe.listview_settings["DocType"] = {
onload: function (me) {
me.page.btn_primary.addClass("hidden");
this.setup_select_primary_button(me);
},
setup_select_primary_button: function (me) {
let actions = [
{
label: __("Add DocType (Form Builder)"),
description: __("Use the form builder to create a new DocType"),
action: () => frappe.set_route("form-builder", "new-doctype"),
},
{
label: __("Add DocType"),
description: __("Create a new DocType"),
action: () => frappe.new_doc("DocType"),
},
];
frappe.utils.add_select_group_button(
me.page.btn_primary.parent(),
actions,
"btn-primary",
"add"
);
},
};

View file

@ -2,6 +2,7 @@
# See license.txt
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.core.doctype.document_naming_settings.document_naming_settings import (
DocumentNamingSettings,
)
@ -11,6 +12,25 @@ from frappe.utils import cint
class TestNamingSeries(FrappeTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.ns_doctype = (
new_doctype(
fields=[
{
"label": "Series",
"fieldname": "naming_series",
"fieldtype": "Select",
"options": f"\n{frappe.generate_hash()}-.###",
}
],
autoname="naming_series:",
)
.insert()
.name
)
def setUp(self):
self.dns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
@ -23,7 +43,7 @@ class TestNamingSeries(FrappeTestCase):
return VALID_SERIES + exisiting_series
def test_naming_preview(self):
self.dns.transaction_type = "Webhook"
self.dns.transaction_type = self.ns_doctype
self.dns.try_naming_series = "AXBZ.####"
serieses = self.dns.preview_series().split("\n")
@ -35,23 +55,22 @@ class TestNamingSeries(FrappeTestCase):
def test_get_transactions(self):
naming_info = self.dns.get_transactions_and_prefixes()
self.assertIn("Webhook", naming_info["transactions"])
self.assertIn(self.ns_doctype, naming_info["transactions"])
existing_naming_series = frappe.get_meta("Webhook").get_field("naming_series").options
existing_naming_series = frappe.get_meta(self.ns_doctype).get_field("naming_series").options
for series in existing_naming_series.split("\n"):
self.assertIn(NamingSeries(series).get_prefix(), naming_info["prefixes"])
def test_default_naming_series(self):
self.assertIn("HOOK", get_default_naming_series("Webhook"))
self.assertIsNone(get_default_naming_series("DocType"))
def test_updates_naming_options(self):
self.dns.transaction_type = "Webhook"
self.dns.transaction_type = self.ns_doctype
test_series = "KOOHBEW.###"
self.dns.naming_series_options = self.dns.get_options() + "\n" + test_series
self.dns.update_series()
self.assertIn(test_series, frappe.get_meta("Webhook").get_naming_series_options())
self.assertIn(test_series, frappe.get_meta(self.ns_doctype).get_naming_series_options())
def test_update_series_counter(self):
for series in self.get_valid_serieses():

View file

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

View file

@ -70,7 +70,7 @@ class File(Document):
else:
self.save_file(content=self.get_content())
self.flags.new_file = True
frappe.local.rollback_observers.append(self)
frappe.db.after_rollback.add(self.on_rollback)
def after_insert(self):
if not self.is_folder:
@ -122,10 +122,16 @@ class File(Document):
self.add_comment_in_reference_doc("Attachment Removed", _("Removed {0}").format(self.file_name))
def on_rollback(self):
rollback_flags = ("new_file", "original_content", "original_path")
def pop_rollback_flags():
for flag in rollback_flags:
self.flags.pop(flag, None)
# following condition is only executed when an insert has been rolledback
if self.flags.new_file:
self._delete_file_on_disk()
self.flags.pop("new_file")
pop_rollback_flags()
return
# if original_content flag is set, this rollback should revert the file to its original state
@ -140,14 +146,14 @@ class File(Document):
with open(file_path, mode) as f:
f.write(self.flags.original_content)
os.fsync(f.fileno())
self.flags.pop("original_content")
pop_rollback_flags()
# used in case file path (File.file_url) has been changed
if self.flags.original_path:
target = self.flags.original_path["old"]
source = self.flags.original_path["new"]
shutil.move(source, target)
self.flags.pop("original_path")
pop_rollback_flags()
def get_name_based_on_parent_folder(self) -> str | None:
if self.folder:
@ -219,7 +225,7 @@ class File(Document):
# Uses os.rename which is an atomic operation
shutil.move(source, target)
self.flags.original_path = {"old": source, "new": target}
frappe.local.rollback_observers.append(self)
frappe.db.after_rollback.add(self.on_rollback)
self.file_url = updated_file_url
update_existing_file_docs(self)
@ -521,7 +527,7 @@ class File(Document):
f.write(self._content)
os.fsync(f.fileno())
frappe.local.rollback_observers.append(self)
frappe.db.after_rollback.add(self.on_rollback)
return file_path

View file

@ -17,7 +17,7 @@ from frappe.core.api.file import (
move_file,
unzip_file,
)
from frappe.core.doctype.file.utils import get_extension
from frappe.core.doctype.file.utils import delete_file, get_extension
from frappe.exceptions import ValidationError
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_files_path
@ -77,6 +77,16 @@ class TestSimpleFile(FrappeTestCase):
self.assertEqual(content, self.test_content)
class TestFSRollbacks(FrappeTestCase):
def test_rollback_from_file_system(self):
file_name = content = frappe.generate_hash()
file = frappe.new_doc("File", file_name=file_name, content=content).insert()
self.assertTrue(file.exists_on_disk())
frappe.db.rollback()
self.assertFalse(file.exists_on_disk())
class TestBase64File(FrappeTestCase):
def setUp(self):
self.attached_to_doctype, self.attached_to_docname = make_test_doc()

View file

@ -4,5 +4,9 @@
frappe.ui.form.on("Patch Log", {
refresh: function (frm) {
frm.disable_save();
frm.add_custom_button(__("Re-Run Patch"), () => {
frm.call("rerun_patch");
});
},
});

View file

@ -22,6 +22,7 @@
"default": "0",
"fieldname": "skipped",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Skipped",
"read_only": 1
},
@ -36,7 +37,7 @@
"icon": "fa fa-cog",
"idx": 1,
"links": [],
"modified": "2023-05-10 19:27:10.883330",
"modified": "2023-06-07 00:00:01.369265",
"modified_by": "Administrator",
"module": "Core",
"name": "Patch Log",

View file

@ -4,11 +4,20 @@
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.model.document import Document
class PatchLog(Document):
pass
@frappe.whitelist()
def rerun_patch(self):
from frappe.modules.patch_handler import run_single
if not frappe.conf.developer_mode:
frappe.throw(_("Re-running patch is only allowed in developer mode."))
run_single(self.patch, force=True)
frappe.msgprint(_("Successfully re-ran patch: {0}").format(self.patch), alert=True)
def before_migrate():

View file

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

View file

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

View file

@ -11,7 +11,7 @@ import frappe
from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs, stop_job
from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import cstr, execute_in_shell
from frappe.utils.background_jobs import is_job_enqueued
from frappe.utils.background_jobs import get_job_status, is_job_enqueued
class TestRQJob(FrappeTestCase):
@ -104,6 +104,26 @@ class TestRQJob(FrappeTestCase):
self.check_status(job, "finished")
self.assertFalse(is_job_enqueued(job_id))
@timeout(20)
def test_enqueue_after_commit(self):
job_id = frappe.generate_hash()
frappe.enqueue(self.BG_JOB, enqueue_after_commit=True, job_id=job_id)
self.assertIsNone(get_job_status(job_id))
frappe.db.commit()
self.assertIsNotNone(get_job_status(job_id))
job_id = frappe.generate_hash()
frappe.enqueue(self.BG_JOB, enqueue_after_commit=True, job_id=job_id)
self.assertIsNone(get_job_status(job_id))
frappe.db.rollback()
self.assertIsNone(get_job_status(job_id))
frappe.db.commit()
self.assertIsNone(get_job_status(job_id))
def test_func(fail=False, sleep=0):
if fail:

View file

@ -9,6 +9,7 @@
"script_type",
"reference_doctype",
"event_frequency",
"cron_format",
"doctype_event",
"api_method",
"allow_guest",
@ -99,7 +100,7 @@
"fieldtype": "Select",
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
"options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long\nCron"
},
{
"fieldname": "module",
@ -132,6 +133,12 @@
"fieldname": "rate_limit_seconds",
"fieldtype": "Int",
"label": "Time Window (Seconds)"
},
{
"depends_on": "eval:doc.event_frequency==='Cron'",
"fieldname": "cron_format",
"fieldtype": "Data",
"label": "Cron Format"
}
],
"index_web_pages_for_search": 1,
@ -141,7 +148,7 @@
"link_fieldname": "server_script"
}
],
"modified": "2023-05-16 11:03:58.282680",
"modified": "2023-05-27 16:33:16.595424",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",

View file

@ -19,7 +19,7 @@ class ServerScript(Document):
self.check_if_compilable_in_restricted_context()
def on_update(self):
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
self.sync_scheduler_events()
def on_trash(self):
@ -52,11 +52,16 @@ class ServerScript(Document):
def sync_scheduler_events(self):
"""Create or update Scheduled Job Type documents for Scheduler Event Server Scripts"""
if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event":
setup_scheduler_events(script_name=self.name, frequency=self.event_frequency)
cron_format = self.cron_format if self.event_frequency == "Cron" else None
setup_scheduler_events(
script_name=self.name, frequency=self.event_frequency, cron_format=cron_format
)
def clear_scheduled_events(self):
"""Deletes existing scheduled jobs by Server Script if self.event_frequency has changed"""
if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"):
"""Deletes existing scheduled jobs by Server Script if self.event_frequency or self.cron_format has changed"""
if self.script_type == "Scheduler Event" and (
self.has_value_changed("event_frequency") or self.has_value_changed("cron_format")
):
for scheduled_job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
@ -163,15 +168,15 @@ class ServerScript(Document):
out.append([key, score])
return out
items = frappe.cache().get_value("server_script_autocompletion_items")
items = frappe.cache.get_value("server_script_autocompletion_items")
if not items:
items = get_keys(get_safe_globals())
items = [{"value": d[0], "score": d[1]} for d in items]
frappe.cache().set_value("server_script_autocompletion_items", items)
frappe.cache.set_value("server_script_autocompletion_items", items)
return items
def setup_scheduler_events(script_name, frequency):
def setup_scheduler_events(script_name: str, frequency: str, cron_format: str | None = None):
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
Args:
@ -188,6 +193,7 @@ def setup_scheduler_events(script_name, frequency):
"method": method,
"frequency": frequency,
"server_script": script_name,
"cron_format": cron_format,
}
).insert()
@ -200,6 +206,7 @@ def setup_scheduler_events(script_name, frequency):
return
doc.frequency = frequency
doc.cron_format = cron_format
doc.save()
frappe.msgprint(_("Scheduled execution for script {0} has updated").format(script_name))

View file

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

View file

@ -3,6 +3,7 @@
import requests
import frappe
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.frappeclient import FrappeClient, FrappeException
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_site_url
@ -103,10 +104,10 @@ class TestServerScript(FrappeTestCase):
def tearDownClass(cls):
frappe.db.commit()
frappe.db.truncate("Server Script")
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
def setUp(self):
frappe.cache().delete_value("server_script_map")
frappe.cache.delete_value("server_script_map")
def test_doctype_event(self):
todo = frappe.get_doc(dict(doctype="ToDo", description="hello")).insert()
@ -283,3 +284,37 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
script1.delete()
script2.delete()
frappe.db.commit()
def test_server_script_scheduled(self):
scheduled_script = frappe.get_doc(
doctype="Server Script",
name="scheduled_script_wo_cron",
script_type="Scheduler Event",
script="""frappe.flags = {"test": True}""",
event_frequency="Hourly",
).insert()
cron_script = frappe.get_doc(
doctype="Server Script",
name="scheduled_script_w_cron",
script_type="Scheduler Event",
script="""frappe.flags = {"test": True}""",
event_frequency="Cron",
cron_format="0 0 1 1 *", # 1st january
).insert()
# Ensure that jobs remain in DB after migrate
sync_jobs()
self.assertTrue(frappe.db.exists("Scheduled Job Type", {"server_script": scheduled_script.name}))
cron_job_name = frappe.db.get_value("Scheduled Job Type", {"server_script": cron_script.name})
self.assertTrue(cron_job_name)
cron_job = frappe.get_doc("Scheduled Job Type", cron_job_name)
self.assertEqual(cron_job.next_execution.day, 1)
self.assertEqual(cron_job.next_execution.month, 1)
cron_script.cron_format = "0 0 2 1 *" # 2nd january
cron_script.save()
cron_job.reload()
self.assertEqual(cron_job.next_execution.day, 2)

View file

@ -72,6 +72,8 @@
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
"attach_view_link",
"welcome_email_template",
"reset_password_template",
"prepared_report_section",
"max_auto_email_report_per_user",
"system_updates_section",
@ -548,13 +550,25 @@
"default": "1",
"fieldname": "enable_telemetry",
"fieldtype": "Check",
"label": "Allow Sending Usage Data for Improving applications"
"label": "Allow Sending Usage Data for Improving Applications"
},
{
"fieldname": "welcome_email_template",
"fieldtype": "Link",
"label": "Welcome Email Template",
"options": "Email Template"
},
{
"fieldname": "reset_password_template",
"fieldtype": "Link",
"label": "Reset Password Template",
"options": "Email Template"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
"modified": "2023-04-23 11:14:59.302851",
"modified": "2023-05-25 13:02:54.808773",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",

View file

@ -64,8 +64,8 @@ class SystemSettings(Document):
def on_update(self):
self.set_defaults()
frappe.cache().delete_value("system_settings")
frappe.cache().delete_value("time_zone")
frappe.cache.delete_value("system_settings")
frappe.cache.delete_value("time_zone")
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()

View file

@ -89,5 +89,5 @@ def create_translations(translation_map, language):
def clear_user_translation_cache(lang):
frappe.cache().hdel(USER_TRANSLATION_KEY, lang)
frappe.cache().hdel(MERGED_TRANSLATION_KEY, lang)
frappe.cache.hdel(USER_TRANSLATION_KEY, lang)
frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang)

View file

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

View file

@ -92,6 +92,7 @@
"generate_keys",
"column_break_65",
"api_secret",
"onboarding_status",
"connections_tab"
],
"fields": [
@ -211,6 +212,7 @@
"read_only": 1
},
{
"allow_in_quick_entry": 1,
"fieldname": "role_profile_name",
"fieldtype": "Link",
"label": "Role Profile",
@ -691,6 +693,13 @@
"fieldname": "desk_settings_section",
"fieldtype": "Section Break",
"label": "Desk Settings"
},
{
"default": "{}",
"fieldname": "onboarding_status",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Onboarding Status"
}
],
"icon": "fa fa-user",
@ -753,7 +762,7 @@
"link_fieldname": "user"
}
],
"modified": "2022-09-19 16:05:46.485242",
"modified": "2023-06-05 17:26:04.127555",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@ -792,4 +801,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
}
}

View file

@ -60,8 +60,8 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
frappe.cache().delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("users_for_mentions")
frappe.cache.delete_key("enabled_users")
def validate(self):
# clear new password
@ -75,6 +75,7 @@ class User(Document):
self.validate_email_type(self.email)
self.validate_email_type(self.name)
self.add_system_manager_role()
self.check_roles_added()
self.set_system_user()
self.set_full_name()
self.check_enable_disable()
@ -142,10 +143,10 @@ class User(Document):
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
if self.has_value_changed("enabled"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("users_for_mentions")
frappe.cache.delete_key("enabled_users")
elif self.has_value_changed("allow_in_mentions") or self.has_value_changed("user_type"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache.delete_key("users_for_mentions")
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
@ -282,6 +283,10 @@ class User(Document):
self.email_new_password(new_password)
except frappe.OutgoingEmailError:
frappe.clear_last_message()
frappe.msgprint(
_("Please setup default outgoing Email Account from Settings > Email Account"), alert=True
)
# email server not set, don't send email
self.log_error("Unable to send new password notification")
@ -325,7 +330,16 @@ class User(Document):
return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "")
def password_reset_mail(self, link):
self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True)
reset_password_template = frappe.db.get_system_setting("reset_password_template")
self.send_login_mail(
_("Password Reset"),
"password_reset",
{"link": link},
now=True,
custom_template=reset_password_template,
)
def send_welcome_mail_to_user(self):
from frappe.utils import get_url
@ -342,6 +356,8 @@ class User(Document):
else:
subject = _("Complete Registration")
welcome_email_template = frappe.db.get_system_setting("welcome_email_template")
self.send_login_mail(
subject,
"new_user",
@ -349,9 +365,10 @@ class User(Document):
link=link,
site_url=get_url(),
),
custom_template=welcome_email_template,
)
def send_login_mail(self, subject, template, add_args, now=None):
def send_login_mail(self, subject, template, add_args, now=None, custom_template=None):
"""send mail with login details"""
from frappe.utils import get_url
from frappe.utils.user import get_user_fullname
@ -374,11 +391,19 @@ class User(Document):
frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None
)
if custom_template:
from frappe.email.doctype.email_template.email_template import get_email_template
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
template=template,
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
delayed=(not now) if now is not None else self.flags.delay_emails,
@ -437,9 +462,9 @@ class User(Document):
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
if self.get("allow_in_mentions"):
frappe.cache().delete_key("users_for_mentions")
frappe.cache.delete_key("users_for_mentions")
frappe.cache().delete_key("enabled_users")
frappe.cache.delete_key("enabled_users")
# delete user permissions
frappe.db.delete("User Permission", {"user": self.name})
@ -649,6 +674,21 @@ class User(Document):
if not self.time_zone:
self.time_zone = get_system_timezone()
def check_roles_added(self):
if self.user_type != "System User" or self.roles or not self.is_new():
return
frappe.msgprint(
_("Newly created user {0} has no roles enabled.").format(frappe.bold(self.name)),
title=_("No Roles Specified"),
indicator="orange",
primary_action={
"label": _("Add Roles"),
"client_action": "frappe.set_route",
"args": ["Form", self.doctype, self.name],
},
)
@frappe.whitelist()
def get_timezones():
@ -720,10 +760,10 @@ def update_password(
user_doc, redirect_url = reset_user_data(user)
# get redirect url from cache
redirect_to = frappe.cache().hget("redirect_after_login", user)
redirect_to = frappe.cache.hget("redirect_after_login", user)
if redirect_to:
redirect_url = redirect_to
frappe.cache().hdel("redirect_after_login", user)
frappe.cache.hdel("redirect_after_login", user)
frappe.local.login_manager.login_as(user)
@ -881,7 +921,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
user.add_roles(default_role)
if redirect_to:
frappe.cache().hset("redirect_after_login", user.name, redirect_to)
frappe.cache.hset("redirect_after_login", user.name, redirect_to)
if user.flags.email_sent:
return 1, _("Please check your email for verification")
@ -1194,4 +1234,4 @@ def get_enabled_users():
enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
return enabled_users
return frappe.cache().get_value("enabled_users", _get_enabled_users)
return frappe.cache.get_value("enabled_users", _get_enabled_users)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,15 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
from frappe.model.document import Document
class ViewLog(Document):
pass
@staticmethod
def clear_old_logs(days=180):
from frappe.query_builder import Interval
from frappe.query_builder.functions import Now
table = frappe.qb.DocType("View Log")
frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))

View file

@ -0,0 +1,95 @@
{
"creation": "2023-05-24 12:53:02.844582",
"dashboard_name": "",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"list_name": "List",
"modified": "2023-05-24 13:21:29.552864",
"modified_by": "Administrator",
"module": "Core",
"name": "User List Tour",
"new_document_form": 0,
"owner": "Administrator",
"page_name": "",
"page_route": "[\"List\",\"User\",\"List\"]",
"reference_doctype": "User",
"report_name": "",
"save_on_complete": 0,
"steps": [
{
"description": "List view shows all the documents for a particular DocType. Here you can see all the current enabled users in the system. ",
"element_selector": ".frappe-list",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Top Center",
"title": "Users List",
"ui_tour": 1
},
{
"description": "These are filters. You can use them to narrow down list of records.",
"element_selector": ".standard-filter-section.flex",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 1,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Bottom",
"title": "Filters",
"ui_tour": 1
},
{
"description": "When standard filters are not enough you can use advance filters. ",
"element_selector": ".filter-selector > .btn-group",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"ondemand_description": "Advance filters are applied on fields with different operators. \n<br>\nClick on \"Apply Filters\" to continue.",
"popover_element": 0,
"position": "Left",
"title": "Advanced Filters",
"ui_tour": 1
},
{
"description": "Let's create a new user.",
"element_selector": ".btn-primary.primary-action",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 1,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 1,
"offset_x": 0,
"offset_y": 0,
"parent_element_selector": "",
"popover_element": 0,
"position": "Bottom",
"title": "New User",
"ui_tour": 1
}
],
"title": "User List Tour",
"track_steps": 1,
"ui_tour": 1,
"view_name": "List",
"workspace_name": ""
}

View file

@ -52,12 +52,15 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user_perms = frappe.utils.user.UserPermissions(user)
user_perms.build_permissions()
can_read = user_perms.can_read # Does not include child tables
include_single_doctypes = filters.get("include_single_doctypes")
single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})]
out = []
for dt in can_read:
if txt.lower().replace("%", "") in dt.lower() and dt not in single_doctypes:
if txt.lower().replace("%", "") in dt.lower() and (
include_single_doctypes or dt not in single_doctypes
):
out.append([dt])
return out

View file

@ -1,13 +1,15 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Elements</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]",
"content": "[{\"id\":\"5nnLaQeoFa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"HXRmktXYHy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"id\":\"pYALX3MwBW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"id\":\"XC78DuYB65\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"id\":\"XPm50Ppq3J\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"id\":\"yoU6nWiT83\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"id\":\"5UgFESBY0N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Format Builder\",\"col\":3}},{\"id\":\"62hseENHbd\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tOCrOgLW1G\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Elements</b></span>\",\"col\":12}},{\"id\":\"BIHjudL0T_\",\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"id\":\"cJ6CVsa8qW\",\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"id\":\"MmEJpjEdGR\",\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"id\":\"2ZdtgxQZqq\",\"type\":\"card\",\"data\":{\"card_name\":\"Customization\",\"col\":4}},{\"id\":\"NPFolijIcb\",\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"id\":\"iK3JQ9RXJE\",\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"id\":\"TiO9FCUUeC\",\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
"is_hidden": 0,
"label": "Build",
"links": [
{
@ -153,57 +155,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Views",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Report",
"link_count": 0,
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
"link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
"link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
"link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -271,20 +222,144 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Views",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Report",
"link_count": 0,
"link_to": "Report",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Print Format",
"link_count": 0,
"link_to": "Print Format",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Workspace",
"link_count": 0,
"link_to": "Workspace",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
"link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
"only_for": "",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart",
"link_count": 0,
"link_to": "Dashboard Chart",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customization",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
"link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
"link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translation",
"link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Navbar Settings",
"link_count": 0,
"link_to": "Navbar Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-09-11 06:41:31.095300",
"modified": "2023-05-24 14:47:24.395259",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 5.0,
"sequence_id": 16.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "List",
"label": "Print Format Builder",
"link_to": "print-format-builder",
"type": "Page"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Client Script",
"link_to": "Client Script",
"type": "DocType"
},
{
"doc_view": "",
"label": "DocType",
@ -293,8 +368,15 @@
},
{
"doc_view": "",
"label": "Workspace",
"link_to": "Workspace",
"label": "Customize Form",
"link_to": "Customize Form",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Server Script",
"link_to": "Server Script",
"type": "DocType"
},
{

View file

@ -1,13 +1,15 @@
{
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
"content": "[{\"id\":\"bc3WecV0uU\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Settings</b></span>\",\"col\":12}},{\"id\":\"_6Jxax2I11\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"rbf1Om8zJG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"xMytWpIImZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"id\":\"Q9DPlmrPpX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oVwctUh0gf\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"hC0b24aSJG\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"JA_iI4Z0yI\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"F1GxSqFKy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"vugObM_K_T\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"XwKthiuAAW\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"EQY7Sfmdxn\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
"creation": "2020-03-02 15:09:40.527211",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
"is_hidden": 0,
"label": "Settings",
"links": [
{
@ -345,17 +347,18 @@
"type": "Link"
}
],
"modified": "2022-08-28 21:41:28.065190",
"modified": "2023-05-24 14:58:44.010999",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 29.0,
"sequence_id": 18.0,
"shortcuts": [
{
"icon": "setting",

View file

@ -2,12 +2,14 @@
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]",
"creation": "2020-03-02 15:12:16.754449",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
"is_hidden": 0,
"label": "Users",
"links": [
{
@ -145,16 +147,18 @@
"type": "Link"
}
],
"modified": "2022-01-13 17:49:08.912772",
"modified": "2023-05-24 14:47:23.619182",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 27.0,
"sequence_id": 13.0,
"shortcuts": [
{
"label": "User",

View file

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

View file

@ -35,7 +35,7 @@ class CustomizeForm(Document):
if not self.doc_type:
return
meta = frappe.get_meta(self.doc_type)
meta = frappe.get_meta(self.doc_type, cached=False)
self.validate_doctype(meta)
@ -172,7 +172,18 @@ class CustomizeForm(Document):
check_email_append_to(self)
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
try:
frappe.db.updatedb(self.doc_type)
except Exception as e:
if frappe.db.is_db_table_size_limit(e):
frappe.throw(
_("You have hit the row size limit on database table: {0}").format(
"<a href='https://docs.erpnext.com/docs/v14/user/manual/en/customize-erpnext/articles/maximum-number-of-fields-in-a-form'>"
"Maximum Number of Fields in a Form</a>"
),
title=_("Database Table Row Size Limit"),
)
raise
if not hasattr(self, "hide_success") or not self.hide_success:
frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True)
@ -181,7 +192,9 @@ class CustomizeForm(Document):
if self.flags.rebuild_doctype_for_global_search:
frappe.enqueue(
"frappe.utils.global_search.rebuild_for_doctype", now=True, doctype=self.doc_type
"frappe.utils.global_search.rebuild_for_doctype",
doctype=self.doc_type,
enqueue_after_commit=True,
)
def set_property_setters(self):
@ -201,11 +214,39 @@ class CustomizeForm(Document):
# action and links
self.set_property_setters_for_actions_and_links(meta)
def set_property_setter_for_field_order(self, meta):
new_order = [df.fieldname for df in self.fields]
existing_order = getattr(meta, "field_order", None)
default_order = [
fieldname for fieldname, df in meta._fields.items() if not getattr(df, "is_custom_field", False)
]
if new_order == default_order:
if existing_order:
delete_property_setter(self.doc_type, "field_order")
return
if existing_order and new_order == json.loads(existing_order):
return
frappe.make_property_setter(
{
"doctype": self.doc_type,
"doctype_or_field": "DocType",
"property": "field_order",
"value": json.dumps(new_order),
},
is_system_generated=False,
)
def set_property_setters_for_doctype(self, meta):
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)
self.set_property_setter_for_field_order(meta)
def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""):
@ -527,6 +568,24 @@ class CustomizeForm(Document):
reset_customization(self.doc_type)
self.fetch_to_customize()
@frappe.whitelist()
def reset_layout(self):
if not self.doc_type:
return
property_setter = frappe.db.get_value(
"Property Setter",
filters={
"doc_type": self.doc_type,
"property": "field_order",
"is_system_generated": False,
},
)
if property_setter:
frappe.delete_doc("Property Setter", property_setter)
self.fetch_to_customize()
@classmethod
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
"""allow type change, if both old_type and new_type are in same field group.

View file

@ -425,3 +425,15 @@ class TestCustomizeForm(FrappeTestCase):
self.assertEqual(
frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
)
def test_custom_field_order(self):
# shuffle fields
customize_form = self.get_customize_form(doctype="ToDo")
customize_form.fields.insert(0, customize_form.fields.pop())
customize_form.save_customization()
field_order_property = json.loads(
frappe.db.get_value("Property Setter", {"doc_type": "ToDo", "property": "field_order"}, "value")
)
self.assertEqual(field_order_property, [df.fieldname for df in frappe.get_meta("ToDo").fields])

View file

@ -1,171 +0,0 @@
{
"charts": [],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
"creation": "2020-03-02 15:15:03.839594",
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "customization",
"idx": 0,
"label": "Customization",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Dashboards",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard",
"link_count": 0,
"link_to": "Dashboard",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart",
"link_count": 0,
"link_to": "Dashboard Chart",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Dashboard Chart Source",
"link_count": 0,
"link_to": "Dashboard Chart Source",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Form Customization",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Customize Form",
"link_count": 0,
"link_to": "Customize Form",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Custom Field",
"link_count": 0,
"link_to": "Custom Field",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Client Script",
"link_count": 0,
"link_to": "Client Script",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "DocType",
"link_count": 0,
"link_to": "DocType",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Other",
"link_count": 2,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Custom Translations",
"link_count": 0,
"link_to": "Translation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Navbar Settings",
"link_count": 0,
"link_to": "Navbar Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2022-08-28 20:56:24.980719",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customization",
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 8.0,
"shortcuts": [
{
"label": "Customize Form",
"link_to": "Customize Form",
"type": "DocType"
},
{
"label": "Custom Role",
"link_to": "Custom Role",
"type": "DocType"
},
{
"label": "Client Script",
"link_to": "Client Script",
"type": "DocType"
},
{
"doc_view": "",
"label": "Server Script",
"link_to": "Server Script",
"type": "DocType"
}
],
"title": "Customization"
}

View file

@ -29,8 +29,8 @@ from frappe.database.utils import (
is_query_type,
)
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
from frappe.model.utils.link_count import flush_local_link_count
from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager
from frappe.utils import cast as cast_fieldtype
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
from frappe.utils.deprecations import deprecated, deprecation_warning
@ -105,6 +105,14 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
self.logger = frappe.logger("database")
self.logger.setLevel("WARNING")
self.before_commit = CallbackManager()
self.after_commit = CallbackManager()
self.before_rollback = CallbackManager()
self.after_rollback = CallbackManager()
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@ -116,13 +124,12 @@ class Database:
self.cur_db_name = self.user
self._conn = self.get_connection()
self._cursor = self._conn.cursor()
frappe.local.rollback_observers = []
try:
if execution_timeout := get_query_execution_timeout():
self.set_execution_timeout(execution_timeout)
except Exception as e:
frappe.logger("database").warning(f"Couldn't set execution timeout {e}")
self.logger.warning(f"Couldn't set execution timeout {e}")
def set_execution_timeout(self, seconds: int):
"""Set session speicifc timeout on exeuction of statements.
@ -285,11 +292,17 @@ class Database:
return self.convert_to_lists(self.last_result)
return self.last_result
def _log_query(self, mogrified_query: str, debug: bool = False, explain: bool = False) -> None:
def _log_query(
self,
mogrified_query: str,
debug: bool = False,
explain: bool = False,
unmogrified_query: str = "",
) -> None:
"""Takes the query and logs it to various interfaces according to the settings."""
_query = None
if frappe.conf.allow_tests and frappe.cache().get_value("flag_print_sql"):
if frappe.conf.allow_tests and frappe.cache.get_value("flag_print_sql"):
_query = _query or str(mogrified_query)
print(_query)
@ -303,6 +316,12 @@ class Database:
_query = _query or str(mogrified_query)
frappe.log(f"<<<< query\n{_query}\n>>>>")
if unmogrified_query and is_query_type(
unmogrified_query, ("alter", "drop", "create", "truncate", "rename")
):
_query = _query or str(mogrified_query)
self.logger.warning("DDL Query made to DB:\n" + _query)
if frappe.flags.in_migrate:
_query = _query or str(mogrified_query)
self.log_touched_tables(_query)
@ -314,7 +333,7 @@ class Database:
# like cursor._transformed_statement from the cursor object. We can also avoid setting
# mogrified_query if we don't need to log it.
mogrified_query = self.lazy_mogrify(query, values)
self._log_query(mogrified_query, debug, explain)
self._log_query(mogrified_query, debug, explain, unmogrified_query=query)
return mogrified_query
def mogrify(self, query: Query, values: QueryValues):
@ -400,7 +419,7 @@ class Database:
@staticmethod
def clear_db_table_cache(query):
if query and is_query_type(query, ("drop", "create")):
frappe.cache().delete_key("db_tables")
frappe.cache.delete_key("db_tables")
def get_description(self):
"""Returns result metadata."""
@ -617,10 +636,10 @@ class Database:
return []
if as_dict:
return values and [values] or []
return [values] if values else []
if isinstance(fields, list):
return [map(values.get, fields)]
return [list(map(values.get, fields))]
else:
r = frappe.qb.get_query(
@ -812,6 +831,7 @@ class Database:
fields=fields,
distinct=distinct,
limit=limit,
validate_filters=True,
)
if isinstance(fields, str) and fields == "*":
as_dict = True
@ -840,6 +860,7 @@ class Database:
order_by=order_by,
distinct=distinct,
limit=limit,
validate_filters=True,
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
return {}
@ -889,15 +910,18 @@ class Database:
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
)
query = frappe.qb.get_query(table=dt, filters=dn, update=True)
query = frappe.qb.get_query(
table=dt,
filters=dn,
update=True,
validate_filters=True,
)
if isinstance(dn, str):
frappe.clear_document_cache(dt, dn)
else:
# TODO: Fix this; doesn't work rn - gavin@frappe.io
# frappe.cache().hdel_keys(dt, "document_cache")
# Workaround: clear all document caches
frappe.cache().delete_value("document_cache")
# No way to guess which documents are modified, clear all of them
frappe.clear_document_cache(dt)
for column, value in to_update.items():
query = query.set(column, value)
@ -949,26 +973,30 @@ class Database:
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
for method in frappe.local.before_commit:
frappe.call(method[0], *(method[1] or []), **(method[2] or {}))
self.before_rollback.reset()
self.after_rollback.reset()
self.before_commit.run()
self.sql("commit")
self.begin() # explicitly start a new transaction
frappe.local.rollback_observers = []
self.flush_realtime_log()
enqueue_jobs_after_commit()
flush_local_link_count()
self.after_commit.run()
def add_before_commit(self, method, args=None, kwargs=None):
frappe.local.before_commit.append([method, args, kwargs])
def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point:
self.sql(f"rollback to savepoint {save_point}")
else:
self.before_commit.reset()
self.after_commit.reset()
@staticmethod
def flush_realtime_log():
for args in frappe.local.realtime_log:
frappe.realtime.emit_via_redis(*args)
self.before_rollback.run()
frappe.local.realtime_log = []
self.sql("rollback")
self.begin()
self.after_rollback.run()
def savepoint(self, save_point):
"""Savepoints work as a nested transaction.
@ -983,21 +1011,6 @@ class Database:
def release_savepoint(self, save_point):
self.sql(f"release savepoint {save_point}")
def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point:
self.sql(f"rollback to savepoint {save_point}")
else:
self.sql("rollback")
self.begin()
for obj in dict.fromkeys(frappe.local.rollback_observers):
if hasattr(obj, "on_rollback"):
obj.on_rollback()
frappe.local.rollback_observers = []
frappe.local.realtime_log = []
frappe.flags.enqueue_after_commit = []
def field_exists(self, dt, fn):
"""Return true of field exists."""
return self.exists("DocField", {"fieldname": fn, "parent": dt})
@ -1054,14 +1067,18 @@ class Database:
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
cache_count = frappe.cache.get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
debug=debug
)[0][0]
count = frappe.qb.get_query(
table=dt,
filters=filters,
fields=Count("*"),
distinct=distinct,
validate_filters=True,
).run(debug=debug)[0][0]
if not filters and cache:
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
frappe.cache.set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@staticmethod
@ -1092,7 +1109,7 @@ class Database:
def get_db_table_columns(self, table) -> list[str]:
"""Returns list of column names from given table."""
columns = frappe.cache().hget("table_columns", table)
columns = frappe.cache.hget("table_columns", table)
if columns is None:
information_schema = frappe.qb.Schema("information_schema")
@ -1104,7 +1121,7 @@ class Database:
)
if columns:
frappe.cache().hset("table_columns", table, columns)
frappe.cache.hset("table_columns", table, columns)
return columns
@ -1119,21 +1136,6 @@ class Database:
"""Returns True if column exists in database."""
return column in self.get_table_columns(doctype)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.column_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def has_index(self, table_name, index_name):
raise NotImplementedError
@ -1194,7 +1196,12 @@ class Database:
Doctype name can be passed directly, it will be pre-pended with `tab`.
"""
filters = filters or kwargs.get("conditions")
query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
query = frappe.qb.get_query(
table=doctype,
filters=filters,
delete=True,
validate_filters=True,
)
if "debug" not in kwargs:
kwargs["debug"] = debug
return query.run(**kwargs)
@ -1284,27 +1291,9 @@ class Database:
return get_next_val(*args, **kwargs)
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import (
RQ_JOB_FAILURE_TTL,
RQ_RESULTS_TTL,
execute_job,
get_queue,
)
if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
for job in frappe.flags.enqueue_after_commit:
q = get_queue(job.get("queue"), is_async=job.get("is_async"))
q.enqueue_call(
execute_job,
timeout=job.get("timeout"),
kwargs=job.get("queue_args"),
failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
job_id=job.get("job_id"),
)
frappe.flags.enqueue_after_commit = []
def get_row_size(self, doctype: str) -> int:
"""Get estimated max row size of any table in bytes."""
raise NotImplementedError
@contextmanager

View file

@ -76,6 +76,10 @@ class MariaDBExceptionUtil:
def is_data_too_long(e: pymysql.Error) -> bool:
return e.args[0] == ER.DATA_TOO_LONG
@staticmethod
def is_db_table_size_limit(e: pymysql.Error) -> bool:
return e.args[0] == ER.TOO_BIG_ROWSIZE
@staticmethod
def is_primary_key_violation(e: pymysql.Error) -> bool:
return (
@ -145,6 +149,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
UnicodeWithAttrs: escape_string,
}
default_port = "3306"
MAX_ROW_SIZE_LIMIT = 65_535 # bytes
def setup_type_map(self):
self.db_type = "mariadb"
@ -200,8 +205,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
return db_size[0].get("database_size")
def log_query(self, query, values, debug, explain):
self.last_query = query = self._cursor._executed
self._log_query(query, debug, explain)
self.last_query = self._cursor._executed
self._log_query(self.last_query, debug, explain, query)
return self.last_query
@staticmethod
@ -318,6 +323,21 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
as_dict=1,
)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.column_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def has_index(self, table_name, index_name):
return self.sql(
"""SHOW INDEX FROM `{table_name}`
@ -415,7 +435,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
to_query = not cached
if cached:
tables = frappe.cache().get_value("db_tables")
tables = frappe.cache.get_value("db_tables")
to_query = not tables
if to_query:
@ -427,6 +447,59 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
.where(information_schema.tables.table_schema != "information_schema")
.run(pluck=True)
)
frappe.cache().set_value("db_tables", tables)
frappe.cache.set_value("db_tables", tables)
return tables
def get_row_size(self, doctype: str) -> int:
"""Get estimated max row size of any table in bytes."""
# Query reused from this answer: https://dba.stackexchange.com/a/313889/274503
# Modification: get values for particular table instead of full summary.
# Reference: https://mariadb.com/kb/en/data-type-storage-requirements/
est_row_size = frappe.db.sql(
"""
SELECT SUM(col_sizes.col_size) AS EST_MAX_ROW_SIZE
FROM (
SELECT
cols.COLUMN_NAME,
CASE cols.DATA_TYPE
WHEN 'tinyint' THEN 1
WHEN 'smallint' THEN 2
WHEN 'mediumint' THEN 3
WHEN 'int' THEN 4
WHEN 'bigint' THEN 8
WHEN 'float' THEN IF(cols.NUMERIC_PRECISION > 24, 8, 4)
WHEN 'double' THEN 8
WHEN 'decimal' THEN ((cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE) DIV 9)*4 + (cols.NUMERIC_SCALE DIV 9)*4 + CEIL(MOD(cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE,9)/2) + CEIL(MOD(cols.NUMERIC_SCALE,9)/2)
WHEN 'bit' THEN (cols.NUMERIC_PRECISION + 7) DIV 8
WHEN 'year' THEN 1
WHEN 'date' THEN 3
WHEN 'time' THEN 3 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'datetime' THEN 5 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'timestamp' THEN 4 + CEIL(cols.DATETIME_PRECISION /2)
WHEN 'char' THEN cols.CHARACTER_OCTET_LENGTH
WHEN 'binary' THEN cols.CHARACTER_OCTET_LENGTH
WHEN 'varchar' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
WHEN 'varbinary' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
WHEN 'tinyblob' THEN 9
WHEN 'tinytext' THEN 9
WHEN 'blob' THEN 10
WHEN 'text' THEN 10
WHEN 'mediumblob' THEN 11
WHEN 'mediumtext' THEN 11
WHEN 'longblob' THEN 12
WHEN 'longtext' THEN 12
WHEN 'enum' THEN 2
WHEN 'set' THEN 8
ELSE 0
END AS col_size
FROM INFORMATION_SCHEMA.COLUMNS cols
WHERE cols.TABLE_NAME = %s
) AS col_sizes;""",
(get_table_name(doctype),),
)
if est_row_size:
return int(est_row_size[0][0])

View file

@ -107,6 +107,10 @@ class PostgresExceptionUtil:
def is_data_too_long(e):
return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION
@staticmethod
def is_db_table_size_limit(e) -> bool:
return False
class PostgresDatabase(PostgresExceptionUtil, Database):
REGEX_CHARACTER = "~"
@ -394,6 +398,21 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
as_dict=1,
)
def get_column_type(self, doctype, column):
"""Returns column type from database."""
information_schema = frappe.qb.Schema("information_schema")
table = get_table_name(doctype)
return (
frappe.qb.from_(information_schema.columns)
.select(information_schema.columns.data_type)
.where(
(information_schema.columns.table_name == table)
& (information_schema.columns.column_name == column)
)
.run(pluck=True)[0]
)
def get_database_list(self):
return self.sql("SELECT datname FROM pg_database", pluck=True)

View file

@ -1,4 +1,3 @@
import itertools
import re
from ast import literal_eval
from types import BuiltinFunctionType
@ -10,6 +9,7 @@ from pypika.queries import QueryBuilder, Table
import frappe
from frappe import _
from frappe.database.operator_map import OPERATOR_MAP
from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.database.utils import DefaultOrderBy, get_doctype_name
from frappe.query_builder import Criterion, Field, Order, functions
from frappe.query_builder.functions import Function, SqlFunctions
@ -25,6 +25,10 @@ BRACKETS_PATTERN = re.compile(r"\(.*?\)|$")
SQL_FUNCTIONS = [sql_function.value for sql_function in SqlFunctions]
COMMA_PATTERN = re.compile(r",\s*(?![^()]*\))")
# less restrictive version of frappe.core.doctype.doctype.doctype.START_WITH_LETTERS_PATTERN
# to allow table names like __Auth
TABLE_NAME_PATTERN = re.compile(r"^[\w -]*$", flags=re.ASCII)
class Engine:
def get_query(
@ -41,15 +45,19 @@ class Engine:
update: bool = False,
into: bool = False,
delete: bool = False,
*,
validate_filters: bool = False,
) -> QueryBuilder:
self.is_mariadb = frappe.db.db_type == "mariadb"
self.is_postgres = frappe.db.db_type == "postgres"
self.validate_filters = validate_filters
if isinstance(table, Table):
self.table = table
self.doctype = get_doctype_name(table.get_sql())
else:
self.doctype = table
self.validate_doctype()
self.table = frappe.qb.DocType(table)
if update:
@ -82,6 +90,10 @@ class Engine:
return self.query
def validate_doctype(self):
if not TABLE_NAME_PATTERN.match(self.doctype):
frappe.throw(_("Invalid DocType: {0}").format(self.doctype))
def apply_fields(self, fields):
# add fields
self.fields = self.parse_fields(fields)
@ -135,9 +147,12 @@ class Engine:
self._apply_filter(field, value, operator, doctype)
def apply_dict_filters(self, filters: dict[str, str | int | list]):
for key in filters:
value = filters.get(key)
self._apply_filter(key, value)
for field, value in filters.items():
operator = "="
if isinstance(value, (list, tuple)):
operator, value = value
self._apply_filter(field, value, operator)
def _apply_filter(
self, field: str, value: str | int | list | None, operator: str = "=", doctype: str | None = None
@ -146,14 +161,16 @@ class Engine:
_value = value
_operator = operator
if isinstance(_field, Field):
if not isinstance(_field, str):
pass
elif dynamic_field := DynamicTableField.parse(field, self.doctype):
elif not self.validate_filters and (
dynamic_field := DynamicTableField.parse(field, self.doctype)
):
# apply implicit join if link field's field is referenced
self.query = dynamic_field.apply_join(self.query)
_field = dynamic_field.field
elif has_function(field):
_field = self.get_function_object(field)
elif self.validate_filters and SPECIAL_CHAR_PATTERN.search(_field):
frappe.throw(_("Invalid filter: {0}").format(_field))
elif not doctype or doctype == self.doctype:
_field = self.table[field]
elif doctype:
@ -168,30 +185,28 @@ class Engine:
(table.parent == self.table.name) & (table.parenttype == self.doctype)
)
if isinstance(_value, (list, tuple)):
_operator, _value = _value
elif isinstance(_value, bool):
if isinstance(_value, bool):
_value = int(_value)
if isinstance(_value, str) and has_function(_value):
_value = self.get_function_object(_value)
if isinstance(_value, (list, tuple)) and not _value:
elif not _value and isinstance(_value, (list, tuple)):
_value = ("",)
# Nested set
if _operator in OPERATOR_MAP["nested_set"]:
hierarchy = _operator
docname = _value
result = get_nested_set_hierarchy_result(self.doctype, docname, hierarchy)
_df = frappe.get_meta(self.doctype).get_field(field)
ref_doctype = _df.options if _df else self.doctype
nodes = get_nested_set_hierarchy_result(ref_doctype, docname, hierarchy)
operator_fn = (
OPERATOR_MAP["not in"]
if hierarchy in ("not ancestors of", "not descendants of")
else OPERATOR_MAP["in"]
)
if result:
result = list(itertools.chain.from_iterable(result))
self.query = self.query.where(operator_fn(_field, result))
if nodes:
self.query = self.query.where(operator_fn(_field, nodes))
else:
self.query = self.query.where(operator_fn(_field, ("",)))
return
@ -506,22 +521,25 @@ def has_function(field):
return True
def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str) -> list[str]:
"""Get matching nodes based on operator."""
table = frappe.qb.DocType(doctype)
try:
lft, rgt = frappe.qb.from_(table).select("lft", "rgt").where(table.name == name).run()[0]
except IndexError:
lft, rgt = None, None
if hierarchy in ("descendants of", "not descendants of"):
if hierarchy in ("descendants of", "not descendants of", "descendants of (inclusive)"):
result = (
frappe.qb.from_(table)
.select(table.name)
.where(table.lft > lft)
.where(table.rgt < rgt)
.orderby(table.lft, order=Order.asc)
.run()
.run(pluck=True)
)
if hierarchy == "descendants of (inclusive)":
result += [name]
else:
# Get ancestor elements of a DocType with a tree structure
result = (
@ -530,6 +548,6 @@ def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
.where(table.lft < lft)
.where(table.rgt > rgt)
.orderby(table.lft, order=Order.desc)
.run()
.run(pluck=True)
)
return result

View file

@ -40,7 +40,7 @@ class DBTable:
if self.is_new():
self.create()
else:
frappe.cache().hdel("table_columns", self.table_name)
frappe.cache.hdel("table_columns", self.table_name)
self.alter()
def create(self):
@ -205,7 +205,6 @@ class DbColumn:
self.default
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
and column_def not in ("text", "longtext")
):
column_def += f" default {frappe.db.escape(self.default)}"
@ -248,7 +247,6 @@ class DbColumn:
self.default_changed(current_def)
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
and not (column_type in ["text", "longtext"])
):
self.table.set_default.append(self)

View file

@ -23,6 +23,7 @@ NestedSetHierarchy = (
"descendants of",
"not ancestors of",
"not descendants of",
"descendants of (inclusive)",
)

View file

@ -230,7 +230,7 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
def get_defaults_for(parent="__default"):
"""get all defaults"""
defaults = frappe.cache().hget("defaults", parent)
defaults = frappe.cache.hget("defaults", parent)
if defaults is None:
# sort descending because first default must get precedence
@ -256,7 +256,7 @@ def get_defaults_for(parent="__default"):
elif d.defvalue is not None:
defaults[d.defkey] = d.defvalue
frappe.cache().hset("defaults", parent, defaults)
frappe.cache.hset("defaults", parent, defaults)
return defaults

View file

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

View file

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

View file

@ -3,47 +3,21 @@
frappe.ui.form.on("Custom HTML Block", {
refresh(frm) {
render_custom_html_block(frm);
if (
!has_common(frappe.user_roles, [
"Administrator",
"System Manager",
"Workspace Manager",
])
) {
frm.set_value("private", true);
} else {
frm.set_df_property("private", "read_only", false);
}
let wrapper = frm.fields_dict["preview"].wrapper;
wrapper.classList.add("mb-3");
frappe.create_shadow_element(wrapper, frm.doc.html, frm.doc.style, frm.doc.script);
},
});
function render_custom_html_block(frm) {
let wrapper = frm.fields_dict["preview"].wrapper;
wrapper.classList.add("mb-3");
let random_id = "custom-block-" + frappe.utils.get_random(5).toLowerCase();
class CustomBlock extends HTMLElement {
constructor() {
super();
// html
let div = document.createElement("div");
div.innerHTML = frappe.dom.remove_script_and_style(frm.doc.html);
// css
let style = document.createElement("style");
style.textContent = frm.doc.style;
// javascript
let script = document.createElement("script");
script.textContent = `
(function() {
let cname = ${JSON.stringify(random_id)};
let root_element = document.querySelector(cname).shadowRoot;
${frm.doc.script}
})();
`;
this.attachShadow({ mode: "open" });
this.shadowRoot?.appendChild(div);
this.shadowRoot?.appendChild(style);
this.shadowRoot?.appendChild(script);
}
}
if (!customElements.get(random_id)) {
customElements.define(random_id, CustomBlock);
}
wrapper.innerHTML = `<${random_id}></${random_id}>`;
}

View file

@ -6,10 +6,10 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"private",
"preview_section",
"preview",
"html_section",
"html_message",
"html",
"javascript_section",
"js_message",
@ -71,12 +71,6 @@
"label": "JS Message",
"options": "<p>To interact with above HTML you will have to use `root_element` as a parent selector.</p><p>For example:</p><pre class=\"p-3 bg-gray-100 border-radius rounded-sm mb-0\" style=\"width: fit-content;\"><code>// here root_element is provided by default\nlet some_class_element = root_element.querySelector('.some-class');\nsome_class_element.textContent = \"New content\";\n</code></pre>"
},
{
"fieldname": "html_message",
"fieldtype": "HTML",
"label": "HTML Message",
"options": "<p>You cannot use global class on elements. The css for those classes will not be applied on this HTML, you will have to rewrite styles again in CSS field</p><p>For Example:</p>\n<pre class=\"p-3 bg-gray-100 border-radius rounded-sm mb-0\" style=\"width: fit-content;\"><code>// style for class m-3 will not work\n<br>&lt;div class=\"m-3\"&gt;&lt;/div&gt;<br>\n<br>// You will have to add style of m-3 in CSS field below like\n<br>.m-3 {\n<br> margin: 14px!important\n<br>}\n</code></pre>"
},
{
"fieldname": "roles_section",
"fieldtype": "Section Break",
@ -87,11 +81,19 @@
"fieldtype": "Table",
"label": "Roles",
"options": "Has Role"
},
{
"default": "0",
"depends_on": "eval: doc.private || doc.__unsaved",
"fieldname": "private",
"fieldtype": "Check",
"label": "Private",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-05-17 17:17:04.232519",
"modified": "2023-05-30 14:33:31.994738",
"modified_by": "Administrator",
"module": "Desk",
"name": "Custom HTML Block",

View file

@ -1,9 +1,25 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt
# import frappe
import frappe
from frappe.model.document import Document
from frappe.query_builder.utils import DocType
class CustomHTMLBlock(Document):
pass
@frappe.whitelist()
def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filters):
# return logged in users private blocks and all public blocks
customHTMLBlock = DocType("Custom HTML Block")
condition_query = frappe.qb.get_query(customHTMLBlock)
return (
condition_query.select(customHTMLBlock.name).where(
(customHTMLBlock.private == 0)
| ((customHTMLBlock.owner == frappe.session.user) & (customHTMLBlock.private == 1))
)
).run()

View file

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

View file

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

View file

@ -2,36 +2,43 @@
// For license information, please see license.txt
frappe.ui.form.on("Form Tour", {
setup: function (frm) {
if (!frm.doc.is_standard || frappe.boot.developer_mode) {
frm.trigger("setup_queries");
}
},
refresh(frm) {
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
frm.trigger("disable_form");
}
frm.add_custom_button(__("Show Tour"), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
} else if (frm.doc.first_document) {
const name = await get_first_document(frm.doc.reference_doctype);
route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
}
route_changed.then(() => {
const tour_name = frm.doc.name;
cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
});
frm.set_query("reference_doctype", () => {
return { filters: { istable: 0 } };
});
frm.set_query("report_name", () => {
if (frm.doc.reference_doctype) {
return {
filters: {
ref_doctype: frm.doc.reference_doctype,
},
};
}
return {};
});
!frm.is_new() && add_custom_button(frm);
},
async report_name(frm) {
if (!frm.doc.ui_tour || !frm.doc.report_name) return;
let { message } = await frappe.db.get_value("Report", frm.doc.report_name, "ref_doctype");
frm.set_value("reference_doctype", message?.ref_doctype || "");
},
async before_save(frm) {
if (
frm.doc.select_view == "List" &&
frm.doc.list_name == "Dashboard" &&
frm.doc.dashboard_name &&
frm.doc.reference_doctype
) {
frappe.throw(
__("Referance Doctype and Dashboard Name both can't be used at the same time.")
);
}
frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm)));
},
disable_form: function (frm) {
frm.set_read_only();
frm.fields
@ -42,18 +49,6 @@ frappe.ui.form.on("Form Tour", {
frm.disable_save();
},
setup_queries(frm) {
frm.set_query("reference_doctype", function () {
return {
filters: {
istable: 0,
},
};
});
frm.trigger("reference_doctype");
},
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
@ -78,9 +73,65 @@ frappe.ui.form.on("Form Tour", {
[""].concat(options)
);
});
if (!frm.doc.ui_tour) {
// remove report name if reference doctype is changed and report name is not valid.
frappe.db
.get_list(
"Report",
{
filters: {
ref_doctype: frm.doc.reference_doctype,
},
},
{ fields: ["name"] }
)
.then((reports) => {
if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) {
frm.set_value("report_name", "");
frm.refresh_field("report_name");
}
});
}
},
});
let add_custom_button = (frm) => {
if (frm.doc.ui_tour) {
frm.add_custom_button(__("Reset"), function () {
frappe.confirm(
__("This will reset this tour and show it to all users. Are you sure?"),
function () {
frappe.call({
method: "frappe.desk.doctype.form_tour.form_tour.reset_tour",
args: {
tour_name: frm.doc.name,
},
});
},
delete frappe.boot.user.onboarding_status[frm.doc.name]
);
});
} else {
frm.add_custom_button(__("Show Tour"), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
let route_changed = null;
if (issingle) {
route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
} else if (frm.doc.first_document) {
const name = await get_first_document(frm.doc.reference_doctype);
route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
}
route_changed.then(() => {
const tour_name = frm.doc.name;
cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
});
});
}
};
frappe.ui.form.on("Form Tour Step", {
form_render(frm, cdt, cdn) {
if (locals[cdt][cdn].is_table_field) {
@ -115,6 +166,10 @@ async function check_if_single(doctype) {
const { message } = await frappe.db.get_value("DocType", doctype, "issingle");
return message.issingle || 0;
}
async function check_if_private_workspace(name) {
const { message } = await frappe.db.get_value("Workspace", name, "public");
return !message.public || 0;
}
async function get_first_document(doctype) {
let docname;
@ -125,3 +180,75 @@ async function get_first_document(doctype) {
return docname || "new";
}
async function get_path(frm) {
let route = [frm.doc.view_name];
switch (route[0]) {
case "Workspaces":
frm.doc.list_name = "";
frm.doc.new_document_form = 0;
frm.doc.report_name = "";
frm.doc.page_name = "";
frm.doc.dashboard_name = "";
frm.doc.reference_doctype = "";
if (!frm.doc.workspace_name) {
route.push("*");
return route;
}
if (await check_if_private_workspace(frm.doc.workspace_name)) {
route.push("private");
}
route.push(frm.doc.workspace_name);
return route;
case "List":
frm.doc.workspace_name = "";
frm.doc.new_document_form = 0;
frm.doc.list_name != "Report" && (frm.doc.report_name = "");
frm.doc.list_name != "Dashboard" && (frm.doc.dashboard_name = "");
frm.doc.page_name = "";
if (frm.doc.list_name == "File") return ["List", "File"];
if (!frm.doc.reference_doctype) {
if (frm.doc.list_name == "Dashboard")
return ["dashboard-view", frm.doc.dashboard_name || "*"];
route.push("*");
} else {
route.push(frm.doc.reference_doctype);
}
route.push(frm.doc.list_name);
return route;
case "Form":
frm.doc.workspace_name = "";
frm.doc.list_name = "";
frm.doc.report_name = "";
frm.doc.page_name = "";
frm.doc.dashboard_name = "";
if (!frm.doc.reference_doctype) {
route.push("*");
frm.doc.new_document_form && route.push("new-*");
return route;
}
route.push(frm.doc.reference_doctype);
if (await check_if_single(frm.doc.reference_doctype)) {
route.push(frm.doc.reference_doctype);
} else if (frm.doc.new_document_form) {
route.push("new-" + frappe.router.slug(frm.doc.reference_doctype));
}
return route;
case "Tree":
frm.doc.workspace_name = "";
frm.doc.list_name = "";
frm.doc.new_document_form = 0;
frm.doc.report_name = "";
frm.doc.page_name = "";
frm.doc.dashboard_name = "";
return route;
case "Page":
frm.doc.workspace_name = "";
frm.doc.list_name = "";
frm.doc.new_document_form = 0;
frm.doc.report_name = "";
frm.doc.dashboard_name = "";
frm.doc.reference_doctype = "";
return [frm.doc.page_name];
}
}

View file

@ -7,27 +7,38 @@
"engine": "InnoDB",
"field_order": [
"title",
"view_name",
"workspace_name",
"list_name",
"report_name",
"dashboard_name",
"new_document_form",
"page_name",
"reference_doctype",
"module",
"column_break_6",
"ui_tour",
"track_steps",
"is_standard",
"save_on_complete",
"first_document",
"include_name_field",
"page_route",
"section_break_3",
"steps"
],
"fields": [
{
"depends_on": "eval:(!doc.ui_tour || doc.ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document",
"options": "DocType",
"reqd": 1
"mandatory_depends_on": "eval:(!doc.ui_tour)",
"options": "DocType"
},
{
"depends_on": "reference_doctype",
"depends_on": "eval:(doc.ui_tour || doc.reference_doctype)",
"fieldname": "steps",
"fieldtype": "Table",
"label": "Steps",
@ -47,6 +58,7 @@
},
{
"default": "0",
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "save_on_complete",
"fieldtype": "Check",
"label": "Save on Completion"
@ -58,10 +70,10 @@
"label": "Is Standard"
},
{
"depends_on": "eval: doc.ui_tour && doc.is_standard",
"fetch_from": "reference_doctype.module",
"fieldname": "module",
"fieldtype": "Link",
"hidden": 1,
"label": "Module",
"options": "Module Def",
"read_only": 1
@ -72,21 +84,101 @@
},
{
"default": "0",
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "first_document",
"fieldtype": "Check",
"label": "Show First Document Tour"
},
{
"default": "0",
"depends_on": "eval:!doc.first_document",
"depends_on": "eval:(!doc.ui_tour && !doc.first_document)",
"fieldname": "include_name_field",
"fieldtype": "Check",
"label": "Include Name Field"
},
{
"default": "0",
"fieldname": "ui_tour",
"fieldtype": "Check",
"label": "UI Tour",
"set_only_once": 1
},
{
"fieldname": "page_route",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Page Route"
},
{
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Dashboard\")",
"fetch_from": ".",
"fieldname": "dashboard_name",
"fieldtype": "Link",
"label": "Select Dashboard",
"options": "Dashboard"
},
{
"depends_on": "ui_tour",
"fieldname": "view_name",
"fieldtype": "Select",
"label": "View",
"mandatory_depends_on": "ui_tour",
"options": "Workspaces\nList\nForm\nTree\nPage"
},
{
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"Workspaces\")",
"fetch_from": ".",
"fieldname": "workspace_name",
"fieldtype": "Link",
"label": "Select Workspace",
"options": "Workspace"
},
{
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")",
"fetch_from": ".",
"fieldname": "page_name",
"fieldtype": "Link",
"label": "Select Page",
"mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")",
"options": "Page"
},
{
"default": "List",
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")",
"fetch_from": ".",
"fieldname": "list_name",
"fieldtype": "Select",
"label": "Select List View",
"mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")",
"options": "List\nReport\nDashboard\nKanban\nGantt\nCalendar\nFile\nImage\nInbox\nMap"
},
{
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Report\")",
"fetch_from": ".",
"fieldname": "report_name",
"fieldtype": "Link",
"label": "Select Report",
"options": "Report"
},
{
"default": "0",
"depends_on": "ui_tour",
"description": "The next tour will start from where the user left off.",
"fieldname": "track_steps",
"fieldtype": "Check",
"label": "Track Steps"
},
{
"default": "0",
"depends_on": "eval: (doc.ui_tour && doc.view_name == \"Form\")",
"fieldname": "new_document_form",
"fieldtype": "Check",
"label": "New Document Form"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-11-24 12:03:45.449311",
"modified": "2023-05-25 11:30:44.396248",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
@ -104,9 +196,14 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "All"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View file

@ -1,27 +1,84 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
class FormTour(Document):
def before_save(self):
meta = frappe.get_meta(self.reference_doctype)
for step in self.steps:
if step.is_table_field and step.parent_fieldname:
parent_field_df = meta.get_field(step.parent_fieldname)
step.child_doctype = parent_field_df.options
field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
if self.is_standard and not self.module:
if self.workspace_name:
self.module = frappe.db.get_value("Workspace", self.workspace_name, "module")
elif self.dashboard_name:
dashboard_doctype = frappe.db.get_value("Dashboard", self.dashboard_name, "module")
self.module = frappe.db.get_value("DocType", dashboard_doctype, "module")
else:
field_df = meta.get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
self.module = "Desk"
if not self.ui_tour:
meta = frappe.get_meta(self.reference_doctype)
for step in self.steps:
if step.is_table_field and step.parent_fieldname:
parent_field_df = meta.get_field(step.parent_fieldname)
step.child_doctype = parent_field_df.options
field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
else:
field_df = meta.get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
def on_update(self):
frappe.cache.delete_key("bootinfo")
if frappe.conf.developer_mode and self.is_standard:
export_to_files([["Form Tour", self.name]], self.module)
def on_trash(self):
frappe.cache.delete_key("bootinfo")
@frappe.whitelist()
def reset_tour(tour_name):
for user in frappe.get_all("User", pluck="name"):
onboarding_status = frappe.parse_json(frappe.db.get_value("User", user, "onboarding_status"))
onboarding_status.pop(tour_name, None)
frappe.db.set_value(
"User", user, "onboarding_status", frappe.as_json(onboarding_status), update_modified=False
)
frappe.cache.hdel("bootinfo", user)
frappe.msgprint(_("Successfully reset onboarding status for all users."), alert=True)
@frappe.whitelist()
def update_user_status(value, step):
from frappe.utils.telemetry import capture
step = frappe.parse_json(step)
tour = frappe.parse_json(value)
capture(
frappe.scrub(f"{step.parent}_{step.title}"),
app="frappe_ui_tours",
properties={"is_completed": tour.is_completed},
)
frappe.db.set_value(
"User", frappe.session.user, "onboarding_status", value, update_modified=False
)
frappe.cache.hdel("bootinfo", frappe.session.user)
def get_onboarding_ui_tours():
if not frappe.get_system_settings("enable_onboarding"):
return []
ui_tours = frappe.get_all("Form Tour", filters={"ui_tour": 1}, fields=["page_route", "name"])
return [[tour.name, json.loads(tour.page_route)] for tour in ui_tours]

View file

@ -0,0 +1,13 @@
import json
import frappe
def execute():
"""Handle introduction of UI tours"""
completed = {}
for tour in frappe.get_all("Form Tour", {"ui_tour": 1}, pluck="name"):
completed[tour] = {"is_complete": True}
User = frappe.qb.DocType("User")
frappe.qb.update(User).set("onboarding_status", json.dumps(completed)).run()

View file

@ -2,20 +2,32 @@
"actions": [],
"creation": "2021-05-21 23:05:45.342114",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"ui_tour",
"is_table_field",
"section_break_2",
"title",
"parent_fieldname",
"fieldname",
"title",
"element_selector",
"parent_element_selector",
"description",
"ondemand_description",
"column_break_2",
"position",
"hide_buttons",
"popover_element",
"modal_trigger",
"offset_x",
"offset_y",
"next_on_click",
"label",
"fieldtype",
"has_next_condition",
"next_step_condition",
"next_form_tour",
"section_break_13",
"child_doctype"
],
@ -31,18 +43,20 @@
"columns": 4,
"fieldname": "description",
"fieldtype": "HTML Editor",
"ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Description",
"reqd": 1
},
{
"depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))",
"depends_on": "eval: (!doc.ui_tour && (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname)))",
"fieldname": "fieldname",
"fieldtype": "Select",
"label": "Fieldname",
"reqd": 1
"mandatory_depends_on": "eval: (!doc.ui_tour)"
},
{
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
@ -70,12 +84,14 @@
},
{
"default": "0",
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "has_next_condition",
"fieldtype": "Check",
"label": "Has Next Condition"
},
{
"default": "0",
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "fieldtype",
"fieldtype": "Data",
"label": "Fieldtype",
@ -83,6 +99,7 @@
},
{
"default": "0",
"depends_on": "eval:(!doc.ui_tour)",
"fieldname": "is_table_field",
"fieldtype": "Check",
"label": "Is Table Field"
@ -105,17 +122,102 @@
"read_only": 1
},
{
"depends_on": "is_table_field",
"depends_on": "eval: (!doc.ui_tour || doc.is_table_field)",
"fieldname": "parent_fieldname",
"fieldtype": "Select",
"label": "Parent Field",
"mandatory_depends_on": "is_table_field"
},
{
"default": "0",
"fetch_from": "next_form_tour.ui_tour",
"fieldname": "ui_tour",
"fieldtype": "Check",
"in_list_view": 1,
"label": "UI Tour"
},
{
"depends_on": "eval:(doc.ui_tour)",
"description": "CSS selector for the element you want to highlight.",
"fieldname": "element_selector",
"fieldtype": "Data",
"label": "Element Selector",
"mandatory_depends_on": "eval:(doc.ui_tour)"
},
{
"depends_on": "eval:(doc.ui_tour)",
"description": "Mozilla doesn't support :has() so you can pass parent selector here as workaround",
"fieldname": "parent_element_selector",
"fieldtype": "Data",
"label": "Parent Element Selector"
},
{
"depends_on": "eval:(doc.ui_tour)",
"fieldname": "next_form_tour",
"fieldtype": "Link",
"label": "Next Form Tour",
"options": "Form Tour"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"description": "Hide Previous, Next and Close button on highlight dialog.",
"fieldname": "hide_buttons",
"fieldtype": "Check",
"label": "Hide Buttons"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"description": "Move to next step when clicked inside highlighted area.",
"fieldname": "next_on_click",
"fieldtype": "Check",
"label": "Next on Click"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"description": "when clicked on element it will focus popover if present.",
"fieldname": "popover_element",
"fieldtype": "Check",
"label": "Popover Element"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"fieldname": "offset_x",
"fieldtype": "Int",
"label": "Offset X"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"fieldname": "offset_y",
"fieldtype": "Int",
"label": "Offset Y"
},
{
"default": "0",
"depends_on": "eval:(doc.ui_tour)",
"description": "Enable if on click\nopens modal.",
"fieldname": "modal_trigger",
"fieldtype": "Check",
"label": "Modal Trigger"
},
{
"columns": 4,
"depends_on": "eval: (doc.popover_element || doc.modal_trigger)",
"fieldname": "ondemand_description",
"fieldtype": "HTML Editor",
"ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Popover or Modal Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-01-27 15:18:36.481801",
"modified": "2023-05-23 13:09:15.923043",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",

View file

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

View file

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

View file

@ -6,8 +6,7 @@ from frappe.model.document import Document
class ListViewSettings(Document):
def on_update(self):
frappe.clear_document_cache(self.doctype, self.name)
pass
@frappe.whitelist()

View file

@ -202,7 +202,11 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
if txt:
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
condition_query = frappe.qb.get_query(doctype, filters=filters)
condition_query = frappe.qb.get_query(
doctype,
filters=filters,
validate_filters=True,
)
return (
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)

View file

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

View file

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

View file

@ -37,10 +37,10 @@ ASSET_KEYS = (
def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
meta = frappe.cache().hget("doctype_form_meta", doctype)
meta = frappe.cache.hget("doctype_form_meta", doctype)
if not meta:
meta = FormMeta(doctype)
frappe.cache().hset("doctype_form_meta", doctype, meta)
frappe.cache.hset("doctype_form_meta", doctype, meta)
else:
meta = FormMeta(doctype)

View file

@ -9,12 +9,14 @@ from frappe.desk.form.load import run_onload
from frappe.model.docstatus import DocStatus
from frappe.monitor import add_data_to_monitor
from frappe.utils.scheduler import is_scheduler_inactive
from frappe.utils.telemetry import capture_doc
@frappe.whitelist()
def savedocs(doc, action):
"""save / submit / update doclist"""
doc = frappe.get_doc(json.loads(doc))
capture_doc(doc)
set_local_name(doc)
# action

View file

@ -0,0 +1,79 @@
{
"creation": "2023-05-18 12:08:23.196462",
"dashboard_name": "",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"list_name": "",
"modified": "2023-05-24 12:43:43.741781",
"modified_by": "Administrator",
"module": "Desk",
"name": "Main Workspace Tour",
"new_document_form": 0,
"owner": "Administrator",
"page_name": "",
"page_route": "[\"Workspaces\",\"*\"]",
"reference_doctype": "",
"report_name": "",
"save_on_complete": 0,
"steps": [
{
"description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.",
"element_selector": "#navbar-search",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"parent_element_selector": ".input-group.search-bar",
"popover_element": 0,
"position": "Left",
"title": "Awesomebar",
"ui_tour": 1
},
{
"description": "These are workspaces. Each module workspace provides insightful information and shortcuts on one page. \n\n<br><br>\n\nTip: You can build custom workspaces for your needs.",
"element_selector": ".col-lg-2.layout-side-section",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Right",
"title": "Workspace List",
"ui_tour": 1
},
{
"description": "<h5 style=\"line-height:1.5;\">Click to visit the Workspace</h5>",
"element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Users\"]",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 1,
"is_table_field": 0,
"modal_trigger": 0,
"next_form_tour": "New Tools Tour",
"next_on_click": 1,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Right",
"title": "Users Workspace",
"ui_tour": 1
}
],
"title": "Main Workspace Tour",
"track_steps": 1,
"ui_tour": 1,
"view_name": "Workspaces",
"workspace_name": ""
}

View file

@ -0,0 +1,62 @@
{
"creation": "2023-05-24 12:50:23.740052",
"dashboard_name": "",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"list_name": "",
"modified": "2023-05-24 13:01:56.539128",
"modified_by": "Administrator",
"module": "Desk",
"name": "Users Workspace Tour",
"new_document_form": 0,
"owner": "Administrator",
"page_name": "",
"page_route": "[\"Workspaces\",\"Users\"]",
"reference_doctype": "",
"report_name": "",
"save_on_complete": 0,
"steps": [
{
"description": "This is Users Workspace. You'll find all shortcuts for user, roles and permission management here.",
"element_selector": ".codex-editor",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
"is_table_field": 0,
"modal_trigger": 0,
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Left",
"title": "Workspace",
"ui_tour": 1
},
{
"description": "This is a shortcut to User DocType. \n<br>\n\nLet's Click on the User shortcut to explore all users in System.",
"element_selector": "[shortcut_name=\"User\"]",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 1,
"is_table_field": 0,
"modal_trigger": 0,
"next_form_tour": "User List Tour",
"next_on_click": 0,
"offset_x": 0,
"offset_y": 0,
"popover_element": 0,
"position": "Right",
"title": "Users Shortcut",
"ui_tour": 1
}
],
"title": "Users Workspace Tour",
"track_steps": 1,
"ui_tour": 1,
"view_name": "Workspaces",
"workspace_name": "Users"
}

View file

@ -36,7 +36,12 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
ToDo = DocType("ToDo")
User = DocType("User")
count = Count("*").as_("count")
filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
filtered_records = frappe.qb.get_query(
doctype,
filters=current_filters,
fields=["name"],
validate_filters=True,
)
return (
frappe.qb.from_(ToDo)

View file

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

View file

@ -19,7 +19,10 @@ frappe.pages["form-builder"].on_page_show = function (wrapper) {
function load_form_builder(wrapper) {
let route = frappe.get_route();
route = route.filter((a) => a);
if (route.length > 1) {
if (route.length > 1 && route[1] === "new-doctype") {
frappe.pages["form-builder"].new_doctype(route[2]);
} else if (route.length > 1) {
let doctype = route[1];
let is_customize_form = route[2] === "customize";
@ -44,159 +47,165 @@ function load_form_builder(wrapper) {
});
});
} else {
let d = new frappe.ui.Dialog({
title: __("Select DocType"),
fields: [
{
label: __("Select DocType"),
fieldname: "doctype",
fieldtype: "Link",
options: "DocType",
only_select: 1,
},
{
label: __("Customize"),
fieldname: "customize",
fieldtype: "Check",
},
],
primary_action_label: __("Edit"),
primary_action({ doctype, customize }) {
if (customize) {
frappe.model.with_doctype(doctype).then(() => {
let meta = frappe.get_meta(doctype);
if (in_list(frappe.model.core_doctypes_list, this.doctype))
frappe.throw(__("Core DocTypes cannot be customized."));
if (meta.issingle)
frappe.throw(__("Single DocTypes cannot be customized."));
if (meta.custom)
frappe.throw(
__(
"Only standard DocTypes are allowed to be customized from Customize Form."
)
);
frappe.set_route("form-builder", doctype, "customize");
});
} else {
frappe.set_route("form-builder", doctype);
}
},
secondary_action_label: __("Create New DocType"),
secondary_action() {
let doctype = d.get_value("doctype") || "";
let non_developer =
frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
d.hide();
let new_d = new frappe.ui.Dialog({
title: __("Create New DocType"),
fields: [
{
label: __("DocType Name"),
fieldname: "doctype_name",
fieldtype: "Data",
default: doctype,
reqd: 1,
},
{ fieldtype: "Column Break" },
{
label: __("Module"),
fieldname: "module",
fieldtype: "Link",
options: "Module Def",
reqd: 1,
},
{ fieldtype: "Section Break" },
{
label: __("Is Submittable"),
fieldname: "is_submittable",
fieldtype: "Check",
description: __(
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
),
depends_on: "eval:!doc.istable && !doc.issingle",
},
{
label: __("Is Child Table"),
fieldname: "istable",
fieldtype: "Check",
description: __("Child Tables are shown as a Grid in other DocTypes"),
depends_on: "eval:!doc.is_submittable && !doc.issingle",
},
{
label: __("Editable Grid"),
fieldname: "editable_grid",
fieldtype: "Check",
depends_on: "istable",
default: 1,
},
{
label: __("Is Single"),
fieldname: "issingle",
fieldtype: "Check",
description: __(
"Single Types have only one record no tables associated. Values are stored in tabSingles"
),
depends_on: "eval:!doc.istable && !doc.is_submittable",
},
{
label: __("Custom?"),
fieldname: "custom",
fieldtype: "Check",
default: non_developer,
read_only: non_developer,
},
],
primary_action_label: __("Create & Continue"),
primary_action(values) {
if (!values.istable) values.editable_grid = 0;
frappe.db
.insert({
doctype: "DocType",
name: values.doctype_name,
module: values.module,
istable: values.istable,
editable_grid: values.editable_grid,
issingle: values.issingle,
custom: values.custom,
is_submittable: values.is_submittable,
permissions: [
{
create: 1,
delete: 1,
email: 1,
export: 1,
print: 1,
read: 1,
report: 1,
role: "System Manager",
share: 1,
write: 1,
},
],
fields: [
{
label: "Title",
fieldname: "title",
fieldtype: "Data",
},
],
})
.then((doc) => {
frappe.set_route("form-builder", doc.name);
});
},
secondary_action_label: __("Back"),
secondary_action() {
new_d.hide();
d.show();
},
});
new_d.show();
},
});
d.show();
frappe.pages["form-builder"].select_doctype();
}
}
frappe.pages["form-builder"].select_doctype = function () {
let d = new frappe.ui.Dialog({
title: __("Select DocType"),
fields: [
{
label: __("Select DocType"),
fieldname: "doctype",
fieldtype: "Link",
options: "DocType",
only_select: 1,
},
{
label: __("Customize"),
fieldname: "customize",
fieldtype: "Check",
},
],
primary_action_label: __("Edit"),
primary_action({ doctype, customize }) {
if (customize) {
frappe.model.with_doctype(doctype).then(() => {
let meta = frappe.get_meta(doctype);
if (in_list(frappe.model.core_doctypes_list, this.doctype))
frappe.throw(__("Core DocTypes cannot be customized."));
if (meta.issingle) frappe.throw(__("Single DocTypes cannot be customized."));
if (meta.custom)
frappe.throw(
__(
"Only standard DocTypes are allowed to be customized from Customize Form."
)
);
frappe.set_route("form-builder", doctype, "customize");
});
} else {
frappe.set_route("form-builder", doctype);
}
},
secondary_action_label: __("Create New DocType"),
secondary_action() {
let doctype = d.get_value("doctype") || "";
d.hide();
frappe.set_route("form-builder", "new-doctype", doctype);
},
});
d.show();
};
frappe.pages["form-builder"].new_doctype = function (doctype) {
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
let new_d = new frappe.ui.Dialog({
title: __("Create New DocType"),
fields: [
{
label: __("DocType Name"),
fieldname: "doctype_name",
fieldtype: "Data",
default: doctype,
reqd: 1,
},
{ fieldtype: "Column Break" },
{
label: __("Module"),
fieldname: "module",
fieldtype: "Link",
options: "Module Def",
reqd: 1,
},
{ fieldtype: "Section Break" },
{
label: __("Is Submittable"),
fieldname: "is_submittable",
fieldtype: "Check",
description: __(
"Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
),
depends_on: "eval:!doc.istable && !doc.issingle",
},
{
label: __("Is Child Table"),
fieldname: "istable",
fieldtype: "Check",
description: __("Child Tables are shown as a Grid in other DocTypes"),
depends_on: "eval:!doc.is_submittable && !doc.issingle",
},
{
label: __("Editable Grid"),
fieldname: "editable_grid",
fieldtype: "Check",
depends_on: "istable",
default: 1,
},
{
label: __("Is Single"),
fieldname: "issingle",
fieldtype: "Check",
description: __(
"Single Types have only one record no tables associated. Values are stored in tabSingles"
),
depends_on: "eval:!doc.istable && !doc.is_submittable",
},
{
label: __("Custom?"),
fieldname: "custom",
fieldtype: "Check",
default: non_developer,
read_only: non_developer,
},
],
primary_action_label: __("Create & Continue"),
primary_action(values) {
if (!values.istable) values.editable_grid = 0;
frappe.db
.insert({
doctype: "DocType",
name: values.doctype_name,
module: values.module,
istable: values.istable,
editable_grid: values.editable_grid,
issingle: values.issingle,
custom: values.custom,
is_submittable: values.is_submittable,
permissions: [
{
create: 1,
delete: 1,
email: 1,
export: 1,
print: 1,
read: 1,
report: 1,
role: "System Manager",
share: 1,
write: 1,
},
],
fields: [
{
label: "Title",
fieldname: "title",
fieldtype: "Data",
},
],
})
.then((doc) => {
frappe.set_route("form-builder", doc.name);
});
},
secondary_action_label: __("Back"),
secondary_action() {
new_d.hide();
window.history.back();
},
});
new_d.show();
};

View file

@ -49,19 +49,14 @@ frappe.pages["setup-wizard"].on_page_load = function (wrapper) {
};
frappe.wizard = new frappe.setup.SetupWizard(wizard_settings);
frappe.setup.run_event("after_load");
let route = frappe.get_route();
if (route) {
frappe.wizard.show_slide(route[1]);
}
frappe.wizard.show_slide(cint(frappe.get_route()[1]));
},
});
});
};
frappe.pages["setup-wizard"].on_page_show = function () {
if (frappe.get_route()[1]) {
frappe.wizard && frappe.wizard.show_slide(frappe.get_route()[1]);
}
frappe.wizard && frappe.wizard.show_slide(cint(frappe.get_route()[1]));
};
frappe.setup.on("before_load", function () {
@ -122,12 +117,10 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
show_slide(id) {
if (id === this.slides.length) {
// show_slide called on last slide
this.action_on_complete();
return;
}
super.show_slide(id);
frappe.set_route(this.page_name, id + "");
frappe.set_route(this.page_name, cstr(id));
}
show_hide_prev_next(id) {
@ -403,7 +396,7 @@ frappe.setup.slides_settings = [
},
{
fieldname: "enable_telemetry",
label: __("Allow Sending Usage Data for Improving applications"),
label: __("Allow Sending Usage Data for Improving Applications"),
fieldtype: "Check",
default: 1,
},

View file

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

View file

@ -119,7 +119,7 @@ def generate_report_result(
"report_summary": report_summary,
"skip_total_row": skip_total_row or 0,
"status": None,
"execution_time": frappe.cache().hget("report_execution_time", report.name) or 0,
"execution_time": frappe.cache.hget("report_execution_time", report.name) or 0,
}
@ -170,7 +170,7 @@ def get_script(report_name):
return {
"script": render_include(script),
"html_format": html_format,
"execution_time": frappe.cache().hget("report_execution_time", report_name) or 0,
"execution_time": frappe.cache.hget("report_execution_time", report_name) or 0,
}

View file

@ -311,8 +311,8 @@ def validate_and_sanitize_search_inputs(fn):
@frappe.whitelist()
def get_names_for_mentions(search_term):
users_for_mentions = frappe.cache().get_value("users_for_mentions", get_users_for_mentions)
user_groups = frappe.cache().get_value("user_groups", get_user_groups)
users_for_mentions = frappe.cache.get_value("users_for_mentions", get_users_for_mentions)
user_groups = frappe.cache.get_value("user_groups", get_user_groups)
filtered_mentions = []
for mention_data in users_for_mentions + user_groups:

View file

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

View file

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

View file

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

View file

@ -107,6 +107,7 @@
},
{
"default": "0",
"depends_on": "eval:doc.use_imap",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"label": "Append Emails to Sent Folder"
@ -133,7 +134,7 @@
"link_fieldname": "domain"
}
],
"modified": "2022-08-19 12:55:06.434541",
"modified": "2023-06-05 12:55:06.434541",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Domain",

View file

@ -3,7 +3,7 @@
frappe.ui.form.on("Email Queue", {
refresh: function (frm) {
if (["Not Sent", "Partially Sent"].indexOf(frm.doc.status) != -1) {
if (["Not Sent", "Partially Sent"].includes(frm.doc.status)) {
let button = frm.add_custom_button("Send Now", function () {
frappe.call({
method: "frappe.email.doctype.email_queue.email_queue.send_now",
@ -16,9 +16,7 @@ frappe.ui.form.on("Email Queue", {
},
});
});
}
if (["Error", "Partially Errored"].indexOf(frm.doc.status) != -1) {
} else if (frm.doc.status == "Error") {
let button = frm.add_custom_button("Retry Sending", function () {
frm.call({
method: "retry_sending",
@ -26,10 +24,8 @@ frappe.ui.form.on("Email Queue", {
name: frm.doc.name,
},
btn: button,
callback: function (r) {
if (!r.exc) {
frm.set_value("status", "Not Sent");
}
callback: function () {
frm.reload_doc();
},
});
});

View file

@ -55,12 +55,14 @@
"default": "Not Sent",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "\nNot Sent\nSending\nSent\nError\nExpired"
"options": "Not Sent\nSending\nSent\nPartially Sent\nError\nExpired"
},
{
"depends_on": "eval:doc.error",
"fieldname": "error",
"fieldtype": "Code",
"label": "Error"
@ -152,7 +154,7 @@
"idx": 1,
"in_create": 1,
"links": [],
"modified": "2023-03-16 12:15:17.850292",
"modified": "2023-06-08 15:31:52.789186",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",

View file

@ -123,31 +123,33 @@ class EmailQueue(Document):
return True
def send(self, is_background_task: bool = False, smtp_server_instance: SMTPServer = None):
def send(self, smtp_server_instance: SMTPServer = None):
"""Send emails to recipients."""
if not self.can_send_now():
return
with SendMailContext(self, is_background_task, smtp_server_instance) as ctx:
with SendMailContext(self, smtp_server_instance) as ctx:
message = None
for recipient in self.recipients:
if not recipient.is_mail_to_be_sent():
if recipient.is_mail_sent():
continue
message = ctx.build_message(recipient.recipient)
method = get_hook_method("override_email_send")
if method:
if method := get_hook_method("override_email_send"):
method(self, self.sender, recipient.recipient, message)
else:
if not frappe.flags.in_test:
ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
ctx.add_to_sent_list(recipient)
ctx.smtp_server.session.sendmail(
from_addr=self.sender, to_addrs=recipient.recipient, msg=message
)
ctx.update_recipient_status_to_sent(recipient)
if frappe.flags.in_test:
frappe.flags.sent_mail = message
return
if ctx.email_account_doc.append_emails_to_sent_folder and ctx.sent_to:
if ctx.email_account_doc.append_emails_to_sent_folder:
ctx.email_account_doc.append_email_to_sent_folder(message)
@staticmethod
@ -177,24 +179,22 @@ class EmailQueue(Document):
@task(queue="short")
def send_mail(email_queue_name, is_background_task=False, smtp_server_instance: SMTPServer = None):
def send_mail(email_queue_name, smtp_server_instance: SMTPServer = None):
"""This is equivalent to EmailQueue.send.
This provides a way to make sending mail as a background job.
"""
record = EmailQueue.find(email_queue_name)
record.send(is_background_task=is_background_task, smtp_server_instance=smtp_server_instance)
record.send(smtp_server_instance=smtp_server_instance)
class SendMailContext:
def __init__(
self,
queue_doc: Document,
is_background_task: bool = False,
smtp_server_instance: SMTPServer = None,
):
self.queue_doc: EmailQueue = queue_doc
self.is_background_task = is_background_task
self.email_account_doc = queue_doc.get_email_account()
self.smtp_server = smtp_server_instance or self.email_account_doc.get_smtp_server()
@ -203,7 +203,9 @@ class SendMailContext:
# Note: smtp session will have to be manually closed
self.retain_smtp_session = bool(smtp_server_instance)
self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()]
self.sent_to_atleast_one_recipient = any(
rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()
)
def __enter__(self):
self.queue_doc.update_status(status="Sending", commit=True)
@ -217,53 +219,35 @@ class SendMailContext:
smtplib.SMTPHeloError,
JobTimeoutException,
]
trace = "".join(traceback.format_tb(exc_tb)) if exc_tb else None
if not self.retain_smtp_session:
self.smtp_server.quit()
self.log_exception(exc_type, exc_val, exc_tb)
if exc_type in exceptions:
email_status = "Partially Sent" if self.sent_to else "Not Sent"
self.queue_doc.update_status(status=email_status, commit=True)
elif exc_type:
if self.queue_doc.retry < get_email_retry_limit():
update_fields = {"status": "Not Sent", "retry": self.queue_doc.retry + 1}
else:
update_fields = {"status": (self.sent_to and "Partially Errored") or "Error"}
self.queue_doc.update_status(**update_fields, commit=True)
else:
email_status = self.is_mail_sent_to_all() and "Sent"
email_status = email_status or (self.sent_to and "Partially Sent") or "Not Sent"
update_fields = {
"status": email_status,
"email_account": self.email_account_doc.name
if self.email_account_doc.is_exists_in_db()
else None,
"status": "Partially Sent" if self.sent_to_atleast_one_recipient else "Not Sent",
"error": trace,
}
self.queue_doc.update_status(**update_fields, commit=True)
elif exc_type:
update_fields = {"error": trace}
if self.queue_doc.retry < get_email_retry_limit():
update_fields.update(
{
"status": "Partially Sent" if self.sent_to_atleast_one_recipient else "Not Sent",
"retry": self.queue_doc.retry + 1,
}
)
else:
update_fields.update({"status": "Error"})
else:
update_fields = {"status": "Sent"}
def log_exception(self, exc_type, exc_val, exc_tb):
if exc_type:
traceback_string = "".join(traceback.format_tb(exc_tb))
traceback_string += f"\n Queue Name: {self.queue_doc.name}"
self.queue_doc.update_status(**update_fields, commit=True)
self.queue_doc.log_error("Email sending failed", traceback_string)
@property
def smtp_session(self):
if frappe.flags.in_test:
return
return self.smtp_server.session
def add_to_sent_list(self, recipient):
# Update recipient status
def update_recipient_status_to_sent(self, recipient):
self.sent_to_atleast_one_recipient = True
recipient.update_db(status="Sent", commit=True)
self.sent_to.append(recipient.recipient)
def is_mail_sent_to_all(self):
return sorted(self.sent_to) == sorted(rec.recipient for rec in self.queue_doc.recipients)
def get_message_object(self, message):
return Parser(policy=SMTPUTF8).parsestr(message)
@ -379,7 +363,7 @@ def retry_sending(name):
doc = frappe.get_doc("Email Queue", name)
doc.check_permission()
if doc and (doc.status == "Error" or doc.status == "Partially Errored"):
if doc and doc.status == "Error":
doc.status = "Not Sent"
for d in doc.recipients:
if d.status != "Sent":

View file

@ -143,7 +143,9 @@ class Newsletter(WebsiteGenerator):
"""Get list of pending recipients of the newsletter. These
recipients may not have receive the newsletter in the previous iteration.
"""
return [x for x in self.newsletter_recipients if x not in self.get_queued_recipients()]
queued_recipients = set(self.get_queued_recipients())
return [x for x in self.newsletter_recipients if x not in queued_recipients]
def queue_all(self):
"""Queue Newsletter to all the recipients generated from the `Email Group` table"""

View file

@ -42,10 +42,10 @@ class Notification(Document):
self.validate_forbidden_types()
self.validate_condition()
self.validate_standard()
frappe.cache().hdel("notifications", self.document_type)
frappe.cache.hdel("notifications", self.document_type)
def on_update(self):
frappe.cache().hdel("notifications", self.document_type)
frappe.cache.hdel("notifications", self.document_type)
path = export_module_json(self, self.is_standard, self.module)
if path:
# js
@ -282,19 +282,8 @@ def get_context(context):
email_ids = email_ids_value.replace(",", "\n")
recipients = recipients + email_ids.split("\n")
if recipient.cc and "{" in recipient.cc:
recipient.cc = frappe.render_template(recipient.cc, context)
if recipient.cc:
recipient.cc = recipient.cc.replace(",", "\n")
cc = cc + recipient.cc.split("\n")
if recipient.bcc and "{" in recipient.bcc:
recipient.bcc = frappe.render_template(recipient.bcc, context)
if recipient.bcc:
recipient.bcc = recipient.bcc.replace(",", "\n")
bcc = bcc + recipient.bcc.split("\n")
cc.extend(get_emails_from_template(recipient.cc, context))
bcc.extend(get_emails_from_template(recipient.bcc, context))
# For sending emails to specified role
if recipient.receiver_by_role:
@ -389,7 +378,7 @@ def get_context(context):
self.message = frappe.utils.md_to_html(self.message)
def on_trash(self):
frappe.cache().hdel("notifications", self.document_type)
frappe.cache.hdel("notifications", self.document_type)
@frappe.whitelist()
@ -485,3 +474,11 @@ def get_assignees(doc):
recipients = [d.allocated_to for d in assignees]
return recipients
def get_emails_from_template(template, context):
if not template:
return ()
emails = frappe.render_template(template, context) if "{" in template else template
return filter(None, emails.replace(",", "\n").split("\n"))

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