Skip to content

Commit 2f0de9a

Browse files
sagarvoramergify[bot]
authored andcommitted
fix(meta): ensure that insert_after is always considered when sorting fields (frappe#18682)
* fix(meta): ensure that `insert_after` is always considered when sorting fields * test: add nosemgrep to comment Co-authored-by: Dany Roberts <danyrt@wahni.com> (cherry picked from commit cb7d25a)
1 parent 515f6c2 commit 2f0de9a

File tree

2 files changed

+106
-41
lines changed

2 files changed

+106
-41
lines changed

frappe/custom/doctype/custom_field/test_custom_field.py

+46-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
# License: MIT. See LICENSE
33

44
import frappe
5+
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
56
from frappe.tests.utils import FrappeTestCase
67

78
test_records = frappe.get_test_records("Custom Field")
89

910

1011
class TestCustomField(FrappeTestCase):
1112
def test_create_custom_fields(self):
12-
from .custom_field import create_custom_fields
13-
1413
create_custom_fields(
1514
{
1615
"Address": [
@@ -37,3 +36,48 @@ def test_create_custom_fields(self):
3736
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_1"))
3837
self.assertTrue(frappe.db.exists("Custom Field", "Address-_test_custom_field_2"))
3938
self.assertTrue(frappe.db.exists("Custom Field", "Contact-_test_custom_field_2"))
39+
40+
def test_custom_field_sorting(self):
41+
try:
42+
custom_fields = {
43+
"ToDo": [
44+
{"fieldname": "a_test_field", "insert_after": "b_test_field"},
45+
{"fieldname": "b_test_field", "insert_after": "status"},
46+
{"fieldname": "c_test_field", "insert_after": "unknown_custom_field"},
47+
{"fieldname": "d_test_field", "insert_after": "status"},
48+
]
49+
}
50+
51+
create_custom_fields(custom_fields, ignore_validate=True)
52+
53+
fields = frappe.get_meta("ToDo", cached=False).fields
54+
55+
for i, field in enumerate(fields):
56+
if field.fieldname == "b_test_field":
57+
self.assertEqual(fields[i - 1].fieldname, "status")
58+
59+
if field.fieldname == "d_test_field":
60+
self.assertEqual(fields[i - 1].fieldname, "a_test_field")
61+
62+
self.assertEqual(fields[-1].fieldname, "c_test_field")
63+
64+
finally:
65+
frappe.db.delete(
66+
"Custom Field",
67+
{
68+
"dt": "ToDo",
69+
"fieldname": (
70+
"in",
71+
(
72+
"a_test_field",
73+
"b_test_field",
74+
"c_test_field",
75+
"d_test_field",
76+
),
77+
),
78+
},
79+
)
80+
81+
# undo changes commited by DDL
82+
# nosemgrep
83+
frappe.db.commit()

frappe/model/meta.py

+60-39
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,13 @@ def process(self):
140140
self.init_field_map()
141141
return
142142

143-
self.add_custom_fields()
143+
has_custom_fields = self.add_custom_fields()
144144
self.apply_property_setters()
145145
self.init_field_map()
146-
self.sort_fields()
146+
147+
if has_custom_fields:
148+
self.sort_fields()
149+
147150
self.get_valid_columns()
148151
self.set_custom_permissions()
149152
self.add_custom_links_and_actions()
@@ -319,7 +322,7 @@ def get_list_fields(self):
319322
return list_fields
320323

321324
def get_custom_fields(self):
322-
return [d for d in self.fields if d.get("is_custom_field")]
325+
return [d for d in self.fields if getattr(d, "is_custom_field", False)]
323326

324327
def get_title_field(self):
325328
"""Return the title field of this doctype,
@@ -358,17 +361,20 @@ def add_custom_fields(self):
358361
if not frappe.db.table_exists("Custom Field"):
359362
return
360363

361-
custom_fields = frappe.db.sql(
362-
"""
363-
SELECT * FROM `tabCustom Field`
364-
WHERE dt = %s AND docstatus < 2
365-
""",
366-
(self.name,),
367-
as_dict=1,
364+
custom_fields = frappe.db.get_values(
365+
"Custom Field",
366+
filters={"dt": self.name},
367+
fieldname="*",
368+
as_dict=True,
369+
order_by="idx",
368370
update={"is_custom_field": 1},
369371
)
370372

373+
if not custom_fields:
374+
return
375+
371376
self.extend("fields", custom_fields)
377+
return True
372378

373379
def apply_property_setters(self):
374380
"""
@@ -452,43 +458,33 @@ def init_field_map(self):
452458
self._fields = {field.fieldname: field for field in self.fields}
453459

454460
def sort_fields(self):
455-
"""sort on basis of insert_after"""
456-
custom_fields = sorted(self.get_custom_fields(), key=lambda df: df.idx)
461+
"""Sort custom fields on the basis of insert_after"""
457462

458-
if custom_fields:
459-
newlist = []
463+
field_order = []
464+
insert_after_map = {}
460465

461-
# if custom field is at top
462-
# insert_after is false
463-
for c in list(custom_fields):
464-
if not c.insert_after:
465-
newlist.append(c)
466-
custom_fields.pop(custom_fields.index(c))
466+
for field in self.fields:
467+
if not getattr(field, "is_custom_field", False):
468+
field_order.append(field.fieldname)
467469

468-
# standard fields
469-
newlist += [df for df in self.get("fields") if not df.get("is_custom_field")]
470+
elif insert_after := getattr(field, "insert_after", None):
471+
insert_after_map.setdefault(insert_after, []).append(field.fieldname)
470472

471-
newlist_fieldnames = [df.fieldname for df in newlist]
472-
for i in range(2):
473-
for df in list(custom_fields):
474-
if df.insert_after in newlist_fieldnames:
475-
cf = custom_fields.pop(custom_fields.index(df))
476-
idx = newlist_fieldnames.index(df.insert_after)
477-
newlist.insert(idx + 1, cf)
478-
newlist_fieldnames.insert(idx + 1, cf.fieldname)
473+
else:
474+
# if custom field is at the top, insert after is None
475+
field_order.insert(0, field.fieldname)
479476

480-
if not custom_fields:
481-
break
477+
if insert_after_map:
478+
_update_field_order_based_on_insert_after(field_order, insert_after_map)
482479

483-
# worst case, add remaining custom fields to last
484-
if custom_fields:
485-
newlist += custom_fields
480+
sorted_fields = []
486481

487-
# renum idx
488-
for i, f in enumerate(newlist):
489-
f.idx = i + 1
482+
for idx, fieldname in enumerate(field_order, 1):
483+
field = self._fields[fieldname]
484+
field.idx = idx
485+
sorted_fields.append(field)
490486

491-
self.fields = newlist
487+
self.fields = sorted_fields
492488

493489
def set_custom_permissions(self):
494490
"""Reset `permissions` with Custom DocPerm if exists"""
@@ -809,3 +805,28 @@ def is_internal(field):
809805
frappe.db.sql_ddl(f"ALTER TABLE `tab{doctype}` {columns_to_remove}")
810806

811807
return DROPPED_COLUMNS
808+
809+
810+
def _update_field_order_based_on_insert_after(field_order, insert_after_map):
811+
"""Update the field order based on insert_after_map"""
812+
813+
retry_field_insertion = True
814+
815+
while retry_field_insertion:
816+
retry_field_insertion = False
817+
818+
for fieldname in list(insert_after_map):
819+
if fieldname not in field_order:
820+
continue
821+
822+
custom_field_index = field_order.index(fieldname)
823+
for custom_field_name in insert_after_map.pop(fieldname):
824+
custom_field_index += 1
825+
field_order.insert(custom_field_index, custom_field_name)
826+
827+
retry_field_insertion = True
828+
829+
if insert_after_map:
830+
# insert_after is an invalid fieldname, add these fields to the end
831+
for fields in insert_after_map.values():
832+
field_order.extend(fields)

0 commit comments

Comments
 (0)