From 538ecc21c56d38c804b0b76b221da29716846717 Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Thu, 20 Aug 2026 12:06:34 +0800 Subject: [PATCH 1/5] export normal view query rows step by step 10000 -> 1000 --- dtable_events/dtable_io/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dtable_events/dtable_io/utils.py b/dtable_events/dtable_io/utils.py index 0c338098..5916aacf 100644 --- a/dtable_events/dtable_io/utils.py +++ b/dtable_events/dtable_io/utils.py @@ -1538,7 +1538,7 @@ def get_rows_from_dtable_db(dtable_db_api, table_name, limit=50000): def get_export_view_rows_from_dtable_db(dtable_db_api, table_name=None, table_id=None, view_name=None, view_id=None, convert_link_id=True, convert_keys=True, convert_date=False): rows = [] - start, limit = 0, 10000 + start, limit = 0, 1000 while True: rows_rsp = dtable_db_api.list_rows( table_name=table_name, From 0424ec64f605a3c3e61fdf0fb4499c32858bc935 Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Thu, 20 Aug 2026 16:38:37 +0800 Subject: [PATCH 2/5] convert view query rows by SQL query instead of dtable-db list-rows API --- dtable_events/dtable_io/__init__.py | 26 +++- dtable_events/dtable_io/utils.py | 29 ++--- dtable_events/tests/sql/column_reference.py | 10 +- dtable_events/tests/sql/sql_test.py | 33 +++++ dtable_events/tests/sql/test_reference.py | 133 ++++++++++++++++++++ dtable_events/utils/sql_generator.py | 52 ++++---- 6 files changed, 242 insertions(+), 41 deletions(-) diff --git a/dtable_events/dtable_io/__init__.py b/dtable_events/dtable_io/__init__.py index 02967c58..0a7c64f4 100644 --- a/dtable_events/dtable_io/__init__.py +++ b/dtable_events/dtable_io/__init__.py @@ -972,6 +972,7 @@ def convert_view_to_excel(dtable_uuid, table_id, view_id, username, id_in_org, u from dtable_events.dtable_io.excel import write_xls_with_type, TEMP_EXPORT_VIEW_DIR, IMAGE_TMP_DIR from dtable_events.dtable_io.utils import get_related_nicknames_from_dtable, escape_sheet_name from dtable_events.utils.dtable_db_api import DTableDBAPI + from dtable_events.utils.sql_generator import pre_filter_to_filter_term import openpyxl target_dir = TEMP_EXPORT_VIEW_DIR + dtable_uuid @@ -1044,8 +1045,29 @@ def convert_view_to_excel(dtable_uuid, table_id, view_id, username, id_in_org, u 'user_department_ids_map': user_department_ids_map } dtable_db_api = DTableDBAPI(username, dtable_uuid, INNER_DTABLE_DB_URL, kwargs=kwargs) - try: - db_rows = get_export_view_rows_from_dtable_db(dtable_db_api, table_id=table_id, view_id=view_id) + + filters = pre_filter_to_filter_term( + target_view.get('filters') or [], + username, + id_in_org, + (user_department_ids_map or {}).get('current_user_department_ids') or [], + (user_department_ids_map or {}).get('current_user_department_and_sub_ids') or [], + ) + filter_conditions = { + 'sorts': target_view.get('sorts'), + 'filters': filters, + 'filter_conjunction': target_view.get('filter_conjunction'), + } + selected_columns_map = {col.get('key'): col for col in cols_without_hidden} + for group_by in (target_view.get('groupbys') or []): + column_key = group_by.get('column_key') + if column_key and column_key not in selected_columns_map: + column = next((c for c in cols if c.get('key') == column_key), None) + if column: + selected_columns_map[column_key] = column + query_column_names = [col.get('name') for col in selected_columns_map.values()] + try: + db_rows = get_export_view_rows_from_dtable_db(dtable_db_api, table_name, cols, filter_conditions, query_column_names=query_column_names, server_only=True) except Exception as e: dtable_io_logger.error('get db rows. ERROR: {}'.format(e)) return diff --git a/dtable_events/dtable_io/utils.py b/dtable_events/dtable_io/utils.py index 5916aacf..af9482fd 100644 --- a/dtable_events/dtable_io/utils.py +++ b/dtable_events/dtable_io/utils.py @@ -1535,24 +1535,21 @@ def get_rows_from_dtable_db(dtable_db_api, table_name, limit=50000): return dtable_rows -def get_export_view_rows_from_dtable_db(dtable_db_api, table_name=None, table_id=None, view_name=None, view_id=None, - convert_link_id=True, convert_keys=True, convert_date=False): +def get_export_view_rows_from_dtable_db(dtable_db_api, table_name, columns, filter_conditions, query_column_names=None, server_only=True): + from dtable_events.utils.sql_generator import filter2sql + rows = [] - start, limit = 0, 1000 + start, limit = 0, 10000 while True: - rows_rsp = dtable_db_api.list_rows( - table_name=table_name, - table_id=table_id, - view_name=view_name, - view_id=view_id, - start=start, - limit=limit, - convert_link_id=convert_link_id, - convert_keys=convert_keys, - convert_date=convert_date - ) - rows.extend(rows_rsp['rows']) - if len(rows_rsp['rows']) < limit: + filter_conditions['start'] = start + filter_conditions['limit'] = limit + sql = filter2sql(table_name, columns, filter_conditions, by_group=False) + if query_column_names: + column_names_str = ', '.join('`%s`' % name for name in query_column_names) + sql = sql.replace('*', column_names_str, 1) + step_rows, _ = dtable_db_api.query(sql, convert=True, server_only=server_only) + rows.extend(step_rows) + if len(step_rows) < limit: break start += limit return rows diff --git a/dtable_events/tests/sql/column_reference.py b/dtable_events/tests/sql/column_reference.py index 752b3d50..12bd89e6 100644 --- a/dtable_events/tests/sql/column_reference.py +++ b/dtable_events/tests/sql/column_reference.py @@ -12,7 +12,15 @@ {'key': 'A47g', 'type': 'auto-number', 'name': 'AutoNo', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'format': '0000', 'max_used_auto_number': 906, 'digits': 4, 'prefix_type': None, 'prefix': None}, 'permission_type': '', 'permitted_users': []}, {'key': 'G0yz', 'type': 'checkbox', 'name': 'CB', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'enable_fill_default_value': False, 'default_value': False}, 'permission_type': '', 'permitted_users': []}, {'key': '6SJV', 'type': 'duration', 'name': 'Du', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'format': 'duration', 'duration_format': 'h:mm:ss'}, 'permission_type': '', 'permitted_users': []}, - {'key': '51r0', 'type': 'number', 'name': 'Num', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'format': 'number', 'precision': 2, 'enable_precision': False, 'enable_fill_default_value': False, 'decimal': 'dot', 'thousands': 'no'}, 'permission_type': '', 'permitted_users': []} + {'key': '51r0', 'type': 'number', 'name': 'Num', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'format': 'number', 'precision': 2, 'enable_precision': False, 'enable_fill_default_value': False, 'decimal': 'dot', 'thousands': 'no'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'F000', 'type': 'formula', 'name': 'FNum', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'result_type': 'number'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'F001', 'type': 'formula', 'name': 'FStr', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'result_type': 'string'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'F002', 'type': 'formula', 'name': 'FDate', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'result_type': 'date', 'format': 'YYYY-MM-DD'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'F003', 'type': 'formula', 'name': 'FBool', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'result_type': 'bool'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'F004', 'type': 'formula', 'name': 'FArr', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'result_type': 'array', 'array_type': 'text'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'L001', 'type': 'link', 'name': 'LNum', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'array_type': 'number', 'result_type': 'array'}, 'permission_type': '', 'permitted_users': []}, + {'key': 'D001', 'type': 'department-single-select', 'name': 'Dept', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'options': [{'id': 1, 'name': 'Dept1'}, {'id': 2, 'name': 'Dept2'}]}, 'permission_type': '', 'permitted_users': []}, + {'key': 'G001', 'type': 'geolocation', 'name': 'Geo', 'editable': True, 'width': 200, 'resizable': True, 'draggable': True, 'data': {'format': 'lng_lat'}, 'permission_type': '', 'permitted_users': []} ] LINK_COLUMN = {"key":"G5J9","type":"link","name":"Link","editable":True,"width":200,"resizable":True,"draggable":True,"data":{"display_column_key":"0000","table_id":"0000","other_table_id":"kzvB","is_internal_link":True,"is_multiple":True,"is_row_from_view":False,"other_view_id":"","link_id":"55Sl","array_type":"text","array_data":None,"result_type":"array"},"permission_type":"","permitted_users":[],"edit_metadata_permission_type":"","edit_metadata_permitted_users":[],"description":None} diff --git a/dtable_events/tests/sql/sql_test.py b/dtable_events/tests/sql/sql_test.py index 1f34ffaf..7da5907c 100644 --- a/dtable_events/tests/sql/sql_test.py +++ b/dtable_events/tests/sql/sql_test.py @@ -7,6 +7,7 @@ from sql.column_reference import TEST_COLUMNS, TABLES, LINK_COLUMN from sql.test_reference import TEST_CONDITIONS, TEST_CONDITIONS_LINK from dtable_events import filter2sql, linkRecords2sql +from dtable_events.utils.sql_generator import pre_filter_to_filter_term class SqlTest(unittest.TestCase): @@ -52,5 +53,37 @@ def test_equal(self): + def test_user_filter_normalization(self): + + def to_sql(filters): + normalized = pre_filter_to_filter_term(filters, 'me@x.com', 'admin-1', [1, 2], [1, 2, 3]) + return self._toSql({'filters': normalized, 'filter_conjunction': 'And'}) + + # collaborator include_me appends current user email + self.assertEqual( + to_sql([{'column_name': 'Colla', 'filter_predicate': 'include_me', 'filter_term': ['a@x.com']}]), + "SELECT * FROM `Table1` WHERE (`Colla` in ('a@x.com', 'me@x.com')) LIMIT 0, 100", + ) + # text is_current_user_ID replaced with id_in_org + self.assertEqual( + to_sql([{'column_name': '名称', 'filter_predicate': 'is_current_user_ID', 'filter_term': ''}]), + "SELECT * FROM `Table1` WHERE (`名称` = 'admin-1') LIMIT 0, 100", + ) + # department current_user_department / current_user_department_and_sub + self.assertEqual( + to_sql([{'column_name': 'Dept', 'filter_predicate': 'is', 'filter_term': 'current_user_department'}]), + "SELECT * FROM `Table1` WHERE (`Dept` IN (1, 2)) LIMIT 0, 100", + ) + self.assertEqual( + to_sql([{'column_name': 'Dept', 'filter_predicate': 'is_not', 'filter_term': 'current_user_department_and_sub'}]), + "SELECT * FROM `Table1` WHERE (`Dept` NOT IN (1, 2, 3)) LIMIT 0, 100", + ) + # nested filter group include_me + self.assertEqual( + to_sql([{'filters': [{'column_name': 'Colla', 'filter_predicate': 'include_me', 'filter_term': ['b@x.com']}], 'filter_conjunction': 'And'}]), + "SELECT * FROM `Table1` WHERE ((`Colla` in ('b@x.com', 'me@x.com'))) LIMIT 0, 100", + ) + + if __name__ == '__main__': unittest.main() diff --git a/dtable_events/tests/sql/test_reference.py b/dtable_events/tests/sql/test_reference.py index 76ea4ad1..e74dc146 100644 --- a/dtable_events/tests/sql/test_reference.py +++ b/dtable_events/tests/sql/test_reference.py @@ -2,6 +2,8 @@ import time from datetime import timedelta, datetime +from dtable_events.utils.sql_generator import ColumnFilterInvalidError + # set timezone os.environ['TZ'] = 'UTC' time.tzset() # 在Unix-like系统上生效 @@ -693,6 +695,137 @@ def get_expected_sql_for_modifier(filter_modifier, column_name): "by_group": True, "expected_error": ValueError }, + + # Formula column, result_type = number + { + "filter_conditions": { + "filters": [ + {'column_name': 'FNum', 'filter_predicate': 'equal', 'filter_term': 5} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`FNum` = 5) LIMIT 0, 100", + "by_group": False, + }, + # Formula column, result_type = string + { + "filter_conditions": { + "filters": [ + {'column_name': 'FStr', 'filter_predicate': 'contains', 'filter_term': 'abc'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`FStr` ilike '%abc%') LIMIT 0, 100", + "by_group": False, + }, + # Formula column, result_type = date + { + "filter_conditions": { + "filters": [ + {'column_name': 'FDate', 'filter_predicate': 'is', 'filter_term': '2021-12-20', 'filter_term_modifier': 'exact_date'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE ((`FDate` >= '2021-12-20' and `FDate` < '2021-12-21')) LIMIT 0, 100", + "by_group": False, + }, + # Formula column, result_type = bool + { + "filter_conditions": { + "filters": [ + {'column_name': 'FBool', 'filter_predicate': 'is', 'filter_term': 'true'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`FBool` = true) LIMIT 0, 100", + "by_group": False, + }, + # Formula column, result_type = array (array_type text) + { + "filter_conditions": { + "filters": [ + {'column_name': 'FArr', 'filter_predicate': 'contains', 'filter_term': 'abc'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`FArr` ilike '%abc%') LIMIT 0, 100", + "by_group": False, + }, + # Link column, array_type = number + { + "filter_conditions": { + "filters": [ + {'column_name': 'LNum', 'filter_predicate': 'equal', 'filter_term': 5} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`LNum` = 5) LIMIT 0, 100", + "by_group": False, + }, + # Department single select + { + "filter_conditions": { + "filters": [ + {'column_name': 'Dept', 'filter_predicate': 'is', 'filter_term': 1} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`Dept` = 1) LIMIT 0, 100", + "by_group": False, + }, + { + "filter_conditions": { + "filters": [ + {'column_name': 'Dept', 'filter_predicate': 'is_any_of', 'filter_term': [1, 2]} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`Dept` IN (1, 2)) LIMIT 0, 100", + "by_group": False, + }, + # Geolocation + { + "filter_conditions": { + "filters": [ + {'column_name': 'Geo', 'filter_predicate': 'is_empty'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`Geo` is null) LIMIT 0, 100", + "by_group": False, + }, + { + "filter_conditions": { + "filters": [ + {'column_name': 'Geo', 'filter_predicate': 'is_not_empty'} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "expected_sql": "SELECT * FROM `Table1` WHERE (`Geo` is not null) LIMIT 0, 100", + "by_group": False, + }, + # Unsupported predicate raises ColumnFilterInvalidError + { + "filter_conditions": { + "filters": [ + {'column_name': 'Num', 'filter_predicate': 'has_any_of', 'filter_term': [1]} + ], + "filter_predicate": 'And', + "sorts": [], + }, + "by_group": False, + "expected_error": ColumnFilterInvalidError, + }, ] TEST_CONDITIONS_LINK = [ diff --git a/dtable_events/utils/sql_generator.py b/dtable_events/utils/sql_generator.py index 37b0900a..68e28712 100644 --- a/dtable_events/utils/sql_generator.py +++ b/dtable_events/utils/sql_generator.py @@ -1406,30 +1406,10 @@ def _validate_filters(self, filters, table_column_keys): return error_msg def _normalize_filter(self, filter_item, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids): - filter_term = filter_item.get('filter_term') - filter_predicate = filter_item.get('filter_predicate') - if filter_predicate == 'include_me': - filter_item['filter_term'].append(username) - if filter_predicate == 'is_current_user_ID': - filter_item['filter_term'] = id_in_org - if filter_term == 'current_user_department': - filter_item['current_user_department'] = current_user_department_ids - if filter_term == 'current_user_department_and_sub': - filter_item['current_user_department_and_sub'] = current_user_department_and_sub_ids - if isinstance(filter_term, list): - if 'current_user_department' in filter_term or 'current_user_department_and_sub' in filter_term: - filter_item['current_user_department'] = current_user_department_ids - filter_item['current_user_department_and_sub'] = current_user_department_and_sub_ids - return filter_item + return normalize_filter(filter_item, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) def _pre_filter_to_filter_term(self, filters, username, id_in_org, current_user_department_ids = [], current_user_department_and_sub_ids = []): - for index, filter_item in enumerate(filters): - sub_filters = filter_item.get('filters') - if sub_filters: - filters[index]['filters'] = [self._normalize_filter(filter, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) for filter in sub_filters] - else: - filters[index] = self._normalize_filter(filter_item, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) - return filters + return pre_filter_to_filter_term(filters, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) def _get_column_by_key(self, column_key): @@ -2964,3 +2944,31 @@ def is_user_filter(filter_item): def has_user_filter(filters): return any([is_user_filter(filter_item) for filter_item in filters]) + + +def normalize_filter(filter_item, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids): + filter_term = filter_item.get('filter_term') + filter_predicate = filter_item.get('filter_predicate') + if filter_predicate == 'include_me': + filter_item['filter_term'].append(username) + if filter_predicate == 'is_current_user_ID': + filter_item['filter_term'] = id_in_org + if filter_term == 'current_user_department': + filter_item['current_user_department'] = current_user_department_ids + if filter_term == 'current_user_department_and_sub': + filter_item['current_user_department_and_sub'] = current_user_department_and_sub_ids + if isinstance(filter_term, list): + if 'current_user_department' in filter_term or 'current_user_department_and_sub' in filter_term: + filter_item['current_user_department'] = current_user_department_ids + filter_item['current_user_department_and_sub'] = current_user_department_and_sub_ids + return filter_item + + +def pre_filter_to_filter_term(filters, username, id_in_org, current_user_department_ids=[], current_user_department_and_sub_ids=[]): + for index, filter_item in enumerate(filters): + sub_filters = filter_item.get('filters') + if sub_filters: + filters[index]['filters'] = [normalize_filter(filter, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) for filter in sub_filters] + else: + filters[index] = normalize_filter(filter_item, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) + return filters From 19b15bb5c86017928a9a71c68cd86a75124684e9 Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Fri, 21 Aug 2026 09:55:52 +0800 Subject: [PATCH 3/5] Recursive normalization filter --- dtable_events/tests/sql/sql_test.py | 34 ++++++++++++++++++++++++++++ dtable_events/utils/sql_generator.py | 7 +++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/dtable_events/tests/sql/sql_test.py b/dtable_events/tests/sql/sql_test.py index 7da5907c..ec3e07c6 100644 --- a/dtable_events/tests/sql/sql_test.py +++ b/dtable_events/tests/sql/sql_test.py @@ -83,6 +83,40 @@ def to_sql(filters): to_sql([{'filters': [{'column_name': 'Colla', 'filter_predicate': 'include_me', 'filter_term': ['b@x.com']}], 'filter_conjunction': 'And'}]), "SELECT * FROM `Table1` WHERE ((`Colla` in ('b@x.com', 'me@x.com'))) LIMIT 0, 100", ) + # deeply nested filter group (3 levels) include_me must still be normalized + self.assertEqual( + to_sql([ + { + 'filters': [ + { + 'filters': [ + {'column_name': 'Colla', 'filter_predicate': 'include_me', 'filter_term': ['c@x.com']}, + ], + 'filter_conjunction': 'And', + }, + ], + 'filter_conjunction': 'And', + }, + ]), + "SELECT * FROM `Table1` WHERE (((`Colla` in ('c@x.com', 'me@x.com')))) LIMIT 0, 100", + ) + # deeply nested filter group (3 levels) current_user_department must still be normalized + self.assertEqual( + to_sql([ + { + 'filters': [ + { + 'filters': [ + {'column_name': 'Dept', 'filter_predicate': 'is', 'filter_term': 'current_user_department'}, + ], + 'filter_conjunction': 'And', + }, + ], + 'filter_conjunction': 'And', + }, + ]), + "SELECT * FROM `Table1` WHERE (((`Dept` IN (1, 2)))) LIMIT 0, 100", + ) if __name__ == '__main__': diff --git a/dtable_events/utils/sql_generator.py b/dtable_events/utils/sql_generator.py index 68e28712..862b84e1 100644 --- a/dtable_events/utils/sql_generator.py +++ b/dtable_events/utils/sql_generator.py @@ -2961,10 +2961,15 @@ def normalize_filter(filter_item, username, id_in_org, current_user_department_i if 'current_user_department' in filter_term or 'current_user_department_and_sub' in filter_term: filter_item['current_user_department'] = current_user_department_ids filter_item['current_user_department_and_sub'] = current_user_department_and_sub_ids + sub_filters = filter_item.get('filters') + if sub_filters: + filter_item['filters'] = [normalize_filter(filter, username, id_in_org, current_user_department_ids, current_user_department_and_sub_ids) for filter in sub_filters] return filter_item -def pre_filter_to_filter_term(filters, username, id_in_org, current_user_department_ids=[], current_user_department_and_sub_ids=[]): +def pre_filter_to_filter_term(filters, username, id_in_org, current_user_department_ids=None, current_user_department_and_sub_ids=None): + current_user_department_ids = current_user_department_ids or [] + current_user_department_and_sub_ids = current_user_department_and_sub_ids or [] for index, filter_item in enumerate(filters): sub_filters = filter_item.get('filters') if sub_filters: From fa9c00cec3b54de82442ad601348d721f246877b Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Fri, 21 Aug 2026 10:51:18 +0800 Subject: [PATCH 4/5] fix grouped duration display format --- dtable_events/dtable_io/excel.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dtable_events/dtable_io/excel.py b/dtable_events/dtable_io/excel.py index 17d0e26d..bad8bdcf 100644 --- a/dtable_events/dtable_io/excel.py +++ b/dtable_events/dtable_io/excel.py @@ -1896,6 +1896,9 @@ def handle_grouped_row(row, ws, cols_without_hidden, column_name_to_column, sub_ formula_value, number_format = parse_formula_number(first_cell_value, group_column.get('data')) c = WriteOnlyCell(ws, value=formula_value) c.number_format = number_format + elif group_column.get('type') == ColumnTypes.DURATION: + cell_value = format_duration(first_cell_value, group_column.get('data')) + c = WriteOnlyCell(ws, value=ILLEGAL_CHARACTERS_RE.sub('', cell_value)) else: cell_value = cell_data2str(first_cell_value) c = WriteOnlyCell(ws, value=ILLEGAL_CHARACTERS_RE.sub('', cell_value)) From f0d87afcac42b06c5f04c11e7ce42d3b1dda3096 Mon Sep 17 00:00:00 2001 From: Alex Happy <1223408988@qq.com> Date: Fri, 21 Aug 2026 11:26:45 +0800 Subject: [PATCH 5/5] Add more nested filters that depend on user information to unit tests. --- dtable_events/tests/sql/sql_test.py | 51 +++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/dtable_events/tests/sql/sql_test.py b/dtable_events/tests/sql/sql_test.py index ec3e07c6..80df384f 100644 --- a/dtable_events/tests/sql/sql_test.py +++ b/dtable_events/tests/sql/sql_test.py @@ -117,6 +117,57 @@ def to_sql(filters): ]), "SELECT * FROM `Table1` WHERE (((`Dept` IN (1, 2)))) LIMIT 0, 100", ) + # deeply nested filter group (3 levels) is_current_user_ID must still be normalized + self.assertEqual( + to_sql([ + { + 'filters': [ + { + 'filters': [ + {'column_name': '名称', 'filter_predicate': 'is_current_user_ID', 'filter_term': ''}, + ], + 'filter_conjunction': 'And', + }, + ], + 'filter_conjunction': 'And', + }, + ]), + "SELECT * FROM `Table1` WHERE (((`名称` = 'admin-1'))) LIMIT 0, 100", + ) + # deeply nested filter group (3 levels) current_user_department_and_sub must still be normalized + self.assertEqual( + to_sql([ + { + 'filters': [ + { + 'filters': [ + {'column_name': 'Dept', 'filter_predicate': 'is', 'filter_term': 'current_user_department_and_sub'}, + ], + 'filter_conjunction': 'And', + }, + ], + 'filter_conjunction': 'And', + }, + ]), + "SELECT * FROM `Table1` WHERE (((`Dept` IN (1, 2, 3)))) LIMIT 0, 100", + ) + # deeply nested filter group (3 levels) list-valued department filter must still be normalized + self.assertEqual( + to_sql([ + { + 'filters': [ + { + 'filters': [ + {'column_name': 'Dept', 'filter_predicate': 'is_any_of', 'filter_term': ['current_user_department_and_sub', 999]}, + ], + 'filter_conjunction': 'And', + }, + ], + 'filter_conjunction': 'And', + }, + ]), + "SELECT * FROM `Table1` WHERE (((`Dept` IN (1, 2, 3, 999)))) LIMIT 0, 100", + ) if __name__ == '__main__':