1212from testgen .common .enums import JobSource
1313from testgen .common .models import with_database_session
1414from testgen .common .models .job_execution import JobExecution
15+ from testgen .common .models .project import Project
1516from testgen .common .models .table_group import TableGroup , TableGroupMinimal
1617from testgen .common .models .test_definition import (
1718 TestDefinition ,
3738 get_connection ,
3839 get_table_group_minimal ,
3940 get_test_suite ,
41+ select_projects_where ,
4042 select_table_groups_minimal_where ,
4143 select_test_definitions_minimal_where ,
4244 select_test_definitions_page ,
@@ -157,11 +159,6 @@ def render(
157159 test_types = run_test_type_lookup_query ().to_dict ("records" )
158160 table_columns = get_columns (str (table_group .id ))
159161 filter_columns_df = get_test_suite_columns (test_suite_id )
160- table_groups = select_table_groups_minimal_where (TableGroup .project_code == project_code )
161- all_test_suites = select_test_suites_minimal_where (
162- TestSuite .table_groups_id .in_ ([str (tg .id ) for tg in table_groups ]),
163- TestSuite .is_monitor .isnot (True ),
164- )
165162
166163 # Build filter options
167164 table_options = sorted (filter_columns_df ["table_name" ].dropna ().unique ().tolist (), key = str .lower )
@@ -230,15 +227,38 @@ def render(
230227
231228 copy_move_dialog = None
232229 if selected := st .session_state .get (TD_COPY_MOVE_DIALOG_KEY ):
230+ editable_project_codes = session .auth .get_projects_with_permission ("edit" )
231+ editable_projects = (
232+ select_projects_where (Project .project_code .in_ (editable_project_codes ))
233+ if editable_project_codes
234+ else []
235+ )
236+ editable_table_groups = select_table_groups_minimal_where (
237+ TableGroup .project_code .in_ (editable_project_codes )
238+ )
239+ editable_test_suites = select_test_suites_minimal_where (
240+ TestSuite .table_groups_id .in_ ([str (tg .id ) for tg in editable_table_groups ]),
241+ TestSuite .is_monitor .isnot (True ),
242+ )
243+ tgs_by_project : dict [str , list ] = {}
244+ for tg in editable_table_groups :
245+ tgs_by_project .setdefault (tg .project_code , []).append (
246+ {"id" : str (tg .id ), "table_groups_name" : tg .table_groups_name }
247+ )
233248 suites_by_tg : dict [str , list ] = {}
234- for ts in all_test_suites :
249+ for ts in editable_test_suites :
235250 suites_by_tg .setdefault (str (ts .table_groups_id ), []).append (
236251 {"id" : str (ts .id ), "test_suite" : ts .test_suite }
237252 )
238253 copy_move_dialog = {
239254 "open" : True ,
240255 "selected" : selected ,
241- "table_groups" : [{"id" : str (tg .id ), "table_groups_name" : tg .table_groups_name } for tg in table_groups ],
256+ "projects" : [
257+ {"project_code" : p .project_code , "project_name" : p .project_name }
258+ for p in editable_projects
259+ ],
260+ "current_project_code" : project_code ,
261+ "table_groups_by_project" : tgs_by_project ,
242262 "current_table_group_id" : str (table_group .id ),
243263 "current_test_suite_id" : str (test_suite .id ),
244264 "test_suites_by_table_group" : suites_by_tg ,
@@ -402,13 +422,22 @@ def on_update_attribute_all(payload: dict) -> None:
402422 TestDefinition .set_status_attribute (attribute , all_ids , value )
403423 st .cache_data .clear ()
404424
425+ def _resolve_target_project (target_tg_id : str ) -> str | None :
426+ target_tg = TableGroup .get_minimal (target_tg_id )
427+ return target_tg .project_code if target_tg else None
428+
405429 @with_database_session
406430 def on_copy_confirmed (payload : dict ) -> None :
407431 ids = payload ["ids" ]
408432 target_tg_id = payload ["target_table_group_id" ]
409433 target_ts_id = payload ["target_test_suite_id" ]
410434 target_table = payload .get ("target_table_name" )
411435 target_col = payload .get ("target_column_name" )
436+ target_project = _resolve_target_project (target_tg_id )
437+ if not target_project or not session .auth .user_has_permission ("edit" , target_project ):
438+ LOG .warning ("Refusing copy to table group %s — user lacks edit permission" , target_tg_id )
439+ st .toast ("You don't have edit permission for the target project." , icon = ":material/error:" )
440+ return
412441 overwrite_ids = st .session_state .pop (TD_COPY_MOVE_OVERWRITE_KEY , [])
413442 if overwrite_ids :
414443 TestDefinition .delete_where (TestDefinition .id .in_ (overwrite_ids ))
@@ -426,6 +455,11 @@ def on_move_confirmed(payload: dict) -> None:
426455 target_ts_id = payload ["target_test_suite_id" ]
427456 target_table = payload .get ("target_table_name" )
428457 target_col = payload .get ("target_column_name" )
458+ target_project = _resolve_target_project (target_tg_id )
459+ if not target_project or not session .auth .user_has_permission ("edit" , target_project ):
460+ LOG .warning ("Refusing move to table group %s — user lacks edit permission" , target_tg_id )
461+ st .toast ("You don't have edit permission for the target project." , icon = ":material/error:" )
462+ return
429463 overwrite_ids = st .session_state .pop (TD_COPY_MOVE_OVERWRITE_KEY , [])
430464 if overwrite_ids :
431465 TestDefinition .delete_where (TestDefinition .id .in_ (overwrite_ids ))
0 commit comments