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):