Merge branch 'develop' into file-permissions
This commit is contained in:
commit
01d8a4ab28
237 changed files with 4083 additions and 1852 deletions
|
|
@ -34,3 +34,9 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
|
|||
|
||||
# db.get_all -> get_all
|
||||
2eec621e95564c359ad22da79501a855c1f32b03
|
||||
|
||||
# minor formatting fix in `user.py`
|
||||
f223bc02490902dfcc32892058f13f343d51fbaf
|
||||
|
||||
# frappe.cache() -> frappe.cache
|
||||
fa6dc03cc87ad74e11609e7373078366fdcb3e1b
|
||||
|
|
|
|||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -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 .
|
||||
|
|
|
|||
13
README.md
13
README.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 & 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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
28
frappe/core/doctype/doctype/doctype_list.js
Normal file
28
frappe/core/doctype/doctype/doctype_list.js
Normal 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"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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))))
|
||||
|
|
|
|||
95
frappe/core/form_tour/user_list_tour/user_list_tour.json
Normal file
95
frappe/core/form_tour/user_list_tour/user_list_tour.json
Normal 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": ""
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 & 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 & 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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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 & 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ NestedSetHierarchy = (
|
|||
"descendants of",
|
||||
"not ancestors of",
|
||||
"not descendants of",
|
||||
"descendants of (inclusive)",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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}>`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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><div class=\"m-3\"></div><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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
0
frappe/desk/doctype/form_tour/patches/__init__.py
Normal file
0
frappe/desk/doctype/form_tour/patches/__init__.py
Normal file
13
frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py
Normal file
13
frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py
Normal 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()
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": ""
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue