Merge branch 'develop' into 36457-report-perm-perf-issue
This commit is contained in:
commit
11c429e5bf
124 changed files with 72769 additions and 52999 deletions
|
|
@ -58,3 +58,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
|
|||
|
||||
# ruff update
|
||||
84ef6ec677c8657c3243ac456a1ef794bfb34a50
|
||||
|
||||
# replace `frappe.flags.in_test` with `frappe.in_test`
|
||||
653c80b8483cc41aef25cd7d66b9b6bb188bf5f8
|
||||
|
|
|
|||
1
.github/helper/documentation.py
vendored
1
.github/helper/documentation.py
vendored
|
|
@ -11,6 +11,7 @@ WEBSITE_REPOS = [
|
|||
DOCUMENTATION_DOMAINS = [
|
||||
"docs.erpnext.com",
|
||||
"frappeframework.com",
|
||||
"docs.frappe.io",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -472,6 +472,11 @@ async function write_assets_json(metafile) {
|
|||
}
|
||||
|
||||
async function update_assets_json_in_cache() {
|
||||
// Redis won't be present during docker image build
|
||||
if (process.env.FRAPPE_DOCKER_BUILD) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update assets_json cache in redis, so that it can be read directly by python
|
||||
let client = get_redis_subscriber("redis_cache");
|
||||
// handle error event to avoid printing stack traces
|
||||
|
|
@ -523,7 +528,7 @@ function run_build_command_for_apps(apps) {
|
|||
log(
|
||||
`\nInstalling dependencies for ${chalk.bold(app)} (because node_modules not found)`
|
||||
);
|
||||
execSync("yarn install", { encoding: "utf8", stdio: "inherit" });
|
||||
execSync("yarn install --frozen-lockfile", { encoding: "utf8", stdio: "inherit" });
|
||||
}
|
||||
|
||||
log("\nRunning build command for", chalk.bold(app));
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ cache: Optional["RedisWrapper"] = None
|
|||
client_cache: Optional["ClientCache"] = None
|
||||
STANDARD_USERS = ("Guest", "Administrator")
|
||||
|
||||
# this global may be subsequently changed by frappe.tests.utils.toggle_test_mode()
|
||||
in_test = False
|
||||
|
||||
_dev_server = int(sbool(os.environ.get("DEV_SERVER", False)))
|
||||
|
||||
if _dev_server:
|
||||
|
|
@ -219,7 +222,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
|
|||
"in_install_db": False,
|
||||
"in_install_app": False,
|
||||
"in_import": False,
|
||||
"in_test": False,
|
||||
"in_test": in_test,
|
||||
"mute_messages": False,
|
||||
"ignore_links": False,
|
||||
"mute_emails": False,
|
||||
|
|
@ -263,7 +266,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force: bool =
|
|||
local.form_dict = _dict()
|
||||
local.preload_assets = {"style": [], "script": [], "icons": []}
|
||||
local.session = _dict()
|
||||
local.dev_server = _dev_server
|
||||
local.dev_server = _dev_server # only for backwards compatibility
|
||||
local.qb = get_query_builder(local.conf.db_type)
|
||||
if not cache or not client_cache:
|
||||
setup_redis_cache_connection()
|
||||
|
|
@ -640,7 +643,7 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None):
|
|||
global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func
|
||||
|
||||
# validate argument types only if request is present
|
||||
in_request_or_test = lambda: getattr(local, "request", None) or local.flags.in_test # noqa: E731
|
||||
in_request_or_test = lambda: getattr(local, "request", None) or in_test # noqa: E731
|
||||
|
||||
# get function from the unbound / bound method
|
||||
# this is needed because functions can be compared, but not methods
|
||||
|
|
@ -740,7 +743,7 @@ def only_for(roles: list[str] | tuple[str] | str, message=False):
|
|||
:param roles: Permitted role(s)
|
||||
"""
|
||||
|
||||
if local.flags.in_test or local.session.user == "Administrator":
|
||||
if in_test or local.session.user == "Administrator":
|
||||
return
|
||||
|
||||
if isinstance(roles, str):
|
||||
|
|
@ -767,7 +770,7 @@ def get_domain_data(module):
|
|||
else:
|
||||
return _dict()
|
||||
except ImportError:
|
||||
if local.flags.in_test:
|
||||
if in_test:
|
||||
return _dict()
|
||||
else:
|
||||
raise
|
||||
|
|
@ -1595,7 +1598,7 @@ def copy_doc(doc: "Document", ignore_no_copy: bool = True) -> "Document":
|
|||
|
||||
fields_to_clear = ["name", "owner", "creation", "modified", "modified_by"]
|
||||
|
||||
if not local.flags.in_test:
|
||||
if not in_test:
|
||||
fields_to_clear.append("docstatus")
|
||||
|
||||
if isinstance(doc, BaseDocument) or hasattr(doc, "as_dict"):
|
||||
|
|
|
|||
108
frappe/api/v2.py
108
frappe/api/v2.py
|
|
@ -15,7 +15,7 @@ from werkzeug.routing import Rule
|
|||
|
||||
import frappe
|
||||
import frappe.client
|
||||
from frappe import _, get_newargs, is_whitelisted
|
||||
from frappe import _, cint, cstr, get_newargs, is_whitelisted
|
||||
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
|
||||
from frappe.handler import is_valid_http_method, run_server_script, upload_file
|
||||
|
||||
|
|
@ -65,17 +65,99 @@ def read_doc(doctype: str, name: str):
|
|||
doc = frappe.get_doc(doctype, name)
|
||||
doc.check_permission("read")
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
return doc
|
||||
_doc = doc.as_dict()
|
||||
|
||||
for key in _doc:
|
||||
df = doc.meta.get_field(key)
|
||||
if df and df.fieldtype == "Link" and isinstance(_doc.get(key), int):
|
||||
_doc[key] = cstr(_doc.get(key))
|
||||
|
||||
return _doc
|
||||
|
||||
|
||||
def document_list(doctype: str):
|
||||
if frappe.form_dict.get("fields"):
|
||||
frappe.form_dict["fields"] = json.loads(frappe.form_dict["fields"])
|
||||
def document_list(doctype: str) -> list[dict[str, Any]]:
|
||||
"""
|
||||
GET /api/v2/document/<doctype>?fields=[...],filters={...},...
|
||||
|
||||
# set limit of records for frappe.get_list
|
||||
frappe.form_dict.limit_page_length = frappe.form_dict.limit or 20
|
||||
# evaluate frappe.get_list
|
||||
return frappe.call(frappe.client.get_list, doctype, **frappe.form_dict)
|
||||
REST API endpoint for fetching doctype records
|
||||
|
||||
Args:
|
||||
doctype: DocType name
|
||||
|
||||
Query Parameters (accessible via frappe.form_dict):
|
||||
fields: JSON string of field names to fetch
|
||||
filters: JSON string of filters to apply
|
||||
order_by: Order by field
|
||||
start: Starting offset for pagination (default: 0)
|
||||
limit: Maximum number of records to fetch (default: 20)
|
||||
group_by: Group by field
|
||||
as_dict: Return results as dictionary (default: True)
|
||||
|
||||
Response:
|
||||
frappe.response["data"]: List of document records as dicts
|
||||
frappe.response["has_next_page"]: Indicates if more pages are available
|
||||
|
||||
Controller Customization:
|
||||
Doctype controllers can customize queries by implementing a static get_list(query) method
|
||||
that receives a QueryBuilder object and returns a modified QueryBuilder.
|
||||
|
||||
Example:
|
||||
class Project(Document):
|
||||
@staticmethod
|
||||
def get_list(query):
|
||||
Project = frappe.qb.DocType("Project")
|
||||
if user_has_role("Project Owner"):
|
||||
query = query.where(Project.owner == frappe.session.user)
|
||||
else:
|
||||
query = query.where(Project.is_private == 0)
|
||||
return query
|
||||
"""
|
||||
from frappe.model.base_document import get_controller
|
||||
|
||||
args = frappe.form_dict
|
||||
fields: list | None = frappe.parse_json(args.get("fields", None))
|
||||
filters: dict | None = frappe.parse_json(args.get("filters", None))
|
||||
order_by: str | None = args.get("order_by", None)
|
||||
start: int = cint(args.get("start", 0))
|
||||
limit: int = cint(args.get("limit", 20))
|
||||
group_by: str | None = args.get("group_by", None)
|
||||
debug: bool = args.get("debug", False)
|
||||
as_dict: bool = args.get("as_dict", True)
|
||||
|
||||
query = frappe.qb.get_query(
|
||||
table=doctype,
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
order_by=order_by,
|
||||
offset=start,
|
||||
limit=limit + 1, # Fetch one extra to check if there's a next page
|
||||
group_by=group_by,
|
||||
ignore_permissions=False,
|
||||
)
|
||||
|
||||
# Check if the doctype controller has a static get_list method
|
||||
controller = get_controller(doctype)
|
||||
if hasattr(controller, "get_list"):
|
||||
try:
|
||||
return_value = controller.get_list(query)
|
||||
|
||||
if return_value is not None:
|
||||
# Validate that the returned value has a run method (is a QueryBuilder-like object)
|
||||
if not hasattr(return_value, "run"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Custom get_list method for {0} must return a QueryBuilder object or None, got {1}"
|
||||
).format(doctype, type(return_value).__name__)
|
||||
)
|
||||
|
||||
query = return_value
|
||||
|
||||
except Exception as e:
|
||||
frappe.throw(_("Error in {0}.get_list: {1}").format(doctype, str(e)))
|
||||
|
||||
data = query.run(as_dict=as_dict, debug=debug)
|
||||
frappe.response["has_next_page"] = len(data) > limit
|
||||
return data[:limit]
|
||||
|
||||
|
||||
def count(doctype: str) -> int:
|
||||
|
|
@ -91,7 +173,7 @@ def create_doc(doctype: str):
|
|||
data.pop("doctype", None)
|
||||
if (name := data.get("name")) and isinstance(name, str):
|
||||
frappe.flags.api_name_set = True
|
||||
return frappe.new_doc(doctype, **data).insert()
|
||||
return frappe.new_doc(doctype, **data).insert().as_dict()
|
||||
|
||||
|
||||
def copy_doc(doctype: str, name: str, ignore_no_copy: bool = True):
|
||||
|
|
@ -118,7 +200,7 @@ def update_doc(doctype: str, name: str):
|
|||
if doc.get("parenttype"):
|
||||
frappe.get_doc(doc.parenttype, doc.parent).save()
|
||||
|
||||
return doc
|
||||
return doc.as_dict()
|
||||
|
||||
|
||||
def delete_doc(doctype: str, name: str):
|
||||
|
|
@ -144,7 +226,9 @@ def execute_doc_method(doctype: str, name: str, method: str | None = None):
|
|||
doc.is_whitelisted(method)
|
||||
|
||||
doc.check_permission(PERMISSION_MAP[frappe.request.method])
|
||||
return doc.run_method(method, **frappe.form_dict)
|
||||
result = doc.run_method(method, **frappe.form_dict)
|
||||
frappe.response.docs.append(doc.as_dict())
|
||||
return result
|
||||
|
||||
|
||||
def run_doc_method(method: str, document: dict[str, Any] | str, kwargs=None):
|
||||
|
|
|
|||
|
|
@ -92,8 +92,6 @@ def application(request: Request):
|
|||
response = None
|
||||
|
||||
try:
|
||||
rollback = True
|
||||
|
||||
init_request(request)
|
||||
|
||||
validate_auth()
|
||||
|
|
@ -127,23 +125,19 @@ def application(request: Request):
|
|||
else:
|
||||
raise NotFound
|
||||
|
||||
except HTTPException as e:
|
||||
return e
|
||||
|
||||
except Exception as e:
|
||||
response = handle_exception(e)
|
||||
response = e.get_response(request.environ) if isinstance(e, HTTPException) else handle_exception(e)
|
||||
if db := getattr(frappe.local, "db", None):
|
||||
db.rollback(chain=True)
|
||||
|
||||
else:
|
||||
rollback = sync_database(rollback)
|
||||
sync_database()
|
||||
|
||||
finally:
|
||||
# Important note:
|
||||
# this function *must* always return a response, hence any exception thrown outside of
|
||||
# try..catch block like this finally block needs to be handled appropriately.
|
||||
|
||||
if rollback and request.method in UNSAFE_HTTP_METHODS and frappe.db:
|
||||
frappe.db.rollback()
|
||||
|
||||
try:
|
||||
run_after_request_hooks(request, response)
|
||||
except Exception:
|
||||
|
|
@ -177,14 +171,13 @@ def init_request(request):
|
|||
# site does not exist
|
||||
raise NotFound
|
||||
|
||||
frappe.connect(set_admin_as_user=False)
|
||||
if frappe.local.conf.maintenance_mode:
|
||||
frappe.connect()
|
||||
if frappe.local.conf.allow_reads_during_maintenance:
|
||||
setup_read_only_mode()
|
||||
else:
|
||||
raise frappe.SessionStopped("Session Stopped")
|
||||
else:
|
||||
frappe.connect(set_admin_as_user=False)
|
||||
|
||||
if request.path.startswith("/api/method/upload_file"):
|
||||
from frappe.core.api.file import get_max_file_size
|
||||
|
||||
|
|
@ -397,21 +390,21 @@ def handle_exception(e):
|
|||
return response
|
||||
|
||||
|
||||
def sync_database(rollback: bool) -> bool:
|
||||
def sync_database():
|
||||
db = getattr(frappe.local, "db", None)
|
||||
if not db:
|
||||
# db isn't initialized, can't commit or rollback
|
||||
return
|
||||
|
||||
# if HTTP method would change server state, commit if necessary
|
||||
if frappe.db and (frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS):
|
||||
frappe.db.commit()
|
||||
rollback = False
|
||||
elif frappe.db:
|
||||
frappe.db.rollback()
|
||||
rollback = False
|
||||
if frappe.local.request.method in UNSAFE_HTTP_METHODS or frappe.local.flags.commit:
|
||||
db.commit(chain=True)
|
||||
else:
|
||||
db.rollback(chain=True)
|
||||
|
||||
# update session
|
||||
if session := getattr(frappe.local, "session_obj", None):
|
||||
if session.update():
|
||||
rollback = False
|
||||
|
||||
return rollback
|
||||
session.update()
|
||||
|
||||
|
||||
# Always initialize sentry SDK if the DSN is sent
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ class AutoRepeat(Document):
|
|||
validate_template(self.message or "")
|
||||
|
||||
def before_insert(self):
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
start_date = getdate(self.start_date)
|
||||
today_date = getdate(today())
|
||||
if start_date <= today_date:
|
||||
|
|
@ -112,7 +112,7 @@ class AutoRepeat(Document):
|
|||
frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", "")
|
||||
|
||||
def validate_reference_doctype(self):
|
||||
if frappe.flags.in_test or frappe.flags.in_patch:
|
||||
if frappe.in_test or frappe.flags.in_patch:
|
||||
return
|
||||
if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
|
||||
frappe.throw(
|
||||
|
|
@ -229,7 +229,7 @@ class AutoRepeat(Document):
|
|||
|
||||
self.disable_auto_repeat()
|
||||
|
||||
if self.reference_document and not frappe.flags.in_test:
|
||||
if self.reference_document and not frappe.in_test:
|
||||
self.notify_error_to_user(error_log)
|
||||
|
||||
def make_new_document(self):
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ def build_missing_files():
|
|||
folder = os.path.join(sites_path, "assets", "frappe", "dist", type)
|
||||
current_asset_files.extend(os.listdir(folder))
|
||||
|
||||
development = frappe.local.conf.developer_mode or frappe.local.dev_server
|
||||
development = frappe.local.conf.developer_mode or frappe._dev_server
|
||||
build_mode = "development" if development else "production"
|
||||
|
||||
assets_json = frappe.read_file("assets/assets.json")
|
||||
|
|
|
|||
|
|
@ -183,6 +183,7 @@ def main(
|
|||
def run_tests_in_light_mode(test_params):
|
||||
from frappe.testing.loader import FrappeTestLoader
|
||||
from frappe.testing.result import FrappeTestResult
|
||||
from frappe.tests.utils import toggle_test_mode
|
||||
|
||||
# init environment
|
||||
frappe.init(test_params.site)
|
||||
|
|
@ -196,6 +197,7 @@ def run_tests_in_light_mode(test_params):
|
|||
frappe.utils.scheduler.disable_scheduler()
|
||||
frappe.clear_cache()
|
||||
|
||||
toggle_test_mode(True)
|
||||
suite = FrappeTestLoader().discover_tests(test_params)
|
||||
result = unittest.TextTestRunner(failfast=test_params.failfast, resultclass=FrappeTestResult).run(suite)
|
||||
if not result.wasSuccessful():
|
||||
|
|
@ -370,6 +372,7 @@ def run_tests(
|
|||
)
|
||||
@click.option("--use-orchestrator", is_flag=True, help="Use orchestrator to run parallel tests")
|
||||
@click.option("--dry-run", is_flag=True, default=False, help="Dont actually run tests")
|
||||
@click.option("--lightmode", is_flag=True, default=False, help="Skips all before test setup")
|
||||
@pass_context
|
||||
def run_parallel_tests(
|
||||
context: CliCtxObj,
|
||||
|
|
@ -379,6 +382,7 @@ def run_parallel_tests(
|
|||
with_coverage=False,
|
||||
use_orchestrator=False,
|
||||
dry_run=False,
|
||||
lightmode=False,
|
||||
):
|
||||
from traceback_with_variables import activate_by_import
|
||||
|
||||
|
|
@ -399,6 +403,7 @@ def run_parallel_tests(
|
|||
build_number=build_number,
|
||||
total_builds=total_builds,
|
||||
dry_run=dry_run,
|
||||
lightmode=lightmode,
|
||||
)
|
||||
mode = "Orchestrator" if use_orchestrator else "Parallel"
|
||||
banner = f"""
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ def build(
|
|||
skip_frappe = False
|
||||
|
||||
# don't minify in developer_mode for faster builds
|
||||
development = frappe.local.conf.developer_mode or frappe.local.dev_server
|
||||
development = frappe.local.conf.developer_mode or frappe._dev_server
|
||||
mode = "development" if development else "production"
|
||||
if production:
|
||||
mode = "production"
|
||||
|
|
|
|||
|
|
@ -38,6 +38,12 @@ class AccessLog(Document):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.write_only()
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
retry=retry_if_exception_type(frappe.DuplicateEntryError),
|
||||
reraise=True,
|
||||
)
|
||||
def make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
|
|
@ -48,41 +54,10 @@ def make_access_log(
|
|||
page=None,
|
||||
columns=None,
|
||||
):
|
||||
_make_access_log(
|
||||
doctype,
|
||||
document,
|
||||
method,
|
||||
file_type,
|
||||
report_name,
|
||||
filters,
|
||||
page,
|
||||
columns,
|
||||
)
|
||||
|
||||
|
||||
@frappe.write_only()
|
||||
@retry(
|
||||
stop=stop_after_attempt(3),
|
||||
retry=retry_if_exception_type(frappe.DuplicateEntryError),
|
||||
reraise=True,
|
||||
)
|
||||
def _make_access_log(
|
||||
doctype=None,
|
||||
document=None,
|
||||
method=None,
|
||||
file_type=None,
|
||||
report_name=None,
|
||||
filters=None,
|
||||
page=None,
|
||||
columns=None,
|
||||
):
|
||||
user = frappe.session.user
|
||||
in_request = frappe.request and frappe.request.method == "GET"
|
||||
|
||||
access_log = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Access Log",
|
||||
"user": user,
|
||||
"user": frappe.session.user,
|
||||
"export_from": doctype,
|
||||
"reference_document": document,
|
||||
"file_type": file_type,
|
||||
|
|
@ -94,14 +69,11 @@ def _make_access_log(
|
|||
}
|
||||
)
|
||||
|
||||
if frappe.flags.read_only:
|
||||
if not frappe.in_test:
|
||||
access_log.deferred_insert()
|
||||
return
|
||||
else:
|
||||
access_log.db_insert()
|
||||
|
||||
# `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview`
|
||||
# dont commit in test mode. It must be tempting to put this block along with the in_request in the
|
||||
# whitelisted method...yeah, don't do it. That part would be executed possibly on a read only DB conn
|
||||
if not frappe.flags.in_test or in_request:
|
||||
frappe.db.commit()
|
||||
|
||||
# only for backward compatibility
|
||||
_make_access_log = make_access_log
|
||||
|
|
|
|||
|
|
@ -579,7 +579,7 @@ def parse_email(email_strings):
|
|||
if not document_parts or len(document_parts) != 2:
|
||||
continue
|
||||
|
||||
doctype = unquote_plus(document_parts[0])
|
||||
doctype = frappe.unscrub(unquote_plus(document_parts[0]))
|
||||
docname = unquote_plus(document_parts[1])
|
||||
yield doctype, docname
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ class DataImport(Document):
|
|||
def start_import(self):
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
run_now = frappe.flags.in_test or frappe.conf.developer_mode
|
||||
run_now = frappe.in_test or frappe.conf.developer_mode
|
||||
if is_scheduler_inactive() and not run_now:
|
||||
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
|
||||
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ class DocType(Document):
|
|||
|
||||
def check_developer_mode(self):
|
||||
"""Throw exception if not developer mode or via patch"""
|
||||
if frappe.flags.in_patch or frappe.flags.in_test:
|
||||
if frappe.flags.in_patch or frappe.in_test:
|
||||
return
|
||||
|
||||
if not frappe.conf.get("developer_mode") and not self.custom:
|
||||
|
|
@ -594,7 +594,7 @@ class DocType(Document):
|
|||
global_search_fields_after_update.append("name")
|
||||
|
||||
if set(global_search_fields_before_update) != set(global_search_fields_after_update):
|
||||
now = (not frappe.request) or frappe.flags.in_test or frappe.flags.in_install
|
||||
now = (not frappe.request) or frappe.in_test or frappe.flags.in_install
|
||||
frappe.enqueue("frappe.utils.global_search.rebuild_for_doctype", now=now, doctype=self.name)
|
||||
|
||||
def set_base_class_for_controller(self):
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ def create_json_gz_file(data, dt, dn, report_name):
|
|||
frappe.scrub(report_name), frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H-M")
|
||||
)
|
||||
encoded_content = frappe.safe_encode(frappe.as_json(data, indent=None, separators=(",", ":")))
|
||||
compressed_content = gzip.compress(encoded_content)
|
||||
compressed_content = gzip.compress(encoded_content, compresslevel=5)
|
||||
|
||||
# Call save() file function to upload and attach the file
|
||||
_file = frappe.get_doc(
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class RoleProfile(Document):
|
|||
self.clear_cache()
|
||||
self.queue_action(
|
||||
"update_all_users",
|
||||
now=frappe.flags.in_test or frappe.flags.in_install,
|
||||
now=frappe.in_test or frappe.flags.in_install,
|
||||
enqueue_after_commit=True,
|
||||
queue="long",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -112,6 +112,16 @@ class RQJob(Document):
|
|||
except InvalidJobOperation:
|
||||
frappe.msgprint(_("Job is not running."), title=_("Invalid Operation"))
|
||||
|
||||
@check_permissions
|
||||
def cancel(self):
|
||||
if self.status == "queued":
|
||||
self.job.cancel()
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("Job is in {0} state and can't be cancelled").format(self.status),
|
||||
title=_("Invalid Operation"),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_count(filters=None) -> int:
|
||||
return len(RQJob.get_matching_job_ids(filters))
|
||||
|
|
|
|||
|
|
@ -26,6 +26,12 @@ def wait_for_completion(job: Job):
|
|||
class TestRQJob(IntegrationTestCase):
|
||||
BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func"
|
||||
|
||||
def setUp(self) -> None:
|
||||
# Cleanup all pending jobs
|
||||
for job in frappe.get_all("RQ Job", {"status": "queued"}):
|
||||
frappe.get_doc("RQ Job", job.name).cancel()
|
||||
return super().setUp()
|
||||
|
||||
def check_status(self, job: Job, status, wait=True):
|
||||
if wait:
|
||||
wait_for_completion(job)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import hashlib
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from functools import lru_cache
|
||||
|
||||
import click
|
||||
from croniter import CroniterBadCronError, croniter
|
||||
|
|
@ -14,6 +15,8 @@ from frappe.model.document import Document
|
|||
from frappe.utils import get_datetime, now_datetime
|
||||
from frappe.utils.background_jobs import enqueue, is_job_enqueued
|
||||
|
||||
parse_cron = lru_cache(croniter) # Cache parsed cron-expressions
|
||||
|
||||
|
||||
class ScheduledJobType(Document):
|
||||
# begin: auto-generated types
|
||||
|
|
@ -132,10 +135,10 @@ class ScheduledJobType(Document):
|
|||
# A dynamic fallback like current time might miss the scheduler interval and job will never start.
|
||||
last_execution = get_datetime(self.last_execution or self.creation)
|
||||
|
||||
next_execution = croniter(self.cron_format, last_execution).get_next(datetime)
|
||||
next_execution = parse_cron(self.cron_format).get_next(datetime, start_time=last_execution)
|
||||
if self.frequency in ("Hourly Maintenance", "Daily Maintenance"):
|
||||
next_execution += timedelta(minutes=maintenance_offset)
|
||||
return croniter(self.cron_format, last_execution).get_next(datetime)
|
||||
return parse_cron(self.cron_format).get_next(datetime, start_time=last_execution)
|
||||
|
||||
def execute(self):
|
||||
if frappe.job:
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from frappe.model.delete_doc import delete_doc
|
|||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.tests.classes.context_managers import change_settings
|
||||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
from frappe.tests.utils import toggle_test_mode
|
||||
from frappe.utils import get_url
|
||||
|
||||
user_module = frappe.core.doctype.user.user
|
||||
|
|
@ -212,13 +213,15 @@ class TestUser(IntegrationTestCase):
|
|||
|
||||
# test password strength while saving user with new password
|
||||
user = frappe.get_doc("User", "test@example.com")
|
||||
frappe.flags.in_test = False
|
||||
user.new_password = "password"
|
||||
self.assertRaises(frappe.exceptions.ValidationError, user.save)
|
||||
user.reload()
|
||||
user.new_password = "Eastern_43A1W"
|
||||
user.save()
|
||||
frappe.flags.in_test = True
|
||||
toggle_test_mode(False)
|
||||
try:
|
||||
user.new_password = "password"
|
||||
self.assertRaises(frappe.exceptions.ValidationError, user.save)
|
||||
user.reload()
|
||||
user.new_password = "Eastern_43A1W"
|
||||
user.save()
|
||||
finally:
|
||||
toggle_test_mode(True)
|
||||
|
||||
def test_comment_mentions(self):
|
||||
comment = """
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class User(Document):
|
|||
self.__new_password = self.new_password
|
||||
self.new_password = ""
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
self.password_strength_test()
|
||||
|
||||
if self.name not in STANDARD_USERS:
|
||||
|
|
@ -269,7 +269,7 @@ class User(Document):
|
|||
self.share_with_self()
|
||||
clear_notifications(user=self.name)
|
||||
frappe.clear_cache(user=self.name)
|
||||
now = frappe.flags.in_test or frappe.flags.in_install
|
||||
now = frappe.in_test or frappe.flags.in_install
|
||||
self.send_password_notification(self.__new_password)
|
||||
frappe.enqueue(
|
||||
"frappe.core.doctype.user.user.create_contact",
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ def get_db(socket=None, host=None, user=None, password=None, port=None, cur_db_n
|
|||
import frappe.database.sqlite.database
|
||||
|
||||
return frappe.database.sqlite.database.SQLiteDatabase(cur_db_name=cur_db_name)
|
||||
elif conf.use_mysqlclient:
|
||||
elif conf.get("use_mysqlclient", 1):
|
||||
import frappe.database.mariadb.mysqlclient
|
||||
|
||||
return frappe.database.mariadb.mysqlclient.MariaDBDatabase(
|
||||
|
|
|
|||
|
|
@ -1147,7 +1147,7 @@ class Database:
|
|||
mode = "READ ONLY" if read_only else ""
|
||||
self.sql(f"START TRANSACTION {mode}")
|
||||
|
||||
def commit(self):
|
||||
def commit(self, *, chain=False):
|
||||
"""Commit current transaction. Calls SQL `COMMIT`."""
|
||||
if self._disable_transaction_control:
|
||||
warnings.warn(message=TRANSACTION_DISABLED_MSG, stacklevel=2)
|
||||
|
|
@ -1158,12 +1158,15 @@ class Database:
|
|||
|
||||
self.before_commit.run()
|
||||
|
||||
self.sql("commit")
|
||||
self.begin() # explicitly start a new transaction
|
||||
if chain:
|
||||
self.sql("commit and chain")
|
||||
else:
|
||||
self.sql("commit")
|
||||
self.begin()
|
||||
|
||||
self.after_commit.run()
|
||||
|
||||
def rollback(self, *, save_point=None):
|
||||
def rollback(self, *, save_point=None, chain=False):
|
||||
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
|
||||
if save_point:
|
||||
self.sql(f"rollback to savepoint {save_point}")
|
||||
|
|
@ -1173,8 +1176,11 @@ class Database:
|
|||
|
||||
self.before_rollback.run()
|
||||
|
||||
self.sql("rollback")
|
||||
self.begin()
|
||||
if chain:
|
||||
self.sql("rollback and chain")
|
||||
else:
|
||||
self.sql("rollback")
|
||||
self.begin()
|
||||
|
||||
self.after_rollback.run()
|
||||
else:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -306,7 +306,7 @@ def read_multi_pdf(output) -> bytes:
|
|||
|
||||
|
||||
@deprecated("frappe.gzip_compress", "unknown", "v17", "Use py3 methods directly (this was compat for py2).")
|
||||
def gzip_compress(data, compresslevel=9):
|
||||
def gzip_compress(data, compresslevel=5):
|
||||
"""Compress data in one shot and return the compressed string.
|
||||
Optional argument is the compression level, in range of 0-9.
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@
|
|||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2025-04-10 13:08:32.540745",
|
||||
"modified": "2025-06-17 15:31:01.945146",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
|
|
@ -310,7 +310,7 @@
|
|||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"role": "Desk User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
|
|
@ -326,6 +326,15 @@
|
|||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
|
|
|
|||
|
|
@ -94,8 +94,8 @@ def enqueue_create_notification(users: list[str] | str, doc: dict):
|
|||
"frappe.desk.doctype.notification_log.notification_log.make_notification_logs",
|
||||
doc=doc,
|
||||
users=users,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=not frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
enqueue_after_commit=not frappe.in_test,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ def send_notification_email(doc: NotificationLog):
|
|||
template="new_notification",
|
||||
args=args,
|
||||
header=[header, "orange"],
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class SystemConsole(Document):
|
|||
frappe.db.commit()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def execute_code(doc):
|
||||
console = frappe.get_doc(json.loads(doc))
|
||||
console.run()
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ def health_check(step: str):
|
|||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
raise
|
||||
frappe.log(frappe.get_traceback())
|
||||
# nosemgrep
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@ def disable_saving_as_public():
|
|||
frappe.flags.in_install
|
||||
or frappe.flags.in_uninstall
|
||||
or frappe.flags.in_patch
|
||||
or frappe.flags.in_test
|
||||
or frappe.in_test
|
||||
or frappe.flags.in_fixtures
|
||||
or frappe.flags.in_migrate
|
||||
)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ def getdoc(doctype, name):
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def getdoctype(doctype, with_parent=False, cached_timestamp=None):
|
||||
def getdoctype(doctype, with_parent=False):
|
||||
"""load doctype"""
|
||||
|
||||
docs = []
|
||||
|
|
@ -75,9 +75,6 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
|
|||
|
||||
frappe.response["user_settings"] = get_user_settings(parent_dt or doctype)
|
||||
|
||||
if cached_timestamp and docs[0].modified == cached_timestamp:
|
||||
return "use_cache"
|
||||
|
||||
frappe.response.docs.extend(docs)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,6 @@ ASSET_KEYS = (
|
|||
"__css",
|
||||
"__list_js",
|
||||
"__calendar_js",
|
||||
"__map_js",
|
||||
"__linked_with",
|
||||
"__messages",
|
||||
"__print_formats",
|
||||
"__workflow_docs",
|
||||
"__form_grid_templates",
|
||||
|
|
@ -60,9 +57,6 @@ class FormMeta(Meta):
|
|||
if self.get("__assets_loaded", False):
|
||||
return
|
||||
|
||||
self.add_search_fields()
|
||||
self.add_linked_document_type()
|
||||
|
||||
if not self.istable:
|
||||
self.add_code()
|
||||
self.add_custom_script()
|
||||
|
|
@ -77,15 +71,10 @@ class FormMeta(Meta):
|
|||
|
||||
def as_dict(self, no_nulls=False):
|
||||
d = super().as_dict(no_nulls=no_nulls)
|
||||
__dict = self.__dict__
|
||||
|
||||
for k in ASSET_KEYS:
|
||||
d[k] = self.get(k)
|
||||
|
||||
# d['fields'] = d.get('fields', [])
|
||||
|
||||
for i, df in enumerate(d.get("fields") or []):
|
||||
for k in ("search_fields", "is_custom_field", "linked_document_type"):
|
||||
df[k] = self.get("fields")[i].get(k)
|
||||
d[k] = __dict.get(k)
|
||||
|
||||
return d
|
||||
|
||||
|
|
@ -186,19 +175,6 @@ class FormMeta(Meta):
|
|||
self.set("__custom_js", form_script)
|
||||
self.set("__custom_list_js", list_script)
|
||||
|
||||
def add_search_fields(self):
|
||||
"""add search fields found in the doctypes indicated by link fields' options"""
|
||||
for df in self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]}):
|
||||
if df.options:
|
||||
try:
|
||||
search_fields = frappe.get_meta(df.options).search_fields
|
||||
except frappe.DoesNotExistError:
|
||||
self._show_missing_doctype_msg(df)
|
||||
|
||||
if search_fields:
|
||||
search_fields = search_fields.split(",")
|
||||
df.search_fields = [sf.strip() for sf in search_fields]
|
||||
|
||||
def _show_missing_doctype_msg(self, df):
|
||||
# A link field is referring to non-existing doctype, this usually happens when
|
||||
# customizations are removed or some custom app is removed but hasn't cleaned
|
||||
|
|
@ -217,14 +193,6 @@ class FormMeta(Meta):
|
|||
|
||||
frappe.throw(msg, title=_("Missing DocType"))
|
||||
|
||||
def add_linked_document_type(self):
|
||||
for df in self.get("fields", {"fieldtype": "Link"}):
|
||||
if df.options:
|
||||
try:
|
||||
df.linked_document_type = frappe.get_meta(df.options).document_type
|
||||
except frappe.DoesNotExistError:
|
||||
self._show_missing_doctype_msg(df)
|
||||
|
||||
def load_print_formats(self):
|
||||
print_formats = frappe.db.sql(
|
||||
"""select * FROM `tabPrint Format`
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from frappe.utils.scheduler import is_scheduler_inactive
|
|||
from frappe.utils.telemetry import capture_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def savedocs(doc, action):
|
||||
"""save / submit / update doclist"""
|
||||
doc = frappe.get_doc(json.loads(doc))
|
||||
|
|
@ -51,7 +51,7 @@ def savedocs(doc, action):
|
|||
frappe.msgprint(frappe._(status_message), indicator="green", alert=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_state=None):
|
||||
"""cancel a doclist"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
|
@ -64,7 +64,7 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat
|
|||
frappe.msgprint(frappe._("Cancelled"), indicator="red", alert=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def discard(doctype: str, name: str | int):
|
||||
"""discard a draft document"""
|
||||
doc = frappe.get_doc(doctype, name)
|
||||
|
|
@ -79,7 +79,7 @@ def send_updated_docs(doc):
|
|||
from .load import get_docinfo
|
||||
|
||||
get_docinfo(doc)
|
||||
|
||||
doc.apply_fieldlevel_read_permissions()
|
||||
d = doc.as_dict()
|
||||
if hasattr(doc, "localname"):
|
||||
d["localname"] = doc.localname
|
||||
|
|
|
|||
|
|
@ -244,7 +244,7 @@ def update_system_settings(args): # nosemgrep
|
|||
"date_format": frappe.db.get_value("Country", args.get("country"), "date_format"),
|
||||
"time_format": frappe.db.get_value("Country", args.get("country"), "time_format"),
|
||||
"number_format": number_format,
|
||||
"enable_scheduler": 1 if not frappe.flags.in_test else 0,
|
||||
"enable_scheduler": 1 if not frappe.in_test else 0,
|
||||
"backup_limit": 3, # Default for downloadable backups
|
||||
"enable_telemetry": cint(args.get("enable_telemetry")),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -315,7 +315,7 @@ def compress(data, args=None):
|
|||
return {"keys": keys, "values": values, "user_info": user_info}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "PUT"])
|
||||
def save_report(name, doctype, report_settings):
|
||||
"""Save reports of type Report Builder from Report View"""
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ def save_report(name, doctype, report_settings):
|
|||
return report.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "DELETE"])
|
||||
def delete_report(name):
|
||||
"""Delete reports of type Report Builder from Report View"""
|
||||
|
||||
|
|
@ -555,7 +555,7 @@ def parse_field(field: str) -> tuple[str | None, str]:
|
|||
return None, key.strip("`")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.whitelist(methods=["POST", "DELETE"])
|
||||
def delete_items():
|
||||
"""delete selected items"""
|
||||
import json
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class EmailAccount(Document):
|
|||
if self.enable_incoming and self.use_imap and len(self.imap_folder) <= 0:
|
||||
frappe.throw(_("You need to set one IMAP folder for {0}").format(frappe.bold(self.email_id)))
|
||||
|
||||
if frappe.local.flags.in_patch or frappe.local.flags.in_test:
|
||||
if frappe.local.flags.in_patch or frappe.in_test:
|
||||
return
|
||||
|
||||
use_oauth = self.auth_method == "OAuth"
|
||||
|
|
@ -363,9 +363,7 @@ class EmailAccount(Document):
|
|||
|
||||
@property
|
||||
def _password(self):
|
||||
raise_exception = not (
|
||||
self.auth_method == "OAuth" or self.no_smtp_authentication or frappe.flags.in_test
|
||||
)
|
||||
raise_exception = not (self.auth_method == "OAuth" or self.no_smtp_authentication or frappe.in_test)
|
||||
return self.get_password(raise_exception=raise_exception)
|
||||
|
||||
@property
|
||||
|
|
@ -565,7 +563,7 @@ class EmailAccount(Document):
|
|||
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
|
||||
|
||||
def _disable_broken_incoming_account(self, description):
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
return
|
||||
self.db_set("enable_incoming", 0)
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ class EmailDomain(Document):
|
|||
def validate(self):
|
||||
"""Validate POP3/IMAP and SMTP connections."""
|
||||
|
||||
if frappe.local.flags.in_patch or frappe.local.flags.in_test or frappe.local.flags.in_install:
|
||||
if frappe.local.flags.in_patch or frappe.in_test or frappe.local.flags.in_install:
|
||||
return
|
||||
|
||||
self.validate_incoming_server_conn()
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ class EmailQueue(Document):
|
|||
message = ctx.build_message(recipient.recipient)
|
||||
if method := get_hook_method("override_email_send"):
|
||||
method(self, self.sender, recipient.recipient, message)
|
||||
elif not frappe.flags.in_test or frappe.flags.testing_email:
|
||||
elif not frappe.in_test or frappe.flags.testing_email:
|
||||
if ctx.email_account_doc.service == "Frappe Mail":
|
||||
is_newsletter = self.reference_doctype == "Newsletter"
|
||||
ctx.frappe_mail_client.send_raw(
|
||||
|
|
@ -200,7 +200,7 @@ class EmailQueue(Document):
|
|||
|
||||
ctx.update_recipient_status_to_sent(recipient)
|
||||
|
||||
if frappe.flags.in_test and not frappe.flags.testing_email:
|
||||
if frappe.in_test and not frappe.flags.testing_email:
|
||||
frappe.flags.sent_mail = message
|
||||
return
|
||||
|
||||
|
|
@ -773,7 +773,7 @@ class QueueBuilder:
|
|||
job_name=frappe.utils.get_job_name(
|
||||
"send_bulk_emails_for", self.reference_doctype, self.reference_name
|
||||
),
|
||||
now=frappe.flags.in_test or send_now,
|
||||
now=frappe.in_test or send_now,
|
||||
queue="long",
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@ class Newsletter(WebsiteGenerator):
|
|||
args["message"] = self.get_message(medium="email")
|
||||
|
||||
is_auto_commit_set = bool(frappe.db.auto_commit_on_many_writes)
|
||||
frappe.db.auto_commit_on_many_writes = not frappe.flags.in_test
|
||||
frappe.db.auto_commit_on_many_writes = not frappe.in_test
|
||||
|
||||
frappe.sendmail(
|
||||
subject=self.subject,
|
||||
|
|
@ -421,7 +421,7 @@ def send_scheduled_email():
|
|||
frappe.db.set_value("Newsletter", newsletter_name, "email_sent", 0)
|
||||
newsletter.log_error("Failed to send newsletter")
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
frappe.flags.is_scheduler_running = False
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ def get_context(context):
|
|||
"frappe.email.doctype.notification.notification.evaluate_alert",
|
||||
doc=doc,
|
||||
alert=self,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
enqueue_after_commit=enqueue_after_commit,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ def get_unsubcribed_url(reference_doctype, reference_name, email, unsubscribe_me
|
|||
@frappe.whitelist(allow_guest=True)
|
||||
def unsubscribe(doctype, name, email):
|
||||
# unsubsribe from comments and communications
|
||||
if not frappe.flags.in_test and not verify_request():
|
||||
if not frappe.in_test and not verify_request():
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -219,6 +219,9 @@ class EmailServer:
|
|||
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
|
||||
frappe.db.set_value("Email Account", self.settings.email_account, "uidnext", uidnext)
|
||||
|
||||
if uid_validity is None:
|
||||
frappe.flags.initial_sync = True
|
||||
|
||||
if not uid_validity or uid_validity != current_uid_validity:
|
||||
# uidvalidity changed & all email uids are reindexed by server
|
||||
frappe.db.set_value(
|
||||
|
|
@ -632,7 +635,7 @@ class InboundMail(Email):
|
|||
def process(self):
|
||||
"""Create communication record from email."""
|
||||
if self.is_sender_same_as_receiver() and not self.is_reply():
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
print("WARN: Cannot pull email. Sender same as recipient inbox")
|
||||
raise SentEmailInInboxError
|
||||
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ class SMTPServer:
|
|||
frappe.request.after_response.add(self.quit)
|
||||
elif frappe.job:
|
||||
frappe.job.after_job.add(self.quit)
|
||||
elif not frappe.flags.in_test:
|
||||
elif not frappe.in_test:
|
||||
# Console?
|
||||
import atexit
|
||||
|
||||
|
|
|
|||
|
|
@ -248,31 +248,21 @@ scheduler_events = {
|
|||
],
|
||||
"daily_long": [],
|
||||
"daily_maintenance": [
|
||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_daily",
|
||||
"frappe.integrations.doctype.google_drive.google_drive.daily_backup",
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_daily",
|
||||
"frappe.desk.notifications.clear_notifications",
|
||||
"frappe.sessions.clear_expired_sessions",
|
||||
"frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.remove_unverified_record",
|
||||
"frappe.integrations.doctype.google_contacts.google_contacts.sync",
|
||||
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
|
||||
"frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
|
||||
],
|
||||
"weekly_long": [
|
||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_weekly",
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_weekly",
|
||||
"frappe.desk.form.document_follow.send_weekly_updates",
|
||||
"frappe.utils.change_log.check_for_update",
|
||||
"frappe.integrations.doctype.google_drive.google_drive.weekly_backup",
|
||||
"frappe.desk.doctype.changelog_feed.changelog_feed.fetch_changelog_feed",
|
||||
],
|
||||
"monthly": [
|
||||
"frappe.email.doctype.auto_email_report.auto_email_report.send_monthly",
|
||||
],
|
||||
"monthly_long": [
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_monthly"
|
||||
],
|
||||
}
|
||||
|
||||
sounds = [
|
||||
|
|
|
|||
|
|
@ -1,47 +0,0 @@
|
|||
// Copyright (c) 2016, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Dropbox Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display(
|
||||
["app_access_key", "app_secret_key"],
|
||||
!frm.doc.__onload?.dropbox_setup_via_site_config
|
||||
);
|
||||
frm.events.take_backup(frm);
|
||||
},
|
||||
|
||||
are_keys_present: function (frm) {
|
||||
return (
|
||||
(frm.doc.app_access_key && frm.doc.app_secret_key) ||
|
||||
frm.doc.__onload?.dropbox_setup_via_site_config
|
||||
);
|
||||
},
|
||||
|
||||
allow_dropbox_access: function (frm) {
|
||||
if (!frm.events.are_keys_present(frm)) {
|
||||
frappe.msgprint(__("App Access Key and/or Secret Key are not present."));
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.get_dropbox_authorize_url",
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
window.open(r.message.auth_url);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
take_backup: function (frm) {
|
||||
if (frm.doc.enabled && (frm.doc.dropbox_refresh_token || frm.doc.dropbox_access_token)) {
|
||||
frm.add_custom_button(__("Take Backup Now"), function () {
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup",
|
||||
freeze: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2016-09-21 10:12:57.399174",
|
||||
"doctype": "DocType",
|
||||
"document_type": "System",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"send_notifications_to",
|
||||
"send_email_for_successful_backup",
|
||||
"backup_frequency",
|
||||
"limit_no_of_backups",
|
||||
"no_of_backups",
|
||||
"file_backup",
|
||||
"app_access_key",
|
||||
"app_secret_key",
|
||||
"allow_dropbox_access",
|
||||
"dropbox_refresh_token",
|
||||
"dropbox_access_token"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "send_notifications_to",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Send Notifications To",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Note: By default emails for failed backups are sent.",
|
||||
"fieldname": "send_email_for_successful_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful Backup"
|
||||
},
|
||||
{
|
||||
"fieldname": "backup_frequency",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Backup Frequency",
|
||||
"options": "\nDaily\nWeekly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "limit_no_of_backups",
|
||||
"fieldtype": "Check",
|
||||
"label": "Limit Number of DB Backups"
|
||||
},
|
||||
{
|
||||
"default": "5",
|
||||
"depends_on": "eval:doc.limit_no_of_backups",
|
||||
"fieldname": "no_of_backups",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of DB Backups"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "file_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "File Backup"
|
||||
},
|
||||
{
|
||||
"fieldname": "app_access_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "App Access Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "app_secret_key",
|
||||
"fieldtype": "Password",
|
||||
"label": "App Secret Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "allow_dropbox_access",
|
||||
"fieldtype": "Button",
|
||||
"label": "Allow Dropbox Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "dropbox_refresh_token",
|
||||
"fieldtype": "Password",
|
||||
"hidden": 1,
|
||||
"label": "Dropbox Refresh Token",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "dropbox_access_token",
|
||||
"fieldtype": "Password",
|
||||
"hidden": 1,
|
||||
"label": "Dropbox Access Token"
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:23.176690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Dropbox Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"read_only": 1,
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,378 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import dropbox
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.offsite_backup_utils import (
|
||||
get_chunk_site,
|
||||
get_latest_backup_file,
|
||||
send_email,
|
||||
validate_file_size,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, encode, get_backups_path, get_files_path, get_request_site_address
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.backups import new_backup
|
||||
|
||||
ignore_list = [".DS_Store"]
|
||||
|
||||
|
||||
class DropboxSettings(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
app_access_key: DF.Data | None
|
||||
app_secret_key: DF.Password | None
|
||||
backup_frequency: DF.Literal["", "Daily", "Weekly"]
|
||||
dropbox_access_token: DF.Password | None
|
||||
dropbox_refresh_token: DF.Password | None
|
||||
enabled: DF.Check
|
||||
file_backup: DF.Check
|
||||
limit_no_of_backups: DF.Check
|
||||
no_of_backups: DF.Int
|
||||
send_email_for_successful_backup: DF.Check
|
||||
send_notifications_to: DF.Data
|
||||
# end: auto-generated types
|
||||
|
||||
def onload(self):
|
||||
if not self.app_access_key and frappe.conf.dropbox_access_key:
|
||||
self.set_onload("dropbox_setup_via_site_config", 1)
|
||||
|
||||
def validate(self):
|
||||
if self.enabled and self.limit_no_of_backups and self.no_of_backups < 1:
|
||||
frappe.throw(_("Number of DB backups cannot be less than 1"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backup():
|
||||
"""Enqueue longjob for taking backup to dropbox"""
|
||||
enqueue(
|
||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup_to_dropbox",
|
||||
queue="long",
|
||||
timeout=1500,
|
||||
)
|
||||
frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour."))
|
||||
|
||||
|
||||
def take_backups_daily():
|
||||
take_backups_if("Daily")
|
||||
|
||||
|
||||
def take_backups_weekly():
|
||||
take_backups_if("Weekly")
|
||||
|
||||
|
||||
def take_backups_if(freq):
|
||||
if frappe.db.get_single_value("Dropbox Settings", "backup_frequency") == freq:
|
||||
take_backup_to_dropbox()
|
||||
|
||||
|
||||
def take_backup_to_dropbox(retry_count=0, upload_db_backup=True):
|
||||
did_not_upload, error_log = [], []
|
||||
try:
|
||||
if cint(frappe.db.get_single_value("Dropbox Settings", "enabled")):
|
||||
validate_file_size()
|
||||
|
||||
did_not_upload, error_log = backup_to_dropbox(upload_db_backup)
|
||||
if did_not_upload:
|
||||
raise Exception
|
||||
|
||||
if cint(frappe.db.get_single_value("Dropbox Settings", "send_email_for_successful_backup")):
|
||||
send_email(True, "Dropbox", "Dropbox Settings", "send_notifications_to")
|
||||
except JobTimeoutException:
|
||||
if retry_count < 2:
|
||||
args = {
|
||||
"retry_count": retry_count + 1,
|
||||
"upload_db_backup": False, # considering till worker timeout db backup is uploaded
|
||||
}
|
||||
enqueue(
|
||||
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backup_to_dropbox",
|
||||
queue="long",
|
||||
timeout=1500,
|
||||
**args,
|
||||
)
|
||||
except Exception:
|
||||
if isinstance(error_log, str):
|
||||
error_message = error_log + "\n" + frappe.get_traceback()
|
||||
else:
|
||||
file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log, strict=False)]
|
||||
error_message = "\n".join(file_and_error) + "\n" + frappe.get_traceback()
|
||||
|
||||
send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message)
|
||||
|
||||
|
||||
def backup_to_dropbox(upload_db_backup=True):
|
||||
# upload database
|
||||
dropbox_settings = get_dropbox_settings()
|
||||
dropbox_client = get_dropbox_client(dropbox_settings)
|
||||
|
||||
if upload_db_backup:
|
||||
if frappe.flags.create_new_backup:
|
||||
backup = new_backup(ignore_files=True)
|
||||
filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
|
||||
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
|
||||
else:
|
||||
filename, site_config = get_latest_backup_file()
|
||||
|
||||
upload_file_to_dropbox(filename, "/database", dropbox_client)
|
||||
upload_file_to_dropbox(site_config, "/database", dropbox_client)
|
||||
|
||||
# delete older databases
|
||||
if dropbox_settings["no_of_backups"]:
|
||||
delete_older_backups(dropbox_client, "/database", dropbox_settings["no_of_backups"])
|
||||
|
||||
# upload files to files folder
|
||||
did_not_upload = []
|
||||
error_log = []
|
||||
|
||||
if dropbox_settings["file_backup"]:
|
||||
upload_from_folder(get_files_path(), 0, "/files", dropbox_client, did_not_upload, error_log)
|
||||
upload_from_folder(
|
||||
get_files_path(is_private=1), 1, "/private/files", dropbox_client, did_not_upload, error_log
|
||||
)
|
||||
|
||||
return did_not_upload, list(set(error_log))
|
||||
|
||||
|
||||
def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not_upload, error_log):
|
||||
if not os.path.exists(path):
|
||||
return
|
||||
|
||||
if is_fresh_upload():
|
||||
response = get_uploaded_files_meta(dropbox_folder, dropbox_client)
|
||||
else:
|
||||
response = frappe._dict({"entries": []})
|
||||
|
||||
path = str(path)
|
||||
|
||||
for f in frappe.get_all(
|
||||
"File",
|
||||
filters={"is_folder": 0, "is_private": is_private, "uploaded_to_dropbox": 0},
|
||||
fields=["file_url", "name", "file_name"],
|
||||
):
|
||||
if not f.file_url:
|
||||
continue
|
||||
filename = f.file_url.rsplit("/", 1)[-1]
|
||||
|
||||
filepath = os.path.join(path, filename)
|
||||
|
||||
if filename in ignore_list:
|
||||
continue
|
||||
|
||||
found = False
|
||||
for file_metadata in response.entries:
|
||||
try:
|
||||
if os.path.basename(filepath) == file_metadata.name and os.stat(
|
||||
encode(filepath)
|
||||
).st_size == int(file_metadata.size):
|
||||
found = True
|
||||
update_file_dropbox_status(f.name)
|
||||
break
|
||||
except Exception:
|
||||
error_log.append(frappe.get_traceback())
|
||||
|
||||
if not found:
|
||||
try:
|
||||
upload_file_to_dropbox(filepath, dropbox_folder, dropbox_client)
|
||||
update_file_dropbox_status(f.name)
|
||||
except Exception:
|
||||
did_not_upload.append(filepath)
|
||||
error_log.append(frappe.get_traceback())
|
||||
|
||||
|
||||
def upload_file_to_dropbox(filename, folder, dropbox_client):
|
||||
"""upload files with chunk of 15 mb to reduce session append calls"""
|
||||
if not os.path.exists(filename):
|
||||
return
|
||||
|
||||
create_folder_if_not_exists(folder, dropbox_client)
|
||||
file_size = os.path.getsize(encode(filename))
|
||||
chunk_size = get_chunk_site(file_size)
|
||||
|
||||
mode = dropbox.files.WriteMode.overwrite
|
||||
|
||||
f = open(encode(filename), "rb")
|
||||
path = f"{folder}/{os.path.basename(filename)}"
|
||||
|
||||
try:
|
||||
if file_size <= chunk_size:
|
||||
dropbox_client.files_upload(f.read(), path, mode)
|
||||
else:
|
||||
upload_session_start_result = dropbox_client.files_upload_session_start(f.read(chunk_size))
|
||||
cursor = dropbox.files.UploadSessionCursor(
|
||||
session_id=upload_session_start_result.session_id, offset=f.tell()
|
||||
)
|
||||
commit = dropbox.files.CommitInfo(path=path, mode=mode)
|
||||
|
||||
while f.tell() < file_size:
|
||||
if (file_size - f.tell()) <= chunk_size:
|
||||
dropbox_client.files_upload_session_finish(f.read(chunk_size), cursor, commit)
|
||||
else:
|
||||
dropbox_client.files_upload_session_append(
|
||||
f.read(chunk_size), cursor.session_id, cursor.offset
|
||||
)
|
||||
cursor.offset = f.tell()
|
||||
except dropbox.exceptions.ApiError as e:
|
||||
if isinstance(e.error, dropbox.files.UploadError):
|
||||
error = f"File Path: {path}\n"
|
||||
error += frappe.get_traceback()
|
||||
frappe.log_error(error)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def create_folder_if_not_exists(folder, dropbox_client):
|
||||
try:
|
||||
dropbox_client.files_get_metadata(folder)
|
||||
except dropbox.exceptions.ApiError as e:
|
||||
# folder not found
|
||||
if isinstance(e.error, dropbox.files.GetMetadataError):
|
||||
dropbox_client.files_create_folder(folder)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
def update_file_dropbox_status(file_name):
|
||||
frappe.db.set_value("File", file_name, "uploaded_to_dropbox", 1, update_modified=False)
|
||||
|
||||
|
||||
def is_fresh_upload():
|
||||
file_name = frappe.db.get_value("File", {"uploaded_to_dropbox": 1}, "name")
|
||||
return not file_name
|
||||
|
||||
|
||||
def get_uploaded_files_meta(dropbox_folder, dropbox_client):
|
||||
try:
|
||||
return dropbox_client.files_list_folder(dropbox_folder)
|
||||
except dropbox.exceptions.ApiError as e:
|
||||
# folder not found
|
||||
if isinstance(e.error, dropbox.files.ListFolderError):
|
||||
return frappe._dict({"entries": []})
|
||||
raise
|
||||
|
||||
|
||||
def get_dropbox_client(dropbox_settings):
|
||||
dropbox_client = dropbox.Dropbox(
|
||||
oauth2_access_token=dropbox_settings["access_token"],
|
||||
oauth2_refresh_token=dropbox_settings["refresh_token"],
|
||||
app_key=dropbox_settings["app_key"],
|
||||
app_secret=dropbox_settings["app_secret"],
|
||||
timeout=None,
|
||||
)
|
||||
|
||||
# checking if the access token has expired
|
||||
dropbox_client.files_list_folder("")
|
||||
if dropbox_settings["access_token"] != dropbox_client._oauth2_access_token:
|
||||
set_dropbox_token(dropbox_client._oauth2_access_token)
|
||||
|
||||
return dropbox_client
|
||||
|
||||
|
||||
def get_dropbox_settings(redirect_uri=False):
|
||||
# NOTE: access token is kept for legacy dropbox apps
|
||||
settings = frappe.get_doc("Dropbox Settings")
|
||||
app_details = {
|
||||
"app_key": settings.app_access_key or frappe.conf.dropbox_access_key,
|
||||
"app_secret": settings.get_password(fieldname="app_secret_key", raise_exception=False)
|
||||
if settings.app_secret_key
|
||||
else frappe.conf.dropbox_secret_key,
|
||||
"refresh_token": settings.get_password("dropbox_refresh_token", raise_exception=False),
|
||||
"access_token": settings.get_password("dropbox_access_token", raise_exception=False),
|
||||
"file_backup": settings.file_backup,
|
||||
"no_of_backups": settings.no_of_backups if settings.limit_no_of_backups else None,
|
||||
}
|
||||
|
||||
if redirect_uri:
|
||||
app_details.update(
|
||||
{
|
||||
"redirect_uri": get_request_site_address(True)
|
||||
+ "/api/method/frappe.integrations.doctype.dropbox_settings.dropbox_settings.dropbox_auth_finish"
|
||||
}
|
||||
)
|
||||
|
||||
if not (app_details["app_key"] and app_details["app_secret"]):
|
||||
raise Exception(_("Please set Dropbox access keys in site config or doctype"))
|
||||
|
||||
return app_details
|
||||
|
||||
|
||||
def delete_older_backups(dropbox_client, folder_path, to_keep):
|
||||
res = dropbox_client.files_list_folder(path=folder_path)
|
||||
files = [f for f in res.entries if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name]
|
||||
|
||||
if len(files) <= to_keep:
|
||||
return
|
||||
|
||||
files.sort(key=lambda item: item.client_modified, reverse=True)
|
||||
for f in files[to_keep:]:
|
||||
dropbox_client.files_delete(os.path.join(folder_path, f.name))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_dropbox_authorize_url():
|
||||
app_details = get_dropbox_settings(redirect_uri=True)
|
||||
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
|
||||
consumer_key=app_details["app_key"],
|
||||
redirect_uri=app_details["redirect_uri"],
|
||||
session={},
|
||||
csrf_token_session_key="dropbox-auth-csrf-token",
|
||||
consumer_secret=app_details["app_secret"],
|
||||
token_access_type="offline",
|
||||
)
|
||||
|
||||
auth_url = dropbox_oauth_flow.start()
|
||||
|
||||
return {"auth_url": auth_url, "args": parse_qs(urlparse(auth_url).query)}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def dropbox_auth_finish():
|
||||
app_details = get_dropbox_settings(redirect_uri=True)
|
||||
callback = frappe.form_dict
|
||||
close = '<p class="text-muted">' + _("Please close this window") + "</p>"
|
||||
|
||||
if not callback.state or not callback.code:
|
||||
frappe.respond_as_web_page(
|
||||
_("Dropbox Setup"),
|
||||
_("Illegal Access Token. Please try again") + close,
|
||||
indicator_color="red",
|
||||
http_status_code=frappe.AuthenticationError.http_status_code,
|
||||
)
|
||||
return
|
||||
|
||||
dropbox_oauth_flow = dropbox.DropboxOAuth2Flow(
|
||||
consumer_key=app_details["app_key"],
|
||||
redirect_uri=app_details["redirect_uri"],
|
||||
session={"dropbox-auth-csrf-token": callback.state},
|
||||
csrf_token_session_key="dropbox-auth-csrf-token",
|
||||
consumer_secret=app_details["app_secret"],
|
||||
)
|
||||
|
||||
token = dropbox_oauth_flow.finish({"state": callback.state, "code": callback.code})
|
||||
set_dropbox_token(token.access_token, token.refresh_token)
|
||||
|
||||
frappe.local.response["type"] = "redirect"
|
||||
frappe.local.response["location"] = "/app/dropbox-settings"
|
||||
|
||||
|
||||
def set_dropbox_token(access_token, refresh_token=None):
|
||||
# NOTE: used doc object instead of db.set_value so that password field is set properly
|
||||
dropbox_settings = frappe.get_single("Dropbox Settings")
|
||||
dropbox_settings.dropbox_access_token = access_token
|
||||
if refresh_token:
|
||||
dropbox_settings.dropbox_refresh_token = refresh_token
|
||||
|
||||
dropbox_settings.save()
|
||||
|
||||
frappe.db.commit()
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestDropboxSettings(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
// Copyright (c) 2019, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Google Drive", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.enable) {
|
||||
frm.dashboard.set_headline(
|
||||
__("To use Google Drive, enable {0}.", [
|
||||
`<a href='/app/google-settings'>${__("Google Settings")}</a>`,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
frappe.realtime.on("upload_to_google_drive", (data) => {
|
||||
if (data.progress) {
|
||||
const progress_title = __("Uploading to Google Drive");
|
||||
frm.dashboard.show_progress(
|
||||
progress_title,
|
||||
(data.progress / data.total) * 100,
|
||||
data.message
|
||||
);
|
||||
if (data.progress === data.total) {
|
||||
frm.dashboard.hide_progress(progress_title);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (frm.doc.enable && frm.doc.refresh_token) {
|
||||
let sync_button = frm.add_custom_button(__("Take Backup"), function () {
|
||||
frappe.show_alert({
|
||||
indicator: "green",
|
||||
message: __("Backing up to Google Drive."),
|
||||
});
|
||||
frappe
|
||||
.call({
|
||||
method: "frappe.integrations.doctype.google_drive.google_drive.take_backup",
|
||||
btn: sync_button,
|
||||
})
|
||||
.then((r) => {
|
||||
frappe.msgprint(r.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.enable && frm.doc.backup_folder_name && !frm.doc.refresh_token) {
|
||||
frm.dashboard.set_headline(
|
||||
__(
|
||||
"Click on <b>Authorize Google Drive Access</b> to authorize Google Drive Access."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (frm.doc.enable && frm.doc.refresh_token && frm.doc.authorization_code) {
|
||||
frm.page.set_indicator("Authorized", "green");
|
||||
}
|
||||
},
|
||||
authorize_google_drive_access: function (frm) {
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.google_drive.google_drive.authorize_access",
|
||||
args: {
|
||||
reauthorize: frm.doc.authorization_code ? 1 : 0,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frm.save();
|
||||
window.open(r.message.url);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2019-08-13 17:24:05.470876",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable",
|
||||
"google_drive_section",
|
||||
"backup_folder_name",
|
||||
"frequency",
|
||||
"email",
|
||||
"send_email_for_successful_backup",
|
||||
"file_backup",
|
||||
"authorize_google_drive_access",
|
||||
"column_break_5",
|
||||
"backup_folder_id",
|
||||
"last_backup_on",
|
||||
"refresh_token",
|
||||
"authorization_code"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable"
|
||||
},
|
||||
{
|
||||
"fieldname": "backup_folder_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Backup Folder Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "authorize_google_drive_access",
|
||||
"fieldtype": "Button",
|
||||
"label": "Authorize Google Drive Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "backup_folder_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Backup Folder ID",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Frequency",
|
||||
"options": "\nDaily\nWeekly",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "refresh_token",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Refresh Token"
|
||||
},
|
||||
{
|
||||
"fieldname": "authorization_code",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Authorization Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "last_backup_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Last Backup On",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Note: By default emails for failed backups are sent.",
|
||||
"fieldname": "send_email_for_successful_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful backup"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "file_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "File Backup"
|
||||
},
|
||||
{
|
||||
"depends_on": "enable",
|
||||
"fieldname": "google_drive_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Google Drive"
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Send Notification To",
|
||||
"options": "Email",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-23 16:03:26.999110",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "Google Drive",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,229 +0,0 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import os
|
||||
from urllib.parse import quote
|
||||
|
||||
from apiclient.http import MediaFileUpload
|
||||
from googleapiclient.errors import HttpError
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.google_oauth import GoogleOAuth
|
||||
from frappe.integrations.offsite_backup_utils import (
|
||||
get_latest_backup_file,
|
||||
send_email,
|
||||
validate_file_size,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_backups_path, get_bench_path
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.backups import new_backup
|
||||
|
||||
|
||||
class GoogleDrive(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
authorization_code: DF.Data | None
|
||||
backup_folder_id: DF.Data | None
|
||||
backup_folder_name: DF.Data
|
||||
email: DF.Data
|
||||
enable: DF.Check
|
||||
file_backup: DF.Check
|
||||
frequency: DF.Literal["", "Daily", "Weekly"]
|
||||
last_backup_on: DF.Datetime | None
|
||||
refresh_token: DF.Data | None
|
||||
send_email_for_successful_backup: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
doc_before_save = self.get_doc_before_save()
|
||||
if doc_before_save and doc_before_save.backup_folder_name != self.backup_folder_name:
|
||||
self.backup_folder_id = ""
|
||||
|
||||
def get_access_token(self):
|
||||
if not self.refresh_token:
|
||||
button_label = frappe.bold(_("Allow Google Drive Access"))
|
||||
raise frappe.ValidationError(_("Click on {0} to generate Refresh Token.").format(button_label))
|
||||
|
||||
oauth_obj = GoogleOAuth("drive")
|
||||
r = oauth_obj.refresh_access_token(
|
||||
self.get_password(fieldname="refresh_token", raise_exception=False)
|
||||
)
|
||||
|
||||
return r.get("access_token")
|
||||
|
||||
|
||||
@frappe.whitelist(methods=["POST"])
|
||||
def authorize_access(reauthorize=False, code=None):
|
||||
"""
|
||||
If no Authorization code get it from Google and then request for Refresh Token.
|
||||
Google Contact Name is set to flags to set_value after Authorization Code is obtained.
|
||||
"""
|
||||
|
||||
oauth_code = frappe.db.get_single_value("Google Drive", "authorization_code") if not code else code
|
||||
oauth_obj = GoogleOAuth("drive")
|
||||
|
||||
if not oauth_code or reauthorize:
|
||||
if reauthorize:
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", "")
|
||||
return oauth_obj.get_authentication_url(
|
||||
{
|
||||
"redirect": f"/app/Form/{quote('Google Drive')}",
|
||||
},
|
||||
)
|
||||
|
||||
r = oauth_obj.authorize(oauth_code)
|
||||
frappe.db.set_single_value(
|
||||
"Google Drive",
|
||||
{"authorization_code": oauth_code, "refresh_token": r.get("refresh_token")},
|
||||
)
|
||||
|
||||
|
||||
def get_google_drive_object():
|
||||
"""Return an object of Google Drive."""
|
||||
account = frappe.get_doc("Google Drive")
|
||||
oauth_obj = GoogleOAuth("drive")
|
||||
|
||||
google_drive = oauth_obj.get_google_service_object(
|
||||
account.get_access_token(),
|
||||
account.get_password(fieldname="indexing_refresh_token", raise_exception=False),
|
||||
)
|
||||
|
||||
return google_drive, account
|
||||
|
||||
|
||||
def check_for_folder_in_google_drive():
|
||||
"""Checks if folder exists in Google Drive else create it."""
|
||||
|
||||
def _create_folder_in_google_drive(google_drive, account):
|
||||
file_metadata = {
|
||||
"name": account.backup_folder_name,
|
||||
"mimeType": "application/vnd.google-apps.folder",
|
||||
}
|
||||
|
||||
try:
|
||||
folder = google_drive.files().create(body=file_metadata, fields="id").execute()
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", folder.get("id"))
|
||||
frappe.db.commit()
|
||||
except HttpError as e:
|
||||
frappe.throw(
|
||||
_("Google Drive - Could not create folder in Google Drive - Error Code {0}").format(e)
|
||||
)
|
||||
|
||||
google_drive, account = get_google_drive_object()
|
||||
|
||||
if account.backup_folder_id:
|
||||
return
|
||||
|
||||
backup_folder_exists = False
|
||||
|
||||
try:
|
||||
google_drive_folders = (
|
||||
google_drive.files().list(q="mimeType='application/vnd.google-apps.folder'").execute()
|
||||
)
|
||||
except HttpError as e:
|
||||
frappe.throw(_("Google Drive - Could not find folder in Google Drive - Error Code {0}").format(e))
|
||||
|
||||
for f in google_drive_folders.get("files"):
|
||||
if f.get("name") == account.backup_folder_name:
|
||||
frappe.db.set_single_value("Google Drive", "backup_folder_id", f.get("id"))
|
||||
frappe.db.commit()
|
||||
backup_folder_exists = True
|
||||
break
|
||||
|
||||
if not backup_folder_exists:
|
||||
_create_folder_in_google_drive(google_drive, account)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backup():
|
||||
"""Enqueue longjob for taking backup to Google Drive"""
|
||||
enqueue(
|
||||
"frappe.integrations.doctype.google_drive.google_drive.upload_system_backup_to_google_drive",
|
||||
queue="long",
|
||||
timeout=1500,
|
||||
)
|
||||
frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour."))
|
||||
|
||||
|
||||
def upload_system_backup_to_google_drive():
|
||||
"""
|
||||
Upload system backup to Google Drive
|
||||
"""
|
||||
# Get Google Drive Object
|
||||
google_drive, account = get_google_drive_object()
|
||||
|
||||
# Check if folder exists in Google Drive
|
||||
check_for_folder_in_google_drive()
|
||||
account.load_from_db()
|
||||
|
||||
validate_file_size()
|
||||
|
||||
if frappe.flags.create_new_backup:
|
||||
set_progress(1, _("Backing up Data."))
|
||||
backup = new_backup()
|
||||
file_urls = []
|
||||
file_urls.append(backup.backup_path_db)
|
||||
file_urls.append(backup.backup_path_conf)
|
||||
|
||||
if account.file_backup:
|
||||
file_urls.append(backup.backup_path_files)
|
||||
file_urls.append(backup.backup_path_private_files)
|
||||
else:
|
||||
file_urls = get_latest_backup_file(with_files=account.file_backup)
|
||||
|
||||
for fileurl in file_urls:
|
||||
if not fileurl:
|
||||
continue
|
||||
|
||||
file_metadata = {"name": os.path.basename(fileurl), "parents": [account.backup_folder_id]}
|
||||
|
||||
try:
|
||||
media = MediaFileUpload(
|
||||
get_absolute_path(filename=fileurl), mimetype="application/gzip", resumable=True
|
||||
)
|
||||
except OSError as e:
|
||||
frappe.throw(_("Google Drive - Could not locate - {0}").format(e))
|
||||
|
||||
try:
|
||||
set_progress(2, _("Uploading backup to Google Drive."))
|
||||
google_drive.files().create(body=file_metadata, media_body=media, fields="id").execute()
|
||||
except HttpError as e:
|
||||
send_email(False, "Google Drive", "Google Drive", "email", error_status=e)
|
||||
|
||||
set_progress(3, _("Uploading successful."))
|
||||
frappe.db.set_single_value("Google Drive", "last_backup_on", frappe.utils.now_datetime())
|
||||
send_email(True, "Google Drive", "Google Drive", "email")
|
||||
return _("Google Drive Backup Successful.")
|
||||
|
||||
|
||||
def daily_backup():
|
||||
drive_settings = frappe.db.get_singles_dict("Google Drive", cast=True)
|
||||
if drive_settings.enable and drive_settings.frequency == "Daily":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
|
||||
def weekly_backup():
|
||||
drive_settings = frappe.db.get_singles_dict("Google Drive", cast=True)
|
||||
if drive_settings.enable and drive_settings.frequency == "Weekly":
|
||||
upload_system_backup_to_google_drive()
|
||||
|
||||
|
||||
def get_absolute_path(filename):
|
||||
file_path = os.path.join(get_backups_path()[2:], os.path.basename(filename))
|
||||
return f"{get_bench_path()}/sites/{file_path}"
|
||||
|
||||
|
||||
def set_progress(progress, message):
|
||||
frappe.publish_realtime(
|
||||
"upload_to_google_drive",
|
||||
dict(progress=progress, total=3, message=message),
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestGoogleDrive(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Copyright (c) 2017, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("S3 Backup Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.clear_custom_buttons();
|
||||
frm.events.take_backup(frm);
|
||||
},
|
||||
|
||||
take_backup: function (frm) {
|
||||
if (frm.doc.access_key_id && frm.doc.secret_access_key) {
|
||||
frm.add_custom_button(__("Take Backup Now"), function () {
|
||||
frm.dashboard.set_headline_alert("S3 Backup Started!");
|
||||
frappe.call({
|
||||
method: "frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
frappe.msgprint(__("S3 Backup complete!"));
|
||||
frm.dashboard.clear_headline();
|
||||
}
|
||||
},
|
||||
});
|
||||
}).addClass("btn-primary");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
{
|
||||
"actions": [],
|
||||
"creation": "2017-09-04 20:57:20.129205",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enabled",
|
||||
"api_access_section",
|
||||
"access_key_id",
|
||||
"column_break_4",
|
||||
"secret_access_key",
|
||||
"notification_section",
|
||||
"notify_email",
|
||||
"column_break_8",
|
||||
"send_email_for_successful_backup",
|
||||
"s3_bucket_details_section",
|
||||
"bucket",
|
||||
"endpoint_url",
|
||||
"column_break_13",
|
||||
"backup_path",
|
||||
"backup_details_section",
|
||||
"frequency",
|
||||
"backup_files"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Automatic Backup"
|
||||
},
|
||||
{
|
||||
"fieldname": "notify_email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Send Notifications To",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "By default, emails are only sent for failed backups.",
|
||||
"fieldname": "send_email_for_successful_backup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send Email for Successful Backup"
|
||||
},
|
||||
{
|
||||
"fieldname": "frequency",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Backup Frequency",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"options": "Daily\nWeekly\nMonthly\nNone",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "access_key_id",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Access Key ID",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "secret_access_key",
|
||||
"fieldtype": "Password",
|
||||
"in_list_view": 1,
|
||||
"label": "Access Key Secret",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "https://s3.amazonaws.com",
|
||||
"description": "Only change this if you want to use other S3 compatible object storage backends.",
|
||||
"fieldname": "endpoint_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Endpoint URL"
|
||||
},
|
||||
{
|
||||
"fieldname": "bucket",
|
||||
"fieldtype": "Data",
|
||||
"label": "Bucket Name",
|
||||
"mandatory_depends_on": "enabled",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "api_access_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "API Access"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "notification_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notification"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_8",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "s3_bucket_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "S3 Bucket Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "enabled",
|
||||
"fieldname": "backup_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Backup Details"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "Backup public and private files along with the database.",
|
||||
"fieldname": "backup_files",
|
||||
"fieldtype": "Check",
|
||||
"label": "Backup Files"
|
||||
},
|
||||
{
|
||||
"description": "If it's empty, it will backup to the root of the bucket.",
|
||||
"fieldname": "backup_path",
|
||||
"fieldtype": "Data",
|
||||
"label": "Backup Path"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-03-15 12:17:49.167012",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Integrations",
|
||||
"name": "S3 Backup Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
# Copyright (c) 2017, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
import os
|
||||
import os.path
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import ClientError
|
||||
from rq.timeouts import JobTimeoutException
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.integrations.offsite_backup_utils import (
|
||||
generate_files_backup,
|
||||
get_latest_backup_file,
|
||||
send_email,
|
||||
validate_file_size,
|
||||
)
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
|
||||
class S3BackupSettings(Document):
|
||||
# begin: auto-generated types
|
||||
# This code is auto-generated. Do not modify anything in this block.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
access_key_id: DF.Data
|
||||
backup_files: DF.Check
|
||||
backup_path: DF.Data | None
|
||||
bucket: DF.Data
|
||||
enabled: DF.Check
|
||||
endpoint_url: DF.Data | None
|
||||
frequency: DF.Literal["Daily", "Weekly", "Monthly", "None"]
|
||||
notify_email: DF.Data
|
||||
secret_access_key: DF.Password
|
||||
send_email_for_successful_backup: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
|
||||
if not self.endpoint_url:
|
||||
self.endpoint_url = "https://s3.amazonaws.com"
|
||||
|
||||
if self.backup_path and self.backup_path[-1] != "/":
|
||||
self.backup_path += "/"
|
||||
|
||||
conn = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=self.access_key_id,
|
||||
aws_secret_access_key=self.get_password("secret_access_key"),
|
||||
endpoint_url=self.endpoint_url,
|
||||
)
|
||||
|
||||
try:
|
||||
# Head_bucket returns a 200 OK if the bucket exists and have access to it.
|
||||
# Requires ListBucket permission
|
||||
conn.head_bucket(Bucket=self.bucket)
|
||||
except ClientError as e:
|
||||
error_code = e.response["Error"]["Code"]
|
||||
bucket_name = frappe.bold(self.bucket)
|
||||
if error_code == "403":
|
||||
msg = _("Do not have permission to access bucket {0}.").format(bucket_name)
|
||||
elif error_code == "404":
|
||||
msg = _("Bucket {0} not found.").format(bucket_name)
|
||||
else:
|
||||
msg = e.args[0]
|
||||
|
||||
frappe.throw(msg)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backup():
|
||||
"""Enqueue longjob for taking backup to s3"""
|
||||
enqueue(
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
|
||||
queue="long",
|
||||
timeout=1500,
|
||||
)
|
||||
frappe.msgprint(_("Queued for backup. It may take a few minutes to an hour."))
|
||||
|
||||
|
||||
def take_backups_daily():
|
||||
take_backups_if("Daily")
|
||||
|
||||
|
||||
def take_backups_weekly():
|
||||
take_backups_if("Weekly")
|
||||
|
||||
|
||||
def take_backups_monthly():
|
||||
take_backups_if("Monthly")
|
||||
|
||||
|
||||
def take_backups_if(freq):
|
||||
if cint(frappe.db.get_single_value("S3 Backup Settings", "enabled")):
|
||||
if frappe.db.get_single_value("S3 Backup Settings", "frequency") == freq:
|
||||
take_backups_s3()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def take_backups_s3(retry_count=0):
|
||||
try:
|
||||
validate_file_size()
|
||||
backup_to_s3()
|
||||
send_email(True, "Amazon S3", "S3 Backup Settings", "notify_email")
|
||||
except JobTimeoutException:
|
||||
if retry_count < 2:
|
||||
args = {"retry_count": retry_count + 1}
|
||||
enqueue(
|
||||
"frappe.integrations.doctype.s3_backup_settings.s3_backup_settings.take_backups_s3",
|
||||
queue="long",
|
||||
timeout=1500,
|
||||
**args,
|
||||
)
|
||||
else:
|
||||
notify()
|
||||
except Exception:
|
||||
notify()
|
||||
|
||||
|
||||
def notify():
|
||||
error_message = frappe.get_traceback()
|
||||
send_email(False, "Amazon S3", "S3 Backup Settings", "notify_email", error_message)
|
||||
|
||||
|
||||
def backup_to_s3():
|
||||
from frappe.utils import get_backups_path
|
||||
from frappe.utils.backups import new_backup
|
||||
|
||||
doc = frappe.get_single("S3 Backup Settings")
|
||||
bucket = doc.bucket
|
||||
path = doc.backup_path or ""
|
||||
backup_files = cint(doc.backup_files)
|
||||
|
||||
conn = boto3.client(
|
||||
"s3",
|
||||
aws_access_key_id=doc.access_key_id,
|
||||
aws_secret_access_key=doc.get_password("secret_access_key"),
|
||||
endpoint_url=doc.endpoint_url or "https://s3.amazonaws.com",
|
||||
)
|
||||
|
||||
if frappe.flags.create_new_backup:
|
||||
backup = new_backup(
|
||||
ignore_files=False,
|
||||
backup_path_db=None,
|
||||
backup_path_files=None,
|
||||
backup_path_private_files=None,
|
||||
force=True,
|
||||
)
|
||||
db_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_db))
|
||||
site_config = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_conf))
|
||||
if backup_files:
|
||||
files_filename = os.path.join(get_backups_path(), os.path.basename(backup.backup_path_files))
|
||||
private_files = os.path.join(
|
||||
get_backups_path(), os.path.basename(backup.backup_path_private_files)
|
||||
)
|
||||
else:
|
||||
if backup_files:
|
||||
db_filename, site_config, files_filename, private_files = get_latest_backup_file(
|
||||
with_files=backup_files
|
||||
)
|
||||
|
||||
if not files_filename or not private_files:
|
||||
generate_files_backup()
|
||||
db_filename, site_config, files_filename, private_files = get_latest_backup_file(
|
||||
with_files=backup_files
|
||||
)
|
||||
|
||||
else:
|
||||
db_filename, site_config = get_latest_backup_file()
|
||||
|
||||
folder = path + os.path.basename(db_filename)[:15] + "/"
|
||||
# for adding datetime to folder name
|
||||
|
||||
upload_file_to_s3(db_filename, folder, conn, bucket)
|
||||
upload_file_to_s3(site_config, folder, conn, bucket)
|
||||
|
||||
if backup_files:
|
||||
if private_files:
|
||||
upload_file_to_s3(private_files, folder, conn, bucket)
|
||||
|
||||
if files_filename:
|
||||
upload_file_to_s3(files_filename, folder, conn, bucket)
|
||||
|
||||
|
||||
def upload_file_to_s3(filename, folder, conn, bucket):
|
||||
destpath = os.path.join(folder, os.path.basename(filename))
|
||||
print("Uploading file:", filename)
|
||||
conn.upload_file(filename, bucket, destpath) # Requires PutObject permission
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
# Copyright (c) 2017, Frappe Technologies and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
|
||||
class TestS3BackupSettings(IntegrationTestCase):
|
||||
pass
|
||||
|
|
@ -115,6 +115,6 @@ def flush_webhook_execution_queue():
|
|||
"frappe.integrations.doctype.webhook.webhook.enqueue_webhook",
|
||||
doc=instance.doc,
|
||||
webhook=instance.webhook,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
queue=instance.webhook.background_jobs_queue or "default",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,13 +16,11 @@ _SCOPES = {
|
|||
}
|
||||
_SERVICES = {
|
||||
"contacts": ("people", "v1"),
|
||||
"drive": ("drive", "v3"),
|
||||
"indexing": ("indexing", "v3"),
|
||||
}
|
||||
_DOMAIN_CALLBACK_METHODS = {
|
||||
"mail": "frappe.email.oauth.authorize_google_access",
|
||||
"contacts": "frappe.integrations.doctype.google_contacts.google_contacts.authorize_access",
|
||||
"drive": "frappe.integrations.doctype.google_drive.google_drive.authorize_access",
|
||||
"indexing": "frappe.website.doctype.website_settings.google_indexing.authorize_access",
|
||||
}
|
||||
|
||||
|
|
@ -34,7 +32,7 @@ class GoogleAuthenticationError(Exception):
|
|||
class GoogleOAuth:
|
||||
OAUTH_URL = "https://oauth2.googleapis.com/token"
|
||||
|
||||
def __init__(self, domain: str, validate: bool = True):
|
||||
def __init__(self, domain: str, validate: bool = True, config=None):
|
||||
self.google_settings = frappe.get_single("Google Settings")
|
||||
self.domain = domain.lower()
|
||||
self.scopes = (
|
||||
|
|
@ -43,6 +41,10 @@ class GoogleOAuth:
|
|||
else _SCOPES[self.domain]
|
||||
)
|
||||
|
||||
if config:
|
||||
_DOMAIN_CALLBACK_METHODS[self.domain] = config["domain_callback_url"]
|
||||
_SERVICES[self.domain] = config["service_version"]
|
||||
|
||||
if validate:
|
||||
self.validate_google_settings()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
# Copyright (c) 2019, Frappe Technologies and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.utils import cint, split_emails
|
||||
|
||||
|
||||
def send_email(success, service_name, doctype, email_field, error_status=None):
|
||||
recipients = get_recipients(doctype, email_field)
|
||||
if not recipients:
|
||||
frappe.log_error(
|
||||
f"No Email Recipient found for {service_name}",
|
||||
f"{service_name}: Failed to send backup status email",
|
||||
)
|
||||
return
|
||||
|
||||
if success:
|
||||
if not frappe.db.get_single_value(doctype, "send_email_for_successful_backup"):
|
||||
return
|
||||
|
||||
subject = "Backup Upload Successful"
|
||||
message = """
|
||||
<h3>Backup Uploaded Successfully!</h3>
|
||||
<p>Hi there, this is just to inform you that your backup was successfully uploaded to your {} bucket. So relax!</p>""".format(
|
||||
service_name
|
||||
)
|
||||
else:
|
||||
subject = "[Warning] Backup Upload Failed"
|
||||
message = f"""
|
||||
<h3>Backup Upload Failed!</h3>
|
||||
<p>Oops, your automated backup to {service_name} failed.</p>
|
||||
<p>Error message: {error_status}</p>
|
||||
<p>Please contact your system manager for more information.</p>"""
|
||||
|
||||
frappe.sendmail(recipients=recipients, subject=subject, message=message)
|
||||
|
||||
|
||||
def get_recipients(doctype, email_field):
|
||||
return split_emails(frappe.db.get_value(doctype, None, email_field))
|
||||
|
||||
|
||||
def get_latest_backup_file(with_files=False):
|
||||
from frappe.utils.backups import BackupGenerator
|
||||
|
||||
odb = BackupGenerator(
|
||||
frappe.conf.db_name,
|
||||
frappe.conf.db_user,
|
||||
frappe.conf.db_password,
|
||||
db_socket=frappe.conf.db_socket,
|
||||
db_host=frappe.conf.db_host,
|
||||
db_port=frappe.conf.db_port,
|
||||
db_type=frappe.conf.db_type,
|
||||
)
|
||||
database, public, private, config = odb.get_recent_backup(older_than=24 * 30)
|
||||
|
||||
if with_files:
|
||||
return database, config, public, private
|
||||
|
||||
return database, config
|
||||
|
||||
|
||||
def get_file_size(file_path, unit="MB"):
|
||||
file_size = os.path.getsize(file_path)
|
||||
|
||||
memory_size_unit_mapper = {"KB": 1, "MB": 2, "GB": 3, "TB": 4}
|
||||
i = 0
|
||||
while i < memory_size_unit_mapper[unit]:
|
||||
file_size = file_size / 1000.0
|
||||
i += 1
|
||||
|
||||
return file_size
|
||||
|
||||
|
||||
def get_chunk_site(file_size):
|
||||
"""this function will return chunk size in megabytes based on file size"""
|
||||
|
||||
file_size_in_gb = cint(file_size / 1024 / 1024)
|
||||
|
||||
MB = 1024 * 1024
|
||||
if file_size_in_gb > 5000:
|
||||
return 200 * MB
|
||||
elif file_size_in_gb >= 3000:
|
||||
return 150 * MB
|
||||
elif file_size_in_gb >= 1000:
|
||||
return 100 * MB
|
||||
elif file_size_in_gb >= 500:
|
||||
return 50 * MB
|
||||
else:
|
||||
return 15 * MB
|
||||
|
||||
|
||||
def validate_file_size():
|
||||
frappe.flags.create_new_backup = True
|
||||
latest_file, site_config = get_latest_backup_file()
|
||||
file_size = get_file_size(latest_file, unit="GB") if latest_file else 0
|
||||
|
||||
if file_size > 1:
|
||||
frappe.flags.create_new_backup = False
|
||||
|
||||
|
||||
def generate_files_backup():
|
||||
from frappe.utils.backups import BackupGenerator
|
||||
|
||||
backup = BackupGenerator(
|
||||
frappe.conf.db_name,
|
||||
frappe.conf.db_user,
|
||||
frappe.conf.db_password,
|
||||
db_socket=frappe.conf.db_socket,
|
||||
db_host=frappe.conf.db_host,
|
||||
db_port=frappe.conf.db_port,
|
||||
db_type=frappe.conf.db_type,
|
||||
)
|
||||
|
||||
backup.set_backup_file_name()
|
||||
backup.zip_files()
|
||||
|
|
@ -12,47 +12,6 @@
|
|||
"is_hidden": 0,
|
||||
"label": "Integrations",
|
||||
"links": [
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Backup",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Dropbox Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "Dropbox Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "S3 Backup Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "S3 Backup Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Google Drive",
|
||||
"link_count": 0,
|
||||
"link_to": "Google Drive",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
|
|
|
|||
31728
frappe/locale/cs.po
Normal file
31728
frappe/locale/cs.po
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: German\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -885,7 +885,7 @@ msgstr "API-Schlüssel kann nicht neu generiert werden"
|
|||
#. Settings'
|
||||
#: frappe/core/doctype/system_settings/system_settings.json
|
||||
msgid "API Logging"
|
||||
msgstr ""
|
||||
msgstr "API-Protokollierung"
|
||||
|
||||
#. Label of the api_method (Data) field in DocType 'Server Script'
|
||||
#: frappe/core/doctype/server_script/server_script.json
|
||||
|
|
@ -895,7 +895,7 @@ msgstr "API-Methode"
|
|||
#. Name of a DocType
|
||||
#: frappe/core/doctype/api_request_log/api_request_log.json
|
||||
msgid "API Request Log"
|
||||
msgstr ""
|
||||
msgstr "API-Anfrage-Protokoll"
|
||||
|
||||
#. Label of the api_secret (Password) field in DocType 'User'
|
||||
#. Label of the api_secret (Password) field in DocType 'Email Account'
|
||||
|
|
@ -4349,7 +4349,7 @@ msgstr "Es können nicht mehrere Drucker einem Druckformat zugeordnet werden."
|
|||
|
||||
#: frappe/public/js/frappe/form/grid.js:1128
|
||||
msgid "Cannot import table with more than 5000 rows."
|
||||
msgstr ""
|
||||
msgstr "Tabelle mit mehr als 5000 Zeilen kann nicht importiert werden."
|
||||
|
||||
#: frappe/model/document.py:1100
|
||||
msgid "Cannot link cancelled document: {0}"
|
||||
|
|
@ -4647,7 +4647,7 @@ msgstr "Aktivieren, falls der Benutzer gezwungen sein soll, vor dem Speichern ei
|
|||
#. Description of the 'Show Full Number' (Check) field in DocType 'Number Card'
|
||||
#: frappe/desk/doctype/number_card/number_card.json
|
||||
msgid "Check to display the full numeric value (e.g., 1,234,567 instead of 1.2M)."
|
||||
msgstr ""
|
||||
msgstr "Aktivieren Sie diese Option, um den vollständigen numerischen Wert anzuzeigen (z.B. 1.234.567 statt 1,2M)."
|
||||
|
||||
#: frappe/email/doctype/newsletter/newsletter.js:20
|
||||
msgid "Checking broken links..."
|
||||
|
|
@ -5192,7 +5192,7 @@ msgstr "Kommentarlimit pro Stunde"
|
|||
|
||||
#: frappe/desk/form/utils.py:75
|
||||
msgid "Comment publicity can only be updated by the original author or a System Manager."
|
||||
msgstr ""
|
||||
msgstr "Die Öffentlichkeit von Kommentaren kann nur vom ursprünglichen Autor oder einem Systemmanager geändert werden."
|
||||
|
||||
#: frappe/model/meta.py:59 frappe/public/js/frappe/form/controls/comment.js:9
|
||||
#: frappe/public/js/frappe/model/meta.js:209
|
||||
|
|
@ -6436,7 +6436,7 @@ msgstr "Täglich lang"
|
|||
#. Option for the 'Frequency' (Select) field in DocType 'Scheduled Job Type'
|
||||
#: frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
|
||||
msgid "Daily Maintenance"
|
||||
msgstr ""
|
||||
msgstr "Tägliche Wartung"
|
||||
|
||||
#. Option for the 'Style' (Select) field in DocType 'Workflow State'
|
||||
#: frappe/workflow/doctype/workflow_state/workflow_state.json
|
||||
|
|
@ -8246,7 +8246,7 @@ msgstr "Laden Sie Ihre Daten herunter"
|
|||
|
||||
#: frappe/core/doctype/prepared_report/prepared_report.js:49
|
||||
msgid "Download as CSV"
|
||||
msgstr ""
|
||||
msgstr "Als CSV downloaden"
|
||||
|
||||
#: frappe/contacts/doctype/contact/contact.js:98
|
||||
msgid "Download vCard"
|
||||
|
|
@ -8354,7 +8354,7 @@ msgstr "Doppelter Name"
|
|||
|
||||
#: frappe/public/js/frappe/form/grid.js:66
|
||||
msgid "Duplicate Row"
|
||||
msgstr ""
|
||||
msgstr "Zeile duplizieren"
|
||||
|
||||
#: frappe/public/js/frappe/form/form.js:209
|
||||
msgid "Duplicate current row"
|
||||
|
|
@ -8727,7 +8727,7 @@ msgstr "E-Mail-Konto nicht eingerichtet. Bitte erstellen Sie ein neues E-Mail-Ko
|
|||
|
||||
#: frappe/email/doctype/email_account/email_account.py:578
|
||||
msgid "Email Account {0} Disabled"
|
||||
msgstr ""
|
||||
msgstr "E-Mail-Konto {0} Deaktiviert"
|
||||
|
||||
#. Label of the email_id (Data) field in DocType 'Address'
|
||||
#. Label of the email_id (Data) field in DocType 'Contact'
|
||||
|
|
@ -8789,7 +8789,7 @@ msgstr "E-Mail-Gruppen-Mitglied"
|
|||
#. Label of the email_header (Data) field in DocType 'Notification Log'
|
||||
#: frappe/desk/doctype/notification_log/notification_log.json
|
||||
msgid "Email Header"
|
||||
msgstr ""
|
||||
msgstr "E-Mail-Kopfzeile"
|
||||
|
||||
#. Label of the email_id (Data) field in DocType 'Contact Email'
|
||||
#. Label of the email_id (Data) field in DocType 'User Email'
|
||||
|
|
@ -8939,7 +8939,7 @@ msgstr "E-Mail nicht mit {0} bestätigt"
|
|||
|
||||
#: frappe/email/doctype/email_queue/email_queue.js:19
|
||||
msgid "Email queue is currently suspended. Resume to automatically send other emails."
|
||||
msgstr ""
|
||||
msgstr "Die E-Mail-Warteschlange ist derzeit unterbrochen. Fortsetzen, um automatisch andere E-Mails zu versenden."
|
||||
|
||||
#. Label of the section_break_udjs (Section Break) field in DocType 'System
|
||||
#. Health Report'
|
||||
|
|
@ -9609,7 +9609,7 @@ msgstr "Führen Sie das Konsolenskript aus"
|
|||
|
||||
#: frappe/public/js/frappe/ui/dropdown_console.js:125
|
||||
msgid "Executing Code"
|
||||
msgstr ""
|
||||
msgstr "Code wird ausgeführt"
|
||||
|
||||
#: frappe/desk/doctype/system_console/system_console.js:18
|
||||
msgid "Executing..."
|
||||
|
|
@ -11817,7 +11817,7 @@ msgstr "hat Rolle"
|
|||
#. Application'
|
||||
#: frappe/core/doctype/installed_application/installed_application.json
|
||||
msgid "Has Setup Wizard"
|
||||
msgstr ""
|
||||
msgstr "Hat einen Setup-Assistenten"
|
||||
|
||||
#. Label of the has_web_view (Check) field in DocType 'DocType'
|
||||
#: frappe/core/doctype/doctype/doctype.json
|
||||
|
|
@ -12231,7 +12231,7 @@ msgstr "Stündlich lang"
|
|||
#. Option for the 'Frequency' (Select) field in DocType 'Scheduled Job Type'
|
||||
#: frappe/core/doctype/scheduled_job_type/scheduled_job_type.json
|
||||
msgid "Hourly Maintenance"
|
||||
msgstr ""
|
||||
msgstr "Stündliche Wartung"
|
||||
|
||||
#. Description of the 'Password Reset Link Generation Limit' (Int) field in
|
||||
#. DocType 'System Settings'
|
||||
|
|
@ -14272,11 +14272,11 @@ msgstr "Letzte 10 aktive Benutzer"
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:628
|
||||
msgid "Last 14 Days"
|
||||
msgstr ""
|
||||
msgstr "Letze 14 Tage"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:632
|
||||
msgid "Last 30 Days"
|
||||
msgstr ""
|
||||
msgstr "Letzte 30 Tage"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:652
|
||||
msgid "Last 6 Months"
|
||||
|
|
@ -14284,11 +14284,11 @@ msgstr "Letzte 6 Monate"
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:624
|
||||
msgid "Last 7 Days"
|
||||
msgstr ""
|
||||
msgstr "Letze 7 Tage"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:636
|
||||
msgid "Last 90 Days"
|
||||
msgstr ""
|
||||
msgstr "Letzte 90 Tage"
|
||||
|
||||
#. Label of the last_active (Datetime) field in DocType 'User'
|
||||
#: frappe/core/doctype/user/user.json
|
||||
|
|
@ -14995,7 +14995,7 @@ msgstr "Protokoll"
|
|||
#. Label of the log_api_requests (Check) field in DocType 'System Settings'
|
||||
#: frappe/core/doctype/system_settings/system_settings.json
|
||||
msgid "Log API Requests"
|
||||
msgstr ""
|
||||
msgstr "API-Anfragen protokollieren"
|
||||
|
||||
#. Label of the log_data_section (Section Break) field in DocType 'Access Log'
|
||||
#: frappe/core/doctype/access_log/access_log.json
|
||||
|
|
@ -15522,7 +15522,7 @@ msgstr "Maximale Länge"
|
|||
#. Label of the max_report_rows (Int) field in DocType 'System Settings'
|
||||
#: frappe/core/doctype/system_settings/system_settings.json
|
||||
msgid "Max Report Rows"
|
||||
msgstr ""
|
||||
msgstr "Max. Berichtszeilen"
|
||||
|
||||
#. Label of the max_value (Int) field in DocType 'Web Form Field'
|
||||
#: frappe/website/doctype/web_form_field/web_form_field.json
|
||||
|
|
@ -16730,11 +16730,11 @@ msgstr "Weiter"
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:684
|
||||
msgid "Next 14 Days"
|
||||
msgstr ""
|
||||
msgstr "Nächste 14 Tage"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:688
|
||||
msgid "Next 30 Days"
|
||||
msgstr ""
|
||||
msgstr "Nächste 30 Tage"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:704
|
||||
msgid "Next 6 Months"
|
||||
|
|
@ -16742,7 +16742,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:680
|
||||
msgid "Next 7 Days"
|
||||
msgstr ""
|
||||
msgstr "Nächste 7 Tage"
|
||||
|
||||
#. Label of the next_action_email_template (Link) field in DocType 'Workflow
|
||||
#. Document State'
|
||||
|
|
@ -18106,7 +18106,7 @@ msgstr "Modul oder Werkzeug öffnen"
|
|||
|
||||
#: frappe/public/js/frappe/ui/keyboard.js:366
|
||||
msgid "Open console"
|
||||
msgstr ""
|
||||
msgstr "Konsole öffnen"
|
||||
|
||||
#: frappe/public/js/print_format_builder/Preview.vue:17
|
||||
msgid "Open in a new tab"
|
||||
|
|
@ -23251,7 +23251,7 @@ msgstr "E-Mail senden am"
|
|||
#. Label of the send_email (Check) field in DocType 'Workflow Document State'
|
||||
#: frappe/workflow/doctype/workflow_document_state/workflow_document_state.json
|
||||
msgid "Send Email On State"
|
||||
msgstr ""
|
||||
msgstr "E-Mail senden bei Status"
|
||||
|
||||
#. Description of the 'Send Print as PDF' (Check) field in DocType 'Print
|
||||
#. Settings'
|
||||
|
|
@ -26629,7 +26629,7 @@ msgstr "Dieser Wert ergibt sich aus dem Feld {1} von {0}"
|
|||
#. Settings'
|
||||
#: frappe/core/doctype/system_settings/system_settings.json
|
||||
msgid "This value specifies the max number of rows that can be rendered in report view. "
|
||||
msgstr ""
|
||||
msgstr "Dieser Wert gibt die maximale Anzahl von Zeilen an, die in der Berichtsansicht dargestellt werden können. "
|
||||
|
||||
#: frappe/website/doctype/web_page/web_page.js:85
|
||||
msgid "This will be automatically generated when you publish the page, you can also enter a route yourself if you wish"
|
||||
|
|
@ -26852,7 +26852,7 @@ msgstr "Zeitstempel"
|
|||
|
||||
#: frappe/desk/doctype/system_console/system_console.js:41
|
||||
msgid "Tip: Try the new dropdown console using"
|
||||
msgstr ""
|
||||
msgstr "Tipp: Probieren Sie die neue Dropdown-Konsole mit"
|
||||
|
||||
#. Label of the title (Data) field in DocType 'DocType State'
|
||||
#. Label of the method (Data) field in DocType 'Error Log'
|
||||
|
|
@ -27018,7 +27018,7 @@ msgstr "Um diesen Schritt als JSON zu exportieren, verknüpfen Sie ihn in einem
|
|||
|
||||
#: frappe/email/doctype/email_account/email_account.js:126
|
||||
msgid "To generate password click {0}"
|
||||
msgstr ""
|
||||
msgstr "Um ein Passwort zu generieren, klicken Sie auf {0}"
|
||||
|
||||
#: frappe/public/js/frappe/views/reports/query_report.js:857
|
||||
msgid "To get the updated report, click on {0}."
|
||||
|
|
@ -27026,7 +27026,7 @@ msgstr "Klicken Sie auf {0}, um den aktualisierten Bericht abzurufen."
|
|||
|
||||
#: frappe/email/doctype/email_account/email_account.js:139
|
||||
msgid "To know more click {0}"
|
||||
msgstr ""
|
||||
msgstr "Für weitere Informationen klicken Sie auf {0}"
|
||||
|
||||
#. Description of the 'Console' (Code) field in DocType 'System Console'
|
||||
#: frappe/desk/doctype/system_console/system_console.json
|
||||
|
|
@ -28925,7 +28925,7 @@ msgstr "Sichtbarkeit"
|
|||
|
||||
#: frappe/public/js/frappe/form/templates/timeline_message_box.html:41
|
||||
msgid "Visible to website/portal users."
|
||||
msgstr ""
|
||||
msgstr "Für Website-/Portalbenutzer sichtbar."
|
||||
|
||||
#. Option for the 'Type' (Select) field in DocType 'Communication'
|
||||
#: frappe/core/doctype/communication/communication.json
|
||||
|
|
@ -29672,11 +29672,11 @@ msgstr "Arbeitsbereiche"
|
|||
|
||||
#: frappe/public/js/frappe/form/footer/form_timeline.js:753
|
||||
msgid "Would you like to publish this comment? This means it will become visible to website/portal users."
|
||||
msgstr ""
|
||||
msgstr "Möchten Sie diesen Kommentar veröffentlichen? Das bedeutet, dass er für die Benutzer der Website/des Portals sichtbar wird."
|
||||
|
||||
#: frappe/public/js/frappe/form/footer/form_timeline.js:757
|
||||
msgid "Would you like to unpublish this comment? This means it will no longer be visible to website/portal users."
|
||||
msgstr ""
|
||||
msgstr "Möchten Sie die Veröffentlichung dieses Kommentars aufheben? Dann ist er für die Benutzer der Website/des Portals nicht mehr sichtbar."
|
||||
|
||||
#: frappe/desk/page/setup_wizard/setup_wizard.py:41
|
||||
msgid "Wrapping up"
|
||||
|
|
@ -29880,7 +29880,7 @@ msgstr "Sie sind nicht berechtigt auf diese Seite zuzugreifen."
|
|||
|
||||
#: frappe/__init__.py:669
|
||||
msgid "You are not permitted to access this resource. Login to access"
|
||||
msgstr ""
|
||||
msgstr "Sie haben keinen Zugriff auf diese Ressource. Melden Sie sich an, um darauf zuzugreifen"
|
||||
|
||||
#: frappe/public/js/frappe/form/sidebar/document_follow.js:131
|
||||
msgid "You are now following this document. You will receive daily updates via email. You can change this in User Settings."
|
||||
|
|
@ -30040,7 +30040,7 @@ msgstr "Von Ihnen erstellt"
|
|||
#: frappe/public/js/frappe/form/footer/version_timeline_content_builder.js:247
|
||||
msgctxt "Form timeline"
|
||||
msgid "You created this document {0}"
|
||||
msgstr ""
|
||||
msgstr "Sie haben dieses Dokument {0} erstellt"
|
||||
|
||||
#: frappe/client.py:417
|
||||
msgid "You do not have Read or Select Permissions for {}"
|
||||
|
|
@ -30256,7 +30256,7 @@ msgstr "Sie haben sich als ein anderer Benutzer über eine andere Registerkarte
|
|||
|
||||
#: frappe/core/doctype/prepared_report/prepared_report.js:57
|
||||
msgid "Your CSV file is being generated and will appear in the Attachments section once ready. Additionally, you will get notified when the file is available for download."
|
||||
msgstr ""
|
||||
msgstr "Ihre CSV-Datei wird generiert und erscheint im Bereich „Anhänge“, sobald sie fertig ist. Sie werden außerdem benachrichtigt, sobald die Datei zum Download bereitsteht."
|
||||
|
||||
#: frappe/desk/page/setup_wizard/setup_wizard.js:397
|
||||
msgid "Your Country"
|
||||
|
|
@ -31010,7 +31010,7 @@ msgstr "über Zuweisungsregel"
|
|||
|
||||
#: frappe/automation/doctype/auto_repeat/auto_repeat.py:242
|
||||
msgid "via Auto Repeat"
|
||||
msgstr ""
|
||||
msgstr "über automatische Wiederholung"
|
||||
|
||||
#: frappe/core/doctype/data_import/importer.py:271
|
||||
#: frappe/core/doctype/data_import/importer.py:292
|
||||
|
|
@ -31302,7 +31302,7 @@ msgstr "Von {0} erstellt"
|
|||
#: frappe/public/js/frappe/form/footer/version_timeline_content_builder.js:250
|
||||
msgctxt "Form timeline"
|
||||
msgid "{0} created this document {1}"
|
||||
msgstr ""
|
||||
msgstr "{0} hat dieses Dokument {1} erstellt"
|
||||
|
||||
#: frappe/public/js/frappe/utils/pretty_date.js:33
|
||||
msgid "{0} d"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:04\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -26351,19 +26351,19 @@ msgstr "Este tablero Kanban será privado"
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:666
|
||||
msgid "This Month"
|
||||
msgstr ""
|
||||
msgstr "Este mes"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:670
|
||||
msgid "This Quarter"
|
||||
msgstr ""
|
||||
msgstr "Este cuarto"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:662
|
||||
msgid "This Week"
|
||||
msgstr ""
|
||||
msgstr "Esta Semana"
|
||||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:674
|
||||
msgid "This Year"
|
||||
msgstr ""
|
||||
msgstr "Este año"
|
||||
|
||||
#: frappe/custom/doctype/customize_form/customize_form.js:220
|
||||
msgid "This action is irreversible. Do you wish to continue?"
|
||||
|
|
@ -29752,7 +29752,7 @@ msgstr "Si"
|
|||
|
||||
#: frappe/public/js/frappe/ui/filters/filter.js:727
|
||||
msgid "Yesterday"
|
||||
msgstr ""
|
||||
msgstr "Ayer"
|
||||
|
||||
#: frappe/public/js/frappe/utils/user.js:33
|
||||
msgctxt "Name of the current user. For example: You edited this 5 hours ago."
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:30\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Persian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -2912,7 +2912,7 @@ msgstr "تخصیص خودکار اسناد به کاربران"
|
|||
|
||||
#: frappe/public/js/frappe/list/list_view.js:128
|
||||
msgid "Automatically applied a filter for recent data. You can disable this behavior from the list view settings."
|
||||
msgstr ""
|
||||
msgstr "به طور خودکار فیلتری برای دادههای اخیر اعمال شد. میتوانید این عملکرد را از تنظیمات نمای لیست غیرفعال کنید."
|
||||
|
||||
#. Label of the auto_account_deletion (Int) field in DocType 'Website Settings'
|
||||
#: frappe/website/doctype/website_settings/website_settings.json
|
||||
|
|
@ -7166,7 +7166,7 @@ msgstr "غیر فعال کردن Refresh خودکار"
|
|||
#. 'List View Settings'
|
||||
#: frappe/desk/doctype/list_view_settings/list_view_settings.json
|
||||
msgid "Disable Automatic Recency Filters"
|
||||
msgstr ""
|
||||
msgstr "فیلترهای خودکار اخیر را غیرفعال کنید"
|
||||
|
||||
#. Label of the disable_change_log_notification (Check) field in DocType
|
||||
#. 'System Settings'
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:04\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -3865,7 +3865,7 @@ msgstr ""
|
|||
#: frappe/public/js/frappe/views/communication.js:76
|
||||
msgctxt "Email Recipients"
|
||||
msgid "CC"
|
||||
msgstr ""
|
||||
msgstr "CC"
|
||||
|
||||
#. Label of the cmd (Data) field in DocType 'Recorder'
|
||||
#: frappe/core/doctype/recorder/recorder.json
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Hungarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -1137,7 +1137,7 @@ msgstr ""
|
|||
#. Label of the add_translate_data (Check) field in DocType 'Report'
|
||||
#: frappe/core/doctype/report/report.json
|
||||
msgid "Add Translate Data"
|
||||
msgstr ""
|
||||
msgstr "Fordítási adatok hozzáadása"
|
||||
|
||||
#. Label of the add_unsubscribe_link (Check) field in DocType 'Email Queue'
|
||||
#: frappe/email/doctype/email_queue/email_queue.json
|
||||
|
|
@ -1988,7 +1988,7 @@ msgstr ""
|
|||
|
||||
#: frappe/model/document.py:550
|
||||
msgid "Amendment Not Allowed"
|
||||
msgstr ""
|
||||
msgstr "Módosítás nem engedélyezett"
|
||||
|
||||
#: frappe/core/doctype/document_naming_settings/document_naming_settings.py:207
|
||||
msgid "Amendment naming rules updated."
|
||||
|
|
@ -2863,7 +2863,7 @@ msgstr ""
|
|||
|
||||
#: frappe/automation/doctype/auto_repeat/auto_repeat.py:227
|
||||
msgid "Auto repeat failed. Please enable auto repeat after fixing the issues."
|
||||
msgstr ""
|
||||
msgstr "Az automatikus ismétlés sikertelen. Kérjük, engedélyezze az automatikus ismétlést a problémák megoldása után."
|
||||
|
||||
#. Option for the 'Type' (Select) field in DocType 'DocField'
|
||||
#. Option for the 'Field Type' (Select) field in DocType 'Custom Field'
|
||||
|
|
@ -2911,7 +2911,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/frappe/list/list_view.js:128
|
||||
msgid "Automatically applied a filter for recent data. You can disable this behavior from the list view settings."
|
||||
msgstr ""
|
||||
msgstr "Automatikusan alkalmazott szűrő a friss adatokra. Ezt a viselkedést a listanézet beállításai között kikapcsolhatja."
|
||||
|
||||
#. Label of the auto_account_deletion (Int) field in DocType 'Website Settings'
|
||||
#: frappe/website/doctype/website_settings/website_settings.json
|
||||
|
|
@ -3761,7 +3761,7 @@ msgstr ""
|
|||
#: frappe/public/js/frappe/views/communication.js:76
|
||||
msgctxt "Email Recipients"
|
||||
msgid "CC"
|
||||
msgstr ""
|
||||
msgstr "CC"
|
||||
|
||||
#. Label of the cmd (Data) field in DocType 'Recorder'
|
||||
#: frappe/core/doctype/recorder/recorder.json
|
||||
|
|
@ -4732,7 +4732,7 @@ msgstr ""
|
|||
#. Label of the client_script (Code) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Client script"
|
||||
msgstr ""
|
||||
msgstr "Ügyfélszkript"
|
||||
|
||||
#: frappe/core/doctype/communication/communication.js:39
|
||||
#: frappe/desk/doctype/todo/todo.js:23
|
||||
|
|
@ -5070,7 +5070,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:32
|
||||
msgid "Communication secret not set"
|
||||
msgstr ""
|
||||
msgstr "A kommunikációs kulcs nincs beállítva"
|
||||
|
||||
#. Name of a DocType
|
||||
#: frappe/website/doctype/company_history/company_history.json
|
||||
|
|
@ -5193,7 +5193,7 @@ msgstr ""
|
|||
#. Label of the condition_description (HTML) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Condition description"
|
||||
msgstr ""
|
||||
msgstr "Feltétel leírás"
|
||||
|
||||
#. Label of the conditions (Table) field in DocType 'Document Naming Rule'
|
||||
#. Label of the conditions (Section Break) field in DocType 'Workflow
|
||||
|
|
@ -5250,7 +5250,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/oauth2.py:120
|
||||
msgid "Confirm Access"
|
||||
msgstr ""
|
||||
msgstr "Hozzáférés megerősítése"
|
||||
|
||||
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py:93
|
||||
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py:101
|
||||
|
|
@ -5556,7 +5556,7 @@ msgstr ""
|
|||
|
||||
#: frappe/desk/page/setup_wizard/setup_wizard.js:234
|
||||
msgid "Could not start up: "
|
||||
msgstr ""
|
||||
msgstr "Nem sikerült elindulni: "
|
||||
|
||||
#: frappe/public/js/frappe/web_form/web_form.js:359
|
||||
msgid "Couldn't save, please check the data you have entered"
|
||||
|
|
@ -7164,7 +7164,7 @@ msgstr ""
|
|||
#. 'List View Settings'
|
||||
#: frappe/desk/doctype/list_view_settings/list_view_settings.json
|
||||
msgid "Disable Automatic Recency Filters"
|
||||
msgstr ""
|
||||
msgstr "Automatikus újbóli szűrők kikapcsolása"
|
||||
|
||||
#. Label of the disable_change_log_notification (Check) field in DocType
|
||||
#. 'System Settings'
|
||||
|
|
@ -9045,7 +9045,7 @@ msgstr ""
|
|||
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.py:51
|
||||
msgid "End"
|
||||
msgstr ""
|
||||
msgstr "Vége"
|
||||
|
||||
#. Label of the end_date (Date) field in DocType 'Auto Repeat'
|
||||
#. Label of the end_date (Date) field in DocType 'Audit Trail'
|
||||
|
|
@ -9743,7 +9743,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:59
|
||||
msgid "Failed to get site info"
|
||||
msgstr ""
|
||||
msgstr "Nem sikerült lekérni a webhely adatait"
|
||||
|
||||
#: frappe/model/virtual_doctype.py:63
|
||||
msgid "Failed to import virtual doctype {}, is controller file present?"
|
||||
|
|
@ -9763,7 +9763,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:94
|
||||
msgid "Failed to request login to Frappe Cloud"
|
||||
msgstr ""
|
||||
msgstr "Nem sikerült bejelentkezést kérni a Frappe Cloud-ba"
|
||||
|
||||
#: frappe/email/doctype/email_queue/email_queue.py:297
|
||||
msgid "Failed to send email with subject:"
|
||||
|
|
@ -9779,7 +9779,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:74
|
||||
msgid "Failed while calling API {0}"
|
||||
msgstr ""
|
||||
msgstr "Sikertelen API hívása közben {0}"
|
||||
|
||||
#. Label of the failing_scheduled_jobs (Table) field in DocType 'System Health
|
||||
#. Report'
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
43678
frappe/locale/nl.po
43678
frappe/locale/nl.po
File diff suppressed because it is too large
Load diff
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Polish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -780,7 +780,7 @@ msgstr ""
|
|||
#. Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Access Control"
|
||||
msgstr ""
|
||||
msgstr "Ustawienia dostępu"
|
||||
|
||||
#. Label of the access_key_id (Data) field in DocType 'S3 Backup Settings'
|
||||
#: frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.json
|
||||
|
|
@ -1902,7 +1902,7 @@ msgstr ""
|
|||
#. Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Allowed embedding domains"
|
||||
msgstr ""
|
||||
msgstr "Dozwolone domeny osadzania"
|
||||
|
||||
#: frappe/public/js/frappe/form/form.js:1256
|
||||
msgid "Allowing DocType, DocType. Be careful!"
|
||||
|
|
@ -2001,7 +2001,7 @@ msgstr ""
|
|||
|
||||
#: frappe/model/document.py:550
|
||||
msgid "Amendment Not Allowed"
|
||||
msgstr ""
|
||||
msgstr "Zmiana niedozwolona"
|
||||
|
||||
#: frappe/core/doctype/document_naming_settings/document_naming_settings.py:207
|
||||
msgid "Amendment naming rules updated."
|
||||
|
|
@ -3059,7 +3059,7 @@ msgstr ""
|
|||
#: frappe/public/js/frappe/views/communication.js:85
|
||||
msgctxt "Email Recipients"
|
||||
msgid "BCC"
|
||||
msgstr ""
|
||||
msgstr "BCC"
|
||||
|
||||
#: frappe/public/js/frappe/file_uploader/ImageCropper.vue:31
|
||||
#: frappe/public/js/frappe/widgets/onboarding_widget.js:181
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-16 15:29\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -1889,7 +1889,7 @@ msgstr ""
|
|||
#. Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Allowed embedding domains"
|
||||
msgstr ""
|
||||
msgstr "Допустимые области встраивания"
|
||||
|
||||
#: frappe/public/js/frappe/form/form.js:1256
|
||||
msgid "Allowing DocType, DocType. Be careful!"
|
||||
|
|
@ -2863,7 +2863,7 @@ msgstr ""
|
|||
|
||||
#: frappe/automation/doctype/auto_repeat/auto_repeat.py:227
|
||||
msgid "Auto repeat failed. Please enable auto repeat after fixing the issues."
|
||||
msgstr ""
|
||||
msgstr "Автоматическое повторение не удалось. Пожалуйста, включите автоповтор после устранения неполадок."
|
||||
|
||||
#. Option for the 'Type' (Select) field in DocType 'DocField'
|
||||
#. Option for the 'Field Type' (Select) field in DocType 'Custom Field'
|
||||
|
|
@ -2911,7 +2911,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/frappe/list/list_view.js:128
|
||||
msgid "Automatically applied a filter for recent data. You can disable this behavior from the list view settings."
|
||||
msgstr ""
|
||||
msgstr "Автоматическое применение фильтра для последних данных. Это поведение можно отключить в настройках представления списка."
|
||||
|
||||
#. Label of the auto_account_deletion (Int) field in DocType 'Website Settings'
|
||||
#: frappe/website/doctype/website_settings/website_settings.json
|
||||
|
|
@ -4732,7 +4732,7 @@ msgstr ""
|
|||
#. Label of the client_script (Code) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Client script"
|
||||
msgstr ""
|
||||
msgstr "Клиентский скрипт"
|
||||
|
||||
#: frappe/core/doctype/communication/communication.js:39
|
||||
#: frappe/desk/doctype/todo/todo.js:23
|
||||
|
|
@ -5070,7 +5070,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:32
|
||||
msgid "Communication secret not set"
|
||||
msgstr ""
|
||||
msgstr "Секрет связи не установлен"
|
||||
|
||||
#. Name of a DocType
|
||||
#: frappe/website/doctype/company_history/company_history.json
|
||||
|
|
@ -5193,7 +5193,7 @@ msgstr ""
|
|||
#. Label of the condition_description (HTML) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Condition description"
|
||||
msgstr ""
|
||||
msgstr "Описание состояния"
|
||||
|
||||
#. Label of the conditions (Table) field in DocType 'Document Naming Rule'
|
||||
#. Label of the conditions (Section Break) field in DocType 'Workflow
|
||||
|
|
@ -5250,7 +5250,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/oauth2.py:120
|
||||
msgid "Confirm Access"
|
||||
msgstr ""
|
||||
msgstr "Подтвердите доступ"
|
||||
|
||||
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py:93
|
||||
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py:101
|
||||
|
|
@ -7164,7 +7164,7 @@ msgstr ""
|
|||
#. 'List View Settings'
|
||||
#: frappe/desk/doctype/list_view_settings/list_view_settings.json
|
||||
msgid "Disable Automatic Recency Filters"
|
||||
msgstr ""
|
||||
msgstr "Отключить автоматические фильтры повторяемости"
|
||||
|
||||
#. Label of the disable_change_log_notification (Check) field in DocType
|
||||
#. 'System Settings'
|
||||
|
|
@ -9743,7 +9743,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:59
|
||||
msgid "Failed to get site info"
|
||||
msgstr ""
|
||||
msgstr "Не удалось получить информацию о сайте"
|
||||
|
||||
#: frappe/model/virtual_doctype.py:63
|
||||
msgid "Failed to import virtual doctype {}, is controller file present?"
|
||||
|
|
@ -9763,7 +9763,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:94
|
||||
msgid "Failed to request login to Frappe Cloud"
|
||||
msgstr ""
|
||||
msgstr "Не удалось запросить вход в Frappe Cloud"
|
||||
|
||||
#: frappe/email/doctype/email_queue/email_queue.py:297
|
||||
msgid "Failed to send email with subject:"
|
||||
|
|
@ -9779,7 +9779,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:74
|
||||
msgid "Failed while calling API {0}"
|
||||
msgstr ""
|
||||
msgstr "Неудача при вызове API {0}"
|
||||
|
||||
#. Label of the failing_scheduled_jobs (Table) field in DocType 'System Health
|
||||
#. Report'
|
||||
|
|
@ -11443,7 +11443,7 @@ msgstr ""
|
|||
#: frappe/core/doctype/doctype/doctype.json
|
||||
#: frappe/custom/doctype/customize_form/customize_form.json
|
||||
msgid "Grid Page Length"
|
||||
msgstr ""
|
||||
msgstr "Длина страницы сетки"
|
||||
|
||||
#: frappe/public/js/frappe/ui/keyboard.js:127
|
||||
msgid "Grid Shortcuts"
|
||||
|
|
@ -12208,7 +12208,7 @@ msgstr ""
|
|||
#. Description of the 'Anonymous responses' (Check) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "If enabled, all responses on the web form will be submitted anonymously"
|
||||
msgstr ""
|
||||
msgstr "Если эта функция включена, все ответы в веб-форме будут отправляться анонимно"
|
||||
|
||||
#. Description of the 'Bypass restricted IP Address check If Two Factor Auth
|
||||
#. Enabled' (Check) field in DocType 'System Settings'
|
||||
|
|
@ -13120,7 +13120,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:111
|
||||
msgid "Invalid Code. Please try again."
|
||||
msgstr ""
|
||||
msgstr "Неверный код. Пожалуйста, попробуйте еще раз."
|
||||
|
||||
#: frappe/integrations/doctype/webhook/webhook.py:87
|
||||
msgid "Invalid Condition: {}"
|
||||
|
|
@ -13247,7 +13247,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/frappe/ui/field_group.js:137
|
||||
msgid "Invalid Values"
|
||||
msgstr ""
|
||||
msgstr "Недопустимые значения"
|
||||
|
||||
#: frappe/integrations/doctype/webhook/webhook.py:116
|
||||
msgid "Invalid Webhook Secret"
|
||||
|
|
@ -14729,7 +14729,7 @@ msgstr ""
|
|||
#. Label of the list_setting_message (HTML) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "List setting message"
|
||||
msgstr ""
|
||||
msgstr "Сообщение о настройке списка"
|
||||
|
||||
#: frappe/public/js/frappe/ui/toolbar/search_utils.js:542
|
||||
msgid "Lists"
|
||||
|
|
@ -14947,7 +14947,7 @@ msgstr ""
|
|||
|
||||
#: frappe/www/login.html:116
|
||||
msgid "Login with Frappe Cloud"
|
||||
msgstr ""
|
||||
msgstr "Вход в систему с помощью Frappe Cloud"
|
||||
|
||||
#: frappe/www/login.html:49
|
||||
msgid "Login with LDAP"
|
||||
|
|
@ -15338,7 +15338,7 @@ msgstr ""
|
|||
#. Label of the max_attachment_size (Int) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Max attachment size"
|
||||
msgstr ""
|
||||
msgstr "Максимальный размер крепления"
|
||||
|
||||
#. Label of the max_auto_email_report_per_user (Int) field in DocType 'System
|
||||
#. Settings'
|
||||
|
|
@ -15415,7 +15415,7 @@ msgstr ""
|
|||
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.py:63
|
||||
msgid "Memory Usage in MB"
|
||||
msgstr ""
|
||||
msgstr "Использование памяти в МБ"
|
||||
|
||||
#. Option for the 'Type' (Select) field in DocType 'Notification Log'
|
||||
#: frappe/desk/doctype/notification_log/notification_log.json
|
||||
|
|
@ -15578,17 +15578,17 @@ msgstr ""
|
|||
#. Label of the meta_description (Small Text) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Meta description"
|
||||
msgstr ""
|
||||
msgstr "Мета-описание"
|
||||
|
||||
#. Label of the meta_image (Attach Image) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Meta image"
|
||||
msgstr ""
|
||||
msgstr "Мета-изображение"
|
||||
|
||||
#. Label of the meta_title (Data) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Meta title"
|
||||
msgstr ""
|
||||
msgstr "Мета-заголовок"
|
||||
|
||||
#: frappe/website/doctype/web_page/web_page.js:110
|
||||
msgid "Meta title for SEO"
|
||||
|
|
@ -18643,7 +18643,7 @@ msgstr ""
|
|||
#. Label of the peak_memory_usage (Int) field in DocType 'Prepared Report'
|
||||
#: frappe/core/doctype/prepared_report/prepared_report.json
|
||||
msgid "Peak Memory Usage"
|
||||
msgstr ""
|
||||
msgstr "Пиковое использование памяти"
|
||||
|
||||
#. Option for the 'Status' (Select) field in DocType 'Data Import'
|
||||
#. Option for the 'Contribution Status' (Select) field in DocType 'Translation'
|
||||
|
|
@ -19283,7 +19283,7 @@ msgstr ""
|
|||
|
||||
#: frappe/email/doctype/email_account/email_account.py:434
|
||||
msgid "Please setup default outgoing Email Account from Tools > Email Account"
|
||||
msgstr ""
|
||||
msgstr "Настройте учетную запись исходящей электронной почты по умолчанию в меню Инструменты > Учетная запись электронной почты"
|
||||
|
||||
#: frappe/public/js/frappe/model/model.js:823
|
||||
msgid "Please specify"
|
||||
|
|
@ -19475,7 +19475,7 @@ msgstr ""
|
|||
#. Name of a report
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.json
|
||||
msgid "Prepared Report Analytics"
|
||||
msgstr ""
|
||||
msgstr "Подготовленный отчет Аналитика"
|
||||
|
||||
#. Name of a role
|
||||
#: frappe/core/doctype/prepared_report/prepared_report.json
|
||||
|
|
@ -22080,11 +22080,11 @@ msgstr ""
|
|||
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.py:57
|
||||
msgid "Runtime in Minutes"
|
||||
msgstr ""
|
||||
msgstr "Время выполнения в минутах"
|
||||
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.py:57
|
||||
msgid "Runtime in Seconds"
|
||||
msgstr ""
|
||||
msgstr "Время выполнения в секундах"
|
||||
|
||||
#. Name of a DocType
|
||||
#. Label of a Link in the Integrations Workspace
|
||||
|
|
@ -22137,7 +22137,7 @@ msgstr ""
|
|||
|
||||
#: frappe/core/doctype/sms_settings/sms_settings.py:110
|
||||
msgid "SMS sent successfully"
|
||||
msgstr ""
|
||||
msgstr "SMS успешно отправлено"
|
||||
|
||||
#: frappe/templates/includes/login/login.js:369
|
||||
msgid "SMS was not sent. Please contact Administrator."
|
||||
|
|
@ -22376,7 +22376,7 @@ msgstr ""
|
|||
#. Label of the scheduled_against (Link) field in DocType 'Scheduler Event'
|
||||
#: frappe/core/doctype/scheduler_event/scheduler_event.json
|
||||
msgid "Scheduled Against"
|
||||
msgstr ""
|
||||
msgstr "Запланировано против"
|
||||
|
||||
#. Label of the scheduled_job_type (Link) field in DocType 'Scheduled Job Log'
|
||||
#: frappe/core/doctype/scheduled_job_log/scheduled_job_log.json
|
||||
|
|
@ -23315,7 +23315,7 @@ msgstr ""
|
|||
#: frappe/email/doctype/email_account/email_account.json
|
||||
#: frappe/email/doctype/email_domain/email_domain.json
|
||||
msgid "Sent Folder Name"
|
||||
msgstr ""
|
||||
msgstr "Имя отправленной папки"
|
||||
|
||||
#. Label of the sent_on (Date) field in DocType 'SMS Log'
|
||||
#: frappe/core/doctype/sms_log/sms_log.json
|
||||
|
|
@ -23612,7 +23612,7 @@ msgstr ""
|
|||
#. Description of the 'Max attachment size' (Int) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Set size in MB"
|
||||
msgstr ""
|
||||
msgstr "Установленный размер в МБ"
|
||||
|
||||
#. Description of the 'Filters Configuration' (Code) field in DocType 'Number
|
||||
#. Card'
|
||||
|
|
@ -23737,7 +23737,7 @@ msgstr ""
|
|||
|
||||
#: frappe/desk/page/setup_wizard/setup_wizard.js:236
|
||||
msgid "Setup failed"
|
||||
msgstr ""
|
||||
msgstr "Сбой установки"
|
||||
|
||||
#. Label of the share (Check) field in DocType 'Custom DocPerm'
|
||||
#. Label of the share (Check) field in DocType 'DocPerm'
|
||||
|
|
@ -24393,7 +24393,7 @@ msgstr ""
|
|||
#. Description of the 'Sent Folder Name' (Data) field in DocType 'Email Domain'
|
||||
#: frappe/email/doctype/email_domain/email_domain.json
|
||||
msgid "Some mailboxes require a different Sent Folder Name e.g. \"INBOX.Sent\""
|
||||
msgstr ""
|
||||
msgstr "Для некоторых почтовых ящиков требуется другое имя папки \"Отправленные\", например \"INBOX.Sent\"."
|
||||
|
||||
#: frappe/public/js/frappe/desk.js:20
|
||||
msgid "Some of the features might not work in your browser. Please update your browser to the latest version."
|
||||
|
|
@ -24503,7 +24503,7 @@ msgstr ""
|
|||
#. 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Specify the domains or origins that are permitted to embed this form. Enter one domain per line (e.g., https://example.com). If no domains are specified, the form can only be embedded on the same origin."
|
||||
msgstr ""
|
||||
msgstr "Укажите домены или источники, которым разрешено встраивать эту форму. Укажите по одному домену в строке (например, https://example.com). Если домены не указаны, форма может быть встроена только в один источник."
|
||||
|
||||
#. Label of the splash_image (Attach Image) field in DocType 'Website Settings'
|
||||
#: frappe/website/doctype/website_settings/website_settings.json
|
||||
|
|
@ -25006,7 +25006,7 @@ msgstr ""
|
|||
#. Label of the button_label (Data) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Submit button label"
|
||||
msgstr ""
|
||||
msgstr "Ярлык кнопки отправки"
|
||||
|
||||
#. Label of the submit_on_creation (Check) field in DocType 'Auto Repeat'
|
||||
#: frappe/automation/doctype/auto_repeat/auto_repeat.json
|
||||
|
|
@ -25109,12 +25109,12 @@ msgstr ""
|
|||
#. Label of the success_message (Text) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Success message"
|
||||
msgstr ""
|
||||
msgstr "Сообщение об успехе"
|
||||
|
||||
#. Label of the success_title (Data) field in DocType 'Web Form'
|
||||
#: frappe/website/doctype/web_form/web_form.json
|
||||
msgid "Success title"
|
||||
msgstr ""
|
||||
msgstr "Название успеха"
|
||||
|
||||
#: frappe/www/update-password.html:81
|
||||
msgid "Success! You are good to go 👍"
|
||||
|
|
@ -25256,7 +25256,7 @@ msgstr ""
|
|||
#. Label of the sync_as_public (Check) field in DocType 'Google Calendar'
|
||||
#: frappe/integrations/doctype/google_calendar/google_calendar.json
|
||||
msgid "Sync events from Google as public"
|
||||
msgstr ""
|
||||
msgstr "Синхронизируйте события из Google как общедоступные"
|
||||
|
||||
#: frappe/custom/doctype/customize_form/customize_form.js:256
|
||||
msgid "Sync on Migrate"
|
||||
|
|
@ -25845,7 +25845,7 @@ msgstr ""
|
|||
|
||||
#: frappe/automation/doctype/auto_repeat/auto_repeat.py:108
|
||||
msgid "The Next Scheduled Date cannot be later than the End Date."
|
||||
msgstr ""
|
||||
msgstr "Следующая запланированная дата не может быть позже даты окончания."
|
||||
|
||||
#: frappe/integrations/doctype/push_notification_settings/push_notification_settings.py:29
|
||||
msgid "The Push Relay Server URL key (`push_relay_server_url`) is missing in your site config"
|
||||
|
|
@ -26935,7 +26935,7 @@ msgstr ""
|
|||
|
||||
#: frappe/core/report/prepared_report_analytics/prepared_report_analytics.js:13
|
||||
msgid "Top 10"
|
||||
msgstr ""
|
||||
msgstr "Топ 10"
|
||||
|
||||
#. Name of a DocType
|
||||
#: frappe/website/doctype/top_bar_item/top_bar_item.json
|
||||
|
|
@ -27164,7 +27164,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/frappe/views/reports/query_report.js:2188
|
||||
msgid "Translate Data"
|
||||
msgstr ""
|
||||
msgstr "Перевести данные"
|
||||
|
||||
#. Label of the translated_doctype (Check) field in DocType 'DocType'
|
||||
#. Label of the translated_doctype (Check) field in DocType 'Customize Form'
|
||||
|
|
@ -27772,7 +27772,7 @@ msgstr ""
|
|||
|
||||
#: frappe/public/js/billing.bundle.js:131
|
||||
msgid "Upgrade plan"
|
||||
msgstr ""
|
||||
msgstr "План модернизации"
|
||||
|
||||
#: frappe/public/js/frappe/list/list_sidebar.js:331
|
||||
msgid "Upgrade your support experience with Frappe Helpdesk"
|
||||
|
|
@ -28578,7 +28578,7 @@ msgstr ""
|
|||
|
||||
#: frappe/core/doctype/file/file.js:4
|
||||
msgid "View File"
|
||||
msgstr ""
|
||||
msgstr "Просмотр файла"
|
||||
|
||||
#: frappe/public/js/frappe/ui/notifications/notifications.js:220
|
||||
msgid "View Full Log"
|
||||
|
|
@ -29211,7 +29211,7 @@ msgstr ""
|
|||
#. in DocType 'System Settings'
|
||||
#: frappe/core/doctype/system_settings/system_settings.json
|
||||
msgid "Will run scheduled jobs only once a day for inactive sites. Set it to 0 to avoid automatically disabling the scheduler."
|
||||
msgstr ""
|
||||
msgstr "Будет запускать запланированные задания только раз в день для неактивных сайтов. Установите значение 0, чтобы избежать автоматического отключения планировщика."
|
||||
|
||||
#: frappe/public/js/frappe/form/print_utils.js:15
|
||||
msgid "With Letter head"
|
||||
|
|
@ -29580,7 +29580,7 @@ msgstr ""
|
|||
|
||||
#: frappe/integrations/frappe_providers/frappecloud_billing.py:28
|
||||
msgid "You are not allowed to access this resource"
|
||||
msgstr ""
|
||||
msgstr "Вы не имеете права доступа к этому ресурсу"
|
||||
|
||||
#: frappe/permissions.py:409
|
||||
msgid "You are not allowed to access this {0} record because it is linked to {1} '{2}' in field {3}"
|
||||
|
|
@ -29716,7 +29716,7 @@ msgstr ""
|
|||
|
||||
#: frappe/handler.py:182
|
||||
msgid "You can only upload JPG, PNG, PDF, TXT, CSV or Microsoft documents."
|
||||
msgstr ""
|
||||
msgstr "Вы можете загружать только документы в форматах JPG, PNG, PDF, TXT, CSV или Microsoft."
|
||||
|
||||
#: frappe/core/doctype/data_export/exporter.py:199
|
||||
msgid "You can only upload upto 5000 records in one go. (may be less in some cases)"
|
||||
|
|
@ -29908,11 +29908,11 @@ msgstr ""
|
|||
|
||||
#: frappe/model/document.py:357
|
||||
msgid "You need the '{0}' permission on {1} {2} to perform this action."
|
||||
msgstr ""
|
||||
msgstr "Для выполнения этого действия вам необходимо разрешение '{0}' на {1} {2} ."
|
||||
|
||||
#: frappe/desk/doctype/workspace/workspace.py:127
|
||||
msgid "You need to be Workspace Manager to delete a public workspace."
|
||||
msgstr ""
|
||||
msgstr "Чтобы удалить публичную рабочую область, необходимо быть менеджером рабочей области."
|
||||
|
||||
#: frappe/desk/doctype/workspace/workspace.py:76
|
||||
msgid "You need to be Workspace Manager to edit this document"
|
||||
|
|
@ -29964,11 +29964,11 @@ msgstr ""
|
|||
|
||||
#: frappe/model/rename_doc.py:391
|
||||
msgid "You need write permission on {0} {1} to merge"
|
||||
msgstr ""
|
||||
msgstr "Для объединения необходимо разрешение на запись на {0} {1} ."
|
||||
|
||||
#: frappe/model/rename_doc.py:386
|
||||
msgid "You need write permission on {0} {1} to rename"
|
||||
msgstr ""
|
||||
msgstr "Чтобы переименовать {0} {1} , вам нужно разрешение на запись."
|
||||
|
||||
#: frappe/client.py:449
|
||||
msgid "You need {0} permission to fetch values from {1} {2}"
|
||||
|
|
@ -31014,7 +31014,7 @@ msgstr ""
|
|||
|
||||
#: frappe/model/document.py:547
|
||||
msgid "{0} cannot be amended because it is not cancelled. Please cancel the document before creating an amendment."
|
||||
msgstr ""
|
||||
msgstr "{0} не может быть изменен, поскольку он не отменен. Пожалуйста, отмените документ перед созданием поправки."
|
||||
|
||||
#: frappe/public/js/form_builder/store.js:190
|
||||
msgid "{0} cannot be hidden and mandatory without any default value"
|
||||
|
|
@ -31540,7 +31540,7 @@ msgstr ""
|
|||
|
||||
#: frappe/utils/print_format.py:148 frappe/utils/print_format.py:192
|
||||
msgid "{0}/{1} complete | Please leave this tab open until completion."
|
||||
msgstr ""
|
||||
msgstr "{0}/{1} complete | Пожалуйста, оставьте эту вкладку открытой до завершения."
|
||||
|
||||
#: frappe/model/base_document.py:1115
|
||||
msgid "{0}: '{1}' ({3}) will get truncated, as max characters allowed is {2}"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ msgstr ""
|
|||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: developers@frappe.io\n"
|
||||
"POT-Creation-Date: 2025-06-08 09:34+0000\n"
|
||||
"PO-Revision-Date: 2025-06-11 14:05\n"
|
||||
"PO-Revision-Date: 2025-06-12 14:56\n"
|
||||
"Last-Translator: developers@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
|
@ -7077,12 +7077,12 @@ msgstr "Borttagen DocType"
|
|||
#. Name of a DocType
|
||||
#: frappe/core/doctype/deleted_document/deleted_document.json
|
||||
msgid "Deleted Document"
|
||||
msgstr "Papperskorg"
|
||||
msgstr "Raderad Dokument"
|
||||
|
||||
#. Label of a Link in the Tools Workspace
|
||||
#: frappe/automation/workspace/tools/tools.json
|
||||
msgid "Deleted Documents"
|
||||
msgstr "Papperskorg"
|
||||
msgstr "Raderade Dokument"
|
||||
|
||||
#. Label of the deleted_name (Data) field in DocType 'Deleted Document'
|
||||
#: frappe/core/doctype/deleted_document/deleted_document.json
|
||||
|
|
@ -13708,7 +13708,7 @@ msgstr "Är Installation Klar?"
|
|||
#: frappe/core/doctype/doctype/doctype_list.js:64
|
||||
#: frappe/desk/doctype/onboarding_step/onboarding_step.json
|
||||
msgid "Is Single"
|
||||
msgstr "Är Enskild"
|
||||
msgstr "Är Singel"
|
||||
|
||||
#. Label of the is_skipped (Check) field in DocType 'Onboarding Step'
|
||||
#: frappe/desk/doctype/onboarding_step/onboarding_step.json
|
||||
|
|
@ -19028,7 +19028,7 @@ msgstr "Tillåtna Roller"
|
|||
#. Option for the 'Address Type' (Select) field in DocType 'Address'
|
||||
#: frappe/contacts/doctype/address/address.json
|
||||
msgid "Personal"
|
||||
msgstr "Personal"
|
||||
msgstr "Personlig"
|
||||
|
||||
#. Name of a DocType
|
||||
#: frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
|
||||
|
|
@ -20178,7 +20178,7 @@ msgstr "Publik"
|
|||
#. Report'
|
||||
#: frappe/desk/doctype/system_health_report/system_health_report.json
|
||||
msgid "Public Files (MB)"
|
||||
msgstr "Publika Filer (MB)"
|
||||
msgstr "Allmänna Filer (MB)"
|
||||
|
||||
#. Label of the publish (Check) field in DocType 'Package Release'
|
||||
#: frappe/core/doctype/package_release/package_release.json
|
||||
|
|
@ -30772,7 +30772,7 @@ msgstr "på_godkänn"
|
|||
#. Option for the 'Doc Event' (Select) field in DocType 'Webhook'
|
||||
#: frappe/integrations/doctype/webhook/webhook.json
|
||||
msgid "on_trash"
|
||||
msgstr "på_papperskorg"
|
||||
msgstr "on_trash"
|
||||
|
||||
#. Option for the 'Doc Event' (Select) field in DocType 'Webhook'
|
||||
#: frappe/integrations/doctype/webhook/webhook.json
|
||||
|
|
|
|||
43678
frappe/locale/vi.po
43678
frappe/locale/vi.po
File diff suppressed because it is too large
Load diff
|
|
@ -977,7 +977,7 @@ class BaseDocument:
|
|||
self.set(df.fieldname, cstr(self.get(df.fieldname)).strip())
|
||||
value = self.get(df.fieldname)
|
||||
|
||||
if value not in options and not (frappe.flags.in_test and value.startswith("_T-")):
|
||||
if value not in options and not (frappe.in_test and value.startswith("_T-")):
|
||||
# show an elaborate message
|
||||
prefix = _("Row #{0}:").format(self.idx) if self.get("parentfield") else ""
|
||||
label = _(self.meta.get_label(df.fieldname))
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ def delete_doc(
|
|||
"frappe.model.delete_doc.delete_dynamic_links",
|
||||
doctype=doc.doctype,
|
||||
name=doc.name,
|
||||
now=frappe.flags.in_test,
|
||||
now=frappe.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ class Document(BaseDocument):
|
|||
if ignore_permissions is not None:
|
||||
self.flags.ignore_permissions = ignore_permissions
|
||||
|
||||
self.flags.ignore_version = frappe.flags.in_test if ignore_version is None else ignore_version
|
||||
self.flags.ignore_version = frappe.in_test if ignore_version is None else ignore_version
|
||||
|
||||
if self.get("__islocal") or not self.get("name"):
|
||||
return self.insert()
|
||||
|
|
@ -1201,7 +1201,9 @@ class Document(BaseDocument):
|
|||
self.docstatus = DocStatus.CANCELLED
|
||||
return self.save()
|
||||
|
||||
def _rename(self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True):
|
||||
def _rename(
|
||||
self, name: str | int, merge: bool = False, force: bool = False, validate_rename: bool = True
|
||||
):
|
||||
"""Rename the document. Triggers frappe.rename_doc, then reloads."""
|
||||
from frappe.model.rename_doc import rename_doc
|
||||
|
||||
|
|
@ -1238,7 +1240,7 @@ class Document(BaseDocument):
|
|||
self.run_method("on_discard")
|
||||
|
||||
@frappe.whitelist()
|
||||
def rename(self, name: str, merge=False, force=False, validate_rename=True):
|
||||
def rename(self, name: str | int, merge=False, force=False, validate_rename=True):
|
||||
"""Rename the document to `name`. This transforms the current object."""
|
||||
return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ def get_dynamic_link_map(for_delete=False):
|
|||
|
||||
Note: Will not map single doctypes
|
||||
"""
|
||||
if getattr(frappe.local, "dynamic_link_map", None) is None or frappe.flags.in_test:
|
||||
if getattr(frappe.local, "dynamic_link_map", None) is None or frappe.in_test:
|
||||
# Build from scratch
|
||||
dynamic_link_map = {}
|
||||
for df in get_dynamic_links():
|
||||
|
|
|
|||
|
|
@ -45,6 +45,9 @@ from frappe.utils import cached_property, cast, cint, cstr
|
|||
from frappe.utils.caching import site_cache
|
||||
from frappe.utils.data import add_to_date, get_datetime
|
||||
|
||||
ListOrTuple = list | tuple
|
||||
SerializableTypes = str | int | float | datetime
|
||||
|
||||
DEFAULT_FIELD_LABELS = {
|
||||
"name": _lt("ID"),
|
||||
"creation": _lt("Created On"),
|
||||
|
|
@ -176,31 +179,7 @@ class Meta(Document):
|
|||
self.check_if_large_table()
|
||||
|
||||
def as_dict(self, no_nulls=False):
|
||||
def serialize(doc):
|
||||
if isinstance(doc, dict):
|
||||
return doc.copy()
|
||||
out = {}
|
||||
for key, value in doc.__dict__.items():
|
||||
if isinstance(value, list | tuple):
|
||||
if not value or not isinstance(value[0], BaseDocument):
|
||||
# non standard list object, skip
|
||||
continue
|
||||
|
||||
value = [serialize(d) for d in value]
|
||||
|
||||
if (not no_nulls and value is None) or isinstance(
|
||||
value, str | int | float | datetime | list | tuple
|
||||
):
|
||||
out[key] = value
|
||||
|
||||
# set empty lists for unset table fields
|
||||
for fieldname in TABLE_DOCTYPES_FOR_DOCTYPE.keys():
|
||||
if out.get(fieldname) is None:
|
||||
out[fieldname] = []
|
||||
|
||||
return out
|
||||
|
||||
return serialize(self)
|
||||
return _serialize(self, no_nulls=no_nulls)
|
||||
|
||||
def get_link_fields(self):
|
||||
return self.get("fields", {"fieldtype": "Link", "options": ["!=", "[Select]"]})
|
||||
|
|
@ -977,6 +956,41 @@ def _update_field_order_based_on_insert_after(field_order, insert_after_map):
|
|||
field_order.extend(fields)
|
||||
|
||||
|
||||
CACHE_PROPERTIES = frozenset(
|
||||
(
|
||||
"_fields",
|
||||
"_table_fields",
|
||||
"_table_doctypes",
|
||||
*(prop for prop, value in vars(Meta).items() if isinstance(value, cached_property)),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _serialize(doc, no_nulls=False, *, is_child=False):
|
||||
out = {}
|
||||
for key, value in doc.__dict__.items():
|
||||
if not is_child:
|
||||
if key in CACHE_PROPERTIES:
|
||||
continue
|
||||
|
||||
if isinstance(value, ListOrTuple):
|
||||
if value and isinstance(value[0], BaseDocument):
|
||||
out[key] = [_serialize(d, no_nulls=no_nulls, is_child=True) for d in value]
|
||||
|
||||
continue
|
||||
|
||||
if (not no_nulls and value is None) or isinstance(value, SerializableTypes):
|
||||
out[key] = value
|
||||
|
||||
if not is_child:
|
||||
# set empty lists for unset table fields
|
||||
for fieldname in TABLE_DOCTYPES_FOR_DOCTYPE:
|
||||
if out.get(fieldname) is None:
|
||||
out[fieldname] = []
|
||||
|
||||
return out
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
# This is DX hack to add all fields from DocType to meta for autocompletions.
|
||||
# Meta is technically doctype + special fields on meta.
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ class TracedValue:
|
|||
|
||||
"""
|
||||
if value in self.forbidden_values:
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
frappe.throw(f"{self.field_name} cannot be set to {value}", AssertionError)
|
||||
else:
|
||||
frappe.throw(f"{self.field_name} cannot be set to {value}")
|
||||
|
|
@ -99,7 +99,7 @@ class TracedValue:
|
|||
try:
|
||||
self.custom_validation(obj, value)
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
if frappe.in_test:
|
||||
frappe.throw(str(e), AssertionError)
|
||||
else:
|
||||
frappe.throw(str(e))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import click
|
|||
import requests
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import make_test_records
|
||||
from frappe.tests.utils import make_test_records, toggle_test_mode
|
||||
|
||||
from .testing.environment import _decorate_all_methods_and_functions_with_type_checker
|
||||
from .testing.result import TestResult
|
||||
|
|
@ -29,12 +29,13 @@ TEST_WEIGHT_OVERRIDES = {
|
|||
|
||||
|
||||
class ParallelTestRunner:
|
||||
def __init__(self, app, site, build_number=1, total_builds=1, dry_run=False):
|
||||
def __init__(self, app, site, build_number=1, total_builds=1, dry_run=False, lightmode=False):
|
||||
self.app = app
|
||||
self.site = site
|
||||
self.build_number = frappe.utils.cint(build_number) or 1
|
||||
self.total_builds = frappe.utils.cint(total_builds)
|
||||
self.dry_run = dry_run
|
||||
self.lightmode = lightmode
|
||||
self.test_file_list = []
|
||||
self.total_test_weight = 0
|
||||
self.test_result = None
|
||||
|
|
@ -53,11 +54,12 @@ class ParallelTestRunner:
|
|||
if self.dry_run:
|
||||
return
|
||||
|
||||
frappe.flags.in_test = True
|
||||
toggle_test_mode(True)
|
||||
frappe.clear_cache()
|
||||
frappe.utils.scheduler.disable_scheduler()
|
||||
_decorate_all_methods_and_functions_with_type_checker()
|
||||
self.before_test_setup()
|
||||
if not self.lightmode:
|
||||
_decorate_all_methods_and_functions_with_type_checker()
|
||||
self.before_test_setup()
|
||||
|
||||
def before_test_setup(self):
|
||||
start_time = time.monotonic()
|
||||
|
|
@ -103,9 +105,12 @@ class ParallelTestRunner:
|
|||
frappe.set_user("Administrator")
|
||||
path, filename = file_info
|
||||
module = self.get_module(path, filename)
|
||||
from frappe.deprecation_dumpster import compat_preload_test_records_upfront
|
||||
|
||||
compat_preload_test_records_upfront([(module, path, filename)])
|
||||
if not self.lightmode:
|
||||
from frappe.deprecation_dumpster import compat_preload_test_records_upfront
|
||||
|
||||
compat_preload_test_records_upfront([(module, path, filename)])
|
||||
|
||||
test_suite = unittest.TestSuite()
|
||||
module_test_cases = unittest.TestLoader().loadTestsFromModule(module)
|
||||
test_suite.addTest(module_test_cases)
|
||||
|
|
|
|||
|
|
@ -246,4 +246,4 @@ frappe.patches.v16_0.move_role_desk_settings_to_user
|
|||
frappe.printing.doctype.print_format.patches.sets_wkhtmltopdf_as_default_for_pdf_generator_field
|
||||
frappe.patches.v14_0.fix_user_settings_collation
|
||||
execute:frappe.call("frappe.core.doctype.system_settings.system_settings.sync_system_settings")
|
||||
frappe.patches.v16_0.social_eps_deprecation_warning
|
||||
frappe.patches.v16_0.add_module_deprecation_warning
|
||||
|
|
|
|||
15
frappe/patches/v16_0/add_module_deprecation_warning.py
Normal file
15
frappe/patches/v16_0/add_module_deprecation_warning.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
module_app_map = {
|
||||
"Social Module/ Energy Points System": ("eps", "system"),
|
||||
"Offsite Backup Integrations (Google Drive, S3, Dropbox)": ("offsite_backups", "intergration"),
|
||||
}
|
||||
for module, (app, system_type) in module_app_map.items():
|
||||
click.secho(
|
||||
f"{module} is moving to a new app and will removed from the framework in version-16.\n"
|
||||
f"Please install the app to continue using the {system_type}: https://github.com/frappe/{app}",
|
||||
fg="yellow",
|
||||
)
|
||||
click.secho("\n")
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
import click
|
||||
|
||||
|
||||
def execute():
|
||||
click.secho(
|
||||
"Social Module/Energy Points System is moving to a new app and will removed from the framework in version-16.\n"
|
||||
"Please install the app to continue using the integration: https://github.com/frappe/eps",
|
||||
fg="yellow",
|
||||
)
|
||||
|
|
@ -70,7 +70,7 @@ class PrintFormat(Document):
|
|||
and not frappe.local.conf.get("developer_mode")
|
||||
and not frappe.flags.in_migrate
|
||||
and not frappe.flags.in_install
|
||||
and not frappe.flags.in_test
|
||||
and not frappe.in_test
|
||||
):
|
||||
frappe.throw(frappe._("Standard Print Format cannot be updated"))
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class PrintStyle(Document):
|
|||
self.standard == 1
|
||||
and not frappe.local.conf.get("developer_mode")
|
||||
and not frappe.flags.in_import
|
||||
and not frappe.flags.in_test
|
||||
and not frappe.in_test
|
||||
):
|
||||
frappe.throw(frappe._("Standard Print Style cannot be changed. Please duplicate to edit."))
|
||||
|
||||
|
|
|
|||
|
|
@ -394,8 +394,8 @@ import "air-datepicker/dist/js/i18n/datepicker.zh.js";
|
|||
"Mart",
|
||||
"April",
|
||||
"Maj",
|
||||
"Juni",
|
||||
"Juli",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Avgust",
|
||||
"Septembar",
|
||||
"Oktobar",
|
||||
|
|
@ -417,7 +417,7 @@ import "air-datepicker/dist/js/i18n/datepicker.zh.js";
|
|||
"Dec",
|
||||
],
|
||||
today: "Danas",
|
||||
clear: "Resetiraj",
|
||||
clear: "Resetuj",
|
||||
dateFormat: "dd/mm/yyyy",
|
||||
timeFormat: "hh:ii",
|
||||
firstDay: 1,
|
||||
|
|
|
|||
|
|
@ -20,19 +20,19 @@ frappe.ui.form.ControlDuration = class ControlDuration extends frappe.ui.form.Co
|
|||
</div>`
|
||||
);
|
||||
this.$wrapper.append(this.$picker);
|
||||
this.build_numeric_input("days", this.duration_options.hide_days);
|
||||
this.build_numeric_input("hours", false);
|
||||
this.build_numeric_input("minutes", false);
|
||||
this.build_numeric_input("seconds", this.duration_options.hide_seconds);
|
||||
this.build_numeric_input("days", this.duration_options.hide_days, 0, "Days");
|
||||
this.build_numeric_input("hours", false, 0, "Hours");
|
||||
this.build_numeric_input("minutes", false, 0, "Minutes");
|
||||
this.build_numeric_input("seconds", this.duration_options.hide_seconds, 0, "Seconds");
|
||||
this.set_duration_picker_value(this.value);
|
||||
this.$picker.hide();
|
||||
this.bind_events();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
build_numeric_input(label, hidden, max) {
|
||||
build_numeric_input(name, hidden, max, label) {
|
||||
let $duration_input = $(`
|
||||
<input class="input-sm duration-input" data-duration="${label}" type="number" min="0" value="0">
|
||||
<input class="input-sm duration-input" data-duration="${name}" type="number" min="0" value="0">
|
||||
`);
|
||||
|
||||
let $input = $(`<div class="row duration-row"></div>`).prepend($duration_input);
|
||||
|
|
@ -41,7 +41,7 @@ frappe.ui.form.ControlDuration = class ControlDuration extends frappe.ui.form.Co
|
|||
$duration_input.attr("max", max);
|
||||
}
|
||||
|
||||
this.inputs[label] = $duration_input;
|
||||
this.inputs[name] = $duration_input;
|
||||
|
||||
let $control = $(`
|
||||
<div class="col duration-col">
|
||||
|
|
|
|||
|
|
@ -41,7 +41,10 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
|
|||
);
|
||||
},
|
||||
() => {
|
||||
this.parse_validate_and_set_in_model("");
|
||||
frappe.model.clear_doc(this.df.options, row.name);
|
||||
|
||||
this.frm.dirty();
|
||||
this.refresh();
|
||||
|
||||
return this.frm.script_manager.trigger(
|
||||
`${this.df.fieldname}_remove`,
|
||||
|
|
|
|||
|
|
@ -233,7 +233,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
|
|||
},
|
||||
},
|
||||
theme: this.df.theme || "snow",
|
||||
readOnly: this.disabled,
|
||||
readOnly: this.disabled || this.df.read_only,
|
||||
bounds: this.quill_container[0],
|
||||
placeholder: this.df.placeholder || "",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -212,55 +212,17 @@ $.extend(frappe.model, {
|
|||
return docfield[0];
|
||||
},
|
||||
|
||||
get_from_localstorage: function (doctype) {
|
||||
if (localStorage["_doctype:" + doctype]) {
|
||||
return JSON.parse(localStorage["_doctype:" + doctype]);
|
||||
}
|
||||
},
|
||||
|
||||
set_in_localstorage: function (doctype, docs) {
|
||||
try {
|
||||
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
|
||||
} catch (e) {
|
||||
// if quota is exceeded, clear local storage and set item
|
||||
console.warn("localStorage quota exceeded, clearing doctype cache");
|
||||
frappe.model.clear_local_storage();
|
||||
localStorage["_doctype:" + doctype] = JSON.stringify(docs);
|
||||
}
|
||||
},
|
||||
|
||||
clear_local_storage: function () {
|
||||
for (var key in localStorage) {
|
||||
if (key.startsWith("_doctype:")) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
with_doctype: function (doctype, callback, async) {
|
||||
if (locals.DocType[doctype]) {
|
||||
callback && callback();
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
let cached_timestamp = null;
|
||||
let meta = null;
|
||||
|
||||
let cached_docs = frappe.model.get_from_localstorage(doctype);
|
||||
|
||||
if (cached_docs) {
|
||||
meta = cached_docs.filter((doc) => doc.name === doctype)[0];
|
||||
if (meta) {
|
||||
cached_timestamp = meta.modified;
|
||||
}
|
||||
}
|
||||
|
||||
return frappe.call({
|
||||
method: "frappe.desk.form.load.getdoctype",
|
||||
type: "GET",
|
||||
args: {
|
||||
doctype: doctype,
|
||||
with_parent: 1,
|
||||
cached_timestamp: cached_timestamp,
|
||||
},
|
||||
async: async,
|
||||
callback: function (r) {
|
||||
|
|
@ -268,12 +230,7 @@ $.extend(frappe.model, {
|
|||
frappe.msgprint(__("Unable to load: {0}", [__(doctype)]));
|
||||
throw "No doctype";
|
||||
}
|
||||
if (r.message == "use_cache") {
|
||||
frappe.model.sync(meta);
|
||||
} else {
|
||||
frappe.model.set_in_localstorage(doctype, r.docs);
|
||||
meta = r.docs[0];
|
||||
}
|
||||
let meta = r.docs[0];
|
||||
frappe.model.init_doctype(meta);
|
||||
|
||||
if (r.user_settings) {
|
||||
|
|
@ -293,13 +250,7 @@ $.extend(frappe.model, {
|
|||
// meta has sugar, like __js and other properties that doc won't have
|
||||
frappe.meta.__doctype_meta = JSON.parse(JSON.stringify(meta));
|
||||
}
|
||||
for (const asset_key of [
|
||||
"__list_js",
|
||||
"__custom_list_js",
|
||||
"__calendar_js",
|
||||
"__map_js",
|
||||
"__tree_js",
|
||||
]) {
|
||||
for (const asset_key of ["__list_js", "__custom_list_js", "__calendar_js", "__tree_js"]) {
|
||||
if (meta[asset_key]) {
|
||||
new Function(meta[asset_key])();
|
||||
}
|
||||
|
|
@ -858,7 +809,7 @@ $.extend(frappe.model, {
|
|||
let meta = frappe.get_meta(doctype);
|
||||
let default_views = ["List", "Report", "Dashboard", "Kanban"];
|
||||
|
||||
if (meta.is_calendar_and_gantt && frappe.views.calendar[doctype]) {
|
||||
if (meta.is_calendar_and_gantt) {
|
||||
let views = ["Calendar", "Gantt"];
|
||||
default_views.push(...views);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,3 +51,7 @@
|
|||
.filter-row div {
|
||||
display: inline-block;
|
||||
}
|
||||
// prevent <ol> numbering conflicts
|
||||
.ql-editor {
|
||||
counter-reset: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,7 +360,7 @@ def start(
|
|||
@administrator_only
|
||||
def stop(*args, **kwargs):
|
||||
frappe.client_cache.set_value(RECORDER_INTERCEPT_FLAG, False)
|
||||
frappe.enqueue(post_process, now=frappe.flags.in_test)
|
||||
frappe.enqueue(post_process, now=frappe.in_test)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ from frappe.utils.data import add_to_date
|
|||
|
||||
@frappe.whitelist()
|
||||
def clear():
|
||||
# updating session causes a commit, explicit commit not needed
|
||||
frappe.local.session_obj.update(force=True)
|
||||
frappe.local.db.commit()
|
||||
clear_user_cache(frappe.session.user)
|
||||
frappe.response["message"] = _("Cache Cleared")
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
|
|||
|
||||
logout_feed(user, reason)
|
||||
frappe.db.delete("Sessions", {"sid": sid})
|
||||
frappe.db.commit()
|
||||
frappe.db.commit(chain=True)
|
||||
|
||||
frappe.cache.hdel("session", sid)
|
||||
|
||||
|
|
@ -199,7 +199,7 @@ def get_csrf_token():
|
|||
|
||||
def generate_csrf_token():
|
||||
frappe.local.session.data.csrf_token = frappe.generate_hash()
|
||||
if not frappe.flags.in_test:
|
||||
if not frappe.in_test:
|
||||
frappe.local.session_obj.update(force=True)
|
||||
|
||||
|
||||
|
|
@ -433,7 +433,7 @@ class Session:
|
|||
|
||||
frappe.db.set_value("User", frappe.session.user, "last_active", now, update_modified=False)
|
||||
|
||||
frappe.db.commit()
|
||||
frappe.db.commit(chain=True)
|
||||
updated_in_db = True
|
||||
frappe.cache.hset("session", self.sid, self.data)
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import tomli
|
|||
|
||||
import frappe
|
||||
import frappe.utils.scheduler
|
||||
from frappe.tests.utils import make_test_records
|
||||
from frappe.tests.utils import make_test_records, toggle_test_mode
|
||||
|
||||
from .runner import TestRunnerError
|
||||
from .utils import debug_timer
|
||||
|
|
@ -53,7 +53,7 @@ def _initialize_test_environment(site, config):
|
|||
raise TestRunnerError(f"Failed to connect to the database: {e}") from e
|
||||
|
||||
# Set various test-related flags
|
||||
frappe.flags.in_test = True
|
||||
toggle_test_mode(True)
|
||||
frappe.flags.print_messages = logger.getEffectiveLevel() < logging.INFO
|
||||
frappe.flags.tests_verbose = logger.getEffectiveLevel() < logging.INFO
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import requests
|
|||
import frappe
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.tests.test_api import FrappeAPITestCase, suppress_stdout
|
||||
from frappe.tests.utils import toggle_test_mode
|
||||
|
||||
authorization_token = None
|
||||
|
||||
|
|
@ -88,7 +89,13 @@ class TestResourceAPIV2(FrappeAPITestCase):
|
|||
def test_copy_document(self):
|
||||
doc = frappe.get_doc(self.DOCTYPE, self.GENERATED_DOCUMENTS[0])
|
||||
|
||||
response = self.get(self.resource(self.DOCTYPE, doc.name, "copy"))
|
||||
# disabled temporarily to assert that `docstatus` is not copied outside of tests
|
||||
toggle_test_mode(False)
|
||||
try:
|
||||
response = self.get(self.resource(self.DOCTYPE, doc.name, "copy"))
|
||||
finally:
|
||||
toggle_test_mode(True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json["data"]
|
||||
|
||||
|
|
@ -106,7 +113,7 @@ class TestResourceAPIV2(FrappeAPITestCase):
|
|||
|
||||
def test_delete_document(self):
|
||||
doc_to_delete = choice(self.GENERATED_DOCUMENTS)
|
||||
response = self.delete(self.resource(self.DOCTYPE, doc_to_delete))
|
||||
response = self.delete(self.resource(self.DOCTYPE, doc_to_delete), data={"sid": self.sid})
|
||||
self.assertEqual(response.status_code, 202)
|
||||
self.assertDictEqual(response.json, {"data": "ok"})
|
||||
|
||||
|
|
|
|||
|
|
@ -71,11 +71,11 @@ class TestDB(IntegrationTestCase):
|
|||
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
|
||||
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("User", {}, ["Max(name)"], order_by=None),
|
||||
frappe.db.get_value("User", {}, [{"MAX": "name"}], order_by=None),
|
||||
frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0],
|
||||
)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value("User", {}, "Min(name)", order_by=None),
|
||||
frappe.db.get_value("User", {}, [{"MIN": "name"}], order_by=None),
|
||||
frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0],
|
||||
)
|
||||
self.assertIn(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ def setup_test_user(set_user=False):
|
|||
|
||||
yield test_user
|
||||
|
||||
test_user.reload()
|
||||
test_user.remove_roles("Blogger")
|
||||
test_user.add_roles(*user_roles)
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue