feat: rename custom field

This commit is contained in:
Ankush Menat 2023-09-22 20:27:49 +05:30
parent 21881e6652
commit 0d5f28d569
6 changed files with 121 additions and 1 deletions

View file

@ -25,6 +25,7 @@ frappe.ui.form.on("Custom Field", {
frm.toggle_enable("dt", frm.doc.__islocal);
frm.trigger("dt");
frm.toggle_reqd("label", !frm.doc.fieldname);
frm.trigger("add_rename_field");
if (frm.doc.is_system_generated) {
frm.dashboard.add_comment(
@ -110,6 +111,29 @@ frappe.ui.form.on("Custom Field", {
frm.fields_dict["options_help"].disp_area.innerHTML = "";
}
},
add_rename_field(frm) {
frm.add_custom_button(__("Rename Fieldname"), () => {
frappe.prompt(
{
fieldtype: "Data",
label: __("Fieldname"),
fieldname: "fieldname",
reqd: 1,
},
function (data) {
frappe.call({
method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname",
args: {
custom_field: frm.doc.name,
fieldname: data.fieldname,
},
});
},
__("Rename Fieldname"),
__("Rename")
);
});
},
});
frappe.utils.has_special_chars = function (t) {

View file

@ -340,3 +340,52 @@ def create_custom_fields(custom_fields: dict, ignore_validate=False, update=True
finally:
frappe.flags.in_create_custom_fields = False
@frappe.whitelist()
def rename_fieldname(custom_field: str, fieldname: str):
frappe.only_for("System Manager")
field: CustomField = frappe.get_doc("Custom Field", custom_field)
parent_doctype = field.dt
old_fieldname = field.fieldname
field.fieldname = fieldname
field.set_fieldname()
new_fieldname = field.fieldname
if field.is_system_generated:
frappe.throw(_("System Generated Fields can not be renamed"))
if frappe.db.has_column(parent_doctype, fieldname):
frappe.throw(_("Can not rename as fieldname {0} is already present on DocType."))
if old_fieldname == new_fieldname:
frappe.msgprint(_("Old and new fieldnames are same."), alert=True)
return
frappe.db.rename_column(parent_doctype, old_fieldname, new_fieldname)
# Update in DB after alter column is successful, alter column will implicitly commit, so it's
# best to commit change on field too to avoid any possible mismatch between two.
field.db_set("fieldname", field.fieldname, notify=True)
_update_fieldname_references(field, old_fieldname, new_fieldname)
frappe.db.commit()
frappe.clear_cache()
def _update_fieldname_references(
field: CustomField, old_fieldname: str, new_fieldname: str
) -> None:
# Passwords are stored in auth table, so column name needs to be updated there.
if field.fieldtype == "Password":
Auth = frappe.qb.Table("__Auth")
frappe.qb.update(Auth).set(Auth.fieldname, new_fieldname).where(
(Auth.doctype == field.dt) & (Auth.fieldname == old_fieldname)
).run()
# Update ordering reference.
frappe.db.set_value(
"Custom Field",
{"insert_after": old_fieldname, "dt": field.dt},
"insert_after",
new_fieldname,
)

View file

@ -2,7 +2,11 @@
# License: MIT. See LICENSE
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.custom.doctype.custom_field.custom_field import (
create_custom_field,
create_custom_fields,
rename_fieldname,
)
from frappe.tests.utils import FrappeTestCase
test_records = frappe.get_test_records("Custom Field")
@ -81,3 +85,23 @@ class TestCustomField(FrappeTestCase):
# undo changes commited by DDL
# nosemgrep
frappe.db.commit()
def test_custom_field_renaming(self):
def gen_fieldname():
return "test_" + frappe.generate_hash()
field = create_custom_field("ToDo", {"label": gen_fieldname()}, is_system_generated=False)
old = field.fieldname
new = gen_fieldname()
data = frappe.generate_hash()
doc = frappe.get_doc({"doctype": "ToDo", old: data, "description": "Something"}).insert()
rename_fieldname(field.name, new)
field.reload()
self.assertEqual(field.fieldname, new)
doc = frappe.get_doc("ToDo", doc.name) # doc.reload doesn't clear old fields.
self.assertEqual(doc.get(new), data)
self.assertFalse(doc.get(old))
field.delete()

View file

@ -1285,6 +1285,9 @@ class Database:
"""Get estimated max row size of any table in bytes."""
raise NotImplementedError
def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
raise NotImplementedError
@contextmanager
def savepoint(catch: type | tuple[type, ...] = Exception):

View file

@ -254,6 +254,20 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
null_constraint = "NOT NULL" if not nullable else ""
return self.sql_ddl(f"ALTER TABLE `{table_name}` MODIFY `{column}` {type} {null_constraint}")
def rename_column(self, doctype: str, old_column_name, new_column_name):
current_data_type = self.get_column_type(doctype, old_column_name)
table_name = get_table_name(doctype)
frappe.db.sql_ddl(
f"""ALTER TABLE `{table_name}`
CHANGE COLUMN `{old_column_name}`
`{new_column_name}`
{current_data_type}"""
# ^ Mariadb requires passing current data type again even if there's no change
# This requirement is gone from v10.5
)
def create_auth_table(self):
self.sql_ddl(
"""create table if not exists `__Auth` (

View file

@ -264,6 +264,12 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
ALTER COLUMN "{column}" {null_constraint}"""
)
def rename_column(self, doctype: str, old_column_name: str, new_column_name: str):
table_name = get_table_name(doctype)
frappe.db.sql_ddl(
f"ALTER TABLE `{table_name}` RENAME COLUMN `{old_column_name}` TO `{new_column_name}`"
)
def create_auth_table(self):
self.sql_ddl(
"""create table if not exists "__Auth" (