diff --git a/frappe/__init__.py b/frappe/__init__.py
index 07f75ecd31..5e790c02ce 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -199,6 +199,7 @@ def init(site, sites_path=None, new_site=False):
}
)
local.rollback_observers = []
+ local.locked_documents = []
local.before_commit = []
local.test_objects = {}
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 67e1de0932..c5e61563f8 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -989,6 +989,16 @@ class Document(BaseDocument):
self.docstatus = DocStatus.cancelled()
return self.save()
+ @whitelist.__func__
+ def _rename(
+ self, name: str, 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
+
+ self.name = rename_doc(doc=self, new=name, merge=merge, force=force, validate=validate_rename)
+ self.reload()
+
@whitelist.__func__
def submit(self):
"""Submit the document. Sets `docstatus` = 1, then saves."""
@@ -999,6 +1009,13 @@ class Document(BaseDocument):
"""Cancel the document. Sets `docstatus` = 2, then saves."""
return self._cancel()
+ @whitelist.__func__
+ def rename(
+ self, name: str, merge: bool = False, force: bool = False, validate_rename: bool = True
+ ):
+ """Rename the document to `name`. This transforms the current object."""
+ return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename)
+
def delete(self, ignore_permissions=False):
"""Delete document."""
frappe.delete_doc(
@@ -1398,21 +1415,22 @@ class Document(BaseDocument):
# See: Stock Reconciliation
from frappe.utils.background_jobs import enqueue
- if hasattr(self, "_" + action):
- action = "_" + action
+ if hasattr(self, f"_{action}"):
+ action = f"_{action}"
- if file_lock.lock_exists(self.get_signature()):
+ try:
+ self.lock()
+ except frappe.DocumentLockedError:
frappe.throw(
_("This document is currently queued for execution. Please try again"),
title=_("Document Queued"),
)
- self.lock()
- enqueue(
+ return enqueue(
"frappe.model.document.execute_action",
- doctype=self.doctype,
- name=self.name,
- action=action,
+ __doctype=self.doctype,
+ __name=self.name,
+ __action=action,
**kwargs,
)
@@ -1433,10 +1451,13 @@ class Document(BaseDocument):
if lock_exists:
raise frappe.DocumentLockedError
file_lock.create_lock(signature)
+ frappe.local.locked_documents.append(self)
def unlock(self):
"""Delete the lock file for this document"""
file_lock.delete_lock(self.get_signature())
+ if self in frappe.local.locked_documents:
+ frappe.local.locked_documents.remove(self)
# validation helpers
def validate_from_to_dates(self, from_date_field, to_date_field):
@@ -1495,12 +1516,12 @@ class Document(BaseDocument):
return f"{doctype}({name})"
-def execute_action(doctype, name, action, **kwargs):
+def execute_action(__doctype, __name, __action, **kwargs):
"""Execute an action on a document (called by background worker)"""
- doc = frappe.get_doc(doctype, name)
+ doc = frappe.get_doc(__doctype, __name)
doc.unlock()
try:
- getattr(doc, action)(**kwargs)
+ getattr(doc, __action)(**kwargs)
except Exception:
frappe.db.rollback()
@@ -1511,4 +1532,4 @@ def execute_action(doctype, name, action, **kwargs):
msg = "
" + frappe.get_traceback() + "
"
doc.add_comment("Comment", _("Action Failed") + "
" + msg)
- doc.notify_update()
+ doc.notify_update()
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index dee364ae8d..a0cd10f967 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -4,12 +4,15 @@ from typing import TYPE_CHECKING, Dict, List, Optional
import frappe
from frappe import _, bold
+from frappe.model.document import Document
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.model.naming import validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
from frappe.query_builder import Field
-from frappe.utils import cint
+from frappe.query_builder.utils import DocType, Table
+from frappe.utils.data import sbool
from frappe.utils.password import rename_password
+from frappe.utils.scheduler import is_scheduler_inactive
if TYPE_CHECKING:
from frappe.model.meta import Meta
@@ -23,10 +26,19 @@ def update_document_title(
title: Optional[str] = None,
name: Optional[str] = None,
merge: bool = False,
+ enqueue: bool = False,
**kwargs,
) -> str:
"""
- Update title from header in form view
+ Update the name or title of a document. Returns `name` if document was renamed,
+ `docname` if renaming operation was queued.
+
+ :param doctype: DocType of the document
+ :param docname: Name of the document
+ :param title: New Title of the document
+ :param name: New Name of the document
+ :param merge: Merge the current Document with the existing one if exists
+ :param enqueue: Enqueue the rename operation, title is updated in current process
"""
# to maintain backwards API compatibility
@@ -38,6 +50,10 @@ def update_document_title(
if not isinstance(obj, (str, type(None))):
frappe.throw(f"{obj=} must be of type str or None")
+ # handle bad API usages
+ merge = sbool(merge)
+ enqueue = sbool(enqueue)
+
doc = frappe.get_doc(doctype, docname)
doc.check_permission(permtype="write")
@@ -49,11 +65,34 @@ def update_document_title(
name_updated = updated_name and (updated_name != doc.name)
if name_updated:
- docname = rename_doc(doctype=doctype, old=docname, new=updated_name, merge=merge)
+ if enqueue and not is_scheduler_inactive():
+ current_name = doc.name
+
+ # before_name hook may have DocType specific validations or transformations
+ transformed_name = doc.run_method("before_rename", current_name, updated_name, merge)
+ if isinstance(transformed_name, dict):
+ transformed_name = transformed_name.get("new")
+ transformed_name = transformed_name or updated_name
+
+ # run rename validations before queueing
+ # use savepoints to avoid partial renames / commits
+ validate_rename(
+ doctype=doctype,
+ old=current_name,
+ new=transformed_name,
+ meta=doc.meta,
+ merge=merge,
+ save_point=True,
+ )
+
+ doc.queue_action("rename", name=transformed_name, merge=merge)
+ else:
+ doc.rename(updated_name, merge=merge)
if title_updated:
try:
- frappe.db.set_value(doctype, docname, title_field, updated_title)
+ setattr(doc, title_field, updated_title)
+ doc.save()
frappe.msgprint(_("Saved"), alert=True, indicator="green")
except Exception as e:
if frappe.db.is_duplicate_entry(e):
@@ -64,44 +103,64 @@ def update_document_title(
)
raise
- return docname
+ return doc.name
def rename_doc(
- doctype: str,
- old: str,
- new: str,
+ doctype: Optional[str] = None,
+ old: Optional[str] = None,
+ new: str = None,
force: bool = False,
merge: bool = False,
ignore_permissions: bool = False,
ignore_if_exists: bool = False,
show_alert: bool = True,
rebuild_search: bool = True,
+ doc: Optional[Document] = None,
+ validate: bool = True,
) -> str:
- """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link"."""
- if not frappe.db.exists(doctype, old):
- frappe.errprint(_("Failed: {0} to {1} because {0} doesn't exist.").format(old, new))
- return
+ """Rename a doc(dt, old) to doc(dt, new) and update all linked fields of type "Link".
- if ignore_if_exists and frappe.db.exists(doctype, new):
- frappe.errprint(_("Failed: {0} to {1} because {1} already exists.").format(old, new))
- return
+ doc: Document object to be renamed.
+ new: New name for the record. If None, and doctype is specified, new name may be automatically generated via before_rename hooks.
+ doctype: DocType of the document. Not required if doc is passed.
+ old: Current name of the document. Not required if doc is passed.
+ force: Allow even if document is not allowed to be renamed.
+ merge: Merge with existing document of new name.
+ ignore_permissions: Ignore user permissions while renaming.
+ ignore_if_exists: Don't raise exception if document with new name already exists. This will quietely overwrite the existing document.
+ show_alert: Display alert if document is renamed successfully.
+ rebuild_search: Rebuild linked doctype search after renaming.
+ validate: Validate before renaming. If False, it is assumed that the caller has already validated.
+ """
+ old_usage_style = doctype and old and new
+ new_usage_style = doc and new
- if old == new:
- frappe.errprint(
- _("Ignored: {0} to {1} no changes made because old and new name are the same.").format(old, new)
+ if not (new_usage_style or old_usage_style):
+ raise TypeError(
+ "{doctype, old, new} or {doc, new} are required arguments for frappe.model.rename_doc"
)
- return
- force = cint(force)
- merge = cint(merge)
+ old = old or doc.name
+ doctype = doctype or doc.doctype
+ force = sbool(force)
+ merge = sbool(merge)
meta = frappe.get_meta(doctype)
- # call before_rename
- old_doc = frappe.get_doc(doctype, old)
- out = old_doc.run_method("before_rename", old, new, merge) or {}
- new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
- new = validate_rename(doctype, new, meta, merge, force, ignore_permissions)
+ if validate:
+ old_doc = doc or frappe.get_doc(doctype, old)
+ out = old_doc.run_method("before_rename", old, new, merge) or {}
+ new = (out.get("new") or new) if isinstance(out, dict) else (out or new)
+ new = validate_rename(
+ doctype=doctype,
+ old=old,
+ new=new,
+ meta=meta,
+ merge=merge,
+ force=force,
+ ignore_permissions=ignore_permissions,
+ ignore_if_exists=ignore_if_exists,
+ )
if not merge:
rename_parent_and_child(doctype, old, new, meta)
@@ -139,11 +198,12 @@ def rename_doc(
rename_password(doctype, old, new)
# update user_permissions
- frappe.db.sql(
- """UPDATE `tabDefaultValue` SET `defvalue`=%s WHERE `parenttype`='User Permission'
- AND `defkey`=%s AND `defvalue`=%s""",
- (new, doctype, old),
- )
+ DefaultValue = DocType("DefaultValue")
+ frappe.qb.update(DefaultValue).set(DefaultValue.defvalue, new).where(
+ (DefaultValue.parenttype == "User Permission")
+ & (DefaultValue.defkey == doctype)
+ & (DefaultValue.defvalue == old)
+ ).run()
if merge:
new_doc.add_comment("Edit", _("merged {0} into {1}").format(frappe.bold(old), frappe.bold(new)))
@@ -207,15 +267,13 @@ def update_user_settings(old: str, new: str, link_fields: List[Dict]) -> None:
# find the user settings for the linked doctypes
linked_doctypes = {d.parent for d in link_fields if not d.issingle}
- user_settings_details = frappe.db.sql(
- """SELECT `user`, `doctype`, `data`
- FROM `__UserSettings`
- WHERE `data` like %s
- AND `doctype` IN ('{doctypes}')""".format(
- doctypes="', '".join(linked_doctypes)
- ),
- (old),
- as_dict=1,
+ UserSettings = Table("__UserSettings")
+
+ user_settings_details = (
+ frappe.qb.from_(UserSettings)
+ .select("user", "doctype", "data")
+ .where(UserSettings.data.like(old) & UserSettings.doctype.isin(linked_doctypes))
+ .run(as_dict=True)
)
# create the dict using the doctype name as key and values as list of the user settings
@@ -240,37 +298,33 @@ def update_customizations(old: str, new: str) -> None:
def update_attachments(doctype: str, old: str, new: str) -> None:
- try:
- if old != "File Data" and doctype != "DocType":
- frappe.db.sql(
- """update `tabFile` set attached_to_name=%s
- where attached_to_name=%s and attached_to_doctype=%s""",
- (new, old, doctype),
- )
- except frappe.db.ProgrammingError as e:
- if not frappe.db.is_column_missing(e):
- raise
+ if doctype != "DocType":
+ File = DocType("File")
+
+ frappe.qb.update(File).set(File.attached_to_name, new).where(
+ (File.attached_to_name == old) & (File.attached_to_doctype == doctype)
+ ).run()
def rename_versions(doctype: str, old: str, new: str) -> None:
- frappe.db.sql(
- """UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""",
- (new, doctype, old),
- )
+ Version = DocType("Version")
+
+ frappe.qb.update(Version).set(Version.docname, new).where(
+ (Version.docname == old) & (Version.ref_doctype == doctype)
+ ).run()
def rename_eps_records(doctype: str, old: str, new: str) -> None:
- epl = frappe.qb.DocType("Energy Point Log")
- (
- frappe.qb.update(epl)
- .set(epl.reference_name, new)
- .where((epl.reference_doctype == doctype) & (epl.reference_name == old))
+ EPL = DocType("Energy Point Log")
+
+ frappe.qb.update(EPL).set(EPL.reference_name, new).where(
+ (EPL.reference_doctype == doctype) & (EPL.reference_name == old)
).run()
def rename_parent_and_child(doctype: str, old: str, new: str, meta: "Meta") -> None:
- # rename the doc
- frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, "%s"), (new, old))
+ frappe.qb.update(doctype).set("name", new).where(Field("name") == old).run()
+
update_autoname_field(doctype, new, meta)
update_child_docs(old, new, meta)
@@ -280,20 +334,36 @@ def update_autoname_field(doctype: str, new: str, meta: "Meta") -> None:
if meta.get("autoname"):
field = meta.get("autoname").split(":")
if field and field[0] == "field":
- frappe.db.sql(
- "UPDATE `tab{0}` SET `{1}`={2} WHERE `name`={2}".format(doctype, field[1], "%s"), (new, new)
- )
+ frappe.qb.update(doctype).set(field[1], new).where(Field("name") == new).run()
def validate_rename(
- doctype: str, new: str, meta: "Meta", merge: bool, force: bool, ignore_permissions: bool
+ doctype: str,
+ old: str,
+ new: str,
+ meta: "Meta",
+ merge: bool,
+ force: bool = False,
+ ignore_permissions: bool = False,
+ ignore_if_exists: bool = False,
+ save_point=False,
) -> str:
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
+ if save_point:
+ _SAVE_POINT = f"validate_rename_{frappe.generate_hash(length=8)}"
+ frappe.db.savepoint(_SAVE_POINT)
+
exists = (
frappe.qb.from_(doctype).where(Field("name") == new).for_update().select("name").run(pluck=True)
)
exists = exists[0] if exists else None
+ if not frappe.db.exists(doctype, old):
+ frappe.throw(_("Can't rename {0} to {1} because {0} doesn't exist.").format(old, new))
+
+ if old == new:
+ frappe.throw(_("No changes made because old and new name are the same.").format(old, new))
+
if merge and not exists:
frappe.throw(_("{0} {1} does not exist, select a new target to merge").format(doctype, new))
@@ -301,7 +371,7 @@ def validate_rename(
# for fixing case, accents
exists = None
- if (not merge) and exists:
+ if not merge and exists and not ignore_if_exists:
frappe.throw(_("Another {0} with name {1} exists, select another name").format(doctype, new))
if not (
@@ -315,6 +385,9 @@ def validate_rename(
# validate naming like it's done in doc.py
new = validate_name(doctype, new)
+ if save_point:
+ frappe.db.rollback(save_point=_SAVE_POINT)
+
return new
@@ -337,9 +410,7 @@ def rename_doctype(doctype: str, old: str, new: str) -> None:
def update_child_docs(old: str, new: str, meta: "Meta") -> None:
# update "parent"
for df in meta.get_table_fields():
- frappe.db.sql(
- "update `tab%s` set parent=%s where parent=%s" % (df.options, "%s", "%s"), (new, old)
- )
+ frappe.qb.update(df.options).set("parent", new).where(Field("parent") == old).run()
def update_link_field_values(link_fields: List[Dict], old: str, new: str, doctype: str) -> None:
@@ -384,57 +455,46 @@ def get_link_fields(doctype: str) -> List[Dict]:
frappe.flags.link_fields = {}
if doctype not in frappe.flags.link_fields:
- link_fields = frappe.db.sql(
- """\
- select parent, fieldname,
- (select issingle from tabDocType dt
- where dt.name = df.parent) as issingle
- from tabDocField df
- where
- df.options=%s and df.fieldtype='Link'""",
- (doctype,),
- as_dict=1,
+ dt = DocType("DocType")
+ df = DocType("DocField")
+ cf = DocType("Custom Field")
+ ps = DocType("Property Setter")
+
+ st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
+ standard_fields = (
+ frappe.qb.from_(df)
+ .select(df.parent, df.fieldname, st_issingle)
+ .where((df.options == doctype) & (df.fieldtype == "Link"))
+ .run(as_dict=True)
)
- # get link fields from tabCustom Field
- custom_link_fields = frappe.db.sql(
- """\
- select dt as parent, fieldname,
- (select issingle from tabDocType dt
- where dt.name = df.dt) as issingle
- from `tabCustom Field` df
- where
- df.options=%s and df.fieldtype='Link'""",
- (doctype,),
- as_dict=1,
+ cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
+ custom_fields = (
+ frappe.qb.from_(cf)
+ .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
+ .where((cf.options == doctype) & (cf.fieldtype == "Link"))
+ .run(as_dict=True)
)
- # add custom link fields list to link fields list
- link_fields += custom_link_fields
-
- # remove fields whose options have been changed using property setter
- property_setter_link_fields = frappe.db.sql(
- """\
- select ps.doc_type as parent, ps.field_name as fieldname,
- (select issingle from tabDocType dt
- where dt.name = ps.doc_type) as issingle
- from `tabProperty Setter` ps
- where
- ps.property_type='options' and
- ps.field_name is not null and
- ps.value=%s""",
- (doctype,),
- as_dict=1,
+ ps_issingle = (
+ frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle")
+ )
+ property_setter_fields = (
+ frappe.qb.from_(ps)
+ .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
+ .where((ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull()))
+ .run(as_dict=True)
)
- link_fields += property_setter_link_fields
-
- frappe.flags.link_fields[doctype] = link_fields
+ frappe.flags.link_fields[doctype] = standard_fields + custom_fields + property_setter_fields
return frappe.flags.link_fields[doctype]
def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
+ CustomField = DocType("Custom Field")
+ PropertySetter = DocType("Property Setter")
+
if frappe.conf.developer_mode:
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
doctype = frappe.get_doc("DocType", name)
@@ -446,23 +506,18 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
if save:
doctype.save()
else:
- frappe.db.sql(
- """update `tabDocField` set options=%s
- where fieldtype=%s and options=%s""",
- (new, fieldtype, old),
- )
+ DocField = DocType("DocField")
+ frappe.qb.update(DocField).set(DocField.options, new).where(
+ (DocField.fieldtype == fieldtype) & (DocField.options == old)
+ ).run()
- frappe.db.sql(
- """update `tabCustom Field` set options=%s
- where fieldtype=%s and options=%s""",
- (new, fieldtype, old),
- )
+ frappe.qb.update(CustomField).set(CustomField.options, new).where(
+ (CustomField.fieldtype == fieldtype) & (CustomField.options == old)
+ ).run()
- frappe.db.sql(
- """update `tabProperty Setter` set value=%s
- where property='options' and value=%s""",
- (new, old),
- )
+ frappe.qb.update(PropertySetter).set(PropertySetter.value, new).where(
+ (PropertySetter.property == "options") & (PropertySetter.value == old)
+ ).run()
def get_select_fields(old: str, new: str) -> List[Dict]:
@@ -470,108 +525,87 @@ def get_select_fields(old: str, new: str) -> List[Dict]:
get select type fields where doctype's name is hardcoded as
new line separated list
"""
+ df = DocType("DocField")
+ dt = DocType("DocType")
+ cf = DocType("Custom Field")
+ ps = DocType("Property Setter")
+
# get link fields from tabDocField
- select_fields = frappe.db.sql(
- """
- select parent, fieldname,
- (select issingle from tabDocType dt
- where dt.name = df.parent) as issingle
- from tabDocField df
- where
- df.parent != %s and df.fieldtype = 'Select' and
- df.options like {0} """.format(
- frappe.db.escape("%" + old + "%")
- ),
- (new,),
- as_dict=1,
+ st_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == df.parent).as_("issingle")
+ standard_fields = (
+ frappe.qb.from_(df)
+ .select(df.parent, df.fieldname, st_issingle)
+ .where((df.parent != new) & (df.fieldtype == "Select") & (df.options.like(f"%{old}%")))
+ .run(as_dict=True)
)
# get link fields from tabCustom Field
- custom_select_fields = frappe.db.sql(
- """
- select dt as parent, fieldname,
- (select issingle from tabDocType dt
- where dt.name = df.dt) as issingle
- from `tabCustom Field` df
- where
- df.dt != %s and df.fieldtype = 'Select' and
- df.options like {0} """.format(
- frappe.db.escape("%" + old + "%")
- ),
- (new,),
- as_dict=1,
+ cf_issingle = frappe.qb.from_(dt).select(dt.issingle).where(dt.name == cf.dt).as_("issingle")
+ custom_select_fields = (
+ frappe.qb.from_(cf)
+ .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle)
+ .where((cf.dt != new) & (cf.fieldtype == "Select") & (cf.options.like(f"%{old}%")))
+ .run(as_dict=True)
)
- # add custom link fields list to link fields list
- select_fields += custom_select_fields
-
# remove fields whose options have been changed using property setter
- property_setter_select_fields = frappe.db.sql(
- """
- select ps.doc_type as parent, ps.field_name as fieldname,
- (select issingle from tabDocType dt
- where dt.name = ps.doc_type) as issingle
- from `tabProperty Setter` ps
- where
- ps.doc_type != %s and
- ps.property_type='options' and
- ps.field_name is not null and
- ps.value like {0} """.format(
- frappe.db.escape("%" + old + "%")
- ),
- (new,),
- as_dict=1,
+ ps_issingle = (
+ frappe.qb.from_(dt).select(dt.issingle).where(dt.name == ps.doc_type).as_("issingle")
+ )
+ property_setter_select_fields = (
+ frappe.qb.from_(ps)
+ .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle)
+ .where(
+ (ps.doc_type != new)
+ & (ps.property == "options")
+ & (ps.field_name.notnull())
+ & (ps.value.like(f"%{old}%"))
+ )
+ .run(as_dict=True)
)
- select_fields += property_setter_select_fields
-
- return select_fields
+ return standard_fields + custom_select_fields + property_setter_select_fields
def update_select_field_values(old: str, new: str):
- frappe.db.sql(
- """
- update `tabDocField` set options=replace(options, %s, %s)
- where
- parent != %s and fieldtype = 'Select' and
- (options like {0} or options like {1})""".format(
- frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
- ),
- (old, new, new),
- )
+ from frappe.query_builder.functions import Replace
- frappe.db.sql(
- """
- update `tabCustom Field` set options=replace(options, %s, %s)
- where
- dt != %s and fieldtype = 'Select' and
- (options like {0} or options like {1})""".format(
- frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
- ),
- (old, new, new),
- )
+ DocField = DocType("DocField")
+ CustomField = DocType("Custom Field")
+ PropertySetter = DocType("Property Setter")
- frappe.db.sql(
- """
- update `tabProperty Setter` set value=replace(value, %s, %s)
- where
- doc_type != %s and field_name is not null and
- property='options' and
- (value like {0} or value like {1})""".format(
- frappe.db.escape("%" + "\n" + old + "%"), frappe.db.escape("%" + old + "\n" + "%")
- ),
- (old, new, new),
- )
+ frappe.qb.update(DocField).set(DocField.options, Replace(DocField.options, old, new)).where(
+ (DocField.fieldtype == "Select")
+ & (DocField.parent != new)
+ & (DocField.options.like(f"%\n{old}%") | DocField.options.like(f"%{old}\n%"))
+ ).run()
+
+ frappe.qb.update(CustomField).set(
+ CustomField.options, Replace(CustomField.options, old, new)
+ ).where(
+ (CustomField.fieldtype == "Select")
+ & (CustomField.dt != new)
+ & (CustomField.options.like(f"%\n{old}%") | CustomField.options.like(f"%{old}\n%"))
+ ).run()
+
+ frappe.qb.update(PropertySetter).set(
+ PropertySetter.value, Replace(PropertySetter.value, old, new)
+ ).where(
+ (PropertySetter.property == "options")
+ & (PropertySetter.field_name.notnull())
+ & (PropertySetter.doc_type != new)
+ & (PropertySetter.value.like(f"%\n{old}%") | PropertySetter.value.like(f"%{old}\n%"))
+ ).run()
def update_parenttype_values(old: str, new: str):
- child_doctypes = frappe.db.get_all(
+ child_doctypes = frappe.get_all(
"DocField",
fields=["options", "fieldname"],
filters={"parent": new, "fieldtype": ["in", frappe.model.table_fields]},
)
- custom_child_doctypes = frappe.db.get_all(
+ custom_child_doctypes = frappe.get_all(
"Custom Field",
fields=["options", "fieldname"],
filters={"dt": new, "fieldtype": ["in", frappe.model.table_fields]},
@@ -586,35 +620,30 @@ def update_parenttype_values(old: str, new: str):
pluck="value",
)
- child_doctypes = list(d["options"] for d in child_doctypes)
- child_doctypes += property_setter_child_doctypes
+ child_doctypes = set(list(d["options"] for d in child_doctypes) + property_setter_child_doctypes)
for doctype in child_doctypes:
- frappe.db.sql(f"update `tab{doctype}` set parenttype=%s where parenttype=%s", (new, old))
+ Table = DocType(doctype)
+ frappe.qb.update(Table).set(Table.parenttype, new).where(Table.parenttype == old).run()
def rename_dynamic_links(doctype: str, old: str, new: str):
+ Singles = DocType("Singles")
for df in get_dynamic_link_map().get(doctype, []):
# dynamic link in single, just one value to check
if frappe.get_meta(df.parent).issingle:
refdoc = frappe.db.get_singles_dict(df.parent)
if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old:
-
- frappe.db.sql(
- """update tabSingles set value=%s where
- field=%s and value=%s and doctype=%s""",
- (new, df.fieldname, old, df.parent),
- )
+ frappe.qb.update(Singles).set(Singles.value, new).where(
+ (Singles.field == df.fieldname) & (Singles.doctype == df.parent) & (Singles.value == old)
+ ).run()
else:
# because the table hasn't been renamed yet!
parent = df.parent if df.parent != new else old
- frappe.db.sql(
- """update `tab{parent}` set {fieldname}=%s
- where {options}=%s and {fieldname}=%s""".format(
- parent=parent, fieldname=df.fieldname, options=df.options
- ),
- (new, doctype, old),
- )
+
+ frappe.qb.update(parent).set(df.fieldname, new).where(
+ (Field(df.options) == doctype) & (Field(df.fieldname) == old)
+ ).run()
def bulk_rename(
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index 6378b2fac1..a19062d209 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -84,16 +84,15 @@ frappe.ui.form.Toolbar = class Toolbar {
message: __("Unchanged")
});
}
- rename_document_title(new_name, new_title, merge=false) {
+ rename_document_title(input_name, input_title, merge=false) {
+ let confirm_message = null;
const docname = this.frm.doc.name;
const title_field = this.frm.meta.title_field || '';
const doctype = this.frm.doctype;
- let confirm_message=null;
-
- if (new_name) {
+ if (input_name) {
const warning = __("This cannot be undone");
- const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), new_name.bold()]);
+ const message = __("Are you sure you want to merge {0} with {1}?", [docname.bold(), input_name.bold()]);
confirm_message = `${message}
${warning}`;
}
@@ -101,22 +100,45 @@ frappe.ui.form.Toolbar = class Toolbar {
return frappe.xcall("frappe.model.rename_doc.update_document_title", {
doctype,
docname,
- name: new_name,
- title: new_title,
+ name: input_name,
+ title: input_title,
+ enqueue: true,
merge,
freeze: true,
freeze_message: __("Updating related fields...")
}).then(new_docname => {
- if (new_name != docname) {
- $(document).trigger("rename", [doctype, docname, new_docname || new_name]);
+ const reload_form = (input_name) => {
+ $(document).trigger("rename", [doctype, docname, input_name]);
if (locals[doctype] && locals[doctype][docname]) delete locals[doctype][docname];
+ this.frm.reload_doc();
+ }
+
+ // handle document renaming queued action
+ if (input_name && (new_docname == docname)) {
+ frappe.socketio.doc_subscribe(doctype, input_name);
+ frappe.realtime.on("doc_update", data => {
+ if (data.doctype == doctype && data.name == input_name) {
+ reload_form(input_name);
+ frappe.show_alert({
+ message: __('Document renamed from {0} to {1}', [docname.bold(), input_name.bold()]),
+ indicator: 'success',
+ });
+ }
+ });
+ frappe.show_alert(
+ __('Document renaming from {0} to {1} has been queued', [docname.bold(), input_name.bold()])
+ );
+ }
+
+ // handle document sync rename action
+ if (input_name && ((new_docname || input_name) != docname)) {
+ reload_form(new_docname || input_name);
}
- this.frm.reload_doc();
});
};
return new Promise((resolve, reject) => {
- if (new_title === this.frm.doc[title_field] && new_name === docname) {
+ if (input_title === this.frm.doc[title_field] && input_name === docname) {
this.show_unchanged_document_alert();
resolve();
} else if (merge) {
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index 71cfa88db1..69aee9b350 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -55,6 +55,10 @@ def DocType(*args, **kwargs):
return frappe.qb.DocType(*args, **kwargs)
+def Table(*args, **kwargs):
+ return frappe.qb.Table(*args, **kwargs)
+
+
def patch_query_execute():
"""Patch the Query Builder with helper execute method
This excludes the use of `frappe.db.sql` method while
diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py
index 38fc7b32bd..8bf76b3e13 100644
--- a/frappe/tests/test_rename_doc.py
+++ b/frappe/tests/test_rename_doc.py
@@ -107,8 +107,25 @@ class TestRenameDoc(unittest.TestCase):
def setUp(self):
frappe.flags.link_fields = {}
+ if self._testMethodName == "test_doc_rename_method":
+ self.property_setter = frappe.get_doc(
+ {
+ "doctype": "Property Setter",
+ "doctype_or_field": "DocType",
+ "doc_type": self.test_doctype,
+ "property": "allow_rename",
+ "property_type": "Check",
+ "value": "1",
+ }
+ ).insert()
+
super().setUp()
+ def tearDown(self) -> None:
+ if self._testMethodName == "test_doc_rename_method":
+ self.property_setter.delete()
+ return super().tearDown()
+
def test_rename_doc(self):
"""Rename an existing document via frappe.rename_doc"""
old_name = choice(self.available_documents)
@@ -247,3 +264,12 @@ class TestRenameDoc(unittest.TestCase):
update_linked_doctypes("User", "ToDo", "str", "str")
self.assertTrue("Function frappe.model.rename_doc.update_linked_doctypes" in stdout.getvalue())
+
+ def test_doc_rename_method(self):
+ name = choice(self.available_documents)
+ new_name = f"{name}-{frappe.generate_hash(length=4)}"
+ doc = frappe.get_doc(self.test_doctype, name)
+ doc.rename(new_name, merge=frappe.db.exists(self.test_doctype, new_name))
+ self.assertEqual(doc.name, new_name)
+ self.available_documents.append(new_name)
+ self.available_documents.remove(name)
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index b2795e16c3..bc89e5279e 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -174,6 +174,11 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
frappe.db.commit()
finally:
+ # background job hygiene: release file locks if unreleased
+ # if this breaks something, move it to failed jobs alone - gavin@frappe.io
+ for doc in frappe.local.locked_documents:
+ doc.unlock()
+
frappe.monitor.stop()
if is_async:
frappe.destroy()
diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py
index d1cda3d0fc..156f2ab04d 100755
--- a/frappe/utils/scheduler.py
+++ b/frappe/utils/scheduler.py
@@ -24,6 +24,15 @@ from frappe.utils.background_jobs import get_jobs
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S"
+def cprint(*args, **kwargs):
+ """Prints only if called from STDOUT"""
+ try:
+ os.get_terminal_size()
+ print(*args, **kwargs)
+ except Exception:
+ pass
+
+
def start_scheduler():
"""Run enqueue_events_for_all_sites every 2 minutes (default).
Specify scheduler_interval in seconds in common_site_config.json"""
@@ -94,9 +103,11 @@ def enqueue_events(site):
def is_scheduler_inactive():
if frappe.local.conf.maintenance_mode:
+ cprint("Maintenance mode is ON")
return True
if frappe.local.conf.pause_scheduler:
+ cprint("frappe.conf.pause_scheduler is SET")
return True
if is_scheduler_disabled():
@@ -107,9 +118,15 @@ def is_scheduler_inactive():
def is_scheduler_disabled():
if frappe.conf.disable_scheduler:
+ cprint("frappe.conf.disable_scheduler is SET")
return True
- return not frappe.utils.cint(frappe.db.get_single_value("System Settings", "enable_scheduler"))
+ scheduler_disabled = not frappe.utils.cint(
+ frappe.db.get_single_value("System Settings", "enable_scheduler")
+ )
+ if scheduler_disabled:
+ cprint("SystemSettings.enable_scheduler is UNSET")
+ return scheduler_disabled
def toggle_scheduler(enable):