Skip to content

Commit c34d457

Browse files
committed
Merge branch 'feat/TG-1075-cross-project-copy' into 'enterprise'
feat(test-definitions): support copy/move across projects (TG-1075) See merge request dkinternal/testgen/dataops-testgen!531
2 parents 104ae70 + ec7e4ab commit c34d457

3 files changed

Lines changed: 70 additions & 9 deletions

File tree

testgen/ui/auth.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from testgen.ui.services.query_cache import (
1313
get_membership_by_user_and_project,
1414
get_user,
15+
select_projects_where,
1516
select_users_where,
1617
)
1718
from testgen.ui.session import session
@@ -55,6 +56,9 @@ def user_has_permission(self, permission: Permission, /, project_code: str | Non
5556
def user_has_project_access(self, project_code: str) -> bool: # noqa: ARG002
5657
return True
5758

59+
def get_projects_with_permission(self, permission: Permission, /) -> list[str]: # noqa: ARG002
60+
return [p.project_code for p in select_projects_where()]
61+
5862
def get_jwt_hashing_key(self) -> bytes:
5963
try:
6064
return get_jwt_signing_key()

testgen/ui/components/frontend/js/pages/test_definitions.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1313,6 +1313,7 @@ const CopyMoveDialogComponent = ({ open, info, onClose }, emit) => {
13131313
const dialogInfo = van.derive(() => getValue(info) ?? null);
13141314
const collision = van.derive(() => dialogInfo.val?.collision ?? null);
13151315

1316+
const targetProjectCode = van.state(null);
13161317
const targetTgId = van.state(null);
13171318
const targetTsId = van.state(null);
13181319
const targetTableName = van.state(null);
@@ -1324,6 +1325,7 @@ const CopyMoveDialogComponent = ({ open, info, onClose }, emit) => {
13241325
const isOpen = open.val;
13251326
if (isOpen && !wasOpen.val) {
13261327
const di = dialogInfo.val;
1328+
targetProjectCode.val = di?.current_project_code ?? null;
13271329
targetTgId.val = di?.current_table_group_id ?? null;
13281330
targetTsId.val = null;
13291331
targetTableName.val = null;
@@ -1334,10 +1336,16 @@ const CopyMoveDialogComponent = ({ open, info, onClose }, emit) => {
13341336
}
13351337
});
13361338

1337-
const tableGroupOptions = van.derive(() =>
1338-
(dialogInfo.val?.table_groups ?? []).map(tg => ({ label: tg.table_groups_name, value: tg.id }))
1339+
const projectOptions = van.derive(() =>
1340+
(dialogInfo.val?.projects ?? []).map(p => ({ label: p.project_name, value: p.project_code }))
13391341
);
13401342

1343+
const tableGroupOptions = van.derive(() => {
1344+
const project = targetProjectCode.val;
1345+
const tgs = dialogInfo.val?.table_groups_by_project?.[project] ?? [];
1346+
return tgs.map(tg => ({ label: tg.table_groups_name, value: tg.id }));
1347+
});
1348+
13411349
const testSuiteOptions = van.derive(() => {
13421350
const tg = targetTgId.val;
13431351
const suites = dialogInfo.val?.test_suites_by_table_group?.[tg] ?? [];
@@ -1414,6 +1422,21 @@ const CopyMoveDialogComponent = ({ open, info, onClose }, emit) => {
14141422
{ class: 'flex-column fx-gap-4 td-form-dialog' },
14151423
() => div({ class: 'text-caption' }, `Selected tests: ${(dialogInfo.val?.selected ?? []).length}`),
14161424

1425+
() => Select({
1426+
label: 'Target Project',
1427+
value: targetProjectCode.val,
1428+
options: projectOptions.val,
1429+
required: true,
1430+
filterable: true,
1431+
onChange: (value) => {
1432+
targetProjectCode.val = value;
1433+
targetTgId.val = null;
1434+
targetTsId.val = null;
1435+
targetTableName.val = null;
1436+
targetColumnName.val = null;
1437+
},
1438+
}),
1439+
14171440
() => Select({
14181441
label: 'Target Table Group',
14191442
value: targetTgId.val,

testgen/ui/views/test_definitions.py

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from testgen.common.enums import JobSource
1313
from testgen.common.models import with_database_session
1414
from testgen.common.models.job_execution import JobExecution
15+
from testgen.common.models.project import Project
1516
from testgen.common.models.table_group import TableGroup, TableGroupMinimal
1617
from testgen.common.models.test_definition import (
1718
TestDefinition,
@@ -37,6 +38,7 @@
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

Comments
 (0)